From 15169972543cf3a0ccb8e62517eec2933e894e5f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 1 Jun 2026 18:35:38 +0200 Subject: [PATCH 01/34] plan: add branch for M5 agent-graph-integration (FE-785) Amp-Thread-ID: https://ampcode.com/threads/T-019e83f6-44c9-741a-a90a-9e9535774b3d Co-authored-by: Amp --- memory/PLAN.md | 1 + 1 file changed, 1 insertion(+) diff --git a/memory/PLAN.md b/memory/PLAN.md index 015259cdf..e461cb8cd 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -116,6 +116,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Name:** Agent ↔ graph integration through the shared command layer (M5) - **Linear:** [FE-785](https://linear.app/hash/issue/FE-785) +- **Branch:** `ln/fe-785-agent-graph-integration` (stacked on `ln/fe-776-graph-layer-prep-profile`) - **Kind:** structural - **Status:** not-started - **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations — including `commitGraph` batch mutations for the `propose-graph` direct-commit path (D53-L, D26-L) — elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances for `project-graph`, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. **The primary A14-L proof runs here:** test whether the LLM can produce structurally-legal `commitGraph` batches against the real CommandExecutor with bounded retry. From 3a9a115b5f676731cb3e0a3595a97776386460b1 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 1 Jun 2026 19:01:13 +0200 Subject: [PATCH 02/34] =?UTF-8?q?move=20src/tui-client/.pi=20=E2=86=92=20s?= =?UTF-8?q?rc/.pi=20per=20D52-L=20source=20topology?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocate Pi extension shell and all product extensions, components, context builders, and tests from src/tui-client/ to src/.pi/ to match the D52-L source topology: src/{.pi, agents, db, graph, session, rpc, web}. Update all import paths, projectRoot() helpers, relative URL depths, and the build:pi-assets script in package.json. Amp-Thread-ID: https://ampcode.com/threads/T-019e840d-172b-736c-839e-e441f7308220 Co-authored-by: Amp --- package.json | 2 +- src/{tui-client => }/.pi/README.md | 0 .../.pi/__tests__/chrome.test.ts | 0 .../.pi/__tests__/extension-registry.test.ts | 12 +++-- .../__tests__/mention-autocomplete.test.ts | 0 .../.pi/__tests__/operational-mode.test.ts | 0 .../.pi/__tests__/prompting.test.ts | 16 ++----- .../structured-exchange-extension.test.ts | 0 ...tructured-exchange-present-request.test.ts | 0 .../structured-exchange-schemas.test.ts | 0 .../.pi/__tests__/structured-exchange.test.ts | 0 .../.pi/__tests__/workspace-dialog.test.ts | 5 +-- .../.pi/components/brunch-identity.ts | 0 src/{tui-client => }/.pi/components/cards.ts | 0 .../.pi/components/workspace-dialog.ts | 0 .../assets/brunch-logo-quad-56x18-240.ansi | 0 .../assets/brunch-logo-quad-56x18.ansi | 0 .../workspace-dialog/assets/brunch.png | Bin .../components/workspace-dialog/component.ts | 2 +- .../.pi/components/workspace-dialog/index.ts | 0 .../.pi/components/workspace-dialog/model.ts | 2 +- .../components/workspace-dialog/preflight.ts | 2 +- src/{tui-client => }/.pi/context/README.md | 0 .../.pi/context/builders/README.md | 0 .../.pi/context/builders/graph-context.ts | 0 .../.pi/context/builders/readiness-context.ts | 0 .../builders/structured-exchange-context.ts | 0 .../.pi/context/compose-brunch-prompt.ts | 0 .../.pi/context/prompt-packs/brunch-base.md | 0 .../prompt-packs/candidate-proposals.md | 0 .../context/prompt-packs/capture-analysis.md | 0 .../.pi/context/prompt-packs/elicit.md | 0 .../.pi/context/prompt-packs/elicitor.md | 0 .../prompt-packs/structured-exchange.md | 0 .../.pi/extensions/alternatives.ts | 0 .../extensions/auto-compaction-anchors.json | 0 src/{tui-client => }/.pi/extensions/chrome.ts | 2 +- .../.pi/extensions/command-policy.ts | 0 .../.pi/extensions/mention-autocomplete.ts | 0 .../.pi/extensions/operational-mode.ts | 0 .../.pi/extensions/prompting.ts | 0 .../.pi/extensions/session-lifecycle.ts | 0 .../extensions/structured-exchange/index.ts | 2 +- .../structured-exchange/present-candidates.ts | 0 .../structured-exchange/present-options.ts | 0 .../structured-exchange/present-question.ts | 0 .../structured-exchange/present-review-set.ts | 0 .../structured-exchange/request-answer.ts | 0 .../structured-exchange/request-choice.ts | 0 .../structured-exchange/request-choices.ts | 0 .../structured-exchange/request-review.ts | 0 .../structured-exchange/schemas/README.md | 0 .../structured-exchange/schemas/capture.ts | 0 .../structured-exchange/schemas/index.ts | 0 .../structured-exchange/schemas/present.ts | 0 .../structured-exchange/schemas/request.ts | 0 .../structured-exchange/schemas/shared.ts | 0 .../shared/editor-fallback.ts | 2 +- .../structured-exchange/shared/markdown.ts | 0 .../structured-exchange/shared/model.ts | 0 .../structured-exchange/shared/recovery.ts | 0 .../.pi/extensions/subagents/config.json | 0 .../.pi/extensions/workspace-dialog.ts | 2 +- src/{tui-client => .pi}/pi-extension-shell.ts | 42 +++++++++--------- src/{tui-client => }/.pi/settings.json | 0 src/brunch-tui.test.ts | 2 +- src/brunch-tui.ts | 8 ++-- src/elicitation-exchange.ts | 4 +- .../structured-exchange-ordering-proof.ts | 6 +-- src/probes/structured-exchange-rpc-proof.ts | 6 +-- src/rpc/handlers.ts | 4 +- src/session-transcript.ts | 2 +- 72 files changed, 53 insertions(+), 70 deletions(-) rename src/{tui-client => }/.pi/README.md (100%) rename src/{tui-client => }/.pi/__tests__/chrome.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/extension-registry.test.ts (93%) rename src/{tui-client => }/.pi/__tests__/mention-autocomplete.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/operational-mode.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/prompting.test.ts (95%) rename src/{tui-client => }/.pi/__tests__/structured-exchange-extension.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/structured-exchange-present-request.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/structured-exchange-schemas.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/structured-exchange.test.ts (100%) rename src/{tui-client => }/.pi/__tests__/workspace-dialog.test.ts (99%) rename src/{tui-client => }/.pi/components/brunch-identity.ts (100%) rename src/{tui-client => }/.pi/components/cards.ts (100%) rename src/{tui-client => }/.pi/components/workspace-dialog.ts (100%) rename src/{tui-client => }/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18-240.ansi (100%) rename src/{tui-client => }/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi (100%) rename src/{tui-client => }/.pi/components/workspace-dialog/assets/brunch.png (100%) rename src/{tui-client => }/.pi/components/workspace-dialog/component.ts (99%) rename src/{tui-client => }/.pi/components/workspace-dialog/index.ts (100%) rename src/{tui-client => }/.pi/components/workspace-dialog/model.ts (99%) rename src/{tui-client => }/.pi/components/workspace-dialog/preflight.ts (97%) rename src/{tui-client => }/.pi/context/README.md (100%) rename src/{tui-client => }/.pi/context/builders/README.md (100%) rename src/{tui-client => }/.pi/context/builders/graph-context.ts (100%) rename src/{tui-client => }/.pi/context/builders/readiness-context.ts (100%) rename src/{tui-client => }/.pi/context/builders/structured-exchange-context.ts (100%) rename src/{tui-client => }/.pi/context/compose-brunch-prompt.ts (100%) rename src/{tui-client => }/.pi/context/prompt-packs/brunch-base.md (100%) rename src/{tui-client => }/.pi/context/prompt-packs/candidate-proposals.md (100%) rename src/{tui-client => }/.pi/context/prompt-packs/capture-analysis.md (100%) rename src/{tui-client => }/.pi/context/prompt-packs/elicit.md (100%) rename src/{tui-client => }/.pi/context/prompt-packs/elicitor.md (100%) rename src/{tui-client => }/.pi/context/prompt-packs/structured-exchange.md (100%) rename src/{tui-client => }/.pi/extensions/alternatives.ts (100%) rename src/{tui-client => }/.pi/extensions/auto-compaction-anchors.json (100%) rename src/{tui-client => }/.pi/extensions/chrome.ts (99%) rename src/{tui-client => }/.pi/extensions/command-policy.ts (100%) rename src/{tui-client => }/.pi/extensions/mention-autocomplete.ts (100%) rename src/{tui-client => }/.pi/extensions/operational-mode.ts (100%) rename src/{tui-client => }/.pi/extensions/prompting.ts (100%) rename src/{tui-client => }/.pi/extensions/session-lifecycle.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/index.ts (97%) rename src/{tui-client => }/.pi/extensions/structured-exchange/present-candidates.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/present-options.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/present-question.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/present-review-set.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/request-answer.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/request-choice.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/request-choices.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/request-review.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/README.md (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/capture.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/index.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/present.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/request.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/schemas/shared.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/shared/editor-fallback.ts (99%) rename src/{tui-client => }/.pi/extensions/structured-exchange/shared/markdown.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/shared/model.ts (100%) rename src/{tui-client => }/.pi/extensions/structured-exchange/shared/recovery.ts (100%) rename src/{tui-client => }/.pi/extensions/subagents/config.json (100%) rename src/{tui-client => }/.pi/extensions/workspace-dialog.ts (98%) rename src/{tui-client => .pi}/pi-extension-shell.ts (64%) rename src/{tui-client => }/.pi/settings.json (100%) diff --git a/package.json b/package.json index 784c61716..59edc744c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "dev": "tsx src/brunch.ts", "build": "tsc -p tsconfig.build.json && npm run build:pi-assets && npm run build:web", - "build:pi-assets": "mkdir -p dist/tui-client/.pi/components/workspace-dialog dist/tui-client/.pi/context/prompt-packs && cp -R src/tui-client/.pi/components/workspace-dialog/assets dist/tui-client/.pi/components/workspace-dialog/ && cp src/tui-client/.pi/context/prompt-packs/*.md dist/tui-client/.pi/context/prompt-packs/", + "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/.pi/context/prompt-packs && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp src/.pi/context/prompt-packs/*.md dist/.pi/context/prompt-packs/", "build:web": "vite build", "test": "vitest --run", "test:watch": "vitest", diff --git a/src/tui-client/.pi/README.md b/src/.pi/README.md similarity index 100% rename from src/tui-client/.pi/README.md rename to src/.pi/README.md diff --git a/src/tui-client/.pi/__tests__/chrome.test.ts b/src/.pi/__tests__/chrome.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/chrome.test.ts rename to src/.pi/__tests__/chrome.test.ts diff --git a/src/tui-client/.pi/__tests__/extension-registry.test.ts b/src/.pi/__tests__/extension-registry.test.ts similarity index 93% rename from src/tui-client/.pi/__tests__/extension-registry.test.ts rename to src/.pi/__tests__/extension-registry.test.ts index fc39cc4dd..0f4eb85b5 100644 --- a/src/tui-client/.pi/__tests__/extension-registry.test.ts +++ b/src/.pi/__tests__/extension-registry.test.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url" import { describe, expect, it } from "vitest" -import { createBrunchPiExtensionShell } from "../../pi-extension-shell.js" +import { createBrunchPiExtensionShell } from "../pi-extension-shell.js" import alternatives from "../extensions/alternatives.js" import chrome from "../extensions/chrome.js" import commandPolicy from "../extensions/command-policy.js" @@ -36,7 +36,7 @@ const extensionDefaults = { } describe("Brunch explicit Pi extension registry", () => { - it("keeps default factory exports for src/tui-client /.pi iteration", () => { + it("keeps default factory exports for src/.pi iteration", () => { for (const [path, factory] of Object.entries(extensionDefaults)) { expect(factory, path).toEqual(expect.any(Function)) } @@ -93,7 +93,7 @@ describe("Brunch explicit Pi extension registry", () => { it("does not retain the filesystem-discovery product-extension protocol", async () => { const shell = await readFile( - join(projectRoot(), "src/tui-client/pi-extension-shell.ts"), + join(projectRoot(), "src/.pi/pi-extension-shell.ts"), "utf8", ) const discoveryExport = ["discover", "BrunchProductExtensionEntries"].join( @@ -186,7 +186,7 @@ function createRecordingExtensionApi() { } async function listExtensionEntrypoints(): Promise { - const extensionsDir = join(projectRoot(), "src/tui-client/.pi/extensions") + const extensionsDir = join(projectRoot(), "src/.pi/extensions") const entries = await readdir(extensionsDir, { withFileTypes: true }) const files: string[] = [] for (const entry of entries) { @@ -210,7 +210,5 @@ async function fileExists(file: string): Promise { } function projectRoot(): string { - return dirname( - dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))), - ) + return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))) } diff --git a/src/tui-client/.pi/__tests__/mention-autocomplete.test.ts b/src/.pi/__tests__/mention-autocomplete.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/mention-autocomplete.test.ts rename to src/.pi/__tests__/mention-autocomplete.test.ts diff --git a/src/tui-client/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/operational-mode.test.ts rename to src/.pi/__tests__/operational-mode.test.ts diff --git a/src/tui-client/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts similarity index 95% rename from src/tui-client/.pi/__tests__/prompting.test.ts rename to src/.pi/__tests__/prompting.test.ts index 4fd6f7555..c1d4b92a3 100644 --- a/src/tui-client/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -14,7 +14,7 @@ import { registerBrunchOperationalModePolicy, } from "../extensions/operational-mode.js" import { registerBrunchPrompting } from "../extensions/prompting.js" -import { createBrunchPiExtensionShell } from "../../pi-extension-shell.js" +import { createBrunchPiExtensionShell } from "../pi-extension-shell.js" function runtimeEntry(state: BrunchAgentState) { return { @@ -246,15 +246,9 @@ describe("Brunch prompt-pack topology", () => { it("does not expose private prompt packs through Pi resource discovery", async () => { const [promptingSource, composerSource] = await Promise.all([ + readFile(join(projectRoot(), "src/.pi/extensions/prompting.ts"), "utf8"), readFile( - join(projectRoot(), "src/tui-client/.pi/extensions/prompting.ts"), - "utf8", - ), - readFile( - join( - projectRoot(), - "src/tui-client/.pi/context/compose-brunch-prompt.ts", - ), + join(projectRoot(), "src/.pi/context/compose-brunch-prompt.ts"), "utf8", ), ]) @@ -267,7 +261,5 @@ describe("Brunch prompt-pack topology", () => { }) function projectRoot(): string { - return dirname( - dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))), - ) + return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))) } diff --git a/src/tui-client/.pi/__tests__/structured-exchange-extension.test.ts b/src/.pi/__tests__/structured-exchange-extension.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/structured-exchange-extension.test.ts rename to src/.pi/__tests__/structured-exchange-extension.test.ts diff --git a/src/tui-client/.pi/__tests__/structured-exchange-present-request.test.ts b/src/.pi/__tests__/structured-exchange-present-request.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/structured-exchange-present-request.test.ts rename to src/.pi/__tests__/structured-exchange-present-request.test.ts diff --git a/src/tui-client/.pi/__tests__/structured-exchange-schemas.test.ts b/src/.pi/__tests__/structured-exchange-schemas.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/structured-exchange-schemas.test.ts rename to src/.pi/__tests__/structured-exchange-schemas.test.ts diff --git a/src/tui-client/.pi/__tests__/structured-exchange.test.ts b/src/.pi/__tests__/structured-exchange.test.ts similarity index 100% rename from src/tui-client/.pi/__tests__/structured-exchange.test.ts rename to src/.pi/__tests__/structured-exchange.test.ts diff --git a/src/tui-client/.pi/__tests__/workspace-dialog.test.ts b/src/.pi/__tests__/workspace-dialog.test.ts similarity index 99% rename from src/tui-client/.pi/__tests__/workspace-dialog.test.ts rename to src/.pi/__tests__/workspace-dialog.test.ts index 48e825cd3..b9307a9b6 100644 --- a/src/tui-client/.pi/__tests__/workspace-dialog.test.ts +++ b/src/.pi/__tests__/workspace-dialog.test.ts @@ -365,10 +365,7 @@ describe("spec/session picker", () => { it("declares pi-tui as a direct dependency", async () => { const manifest = JSON.parse( - await readFile( - new URL("../../../../package.json", import.meta.url), - "utf8", - ), + await readFile(new URL("../../../package.json", import.meta.url), "utf8"), ) as { dependencies?: Record } expect(manifest.dependencies).toHaveProperty("@earendil-works/pi-tui") diff --git a/src/tui-client/.pi/components/brunch-identity.ts b/src/.pi/components/brunch-identity.ts similarity index 100% rename from src/tui-client/.pi/components/brunch-identity.ts rename to src/.pi/components/brunch-identity.ts diff --git a/src/tui-client/.pi/components/cards.ts b/src/.pi/components/cards.ts similarity index 100% rename from src/tui-client/.pi/components/cards.ts rename to src/.pi/components/cards.ts diff --git a/src/tui-client/.pi/components/workspace-dialog.ts b/src/.pi/components/workspace-dialog.ts similarity index 100% rename from src/tui-client/.pi/components/workspace-dialog.ts rename to src/.pi/components/workspace-dialog.ts diff --git a/src/tui-client/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18-240.ansi b/src/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18-240.ansi similarity index 100% rename from src/tui-client/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18-240.ansi rename to src/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18-240.ansi diff --git a/src/tui-client/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi b/src/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi similarity index 100% rename from src/tui-client/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi rename to src/.pi/components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi diff --git a/src/tui-client/.pi/components/workspace-dialog/assets/brunch.png b/src/.pi/components/workspace-dialog/assets/brunch.png similarity index 100% rename from src/tui-client/.pi/components/workspace-dialog/assets/brunch.png rename to src/.pi/components/workspace-dialog/assets/brunch.png diff --git a/src/tui-client/.pi/components/workspace-dialog/component.ts b/src/.pi/components/workspace-dialog/component.ts similarity index 99% rename from src/tui-client/.pi/components/workspace-dialog/component.ts rename to src/.pi/components/workspace-dialog/component.ts index b74c3444f..c0225858b 100644 --- a/src/tui-client/.pi/components/workspace-dialog/component.ts +++ b/src/.pi/components/workspace-dialog/component.ts @@ -14,7 +14,7 @@ import { import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from "../../../../workspace-session-coordinator.js" +} from "../../../workspace-session-coordinator.js" import { formatBrunchProductIdentity, readBrunchAnsiLogo, diff --git a/src/tui-client/.pi/components/workspace-dialog/index.ts b/src/.pi/components/workspace-dialog/index.ts similarity index 100% rename from src/tui-client/.pi/components/workspace-dialog/index.ts rename to src/.pi/components/workspace-dialog/index.ts diff --git a/src/tui-client/.pi/components/workspace-dialog/model.ts b/src/.pi/components/workspace-dialog/model.ts similarity index 99% rename from src/tui-client/.pi/components/workspace-dialog/model.ts rename to src/.pi/components/workspace-dialog/model.ts index 2e19b102b..005ca405a 100644 --- a/src/tui-client/.pi/components/workspace-dialog/model.ts +++ b/src/.pi/components/workspace-dialog/model.ts @@ -2,7 +2,7 @@ import type { WorkspaceLaunchInventory, WorkspaceLaunchSession, SpecSessionActivationDecision, -} from "../../../../workspace-session-coordinator.js" +} from "../../../workspace-session-coordinator.js" export type WorkspaceSelectionStage = { stage: "home" } | { stage: "newSpecTitle" diff --git a/src/tui-client/.pi/components/workspace-dialog/preflight.ts b/src/.pi/components/workspace-dialog/preflight.ts similarity index 97% rename from src/tui-client/.pi/components/workspace-dialog/preflight.ts rename to src/.pi/components/workspace-dialog/preflight.ts index b8e9dd9cb..2b6504705 100644 --- a/src/tui-client/.pi/components/workspace-dialog/preflight.ts +++ b/src/.pi/components/workspace-dialog/preflight.ts @@ -4,7 +4,7 @@ import { ProcessTerminal, TUI, type Terminal } from "@earendil-works/pi-tui" import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from "../../../../workspace-session-coordinator.js" +} from "../../../workspace-session-coordinator.js" import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, diff --git a/src/tui-client/.pi/context/README.md b/src/.pi/context/README.md similarity index 100% rename from src/tui-client/.pi/context/README.md rename to src/.pi/context/README.md diff --git a/src/tui-client/.pi/context/builders/README.md b/src/.pi/context/builders/README.md similarity index 100% rename from src/tui-client/.pi/context/builders/README.md rename to src/.pi/context/builders/README.md diff --git a/src/tui-client/.pi/context/builders/graph-context.ts b/src/.pi/context/builders/graph-context.ts similarity index 100% rename from src/tui-client/.pi/context/builders/graph-context.ts rename to src/.pi/context/builders/graph-context.ts diff --git a/src/tui-client/.pi/context/builders/readiness-context.ts b/src/.pi/context/builders/readiness-context.ts similarity index 100% rename from src/tui-client/.pi/context/builders/readiness-context.ts rename to src/.pi/context/builders/readiness-context.ts diff --git a/src/tui-client/.pi/context/builders/structured-exchange-context.ts b/src/.pi/context/builders/structured-exchange-context.ts similarity index 100% rename from src/tui-client/.pi/context/builders/structured-exchange-context.ts rename to src/.pi/context/builders/structured-exchange-context.ts diff --git a/src/tui-client/.pi/context/compose-brunch-prompt.ts b/src/.pi/context/compose-brunch-prompt.ts similarity index 100% rename from src/tui-client/.pi/context/compose-brunch-prompt.ts rename to src/.pi/context/compose-brunch-prompt.ts diff --git a/src/tui-client/.pi/context/prompt-packs/brunch-base.md b/src/.pi/context/prompt-packs/brunch-base.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/brunch-base.md rename to src/.pi/context/prompt-packs/brunch-base.md diff --git a/src/tui-client/.pi/context/prompt-packs/candidate-proposals.md b/src/.pi/context/prompt-packs/candidate-proposals.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/candidate-proposals.md rename to src/.pi/context/prompt-packs/candidate-proposals.md diff --git a/src/tui-client/.pi/context/prompt-packs/capture-analysis.md b/src/.pi/context/prompt-packs/capture-analysis.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/capture-analysis.md rename to src/.pi/context/prompt-packs/capture-analysis.md diff --git a/src/tui-client/.pi/context/prompt-packs/elicit.md b/src/.pi/context/prompt-packs/elicit.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/elicit.md rename to src/.pi/context/prompt-packs/elicit.md diff --git a/src/tui-client/.pi/context/prompt-packs/elicitor.md b/src/.pi/context/prompt-packs/elicitor.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/elicitor.md rename to src/.pi/context/prompt-packs/elicitor.md diff --git a/src/tui-client/.pi/context/prompt-packs/structured-exchange.md b/src/.pi/context/prompt-packs/structured-exchange.md similarity index 100% rename from src/tui-client/.pi/context/prompt-packs/structured-exchange.md rename to src/.pi/context/prompt-packs/structured-exchange.md diff --git a/src/tui-client/.pi/extensions/alternatives.ts b/src/.pi/extensions/alternatives.ts similarity index 100% rename from src/tui-client/.pi/extensions/alternatives.ts rename to src/.pi/extensions/alternatives.ts diff --git a/src/tui-client/.pi/extensions/auto-compaction-anchors.json b/src/.pi/extensions/auto-compaction-anchors.json similarity index 100% rename from src/tui-client/.pi/extensions/auto-compaction-anchors.json rename to src/.pi/extensions/auto-compaction-anchors.json diff --git a/src/tui-client/.pi/extensions/chrome.ts b/src/.pi/extensions/chrome.ts similarity index 99% rename from src/tui-client/.pi/extensions/chrome.ts rename to src/.pi/extensions/chrome.ts index e0ed6a98b..4813819d0 100644 --- a/src/tui-client/.pi/extensions/chrome.ts +++ b/src/.pi/extensions/chrome.ts @@ -8,7 +8,7 @@ import { BRUNCH_COMPACT_WORDMARK } from "../components/brunch-identity.js" import type { WorkspaceSessionChromeState, WorkspaceSessionReadyState, -} from "../../../workspace-session-coordinator.js" +} from "../../workspace-session-coordinator.js" export type BrunchChromeStage = "idle" | "streaming" | "observer-review" export type BrunchChromeWorkerStatus = "idle" | "queued" | "running" | "blocked" diff --git a/src/tui-client/.pi/extensions/command-policy.ts b/src/.pi/extensions/command-policy.ts similarity index 100% rename from src/tui-client/.pi/extensions/command-policy.ts rename to src/.pi/extensions/command-policy.ts diff --git a/src/tui-client/.pi/extensions/mention-autocomplete.ts b/src/.pi/extensions/mention-autocomplete.ts similarity index 100% rename from src/tui-client/.pi/extensions/mention-autocomplete.ts rename to src/.pi/extensions/mention-autocomplete.ts diff --git a/src/tui-client/.pi/extensions/operational-mode.ts b/src/.pi/extensions/operational-mode.ts similarity index 100% rename from src/tui-client/.pi/extensions/operational-mode.ts rename to src/.pi/extensions/operational-mode.ts diff --git a/src/tui-client/.pi/extensions/prompting.ts b/src/.pi/extensions/prompting.ts similarity index 100% rename from src/tui-client/.pi/extensions/prompting.ts rename to src/.pi/extensions/prompting.ts diff --git a/src/tui-client/.pi/extensions/session-lifecycle.ts b/src/.pi/extensions/session-lifecycle.ts similarity index 100% rename from src/tui-client/.pi/extensions/session-lifecycle.ts rename to src/.pi/extensions/session-lifecycle.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/index.ts b/src/.pi/extensions/structured-exchange/index.ts similarity index 97% rename from src/tui-client/.pi/extensions/structured-exchange/index.ts rename to src/.pi/extensions/structured-exchange/index.ts index a9421c4f4..cbe5fb034 100644 --- a/src/tui-client/.pi/extensions/structured-exchange/index.ts +++ b/src/.pi/extensions/structured-exchange/index.ts @@ -18,7 +18,7 @@ import { REQUEST_CHOICE_TOOL, requestChoiceTool } from "./request-choice.js" import { REQUEST_CHOICES_TOOL, requestChoicesTool } from "./request-choices.js" import { REQUEST_REVIEW_TOOL, requestReviewTool } from "./request-review.js" -export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from "../../../../structured-exchange.js" +export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from "../../../structured-exchange.js" export { buildStructuredExchangeEditorPrefill, diff --git a/src/tui-client/.pi/extensions/structured-exchange/present-candidates.ts b/src/.pi/extensions/structured-exchange/present-candidates.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/present-candidates.ts rename to src/.pi/extensions/structured-exchange/present-candidates.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/present-options.ts b/src/.pi/extensions/structured-exchange/present-options.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/present-options.ts rename to src/.pi/extensions/structured-exchange/present-options.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/present-question.ts b/src/.pi/extensions/structured-exchange/present-question.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/present-question.ts rename to src/.pi/extensions/structured-exchange/present-question.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/present-review-set.ts b/src/.pi/extensions/structured-exchange/present-review-set.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/present-review-set.ts rename to src/.pi/extensions/structured-exchange/present-review-set.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/request-answer.ts b/src/.pi/extensions/structured-exchange/request-answer.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/request-answer.ts rename to src/.pi/extensions/structured-exchange/request-answer.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/request-choice.ts b/src/.pi/extensions/structured-exchange/request-choice.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/request-choice.ts rename to src/.pi/extensions/structured-exchange/request-choice.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/request-choices.ts b/src/.pi/extensions/structured-exchange/request-choices.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/request-choices.ts rename to src/.pi/extensions/structured-exchange/request-choices.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/request-review.ts b/src/.pi/extensions/structured-exchange/request-review.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/request-review.ts rename to src/.pi/extensions/structured-exchange/request-review.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/README.md b/src/.pi/extensions/structured-exchange/schemas/README.md similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/README.md rename to src/.pi/extensions/structured-exchange/schemas/README.md diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/capture.ts b/src/.pi/extensions/structured-exchange/schemas/capture.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/capture.ts rename to src/.pi/extensions/structured-exchange/schemas/capture.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/index.ts b/src/.pi/extensions/structured-exchange/schemas/index.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/index.ts rename to src/.pi/extensions/structured-exchange/schemas/index.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/present.ts b/src/.pi/extensions/structured-exchange/schemas/present.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/present.ts rename to src/.pi/extensions/structured-exchange/schemas/present.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/request.ts b/src/.pi/extensions/structured-exchange/schemas/request.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/request.ts rename to src/.pi/extensions/structured-exchange/schemas/request.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/schemas/shared.ts b/src/.pi/extensions/structured-exchange/schemas/shared.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/schemas/shared.ts rename to src/.pi/extensions/structured-exchange/schemas/shared.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/shared/editor-fallback.ts b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts similarity index 99% rename from src/tui-client/.pi/extensions/structured-exchange/shared/editor-fallback.ts rename to src/.pi/extensions/structured-exchange/shared/editor-fallback.ts index cc252267b..751cfa6f1 100644 --- a/src/tui-client/.pi/extensions/structured-exchange/shared/editor-fallback.ts +++ b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts @@ -3,7 +3,7 @@ import { type StructuredExchangeAnswer, type StructuredExchangeMode, type StructuredExchangeOption, -} from "../../../../../structured-exchange.js" +} from "../../../../structured-exchange.js" import { isRecord } from "./model.js" export interface StructuredExchangeEditorPrefillParams { diff --git a/src/tui-client/.pi/extensions/structured-exchange/shared/markdown.ts b/src/.pi/extensions/structured-exchange/shared/markdown.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/shared/markdown.ts rename to src/.pi/extensions/structured-exchange/shared/markdown.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/shared/model.ts b/src/.pi/extensions/structured-exchange/shared/model.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/shared/model.ts rename to src/.pi/extensions/structured-exchange/shared/model.ts diff --git a/src/tui-client/.pi/extensions/structured-exchange/shared/recovery.ts b/src/.pi/extensions/structured-exchange/shared/recovery.ts similarity index 100% rename from src/tui-client/.pi/extensions/structured-exchange/shared/recovery.ts rename to src/.pi/extensions/structured-exchange/shared/recovery.ts diff --git a/src/tui-client/.pi/extensions/subagents/config.json b/src/.pi/extensions/subagents/config.json similarity index 100% rename from src/tui-client/.pi/extensions/subagents/config.json rename to src/.pi/extensions/subagents/config.json diff --git a/src/tui-client/.pi/extensions/workspace-dialog.ts b/src/.pi/extensions/workspace-dialog.ts similarity index 98% rename from src/tui-client/.pi/extensions/workspace-dialog.ts rename to src/.pi/extensions/workspace-dialog.ts index 97f86d7f6..dfda09c8a 100644 --- a/src/tui-client/.pi/extensions/workspace-dialog.ts +++ b/src/.pi/extensions/workspace-dialog.ts @@ -7,7 +7,7 @@ import { type WorkspaceSessionReadyState, type SpecSessionActivationCoordinator, type SpecSessionActivationDecision, -} from "../../../workspace-session-coordinator.js" +} from "../../workspace-session-coordinator.js" import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, diff --git a/src/tui-client/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts similarity index 64% rename from src/tui-client/pi-extension-shell.ts rename to src/.pi/pi-extension-shell.ts index b30bfd77c..1fc76da98 100644 --- a/src/tui-client/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -3,30 +3,30 @@ import { type ExtensionFactory, } from "@earendil-works/pi-coding-agent" -import { registerBrunchAlternatives } from "./.pi/extensions/alternatives.js" -import { registerBrunchChrome } from "./.pi/extensions/chrome.js" -import { registerBrunchBranchPolicyHandlers } from "./.pi/extensions/command-policy.js" -import { type GraphMentionSource } from "./.pi/extensions/mention-autocomplete.js" +import { registerBrunchAlternatives } from "./extensions/alternatives.js" +import { registerBrunchChrome } from "./extensions/chrome.js" +import { registerBrunchBranchPolicyHandlers } from "./extensions/command-policy.js" +import { type GraphMentionSource } from "./extensions/mention-autocomplete.js" import { FIXTURE_GRAPH_MENTION_SOURCE, registerBrunchMentionAutocomplete, -} from "./.pi/extensions/mention-autocomplete.js" -import { registerBrunchOperationalModePolicy } from "./.pi/extensions/operational-mode.js" -import { registerBrunchPrompting } from "./.pi/extensions/prompting.js" -import { registerBrunchSessionBoundary } from "./.pi/extensions/session-lifecycle.js" -import { registerStructuredExchange } from "./.pi/extensions/structured-exchange/index.js" -import { type BrunchChromeState } from "./.pi/extensions/chrome.js" -import { type BrunchSessionBoundaryHandler } from "./.pi/extensions/session-lifecycle.js" -import { type BrunchSpecSessionPickerOptions } from "./.pi/extensions/workspace-dialog.js" -import { registerBrunchWorkspaceDialog } from "./.pi/extensions/workspace-dialog.js" +} from "./extensions/mention-autocomplete.js" +import { registerBrunchOperationalModePolicy } from "./extensions/operational-mode.js" +import { registerBrunchPrompting } from "./extensions/prompting.js" +import { registerBrunchSessionBoundary } from "./extensions/session-lifecycle.js" +import { registerStructuredExchange } from "./extensions/structured-exchange/index.js" +import { type BrunchChromeState } from "./extensions/chrome.js" +import { type BrunchSessionBoundaryHandler } from "./extensions/session-lifecycle.js" +import { type BrunchSpecSessionPickerOptions } from "./extensions/workspace-dialog.js" +import { registerBrunchWorkspaceDialog } from "./extensions/workspace-dialog.js" -export { registerBrunchAlternatives } from "./.pi/extensions/alternatives.js" -export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from "./.pi/extensions/command-policy.js" +export { registerBrunchAlternatives } from "./extensions/alternatives.js" +export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from "./extensions/command-policy.js" export { registerBrunchMentionAutocomplete, type GraphMentionCandidate, type GraphMentionSource, -} from "./.pi/extensions/mention-autocomplete.js" +} from "./extensions/mention-autocomplete.js" export { BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, DEFAULT_BRUNCH_AGENT_STATE, @@ -45,8 +45,8 @@ export { type OperationalModeDefinition, type OperationalModeId, type ResolvedBrunchAgentState, -} from "./.pi/extensions/operational-mode.js" -export { registerBrunchPrompting } from "./.pi/extensions/prompting.js" +} from "./extensions/operational-mode.js" +export { registerBrunchPrompting } from "./extensions/prompting.js" export { chromeStateForWorkspace, projectBrunchChromeFooterLines, @@ -58,13 +58,13 @@ export { type BrunchChromeState, type BrunchChromeUi, type BrunchChromeWorkerStatus, -} from "./.pi/extensions/chrome.js" +} from "./extensions/chrome.js" export { bindBrunchSessionBoundary, registerBrunchSessionBoundary, registerBrunchSessionBoundaryRefreshHandlers, type BrunchSessionBoundaryHandler, -} from "./.pi/extensions/session-lifecycle.js" +} from "./extensions/session-lifecycle.js" export { BRUNCH_WORKSPACE_COMMAND, BRUNCH_WORKSPACE_SHORTCUT, @@ -72,7 +72,7 @@ export { runBrunchWorkspaceAction, runBrunchWorkspaceCommand, type BrunchSpecSessionPickerOptions, -} from "./.pi/extensions/workspace-dialog.js" +} from "./extensions/workspace-dialog.js" export interface BrunchPiExtensionShellOptions extends BrunchSpecSessionPickerOptions { diff --git a/src/tui-client/.pi/settings.json b/src/.pi/settings.json similarity index 100% rename from src/tui-client/.pi/settings.json rename to src/.pi/settings.json diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index ae660c1f9..0667acad2 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -31,7 +31,7 @@ import { registerBrunchOperationalModePolicy, runBrunchWorkspaceCommand, runBrunchWorkspaceAction, -} from "./tui-client/pi-extension-shell.js" +} from "./.pi/pi-extension-shell.js" import { createWorkspaceSessionCoordinator, verifyWorkspaceSessionStores, diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index 21f3e21eb..9bc53351e 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -20,8 +20,8 @@ import { import { chromeStateForWorkspace, createBrunchPiExtensionShell, -} from "./tui-client/pi-extension-shell.js" -import { runWorkspaceDialogPreflight } from "./tui-client/.pi/components/workspace-dialog.js" +} from "./.pi/pi-extension-shell.js" +import { runWorkspaceDialogPreflight } from "./.pi/components/workspace-dialog.js" import { applyBrunchOfflineDefault, brunchResourceLoaderOptions, @@ -47,8 +47,8 @@ export { type BrunchChromeStage, type BrunchChromeState, type BrunchChromeWorkerStatus, -} from "./tui-client/pi-extension-shell.js" -export { runWorkspaceDialogPreflight } from "./tui-client/.pi/components/workspace-dialog.js" +} from "./.pi/pi-extension-shell.js" +export { runWorkspaceDialogPreflight } from "./.pi/components/workspace-dialog.js" export type BrunchTuiCoordinator = SpecSessionActivationCoordinator & WorkspaceSessionBoundaryCoordinator diff --git a/src/elicitation-exchange.ts b/src/elicitation-exchange.ts index 0dea852b9..7c6443b30 100644 --- a/src/elicitation-exchange.ts +++ b/src/elicitation-exchange.ts @@ -16,11 +16,11 @@ import { isTerminalStructuredExchangeResultDetails } from "./structured-exchange import { isStructuredExchangePresentDetails, isStructuredExchangeRequestDetails, -} from "./tui-client/.pi/extensions/structured-exchange/shared/recovery.js" +} from "./.pi/extensions/structured-exchange/shared/recovery.js" import type { StructuredExchangePresentDetails, StructuredExchangeRequestDetails, -} from "./tui-client/.pi/extensions/structured-exchange/shared/model.js" +} from "./.pi/extensions/structured-exchange/shared/model.js" const PROMPT_SIDE_CUSTOM_TYPES = new Set([ "brunch.elicitation_prompt", diff --git a/src/probes/structured-exchange-ordering-proof.ts b/src/probes/structured-exchange-ordering-proof.ts index 941a135cd..99b5c3236 100644 --- a/src/probes/structured-exchange-ordering-proof.ts +++ b/src/probes/structured-exchange-ordering-proof.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url" import type { StructuredExchangePresentDetails, StructuredExchangeRequestDetails, -} from "../tui-client/.pi/extensions/structured-exchange/index.js" +} from "../.pi/extensions/structured-exchange/index.js" interface OrderingScenario { mission: string @@ -167,9 +167,7 @@ export async function runStructuredExchangeOrderingProof( async function writeOrderingExtension(cwd: string): Promise { const extensionPath = join(cwd, "structured-exchange-ordering-extension.ts") - const adapterPath = resolve( - "src/tui-client/.pi/extensions/structured-exchange/index.ts", - ) + const adapterPath = resolve("src/.pi/extensions/structured-exchange/index.ts") const content = ` import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { diff --git a/src/probes/structured-exchange-rpc-proof.ts b/src/probes/structured-exchange-rpc-proof.ts index e495d7ac6..965a2e26b 100644 --- a/src/probes/structured-exchange-rpc-proof.ts +++ b/src/probes/structured-exchange-rpc-proof.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os" import { join, resolve } from "node:path" import { fileURLToPath } from "node:url" -import type { StructuredExchangeToolResultDetails } from "../tui-client/.pi/extensions/structured-exchange/index.js" +import type { StructuredExchangeToolResultDetails } from "../.pi/extensions/structured-exchange/index.js" interface ProbeMetadata { name: string @@ -142,9 +142,7 @@ export async function runStructuredExchangeRpcProof( async function writeProofExtension(cwd: string): Promise { const extensionPath = join(cwd, "structured-exchange-rpc-proof-extension.ts") - const adapterPath = resolve( - "src/tui-client/.pi/extensions/structured-exchange/index.ts", - ) + const adapterPath = resolve("src/.pi/extensions/structured-exchange/index.ts") const content = ` import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index 1cb376cc4..ba3ef6dee 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -13,8 +13,8 @@ import { projectLinearElicitationExchangeProjection, projectLinearTranscriptDisplayProjection, } from "../elicitation-exchange.js" -import { isStructuredExchangePresentDetails } from "../tui-client/.pi/extensions/structured-exchange/shared/recovery.js" -import type { StructuredExchangePresentDetails } from "../tui-client/.pi/extensions/structured-exchange/shared/model.js" +import { isStructuredExchangePresentDetails } from "../.pi/extensions/structured-exchange/shared/recovery.js" +import type { StructuredExchangePresentDetails } from "../.pi/extensions/structured-exchange/shared/model.js" import { createJsonRpcFailure, createJsonRpcSuccess, diff --git a/src/session-transcript.ts b/src/session-transcript.ts index 2ac2832d8..15d9391c7 100644 --- a/src/session-transcript.ts +++ b/src/session-transcript.ts @@ -14,7 +14,7 @@ import type { import { isStructuredExchangePresentDetails, isStructuredExchangeRequestDetails, -} from "./tui-client/.pi/extensions/structured-exchange/shared/recovery.js" +} from "./.pi/extensions/structured-exchange/shared/recovery.js" type TranscriptEntry = FileEntry From 208acc0570af7f4afa4b935693f17b5b53fefe1c Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 1 Jun 2026 19:07:45 +0200 Subject: [PATCH 03/34] wire commit_graph and read_graph Pi tools through CommandExecutor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M5 slice 1: A14-L proof-of-life — the agent can now call commit_graph to atomically create nodes+edges and read_graph to inspect graph state. - command-adapter.ts: Pi tool params → CommandExecutor input translation and CommandResult → tool result text formatting - graph/index.ts registrar: registerBrunchGraph with TypeBox v1.x parameter schemas using StringEnum for enums - Pre-bound GraphSnapshotReaders so extensions never import db/ (I26-L) - Re-export enum const arrays from graph/index.ts for I26-L compliance - Extension shell wires graph tools when options.graph is provided - 9 integration tests: translate, format, end-to-end commit+read, I34-L all-or-nothing rollback, not-found handling Amp-Thread-ID: https://ampcode.com/threads/T-019e840d-172b-736c-839e-e441f7308220 Co-authored-by: Amp --- src/.pi/__tests__/graph-tools.test.ts | 235 ++++++++++++++++++++ src/.pi/extensions/graph/command-adapter.ts | 215 ++++++++++++++++++ src/.pi/extensions/graph/index.ts | 206 +++++++++++++++++ src/.pi/pi-extension-shell.ts | 14 ++ src/graph/index.ts | 12 + 5 files changed, 682 insertions(+) create mode 100644 src/.pi/__tests__/graph-tools.test.ts create mode 100644 src/.pi/extensions/graph/command-adapter.ts create mode 100644 src/.pi/extensions/graph/index.ts diff --git a/src/.pi/__tests__/graph-tools.test.ts b/src/.pi/__tests__/graph-tools.test.ts new file mode 100644 index 000000000..f22c6439e --- /dev/null +++ b/src/.pi/__tests__/graph-tools.test.ts @@ -0,0 +1,235 @@ +/** + * Graph tool integration tests. + * + * Tests the commit_graph and read_graph tools end-to-end through + * the command adapter → CommandExecutor → snapshot reader chain. + * + * SPEC: D4-L, D20-L, D52-L, D53-L, I26-L, I34-L, A14-L + */ + +import { describe, beforeEach, it, expect } from "vitest" + +import { CommandExecutor } from "../../graph/command-executor.js" +import { getGraphOverview, getNodeNeighborhood } from "../../graph/snapshot.js" +import { createDb } from "../../db/connection.js" +import type { BrunchDb } from "../../db/connection.js" +import { + translateCommitGraph, + formatCommitGraphResult, + formatGraphOverview, + formatNeighborhoodResult, +} from "../extensions/graph/command-adapter.js" +import type { GraphSnapshotReaders } from "../extensions/graph/index.js" + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function createTestDb(): BrunchDb { + return createDb(":memory:") +} + +function createSnapshots(db: BrunchDb): GraphSnapshotReaders { + return { + getGraphOverview: () => getGraphOverview(db), + getNodeNeighborhood: (nodeId, options) => + getNodeNeighborhood(db, nodeId, options), + } +} + +// --------------------------------------------------------------------------- +// command-adapter: translateCommitGraph +// --------------------------------------------------------------------------- + +describe("translateCommitGraph", () => { + it("translates flat tool params into CommitGraphInput", () => { + const input = translateCommitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "Test goal" }, + { + ref: "n2", + plane: "intent", + kind: "requirement", + title: "Test req", + body: "details", + }, + ], + edges: [ + { category: "dependency", source: "n2", target: "n1" }, + { + category: "support", + source: { existing: 42 }, + target: "n1", + stance: "for", + }, + ], + }) + + expect(input.nodes).toHaveLength(2) + expect(input.nodes[0]!.ref).toBe("n1") + expect(input.edges).toHaveLength(2) + expect(input.edges[0]!.source).toBe("n2") + expect(input.edges[1]!.source).toEqual({ existing: 42 }) + }) +}) + +// --------------------------------------------------------------------------- +// command-adapter: formatCommitGraphResult +// --------------------------------------------------------------------------- + +describe("formatCommitGraphResult", () => { + it("formats success with node refs and edge ids", () => { + const text = formatCommitGraphResult({ + status: "success", + lsn: 5, + nodes: { n1: 1, n2: 2 }, + edges: [10, 11], + }) + + expect(text).toContain("Graph committed successfully") + expect(text).toContain("LSN 5") + expect(text).toContain("n1 → #1") + expect(text).toContain("#10") + }) + + it("formats structural_illegal with diagnostics", () => { + const text = formatCommitGraphResult({ + status: "structural_illegal", + diagnostics: [ + { field: "nodes[0].kind", message: '"invalid" is not a valid kind' }, + { field: "edges[0].stance", message: "stance required for proof" }, + ], + }) + + expect(text).toContain("STRUCTURAL_ILLEGAL") + expect(text).toContain("nodes[0].kind") + expect(text).toContain("edges[0].stance") + }) +}) + +// --------------------------------------------------------------------------- +// command-adapter: formatGraphOverview +// --------------------------------------------------------------------------- + +describe("formatGraphOverview", () => { + it("reports empty graph", () => { + const text = formatGraphOverview({ + nodes: [], + edges: [], + nodeCount: 0, + edgeCount: 0, + lsn: 0, + }) + + expect(text).toContain("empty") + }) +}) + +// --------------------------------------------------------------------------- +// End-to-end: commit then read +// --------------------------------------------------------------------------- + +describe("graph tools end-to-end", () => { + let db: BrunchDb + let executor: CommandExecutor + let snapshots: GraphSnapshotReaders + + beforeEach(() => { + db = createTestDb() + executor = new CommandExecutor(db) + snapshots = createSnapshots(db) + }) + + it("commit_graph creates nodes and edges readable by read_graph", () => { + // Commit a small graph + const input = translateCommitGraph({ + nodes: [ + { ref: "n1", plane: "intent", kind: "goal", title: "Build auth" }, + { + ref: "n2", + plane: "intent", + kind: "requirement", + title: "JWT tokens", + }, + ], + edges: [{ category: "dependency", source: "n2", target: "n1" }], + }) + const result = executor.commitGraph(input) + expect(result.status).toBe("success") + + // Read the graph + const overview = snapshots.getGraphOverview() + const text = formatGraphOverview(overview) + + expect(overview.nodeCount).toBe(2) + expect(overview.edgeCount).toBe(1) + expect(text).toContain("Build auth") + expect(text).toContain("JWT tokens") + expect(text).toContain("dependency") + }) + + it("commit_graph returns diagnostics on invalid batch", () => { + const input = translateCommitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "not_a_kind", title: "Bad" }], + edges: [], + }) + const result = executor.commitGraph(input) + expect(result.status).toBe("structural_illegal") + + if (result.status === "structural_illegal") { + const text = formatCommitGraphResult(result) + expect(text).toContain("STRUCTURAL_ILLEGAL") + expect(text).toContain("not_a_kind") + } + }) + + it("commit_graph with edge validation failure rolls back nodes (I34-L)", () => { + const input = translateCommitGraph({ + nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "A goal" }], + edges: [ + // stance required for proof but missing + { category: "proof", source: "n1", target: "n1" }, + ], + }) + const result = executor.commitGraph(input) + expect(result.status).toBe("structural_illegal") + + // Node should NOT have been created (all-or-nothing) + const overview = snapshots.getGraphOverview() + expect(overview.nodeCount).toBe(0) + }) + + it("read_graph neighborhood returns node details", () => { + // Create a node first + const input = translateCommitGraph({ + nodes: [ + { + ref: "n1", + plane: "intent", + kind: "goal", + title: "Main goal", + body: "A detailed goal", + }, + ], + edges: [], + }) + const commitResult = executor.commitGraph(input) + expect(commitResult.status).toBe("success") + + if (commitResult.status === "success") { + const nodeId = commitResult.nodes["n1"]! + const result = snapshots.getNodeNeighborhood(nodeId) + const text = formatNeighborhoodResult(result) + + expect(text).toContain("Main goal") + expect(text).toContain("A detailed goal") + } + }) + + it("read_graph neighborhood for missing node returns not_found", () => { + const result = snapshots.getNodeNeighborhood(999) + const text = formatNeighborhoodResult(result) + + expect(text).toContain("not found") + }) +}) diff --git a/src/.pi/extensions/graph/command-adapter.ts b/src/.pi/extensions/graph/command-adapter.ts new file mode 100644 index 000000000..69cc611b5 --- /dev/null +++ b/src/.pi/extensions/graph/command-adapter.ts @@ -0,0 +1,215 @@ +/** + * Pi tool → CommandExecutor translation seam. + * + * SPEC: D4-L, D20-L, D52-L, D53-L + * + * This module translates Pi tool parameters (flat JSON from LLM tool calls) + * into CommandExecutor input types and formats CommandExecutor results into + * Pi tool result content. It does NOT import from db/ — all graph access + * routes through CommandExecutor and snapshot readers. + */ + +import type { + BatchEdgeInput, + BatchEdgeRef, + BatchNodeInput, + CommitGraphInput, + CommitGraphResult, + CommitGraphSuccess, + Diagnostic, + StructuralIllegal, +} from "../../../graph/command-executor.js" +import type { + GraphOverview, + NeighborhoodResult, +} from "../../../graph/snapshot.js" + +// --------------------------------------------------------------------------- +// commit-graph: Pi params → CommitGraphInput +// --------------------------------------------------------------------------- + +/** Shape of a node as received from the LLM tool call. */ +export interface ToolCommitNode { + readonly ref: string + readonly plane: string + readonly kind: string + readonly title: string + readonly body?: string + readonly basis?: string + readonly source?: string + readonly detail?: unknown +} + +/** Shape of an edge as received from the LLM tool call. */ +export interface ToolCommitEdge { + readonly category: string + readonly source: string | { readonly existing: number } + readonly target: string | { readonly existing: number } + readonly stance?: string + readonly rationale?: string +} + +/** Shape of the commit_graph tool params from the LLM. */ +export interface ToolCommitGraphParams { + readonly nodes: readonly ToolCommitNode[] + readonly edges: readonly ToolCommitEdge[] +} + +/** + * Translate Pi tool params into a CommandExecutor CommitGraphInput. + * + * The translation is thin — structural validation happens in the CommandExecutor. + */ +export function translateCommitGraph( + params: ToolCommitGraphParams, +): CommitGraphInput { + const nodes: BatchNodeInput[] = params.nodes.map((n) => ({ + ref: n.ref, + plane: n.plane as BatchNodeInput["plane"], + kind: n.kind, + title: n.title, + body: n.body, + basis: n.basis as BatchNodeInput["basis"], + source: n.source, + detail: n.detail, + })) + + const edges: BatchEdgeInput[] = params.edges.map((e) => ({ + category: e.category, + source: resolveEdgeRef(e.source), + target: resolveEdgeRef(e.target), + stance: e.stance, + rationale: e.rationale, + })) + + return { nodes, edges } +} + +function resolveEdgeRef( + ref: string | { readonly existing: number }, +): BatchEdgeRef { + if (typeof ref === "string") return ref + return { existing: ref.existing } +} + +// --------------------------------------------------------------------------- +// Result formatting +// --------------------------------------------------------------------------- + +/** + * Format a CommitGraphResult as Pi tool result text content. + * + * On success: human-readable summary with created ids. + * On structural_illegal: diagnostic listing for agent self-correction. + */ +export function formatCommitGraphResult(result: CommitGraphResult): string { + if (result.status === "success") { + return formatCommitSuccess(result) + } + return formatDiagnostics(result) +} + +function formatCommitSuccess(result: CommitGraphSuccess): string { + const nodeEntries = Object.entries(result.nodes) + const lines: string[] = [`Graph committed successfully (LSN ${result.lsn}).`] + + if (nodeEntries.length > 0) { + lines.push( + `Nodes created: ${nodeEntries.map(([ref, id]) => `${ref} → #${id}`).join(", ")}`, + ) + } + if (result.edges.length > 0) { + lines.push( + `Edges created: ${result.edges.map((id) => `#${id}`).join(", ")}`, + ) + } + + return lines.join("\n") +} + +function formatDiagnostics(result: StructuralIllegal): string { + const lines: string[] = [ + "STRUCTURAL_ILLEGAL: The batch was rejected. Fix the following issues and retry:", + "", + ] + + for (const d of result.diagnostics) { + lines.push(`- ${d.field}: ${d.message}`) + } + + return lines.join("\n") +} + +// --------------------------------------------------------------------------- +// read-graph: overview formatting +// --------------------------------------------------------------------------- + +/** + * Format a GraphOverview as readable text for the agent. + */ +export function formatGraphOverview(overview: GraphOverview): string { + if (overview.nodeCount === 0) { + return "The graph is empty (no nodes or edges)." + } + + const lines: string[] = [ + `Graph overview (LSN ${overview.lsn}): ${overview.nodeCount} node(s), ${overview.edgeCount} edge(s).`, + "", + ] + + for (const node of overview.nodes) { + const detail = node.detail ? ` [has detail]` : "" + lines.push( + `- [#${node.id}] ${node.plane}/${node.kind}: "${node.title}"${detail}`, + ) + } + + if (overview.edges.length > 0) { + lines.push("") + for (const edge of overview.edges) { + const stance = edge.stance ? ` (${edge.stance})` : "" + lines.push( + `- Edge #${edge.id}: #${edge.sourceId} —[${edge.category}${stance}]→ #${edge.targetId}`, + ) + } + } + + return lines.join("\n") +} + +/** + * Format a NeighborhoodResult as readable text for the agent. + */ +export function formatNeighborhoodResult(result: NeighborhoodResult): string { + if (result.status === "not_found") { + return "Node not found." + } + + const { anchor, neighbors, edges } = result + const lines: string[] = [ + `Neighborhood of [#${anchor.id}] ${anchor.plane}/${anchor.kind}: "${anchor.title}"`, + ] + + if (anchor.body) { + lines.push(`Body: ${anchor.body}`) + } + + if (neighbors.length > 0) { + lines.push("", "Neighbors:") + for (const n of neighbors) { + lines.push(` - [#${n.id}] ${n.plane}/${n.kind}: "${n.title}"`) + } + } + + if (edges.length > 0) { + lines.push("", "Edges:") + for (const e of edges) { + const stance = e.stance ? ` (${e.stance})` : "" + lines.push( + ` - #${e.id}: #${e.sourceId} —[${e.category}${stance}]→ #${e.targetId}`, + ) + } + } + + return lines.join("\n") +} diff --git a/src/.pi/extensions/graph/index.ts b/src/.pi/extensions/graph/index.ts new file mode 100644 index 000000000..564c2a88a --- /dev/null +++ b/src/.pi/extensions/graph/index.ts @@ -0,0 +1,206 @@ +/** + * Graph tool registrar — wires commit_graph and read_graph as Pi tools. + * + * SPEC: D4-L (one mutation surface), D20-L (CommandExecutor boundary), + * D52-L (graph/ imports db/; .pi/extensions/ imports graph/), + * D53-L (commitGraph atomic batch), I26-L (no db/ imports here) + * + * This module does NOT import from db/. All graph access routes through + * the CommandExecutor and snapshot reader functions passed as explicit + * dependencies from the extension shell. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" +import { StringEnum } from "@earendil-works/pi-ai" +import { Type } from "typebox" + +import type { CommandExecutor } from "../../../graph/command-executor.js" +import type { + GraphOverview, + NeighborhoodResult, +} from "../../../graph/snapshot.js" +import { + INTENT_KINDS, + ORACLE_KINDS, + DESIGN_KINDS, + PLAN_KINDS, + EDGE_CATEGORIES, + EDGE_STANCES, + NODE_BASES, +} from "../../../graph/index.js" +import { + translateCommitGraph, + formatCommitGraphResult, + formatGraphOverview, + formatNeighborhoodResult, +} from "./command-adapter.js" + +// --------------------------------------------------------------------------- +// Dependencies injected by the extension shell +// --------------------------------------------------------------------------- + +/** Pre-bound snapshot readers so the extension never touches db/ directly. */ +export interface GraphSnapshotReaders { + readonly getGraphOverview: () => GraphOverview + readonly getNodeNeighborhood: ( + nodeId: number, + options?: { hops?: number }, + ) => NeighborhoodResult +} + +export interface BrunchGraphDeps { + readonly commandExecutor: CommandExecutor + readonly snapshots: GraphSnapshotReaders +} + +// --------------------------------------------------------------------------- +// Tool parameter schemas (TypeBox v1.x / Pi's typebox) +// --------------------------------------------------------------------------- + +const ALL_KINDS = [ + ...INTENT_KINDS, + ...ORACLE_KINDS, + ...DESIGN_KINDS, + ...PLAN_KINDS, +] as const + +const CommitNodeSchema = Type.Object({ + ref: Type.String({ + description: "Temporary batch reference id (e.g. 'n1', 'n2')", + }), + plane: StringEnum(["intent", "oracle", "design", "plan"] as const), + kind: StringEnum([...ALL_KINDS]), + title: Type.String({ description: "Node title — must be non-empty" }), + body: Type.Optional(Type.String({ description: "Extended description" })), + basis: Type.Optional(StringEnum([...NODE_BASES])), + source: Type.Optional( + Type.String({ + description: "Epistemic attribution (e.g. 'stakeholder', 'derived')", + }), + ), + detail: Type.Optional( + Type.Unknown({ + description: + "Per-kind detail: decision requires {chosen_option, rejected, rationale}; term requires {definition, aliases?}", + }), + ), +}) + +const EdgeRefSchema = Type.Union([ + Type.String({ description: "Intra-batch ref (e.g. 'n1')" }), + Type.Object({ + existing: Type.Number({ description: "Id of an existing node" }), + }), +]) + +const CommitEdgeSchema = Type.Object({ + category: StringEnum([...EDGE_CATEGORIES]), + source: EdgeRefSchema, + target: EdgeRefSchema, + stance: Type.Optional(StringEnum([...EDGE_STANCES])), + rationale: Type.Optional(Type.String()), +}) + +const CommitGraphParams = Type.Object({ + nodes: Type.Array(CommitNodeSchema, { + description: "Nodes to create in this batch", + }), + edges: Type.Array(CommitEdgeSchema, { + description: "Edges to create, referencing batch refs or existing node ids", + }), +}) + +const ReadGraphParams = Type.Object({ + mode: StringEnum(["overview", "neighborhood"] as const), + node_id: Type.Optional( + Type.Number({ + description: "Required for neighborhood mode — the anchor node id", + }), + ), + hops: Type.Optional( + Type.Number({ description: "Neighborhood traversal depth (default: 1)" }), + ), +}) + +// --------------------------------------------------------------------------- +// Registrar +// --------------------------------------------------------------------------- + +export function registerBrunchGraph( + pi: ExtensionAPI, + deps: BrunchGraphDeps, +): void { + const { commandExecutor, snapshots } = deps + + // ── commit_graph ──────────────────────────────────────────────────── + pi.registerTool({ + name: "commit_graph", + label: "Commit Graph", + description: + "Atomically create a batch of nodes and edges in the specification graph. " + + "Each node gets a temporary batch ref (e.g. 'n1') that edges can reference. " + + "Edges can also reference existing nodes by id via {existing: }. " + + "The entire batch succeeds or fails atomically.", + promptSnippet: + "Atomically commit nodes and edges to the specification graph", + promptGuidelines: [ + "Use commit_graph to persist specification elements (goals, requirements, decisions, etc.) after the user has accepted the concept.", + "Each node must have a unique batch `ref` string. Edges reference nodes by their `ref` or by `{existing: }` for nodes already in the graph.", + "If commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics, fix the issues, and retry. Do not show intermediate failures to the user.", + "The `stance` field is required on `proof` and `support` edges, and invalid on all other categories.", + "Node kinds `decision` and `term` require a `detail` object; all other kinds must omit `detail`.", + ], + parameters: CommitGraphParams, + + async execute(_toolCallId, params) { + const input = translateCommitGraph(params) + const result = commandExecutor.commitGraph(input) + const text = formatCommitGraphResult(result) + + return { + content: [{ type: "text" as const, text }], + details: result, + } + }, + }) + + // ── read_graph ────────────────────────────────────────────────────── + pi.registerTool({ + name: "read_graph", + label: "Read Graph", + description: + "Read the current specification graph. " + + "Use mode 'overview' for a full graph summary, or " + + "mode 'neighborhood' with a node_id to see a specific node and its neighbors.", + promptSnippet: + "Read the specification graph (overview or node neighborhood)", + promptGuidelines: [ + "Use read_graph with mode 'overview' to see all nodes and edges before committing new graph elements.", + "Use read_graph with mode 'neighborhood' and a node_id to inspect a specific node and its connections.", + ], + parameters: ReadGraphParams, + + async execute(_toolCallId, params) { + let text: string + + if (params.mode === "overview") { + text = formatGraphOverview(snapshots.getGraphOverview()) + } else { + if (params.node_id == null) { + throw new Error("node_id is required for neighborhood mode") + } + text = formatNeighborhoodResult( + snapshots.getNodeNeighborhood( + params.node_id, + params.hops != null ? { hops: params.hops } : undefined, + ), + ) + } + + return { + content: [{ type: "text" as const, text }], + details: {}, + } + }, + }) +} diff --git a/src/.pi/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts index 1fc76da98..cc091bfa2 100644 --- a/src/.pi/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -19,6 +19,10 @@ import { type BrunchChromeState } from "./extensions/chrome.js" import { type BrunchSessionBoundaryHandler } from "./extensions/session-lifecycle.js" import { type BrunchSpecSessionPickerOptions } from "./extensions/workspace-dialog.js" import { registerBrunchWorkspaceDialog } from "./extensions/workspace-dialog.js" +import { + registerBrunchGraph, + type BrunchGraphDeps, +} from "./extensions/graph/index.js" export { registerBrunchAlternatives } from "./extensions/alternatives.js" export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from "./extensions/command-policy.js" @@ -74,9 +78,16 @@ export { type BrunchSpecSessionPickerOptions, } from "./extensions/workspace-dialog.js" +export { + registerBrunchGraph, + type BrunchGraphDeps, + type GraphSnapshotReaders, +} from "./extensions/graph/index.js" + export interface BrunchPiExtensionShellOptions extends BrunchSpecSessionPickerOptions { graphMentionSource?: GraphMentionSource + graph?: BrunchGraphDeps } type BrunchProductExtensionRegistrar = ( @@ -101,6 +112,9 @@ export function createBrunchPiExtensionShell( registerBrunchAlternatives, registerStructuredExchange, (api) => registerBrunchWorkspaceDialog(api, options), + ...(options.graph + ? [(api: ExtensionAPI) => registerBrunchGraph(api, options.graph!)] + : []), ] for (const registerExtension of extensions) { diff --git a/src/graph/index.ts b/src/graph/index.ts index 6273eeecc..47a999718 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -10,6 +10,18 @@ export type { EdgeId, Lsn, NodeId } from "./atoms.js" +// Re-export shared enum const arrays so extensions can build +// tool parameter schemas without importing db/ directly (I26-L). +export { + EDGE_CATEGORIES, + EDGE_STANCES, + INTENT_KINDS, + ORACLE_KINDS, + DESIGN_KINDS, + PLAN_KINDS, + NODE_BASES, +} from "../db/schema.js" + export type { EdgeBasis, EdgeCategory, From da240fd8aaee6206720ddc6867728ce2df933d4d Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 1 Jun 2026 19:10:38 +0200 Subject: [PATCH 04/34] =?UTF-8?q?reconcile=20SPEC.md=20and=20PLAN.md:=20pa?= =?UTF-8?q?th=20refs=20=E2=86=92=20src/.pi/,=20M5=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all src/tui-client/.pi/ path references to src/.pi/ in both canonical planning docs. Mark agent-graph-integration as in-progress with execution pointer noting the topology move and graph tool wiring. Amp-Thread-ID: https://ampcode.com/threads/T-019e840d-172b-736c-839e-e441f7308220 Co-authored-by: Amp --- memory/PLAN.md | 14 +++++++------- memory/SPEC.md | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index e461cb8cd..900f1464c 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -85,7 +85,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Objective:** Broader prep envelope before `graph-data-plane` (M4) CRUD work begins. Two strands under one frontier/branch: **(a) Pi harness sealing** — Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`; operational mode / role preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries and reconstructed at turn boundaries. **(b) Graph-model lock-and-materialize** — lock the conceptual edge and node contracts in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), stub the type/policy surface under `src/graph/`, and prove the A20-L persistence seam now: the Drizzle line, row-schema derivation path, monotonic counter allocation, change-log shape, and Pi `registerTool` round-trip so M4 CRUD lands on settled persistence/schema-derivation foundations. - **Why now / unlocks:** FE-744 proved multiple Pi extension seams and exposed the exact weak point: ambient resource discovery and settings policy had to be sealed before future `elicit` vs `execute` work could depend on product-owned prompt/tool posture. Once sealing is in code, the cheapest remaining moves before M4 CRUD are conceptual rather than persistence-mechanical: lock the graph model so review-set drafts, reconciliation needs, snapshot bucketing, and CommandExecutor result discriminants share a stable contract; retire the speculative named-relation catalogue and brainstormed edge taxonomy; settle the persistence/schema-derivation toolchain (A20-L). De-risks M5/M6/M7 before graph tools, capture/reviewer jobs, and authority gating depend on the embedded harness or the graph data plane. - **Acceptance:** - - **Sealing strand:** A `BrunchPiProfile` (or equivalent module boundary) owns settings policy, resource-loader options, extension factories, keybinding/command policy, tool policy, and prompt policy; tests prove ambient context files/extensions/skills/prompt templates/themes do not load while explicit Brunch-owned extension-discovered resources can load intentionally through Pi `resources_discover`; settings that affect product behavior are overridden/sealed or documented as a Pi upstream seam; runtime extension factories load explicitly from `src/tui-client/pi-extension-shell.ts` / `src/tui-client/.pi/extensions/*` and reusable TUI components under `src/tui-client/.pi/components/*`, with no root project-local Pi discovery path as product runtime. Full selected-state transcript entries under `brunch.agent_runtime_state` can be appended by Brunch helpers and replayed to reconstruct active operational mode, role preset/runtime bundle, strategy, and lens; turn prep composes prompt packs from base Brunch prompt + operational mode + role preset + strategy + lens + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules; `elicit` suppresses execute/dangerous tools such as raw `bash`/`write` unless explicitly allowed by the active bundle. + - **Sealing strand:** A `BrunchPiProfile` (or equivalent module boundary) owns settings policy, resource-loader options, extension factories, keybinding/command policy, tool policy, and prompt policy; tests prove ambient context files/extensions/skills/prompt templates/themes do not load while explicit Brunch-owned extension-discovered resources can load intentionally through Pi `resources_discover`; settings that affect product behavior are overridden/sealed or documented as a Pi upstream seam; runtime extension factories load explicitly from `src/.pi/pi-extension-shell.ts` / `src/.pi/extensions/*` and reusable TUI components under `src/.pi/components/*`, with no root project-local Pi discovery path as product runtime. Full selected-state transcript entries under `brunch.agent_runtime_state` can be appended by Brunch helpers and replayed to reconstruct active operational mode, role preset/runtime bundle, strategy, and lens; turn prep composes prompt packs from base Brunch prompt + operational mode + role preset + strategy + lens + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules; `elicit` suppresses execute/dangerous tools such as raw `bash`/`write` unless explicitly allowed by the active bundle. - **Graph-model strand:** Phase 1 edge contract locked in `docs/design/GRAPH_MODEL.md` and materialized as type/policy stubs under `src/graph/` (✓ landed at commit `100585a1`). Phase 2 node contract locked (✓ landed at commits `b6ecec1e`–`8346e23d`): 11 intent kinds in 3 derived categories (basic/structural/reasoning), common `GraphNode` shape with `detail` JSON column for `decision`/`term`, `provenance` retired from both nodes and edges, `framing_as` retired, `source` as free-form epistemic attribution, modality-of-claim + source-question agent rubric, context promotion heuristic. Materialized as `src/graph/schema/nodes.ts` and reflected in SPEC via D54-L/D55-L/D56-L/D57-L plus I36-L/I37-L. A20-L spike produces a verdict on the persistence seam over one representative intent-plane slice: Drizzle line choice, row-schema derivation path (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent), monotonic counter allocation, change-log writes, and Pi `registerTool` round-trip. - **Verification:** Inner — profile/runtimestate unit tests, prompt-composition snapshot tests, tool-policy contract tests, edge/node schema unit tests, category-policy unit tests. Middle — ambient `.pi/` fixture/audit tests proving disabled discovery and sealed settings; explicit Brunch resource-injection test proving extension factories may inject Brunch-owned skills/prompts despite ambient `noSkills`/`noPromptTemplates`; JSONL reload/projection tests for runtime init/switch entries; before-agent-start/tool-call policy tests for `elicit`; persistence spike tests covering one representative table, one insert/select cycle, monotonic counter allocation, change-log append shape, and one Pi `registerTool` parameter binding; I26-L grep-based architectural test wired alongside the first Drizzle import so the single-schema-vocabulary boundary stays enforced. Outer — manual TUI/RPC smoke that active role/lens/strategy changes are inspectable in transcript and reflected in prompt/tool posture rather than hidden UI state. - **Cross-cutting obligations:** Do not expose Pi's generic extension/skill/prompt/theme configuration to Brunch users; do not make Pi skills the primary authority for core operational prompts; keep raw Pi RPC behind Brunch adapters; keep runtime state linear-transcript-backed and compatible with compaction/session-boundary lifecycle hooks (`session_start`, `resources_discover`, `before_agent_start`, `context`, `tool_call`, `session_before_switch`, `session_before_compact`, `session_shutdown`). Graph-model lock work must trace to `docs/design/GRAPH_MODEL.md`; node lock must preserve the closed-edge-set invariants (immutable accepted-edge identity, `dependency`-only auto-cascade, separate `ReconciliationNeed` substrate with `{kind:'edge'|'node_pair'}` target). The persistence spike is throwaway scope — one representative slice, no broad imports until the verdict lands; if the current beta line blocks (migrations, SQLite fidelity, schema-derivation bugs, or ergonomics), pick the simpler working adapter/line and continue without re-opening M4 design. @@ -118,7 +118,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Linear:** [FE-785](https://linear.app/hash/issue/FE-785) - **Branch:** `ln/fe-785-agent-graph-integration` (stacked on `ln/fe-776-graph-layer-prep-profile`) - **Kind:** structural -- **Status:** not-started +- **Status:** in-progress - **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations — including `commitGraph` batch mutations for the `propose-graph` direct-commit path (D53-L, D26-L) — elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances for `project-graph`, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. **The primary A14-L proof runs here:** test whether the LLM can produce structurally-legal `commitGraph` batches against the real CommandExecutor with bounded retry. - **Acceptance:** @@ -153,11 +153,11 @@ The POC should maximize assumption falsification rather than merely implement mi └── if observer/auditor queues land → backstops only, not primary capture freshness path ``` - **Implementation layout:** Per D52-L, graph domain logic lives in `src/graph/` (CommandExecutor, readers, policy, validators, snapshot functions) and persistence in `src/db/`. The Pi-facing adapter goes in one explicit product extension directory, `src/.pi/extensions/graph/`, imported by `src/.pi/pi-extension-shell.ts` as `registerBrunchGraph` rather than discovered dynamically. Use `graph/index.ts` only to register Pi tools, message renderers, and event hooks. Keep tool definitions in `graph/tools/*` (`read-graph`, `commit-graph`, `create-intent-node`, `update-intent-node`, `link-intent-nodes`, `accept-review-set`), boundary schemas in `graph/schemas/*` (`tool-inputs`, `tool-results`, `custom-entries`), transcript helpers in `graph/transcript/*` (`entries`, `projections`, `renderers`), synchronous capture in `graph/capture/post-exchange-capture.ts`, reviewer target enforcement in `graph/reviewer/reviewer-writes.ts`, and the Pi→CommandExecutor translation seam in `graph/command-adapter.ts`. The extension directory must not own SQLite/Drizzle persistence, LSN allocation, structural graph validators, reviewer-agent implementation, or capture model/prompt machinery; those are Brunch product/core modules passed into the extension through explicit shell options such as `{ graph: { commandExecutor, capturePostExchange?, reviewerWrites? } }`. Agent prompts, strategy definitions (including `propose-graph` and `project-graph`), lens definitions, and context builders live in `src/agents/` per D52-L. -- **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, proposal-entry schema validation (`brunch.review_set_proposal` must declare `epistemic_status` and support/grounding coverage), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposals become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/tui-client/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — kernel-card-output coverage assertions begin landing through targeted probe runs; first batch-proposal probe (e.g. `propose-scenarios-with-tradeoffs`) replays through review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. +- **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, proposal-entry schema validation (`brunch.review_set_proposal` must declare `epistemic_status` and support/grounding coverage), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposals become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — kernel-card-output coverage assertions begin landing through targeted probe runs; first batch-proposal probe (e.g. `propose-scenarios-with-tradeoffs`) replays through review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L - **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** before implementation, run oracle/scoping pressure on A14-L and A22-L: define the smallest replay/probe set that can reveal over-capture, missed obvious facts, dry-run-invalid review-set drafts, whether plain-prose `preface` is sufficient for low-confidence implications, and how any `capture_*` ANALYSIS entries will be compared against committed graph mutations. +- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. Next: validate the tools work with a real LLM (A14-L outer-loop proof), then scope capture/reviewer slices. ### subagents-for-proposal-diversity @@ -166,7 +166,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Kind:** optional enhancement - **Status:** deferred (lands when `agent-and-graph-integration` is far enough along to benefit; never a blocker for M0–M9) - **Objective:** Register a single `subagent` Pi tool per D44-L so the main agent can (a) fan out blocking data-gathering calls (scout / researcher / graph-reader) in parallel to ground proposals, then (b) fan out parallel `proposer` invocations to generate diverse candidate variants — the subagent realization of `ln-design`'s "design it twice" pattern and `ln-oracles`'s parallel-fan-out — and finally compose `brunch.review_set_proposal` entries from those variants via the D31-L meta-rubric. Subagent results return as tool content; no `CommandExecutor` access; no Brunch RPC access; isolated `pi --no-session --no-skills --no-extensions` subprocesses inheriting Brunch Pi Profile sealing. -- **Acceptance:** `subagent` tool registered with `{ agent, task }` and `{ tasks: [] }` parameters; starter agents scout/researcher/graph-reader/proposer land as markdown files with TypeBox-validated frontmatter under `src/tui-client/.pi/extensions/subagents/agents/`; proposer is system-prompt-only (no tools) and produces exactly one variant per invocation; argv shape per spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent tool allowlist / model / system-prompt path; concurrency cap honored from [src/tui-client/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/subagents/config.json); subagents have no inherited conversation context so the task string must carry everything; result text returns as tool result content with no transcript side-effects; at least one batch-proposal probe exercises a `tasks: []` parallel `proposer` fan-out (≥ 2 variants) feeding a single `brunch.review_set_proposal` composed by the main agent via the D31-L meta-rubric. +- **Acceptance:** `subagent` tool registered with `{ agent, task }` and `{ tasks: [] }` parameters; starter agents scout/researcher/graph-reader/proposer land as markdown files with TypeBox-validated frontmatter under `src/.pi/extensions/subagents/agents/`; proposer is system-prompt-only (no tools) and produces exactly one variant per invocation; argv shape per spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent tool allowlist / model / system-prompt path; concurrency cap honored from [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json); subagents have no inherited conversation context so the task string must carry everything; result text returns as tool result content with no transcript side-effects; at least one batch-proposal probe exercises a `tasks: []` parallel `proposer` fan-out (≥ 2 variants) feeding a single `brunch.review_set_proposal` composed by the main agent via the D31-L meta-rubric. - **Verification:** Inner — `subagent` tool argv-shape tests; TypeBox schema validation of agent frontmatter and `config.json`; per-starter-agent tool-allowlist conformance (proposer must have an empty tool set). Middle — isolation audit (no ambient `.pi/` resources reachable; parent `CommandExecutor` / Brunch RPC handlers absent from subprocess environment); subprocess streaming / abort propagation tests; parallel-fan-out independence test (two `proposer` invocations with distinct framings produce structurally distinct outputs). Outer — proposal-generation probe invokes scout/researcher/graph-reader to ground, then parallel `proposer` variants, and surfaces the composed review-set proposal with grounding-bundle coverage and `epistemic_status` consistent with the gathered evidence; meta-rubric application visible in the comparison rendering. - **Cross-cutting obligations:** Preserve the single-authority mutation rule (`CommandExecutor` only — subagents never bypass it) and the sealed Pi Profile (no ambient `.pi/` leakage through the subprocess boundary). Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. Worker-style write-capable subagents are deferred until an execute operational mode exists. - **Traceability:** R20 / D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L, D44-L / I2-L, I11-L, I24-L, I29-L @@ -218,7 +218,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Status:** not-started - **Objective:** Compaction preserves graph, coherence, and continuity anchors per D43-L; interest sets can widen beyond direct reads when needed; conflict signaling remains intelligible at long horizons. - **Acceptance:** Long-horizon adversarial brief (50+ turns) replays through compaction with `lastSeenLsn`, interest set, and session binding preserved; spec/session changes across compaction boundaries do not desync; the auto-compaction extension renders the configured preserved-anchor set byte-stable so active spec, in-flight side-task / deferred-auditor-job / reviewer-job bookkeeping, latest `brunch.agent_runtime_state`, latest `brunch.establishment_offer`, latest `brunch.lens_switch`, unresolved staleness hints, and active review-set leaves remain intelligible after compaction; ambient-affordance chrome continues to render the current offer; auto-compaction failure falls through to Pi default compaction rather than dropping anchors silently. -- **Verification:** Inner gate plus continuity-metadata unit tests and TypeBox schema validation of [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json). Middle — compaction round-trip/property tests for `lastSeenLsn`, interest set, session binding, graph/coherence anchors, active side-task/deferred-auditor/reviewer bookkeeping, latest-establishment-offer/lens/runtime-state reconstruction; deterministic anchor-rendering tests (same branch + same config → same header bytes); fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer — long-horizon probe passes, including continuity checks for side-task, interest-set, runtime-state, and establishment-offer state when present. +- **Verification:** Inner gate plus continuity-metadata unit tests and TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json). Middle — compaction round-trip/property tests for `lastSeenLsn`, interest set, session binding, graph/coherence anchors, active side-task/deferred-auditor/reviewer bookkeeping, latest-establishment-offer/lens/runtime-state reconstruction; deterministic anchor-rendering tests (same branch + same config → same header bytes); fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer — long-horizon probe passes, including continuity checks for side-task, interest-set, runtime-state, and establishment-offer state when present. - **Cross-cutting obligations:** Preserve the coherence anchors, session binding, session continuity metadata, and side-task/deferred-auditor/spec state that earlier milestones attached to the shared transcript/event substrate; preserve lens state only if a lens subsystem has landed by then. The auto-compaction extension is the canonical owner of `session_before_compact`; product code paths that touch compaction must compose with it rather than register a parallel hook. - **Traceability:** R15 / D6-L, D15-L, D43-L / I12-L, I28-L - **Design docs:** [prd.md §Continuity, Divergence, and Coherence](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md) @@ -297,7 +297,7 @@ Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jso nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) - agent-graph-integration [not-started] (M5) + agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] authority-model [not-started] (M6) turn-boundary-reconciliation [not-started] (M7) diff --git a/memory/SPEC.md b/memory/SPEC.md index 1dc43b9da..da873b9c2 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -125,11 +125,11 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D1-L — Depend on `pi-coding-agent`, not only `pi-agent-core`.** The POC reuses the coding-agent service bundle, TUI/print adapters, RPC machinery, session logging, and tool plumbing. Dropping down to `pi-agent-core` is a fallback if Brunch proves too different. Depends on: A1-L. Supersedes: —. - **D2-L — Brunch is an opinionated product, not a pi platform shell.** The POC hardcodes its toolset, system prompt, and policy doctrine; scopes state to `.brunch/`; and hides pi's generic extension surface from end users. Depends on: A1-L. Supersedes: —. -- **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture routes TUI launch policy through `src/brunch-pi-profile.ts`, creates an in-memory Brunch-owned `SettingsManager` policy instead of reading ambient global/project `.pi/settings.json`, disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell, and defaults Brunch-launched Pi to offline mode; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/tui-client/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/tui-client/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/tui-client/.pi/extensions/*`, and reusable Pi TUI components live under `src/tui-client/.pi/components/*`, so they can also be iterated by launching Pi from `src/tui-client` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; TUI-client extension/component tests live under `src/tui-client/.pi/__tests__/`. The profile boundary now owns the audited behavior-shaping settings list in code (`BRUNCH_SETTINGS_POLICY` / `BRUNCH_SETTINGS_AUDITED_GETTERS`), with hostile ambient settings and reload-resilience tests covering shell path/prefix, npm command, ambient resources, skill commands, double-escape behavior, compaction/retry, image/terminal/UI, transport/theme/changelog, and telemetry settings. Remaining profile work is runtime-state/prompt/tool posture, not ambient settings file leakage. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/tui-client/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. -- **D40-L — Runtime posture is a transcript-backed Brunch state machine, not hidden extension memory.** Brunch distinguishes operational modes (`elicit`, future `execute`) from agent roles (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor roles) and from strategies/lenses. The active top-level role is selected through a role preset/runtime bundle that derives model, thinking level, prompt packs, allowed strategies/lenses, and tool policy rather than storing each knob independently. Brunch runtime helpers append full selected-state product custom entries under `brunch.agent_runtime_state` with `reason: "init" | "switch"`; turn preparation projects the latest valid linear transcript snapshot into prompt and tool posture. Brunch product prompt packs are private code-composed assets under `src/tui-client/.pi/context/prompt-packs/*`, composed only through `src/tui-client/.pi/context/compose-brunch-prompt.ts` and appended by the explicit `src/tui-client/.pi/extensions/prompting.ts` product extension; they are not Pi prompt templates, skills, context-file discovery, or user-invoked slash-command resources. The current `elicit` tool policy is a denylist over side-effecting tools (`bash`, `edit`, `write`) plus user-shell interception, so new safe Brunch extension tools are not hidden by a stale allowlist. The Pi extension module that owns tool policy is `src/tui-client/.pi/extensions/operational-mode.ts`, while product prompting is owned separately by `src/tui-client/.pi/extensions/prompting.ts`; neither should duplicate the other's control-plane responsibility. Depends on: D17-L, D23-L, D25-L, D39-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority for agent behavior, modeling read-only posture as a volatile allowlist of every safe tool, or exposing Brunch prompt packs through Pi resource discovery. -- **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/tui-client/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. +- **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture routes TUI launch policy through `src/brunch-pi-profile.ts`, creates an in-memory Brunch-owned `SettingsManager` policy instead of reading ambient global/project `.pi/settings.json`, disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell, and defaults Brunch-launched Pi to offline mode; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/.pi/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/.pi/extensions/*`, and reusable Pi TUI components live under `src/.pi/components/*`, so they can also be iterated by launching Pi from `src/` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; extension/component tests live under `src/.pi/__tests__/`. The profile boundary now owns the audited behavior-shaping settings list in code (`BRUNCH_SETTINGS_POLICY` / `BRUNCH_SETTINGS_AUDITED_GETTERS`), with hostile ambient settings and reload-resilience tests covering shell path/prefix, npm command, ambient resources, skill commands, double-escape behavior, compaction/retry, image/terminal/UI, transport/theme/changelog, and telemetry settings. Remaining profile work is runtime-state/prompt/tool posture, not ambient settings file leakage. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. +- **D40-L — Runtime posture is a transcript-backed Brunch state machine, not hidden extension memory.** Brunch distinguishes operational modes (`elicit`, future `execute`) from agent roles (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor roles) and from strategies/lenses. The active top-level role is selected through a role preset/runtime bundle that derives model, thinking level, prompt packs, allowed strategies/lenses, and tool policy rather than storing each knob independently. Brunch runtime helpers append full selected-state product custom entries under `brunch.agent_runtime_state` with `reason: "init" | "switch"`; turn preparation projects the latest valid linear transcript snapshot into prompt and tool posture. Brunch product prompt packs are private code-composed assets under `src/.pi/context/prompt-packs/*`, composed only through `src/.pi/context/compose-brunch-prompt.ts` and appended by the explicit `src/.pi/extensions/prompting.ts` product extension; they are not Pi prompt templates, skills, context-file discovery, or user-invoked slash-command resources. The current `elicit` tool policy is a denylist over side-effecting tools (`bash`, `edit`, `write`) plus user-shell interception, so new safe Brunch extension tools are not hidden by a stale allowlist. The Pi extension module that owns tool policy is `src/.pi/extensions/operational-mode.ts`, while product prompting is owned separately by `src/.pi/extensions/prompting.ts`; neither should duplicate the other's control-plane responsibility. Depends on: D17-L, D23-L, D25-L, D39-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority for agent behavior, modeling read-only posture as a volatile allowlist of every safe tool, or exposing Brunch prompt packs through Pi resource discovery. +- **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. - **D35-L — Dynamic TUI chrome is a Brunch projection wrapper over Pi UI primitives.** Downstream TUI affordances should call a Brunch-owned renderer (`renderBrunchChrome` or its successor) with one activated product-state snapshot rather than scattering raw `ctx.ui.setHeader`, `setFooter`, `setWidget`, title, or working-indicator calls. The wrapper is stateless projection over canonical workspace/session/graph facts, including the real activated session id, while its TUI footer compositor may read Pi footer telemetry (`getGitBranch`, foreign `getExtensionStatuses`) at render time. Brunch chrome does not publish a `brunch.chrome` status key; `ctx.ui.setStatus(key, text)` remains a lateral contribution channel for other extensions and future dynamic Brunch state. RPC clients should rely only on surfaces Pi actually emits for the wrapper (currently diagnostic widget/title, plus any future explicit status adapter) because header/footer/working-indicator are TUI-only in current Pi RPC mode. Session display names are likewise product projections over Pi session metadata: Brunch may append Pi `session_info` entries, but generated names must characterize the selected spec/session transcript rather than replace spec identity or graph truth. Depends on: D2-L, D21-L, D34-L, A18-L. Supersedes: treating Pi UI methods as direct downstream affordance APIs, rendering placeholder session state such as `unbound` after a session is activated, or consuming the status-key namespace for chrome's own static summary. -- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by axis (`modes/`, `strategies/`, `lenses/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, and the state definitions that drive mode/role/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/tui-client/.pi/context/`. +- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by axis (`modes/`, `strategies/`, `lenses/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, and the state definitions that drive mode/role/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. #### Data model & vocabulary @@ -210,7 +210,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D28-L — Regenerated review-set proposals are appended as successor entries in the linear Pi JSONL session; projection helpers filter to the accepted set for context economy.** When the user requests changes, the agent appends a successor proposal entry that references its predecessor via `supersedes`; prior proposals are *not* deleted from JSONL but remain visible as raw transcript history. This stays within Brunch's linear transcript policy — no Pi branching is created. Pi JSONL is treated as a "capture everything" store for replay and audit. Projection helpers used to drive the agent (context injection, summarization) walk the `supersedes` chain and surface only the latest (or ultimately accepted) proposal — the agent does not re-process every superseded proposal as live context. The reviewer likewise sees only the accepted set, not the regeneration history. Depends on: D6-L, D12-L, D17-L, D24-L, D27-L. Supersedes: any "in-place edit" or "fork-on-regenerate" mental model. - **D29-L — Reviewer is an async advisory role with narrow write authority.** After a batch acceptance closes, Brunch may enqueue a reviewer job keyed by session id plus the batch-acceptance entry id; the job survives process restart and analyzes the accepted batch plus its graph neighborhood for coherence, completeness, and gaps. **Reviewer writes only `reconciliation_need` records via the `CommandExecutor`**; it never writes graph entities, edges, change-log entries directly, or any other record class. Findings reach the user through next-turn delivery as advisory items on the reconciliation-need surface — the batch acceptance remains the user's atomic commitment and the reviewer cannot amend it. (Suggestion-shaped findings may later route to candidate-artefacts when that substrate exists; the POC routes everything to reconciliation needs.) Depends on: A16-L, D4-L, D8-L, D15-L, D17-L, D18-L, D20-L, D27-L. Supersedes: any "reviewer may quietly amend the graph" mental model. - **D24-L — Brunch POC enforces a linear transcript policy over Pi JSONL.** Pi's session tree is a substrate capability, not a Brunch product surface. Until branch-aware continuity/coherence is explicitly designed, Brunch-controlled interactive/runtime flows block `/tree`, `/fork`, and `/clone` through the thinnest available Pi hooks; transcript readers reject non-linear session files instead of flattening, adapting, migrating, or selecting a branch. This is intentional fail-fast pre-release posture: avoid compatibility debt with Pi internals or earlier Brunch revisions, and keep wrapper/adapter layers minimal. Depends on: D6-L, D11-L, D13-L. Supersedes: treating active-branch projection as Brunch product semantics. -- **D43-L — Auto-compaction is a Brunch-owned `session_before_compact` extension whose anchor preservation contract is an externalized JSON config.** Brunch always owns this hook because Pi's default summary cannot know about Brunch's transcript-native continuity entries. The extension composes a deterministic preserved-anchor header (rendered byte-stable from the configured anchor set against the pre-compaction branch) with an LLM-generated narrative summary, then returns Pi's standard `{ compaction: { summary, firstKeptEntryId, tokensBefore } }` shape. The summarization model is resolved through the active runtime bundle (D40-L) — typically a cheap/fast "compaction" preset (e.g. Gemini Flash, Haiku) — with fallback to Pi's default compaction on missing auth, empty output, or unexpected error so compaction is never gated on extension success. The anchor contract lives in [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json) as `{ kind, select, rationale }` rules (`select ∈ first | latest | active-leaves | all-unresolved`) so it can be reviewed and updated without SPEC churn; the file is validated through a D41-L-compatible runtime schema when the module lands. Brunch-initiated proactive compaction (post-`acceptReviewSet`, on shutdown) and reactor-side compaction triggers are deferred. Session-scoped continuity metadata (`lastSeenLsn`, interest sets) is *projected* from the change log plus the preserved anchor entries — it is not itself an anchor and never appears in the JSON. Depends on: D6-L, D15-L, D17-L, D40-L, D41-L. Supersedes: relying on Pi's default `session_before_compact` summary to keep Brunch-specific continuity intelligible. +- **D43-L — Auto-compaction is a Brunch-owned `session_before_compact` extension whose anchor preservation contract is an externalized JSON config.** Brunch always owns this hook because Pi's default summary cannot know about Brunch's transcript-native continuity entries. The extension composes a deterministic preserved-anchor header (rendered byte-stable from the configured anchor set against the pre-compaction branch) with an LLM-generated narrative summary, then returns Pi's standard `{ compaction: { summary, firstKeptEntryId, tokensBefore } }` shape. The summarization model is resolved through the active runtime bundle (D40-L) — typically a cheap/fast "compaction" preset (e.g. Gemini Flash, Haiku) — with fallback to Pi's default compaction on missing auth, empty output, or unexpected error so compaction is never gated on extension success. The anchor contract lives in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) as `{ kind, select, rationale }` rules (`select ∈ first | latest | active-leaves | all-unresolved`) so it can be reviewed and updated without SPEC churn; the file is validated through a D41-L-compatible runtime schema when the module lands. Brunch-initiated proactive compaction (post-`acceptReviewSet`, on shutdown) and reactor-side compaction triggers are deferred. Session-scoped continuity metadata (`lastSeenLsn`, interest sets) is *projected* from the change log plus the preserved anchor entries — it is not itself an anchor and never appears in the JSON. Depends on: D6-L, D15-L, D17-L, D40-L, D41-L. Supersedes: relying on Pi's default `session_before_compact` summary to keep Brunch-specific continuity intelligible. #### Schema & validation @@ -222,7 +222,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D21-L — Workspace session coordination is the spec/session boot seam.** Brunch owns a narrow `WorkspaceSessionCoordinator` for boot, spec inventory, spec/session selection, selected-session reopening, and `/new` session creation. It is the only product module allowed to create or open Pi sessions for Brunch user flows and the only module allowed to write `brunch.session_binding`; callers inspect workspace inventory and activate a product decision rather than mutating a session's bound spec directly. The coordinator hides `SessionManager.create/open/continueRecent(cwd, ".brunch/sessions/")`, internal session-start binding for pi-created replacement sessions, `.brunch/state.json` current-spec and current-session-file acceleration, binding validation, and chrome-state derivation. Because pi defers appending session JSONL until an assistant message exists, the coordinator flushes Brunch's binding when it is created, refreshes it at `before_agent_start`, and performs the final pre-assistant flush from Brunch's internal assistant `message_start` hook after pi has persisted the user message but before assistant persistence; each flush reloads the session file so pi's next assistant append does not duplicate the already-written prefix. Depends on: D6-L, D11-L. Supersedes: the loose `SpecRegistry` + caller-orchestrated session-binding mental model, and treating `.brunch/state.json` as an implicit instruction to resume without user-visible Brunch flow. - **D22-L — TUI boot is Brunch-owned before Pi interactive runtime begins.** Brunch's TUI mode may use `@earendil-works/pi-tui` directly for a pre-Pi startup gate that selects or creates the active spec/session before `InteractiveMode.run()`. After activation, persistent chrome is mounted by an internal Brunch extension through Pi's public UI seams. Brunch does not fork pi, monkeypatch `InteractiveMode`, or expose generic pi extension configuration to users for product boot/chrome. Depends on: D2-L, D21-L, D36-L. Supersedes: private-header/monkeypatch approaches for M0 chrome and raw readline-only spec selection as the durable TUI product flow. - **D12-L — Elicitation-first interaction, transcript-native structured prompts.** Brunch treats system/assistant prompts and user responses as Pi transcript truth. Structured action/choice/freeform surfaces may be represented by Brunch custom entries when needed, but there is no DB-owned prompt/response entity; at idle, the session waits on a system/assistant-originated elicitation prompt. Depends on: D6-L, D11-L. Supersedes: —. -- **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction may be represented through the thinnest Pi-supported transcript seam for its shape. The current preferred seam for Brunch structured exchanges is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/tui-client/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details; runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof now drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Brunch custom messages/entries remain valid for establishment offers, review-set proposals, annotations, and future product-native displays, but they are not mandatory for every structured exchange. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. +- **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction may be represented through the thinnest Pi-supported transcript seam for its shape. The current preferred seam for Brunch structured exchanges is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details; runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof now drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Brunch custom messages/entries remain valid for establishment offers, review-set proposals, annotations, and future product-native displays, but they are not mandatory for every structured exchange. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. - **D38-L — JSON-over-editor is the Pi-RPC compatibility seam for complex extension UI, not a second product API.** Pi RPC supports `ctx.ui.select`, `confirm`, `input`, and `editor`, but not `ctx.ui.custom()`. When a structured-exchange tool needs a complex shape (multi-select, review-style response, or a deferred multi-question/questionnaire shape) over raw Pi RPC, the tool may call `ctx.ui.editor()` with schema-tagged JSON prefill and validate the returned JSON before producing normal `toolResult.content` plus self-contained `toolResult.details`. A Brunch-aware adapter may render that JSON as a native product form and translate the user response back into Pi's documented `extension_ui_response`; public clients still speak Brunch RPC methods/events, not ad hoc raw Pi RPC extensions. Depends on: D5-L, D19-L, D33-L, D37-L. Supersedes: inventing unsupported Pi RPC command types for Brunch interactions or exposing raw editor JSON as the product UX. - **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text, linked structured response entries, and/or terminal structured-exchange toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. - **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. @@ -235,11 +235,11 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D46-L — Commitment posture pins projected claims through cohesive review sets.** Design and oracle lenses may create accepted graph material before commitment posture, but pinning is a separate projection step. In `pinning` posture, design-oriented commitments default first: Brunch projects requirement/invariant-like intent claims from the current intent/design/oracle graph plus support/provenance edges. Oracle-oriented commitments default second: Brunch projects criterion/check-obligation/example-like verification claims plus support/provenance edges to the pinned commitments and oracle material. Review sets are focus-primary rather than globally homogeneous: a design commitment set primarily pins requirement/invariant-like claims with support edges; an oracle commitment set primarily pins criteria/check/example-like claims with support edges. Approval accepts the cohesive batch as a whole through `acceptReviewSet`; request-changes regenerates a successor set; partial approval and accept-with-edits remain unrepresentable. Depends on: D27-L, D28-L, D45-L. Supersedes: per-item requirement/criterion confirmation and treating design/oracle commitment phases as first permission to discuss design/oracle topics. - **D47-L — Structured-exchange `preface` is the near-term carrier for non-committed elicitor interpretation.** The structured-exchange payload's plain prose `preface` summarizes working context before the next question: exploratory file-reading/tool-use findings, implied graph candidates, low-confidence edges, and the rationale for what is being asked next. Preface text is transcript truth and user-visible orientation, but it is not graph truth, not candidate-artefact schema, and not a hidden side store. High-confidence facts still commit through `CommandExecutor`; low-confidence implications stay in preface/question material until clarified, accepted, or escalated to reconciliation needs. Future `capture_*` analysis entries provide a separate post-exchange/review evidence surface for candidate semantic changes; they do not replace preface as next-question orientation and do not become graph truth. Structured candidate metadata is deferred until fixtures/projections prove plain prose is insufficient. Depends on: D12-L, D18-L, D37-L, D50-L. Supersedes: inventing a candidate-artefact substrate merely to carry ordinary next-question disambiguation material. - **D50-L — `capture_*` tools persist transcript-native ANALYSIS, not graph mutations.** Brunch may add a third structured-exchange tool family such as `capture_analysis` alongside `present_*` and `request_*`. A `capture_*` tool returns a normal persisted Pi `toolResult` with Brunch details and markdown content describing likely graph/node/edge changes, grouped into high-confidence candidates that could be committed later and low-confidence candidates that should drive clarification. `capture_*` output is transcript-visible evidence for Markdown/ASCII review and later graph-mutation cross-checking, but it is not graph truth and never bypasses the `CommandExecutor`. Product UI should hide capture analysis entirely if Pi exposes a supported hide seam; otherwise `renderResult` should be maximally collapsed/minimal while preserving full persisted `toolResult.content`/`details` for transcript renderers. The current schema layer deliberately defines only minimum capture details (`schema`, `v`, `exchange_id`, `tool_meta`) and rejects graph payloads; richer analysis payloads and shared component subparts (`Preface`, prompt body, option list, answer summary, capture analysis) require a later `ln-design` pass before implementation. Depends on: D12-L, D17-L, D18-L, D37-L, D41-L, D47-L. Supersedes: using ad hoc hidden custom entries, probe-only side files, or graph writes as the first carrier for pre-graph analysis. -- **D44-L — Subagents are main-agent-invoked, blocking Pi tool calls that gather data and propose variants for candidate-proposal generation.** Brunch may register a single `subagent` Pi tool whose parameters are `{ agent, task }` or `{ tasks: [] }` (parallel). Each invocation runs as an isolated `pi --mode json -p --no-session --no-skills --no-extensions` subprocess inheriting Brunch's sealed Pi Profile (D39-L); the subagent has no inherited conversation context so the task string must carry everything it needs. Agent definitions are declarative markdown files under `src/tui-client/.pi/extensions/subagents/agents/*.md` with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. Concurrency cap lives in an externalized [src/tui-client/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/subagents/config.json) (default 4) so it can be reviewed and updated without SPEC churn. The subagent's result text is returned directly to the main agent as tool result content; subagents do not append custom messages to the session log on their own behalf, do not invoke the `CommandExecutor`, and do not gain access to the parent's Brunch RPC handlers. POC starter agents split into two families: +- **D44-L — Subagents are main-agent-invoked, blocking Pi tool calls that gather data and propose variants for candidate-proposal generation.** Brunch may register a single `subagent` Pi tool whose parameters are `{ agent, task }` or `{ tasks: [] }` (parallel). Each invocation runs as an isolated `pi --mode json -p --no-session --no-skills --no-extensions` subprocess inheriting Brunch's sealed Pi Profile (D39-L); the subagent has no inherited conversation context so the task string must carry everything it needs. Agent definitions are declarative markdown files under `src/.pi/extensions/subagents/agents/*.md` with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. Concurrency cap lives in an externalized [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json) (default 4) so it can be reviewed and updated without SPEC churn. The subagent's result text is returned directly to the main agent as tool result content; subagents do not append custom messages to the session log on their own behalf, do not invoke the `CommandExecutor`, and do not gain access to the parent's Brunch RPC handlers. POC starter agents split into two families: - **Data gatherers** — read-only context fetchers whose output grounds proposals: **scout** (codebase recon: `read`, `grep`, `find`, `ls`), **researcher** (web research: `web_search`, `web_fetch`), and **graph-reader** (read-only Brunch graph projection tools). - **Variant proposer** — **proposer** (no tools): given a grounding bundle plus a batch-proposal lens frame, emits exactly one well-formed variant of a candidate proposal. The main agent achieves diversity by issuing parallel `tasks: []` invocations of `proposer` with intentionally distinct framings — the subagent realization of the "design it twice" pattern from `ln-design` and the parallel fan-out anticipated by `ln-oracles`. Each `proposer` invocation runs in its own isolated context so variants don't cross-contaminate; the main agent collects N outputs and composes the comparison via the D31-L meta-rubric (and/or project-specific axes) before writing a `brunch.review_set_proposal` entry through the elicitor flow. `proposer` is system-prompt-only by design: its grounding inputs come entirely through the task string the main agent assembles from preceding `scout` / `researcher` / `graph-reader` calls. This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. -- **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/state.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/state.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/tui-client/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/state.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. +- **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/state.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/state.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/state.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. ### Critical Invariants @@ -272,8 +272,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I24-L | A Brunch-launched Pi runtime does not load ambient user/project Pi context files, extensions, skills, prompt templates, themes, or behavior-shaping settings unless the Brunch Pi Profile explicitly allows them; Brunch-owned extension-discovered resources are identified as intentional product resources. | covered for TUI-launch profile boundary by contract tests: ambient resource flags and explicit extension factories are preserved; hostile ambient global/project settings are ignored by the in-memory Brunch settings policy before and after reload; audited Pi settings getters are tracked in `src/brunch-pi-profile.ts`. Subagent subprocess inheritance remains future coverage under I29-L. | D2-L, D39-L | | I25-L | The active operational mode, role preset/runtime bundle, strategy, and lens are reconstructable from linear transcript entries at turn start; tool gating follows the reconstructed operational mode so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted by the bundle. | covered for the current POC runtime bundle: runtime-state append/project/switch helpers write full `brunch.agent_runtime_state` snapshots to Pi custom entries, init is idempotent, switches carry previous state, malformed/impossible mode-role-strategy-lens entries are ignored or rejected, real SessionManager JSONL reload projects the latest valid state, and session-start/before-agent-start prompt/tool policy derives from the transcript-projected state without extension-local memory. | D17-L, D23-L, D40-L | | I27-L | Session-name generation is best-effort presentation metadata only: lifecycle hooks may append Pi `session_info` entries, but naming failures never block shutdown/session replacement and generated names never mutate spec identity, session binding, or graph truth. | planned (session-lifecycle naming tests with empty transcript/auth failure/success paths; picker projection tests read session names when present) | D6-L, D21-L, D35-L, D42-L | -| I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/tui-client/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | -| I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | +| I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | +| I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | | I29-L | Subagent subprocesses inherit Brunch Pi Profile sealing: every `subagent` tool invocation spawns `pi --mode json -p --no-session --no-skills --no-extensions` with an explicit per-agent tool allowlist and per-agent model; subagents never load ambient user/project `.pi/` skills, prompts, themes, extensions, context files, or behavior-shaping settings; subagents never gain direct access to the parent's `CommandExecutor`, Brunch RPC handlers, or graph persistence; subagent results return to the main agent only as tool result content (no side-effect transcript writes). | planned (subagent subprocess argv tests; isolation audit asserting absent ambient-resource leakage; tool-allowlist conformance test per starter agent) | D2-L, D39-L, D44-L; I2-L, I11-L, I24-L | | I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec grade/posture updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | planned (M5 capture fixtures comparing committed graph facts and preface-only interpretations against transcript evidence) | D18-L, D47-L; A22-L | | I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade/posture mutations route through `CommandExecutor` and carry provenance. | planned (M4 schema/command tests for spec row updates; M5 prompt/tool-policy tests for grade-gated availability) | D20-L, D45-L | @@ -306,7 +306,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/tui-client/.pi/context/` layout migrates to this structure. +- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/.pi/context/` layout migrates to this structure. - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` and `elicitation_posture` live on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade/posture diverge. Before these fields drive hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade/posture. - Core role/lens prompting should usually be product prompt packs rather than Pi skills. Pi skills remain available as Brunch-owned explicit resources when progressive disclosure is the right mechanism, but they are not the primary authority for operational mode/tool policy. @@ -420,10 +420,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Side task** | Main-agent-invoked, non-blocking work item tracked by the Brunch `SideTaskRegistry`. The main agent fires it and does not await a return value; the only path it influences the main agent is by appending a custom-message status update to the session log that arrives at the next-turn boundary via `prepareNextTurn`. Side-task writes route through the `CommandExecutor`. Distinct from Subagent (blocking) and Side chat (user-invoked). | | **Subagent** | Main-agent-invoked, **blocking** Pi tool call (`subagent`) that runs an isolated `pi` subprocess with a per-agent tool allowlist and per-agent model. Has no inherited conversation context, no `CommandExecutor` access, and no Brunch RPC access. Result text returns directly as tool result content. POC starter agents split into **data gatherers** (scout / researcher / graph-reader — read-only context fetchers that ground proposals) and a **variant proposer** (proposer — system-prompt-only; one variant per invocation, fan-out via parallel mode realizes the "design it twice" pattern). | | **Proposer subagent** | The system-prompt-only starter subagent that emits exactly one well-formed candidate-proposal variant per invocation given a grounding bundle plus a batch-proposal lens frame. Diversity arises from parallel `tasks: []` invocations with intentionally distinct framings; the main agent assembles outputs into a `brunch.review_set_proposal` via the D31-L meta-rubric. Realizes the "design it twice" / parallel-fan-out pattern from `ln-design` and `ln-oracles` skills in subagent form. | -| **Subagent registry** | The set of registered subagent definitions loaded from `src/tui-client/.pi/extensions/subagents/agents/*.md` at extension activation. Brunch-owned only for the POC; cross-extension agent registration is deferred. | +| **Subagent registry** | The set of registered subagent definitions loaded from `src/.pi/extensions/subagents/agents/*.md` at extension activation. Brunch-owned only for the POC; cross-extension agent registration is deferred. | | **Subagent agent definition** | A markdown file with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. The frontmatter is the registry contract; the body is the subagent's standing instructions. | -| **Auto-compaction extension** | The Brunch-owned `session_before_compact` extension (`src/tui-client/.pi/extensions/auto-compaction.ts`) that renders the preserved anchor set as a deterministic markdown header and prepends it to an LLM-generated narrative summary. Resolves its summarization model through the active runtime bundle; falls through to Pi default compaction on auth/empty-output/unexpected errors. | -| **Preserved anchor set** | The configured list of transcript entry kinds and selection rules that must survive compaction byte-stable. Canonical source is [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json); each rule is `{ kind, select, rationale }` where `select ∈ first | latest | active-leaves | all-unresolved`. Externalized so it can be reviewed and updated for correctness without SPEC churn. | +| **Auto-compaction extension** | The Brunch-owned `session_before_compact` extension (`src/.pi/extensions/auto-compaction.ts`) that renders the preserved anchor set as a deterministic markdown header and prepends it to an LLM-generated narrative summary. Resolves its summarization model through the active runtime bundle; falls through to Pi default compaction on auth/empty-output/unexpected errors. | +| **Preserved anchor set** | The configured list of transcript entry kinds and selection rules that must survive compaction byte-stable. Canonical source is [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json); each rule is `{ kind, select, rationale }` where `select ∈ first | latest | active-leaves | all-unresolved`. Externalized so it can be reviewed and updated for correctness without SPEC churn. | | **Anchor contract** | The data inside the preserved-anchor JSON config — distinct from the rendering policy (which lives in code) and the LLM summarization (which is bundle-resolved). | | **World update** | `worldUpdate` custom message synthesised in `prepareNextTurn` summarising relevant graph changes since the session's `lastSeenLsn`. | | **Mention ledger** | Per-session `(entity_id, snapshotted_lsn)` record driving discretionary staleness hints when an entity has changed since the agent last saw it. | @@ -566,8 +566,8 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I24-L | Sealed-profile tests: resource-loader options disable ambient discovery; inline Brunch extension resources still load intentionally through `resources_discover`; settings/keybinding/tool/prompt policy audit proves no ambient user/project `.pi/` setting changes Brunch product behavior. | | I25-L | Runtime-state tests: append init/switch custom entries, reload the linear transcript, reconstruct the active operational mode/role preset/strategy/lens, and verify before-agent-start/tool-call policy suppresses disallowed tools for `elicit`. | | I26-L | Structured-exchange schema tests prove the acknowledged Zod seam parses and exports JSON Schema; future M4 architectural tests should grep/import-audit schema libraries and Drizzle row-schema derivation boundaries. | -| I28-L | Inner — TypeBox schema validation of [src/tui-client/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/tui-client/.pi/extensions/auto-compaction-anchors.json) shape; deterministic anchor-rendering unit tests (same branch + same config → same header bytes). Middle (M9) — compaction round-trip property tests across all configured anchors and selection rules; fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer (M9) — long-horizon adversarial fixture confirms session binding, latest runtime state, latest establishment offer, in-flight side-task results, and unresolved staleness hints remain agent-intelligible post-compaction. | -| I29-L | Inner — argv-shape tests for the `subagent` tool prove every spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent `--tools`/`--extension`/`--models`/`--append-system-prompt` set; TypeBox schema validation of `src/tui-client/.pi/extensions/subagents/agents/*.md` frontmatter and `src/tui-client/.pi/extensions/subagents/config.json`. Middle — isolation audit (no ambient `.pi/` resources reachable inside the subprocess; tool-allowlist conformance per starter agent; parent `CommandExecutor`/Brunch RPC handlers absent from subprocess environment). Outer — probe-driven proposal-generation runs invoking scout/researcher/graph-reader confirm grounding inputs flow through subagent outputs into review-set proposals without bypassing primary authority. | +| I28-L | Inner — TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) shape; deterministic anchor-rendering unit tests (same branch + same config → same header bytes). Middle (M9) — compaction round-trip property tests across all configured anchors and selection rules; fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer (M9) — long-horizon adversarial fixture confirms session binding, latest runtime state, latest establishment offer, in-flight side-task results, and unresolved staleness hints remain agent-intelligible post-compaction. | +| I29-L | Inner — argv-shape tests for the `subagent` tool prove every spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent `--tools`/`--extension`/`--models`/`--append-system-prompt` set; TypeBox schema validation of `src/.pi/extensions/subagents/agents/*.md` frontmatter and `src/.pi/extensions/subagents/config.json`. Middle — isolation audit (no ambient `.pi/` resources reachable inside the subprocess; tool-allowlist conformance per starter agent; parent `CommandExecutor`/Brunch RPC handlers absent from subprocess environment). Outer — probe-driven proposal-generation runs invoking scout/researcher/graph-reader confirm grounding inputs flow through subagent outputs into review-set proposals without bypassing primary authority. | | I30-L | M5 post-exchange capture fixtures: compare committed graph facts, reconciliation needs, and preface-only interpretations against transcript evidence; known ambiguous exchanges must not silently become graph truth. | | I31-L | M4/M5 spec-row command tests for grade/posture updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement. | | I32-L | FE-744 public-RPC structured-exchange parity proof: `rpc.discover` contract tests, pending/respond lifecycle tests, deterministic permutation run over Brunch JSON-RPC only, no repeated deterministic prompts, and parity assertions over the resulting Pi JSONL, transcript display, and elicitation-exchange projections. | From b802fbc9cd7e3716b2c43358a9de345bcf24ef26 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Mon, 1 Jun 2026 19:18:56 +0200 Subject: [PATCH 05/34] stubs for future --- src/.pi/extensions/commands.ts | 14 ++++++++++++++ src/.pi/extensions/snapshot-cwd.ts | 0 src/.pi/extensions/snapshot-graph.ts | 0 src/.pi/extensions/snapshot-nodes.ts | 0 4 files changed, 14 insertions(+) create mode 100644 src/.pi/extensions/commands.ts create mode 100644 src/.pi/extensions/snapshot-cwd.ts create mode 100644 src/.pi/extensions/snapshot-graph.ts create mode 100644 src/.pi/extensions/snapshot-nodes.ts diff --git a/src/.pi/extensions/commands.ts b/src/.pi/extensions/commands.ts new file mode 100644 index 000000000..5a634603c --- /dev/null +++ b/src/.pi/extensions/commands.ts @@ -0,0 +1,14 @@ +/** @file commands.ts + * + * NOTES + +- can we get skill-like namespaced commands e.g. /brunch:continue, /brunch:switch + - investigate the source code to figure out how the skill/command matching is done + - hackable? --would look a little bit more branded +- /continue + - a slash command that you could use to continue, if interrupted/recovering + - a system prompt insertion that just says that "if you see the word continue on its own, it means to continue the brunch flow" + - should set listeners for *any user action that cancels the flow* (like the escape key or whatever) + - insert a `setStatus` line above the input reminding the user that they can type /continue to re-enter the brunch flow + + */ diff --git a/src/.pi/extensions/snapshot-cwd.ts b/src/.pi/extensions/snapshot-cwd.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/.pi/extensions/snapshot-graph.ts b/src/.pi/extensions/snapshot-graph.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/.pi/extensions/snapshot-nodes.ts b/src/.pi/extensions/snapshot-nodes.ts new file mode 100644 index 000000000..e69de29bb From 445bcce8356c77b0a91c62b782d4fd183294ec66 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 09:19:18 +0200 Subject: [PATCH 06/34] config and package updates, maybe breaking --- .amp/settings.json | 8 - .codex/config.toml | 3 - .cursor/mcp.json | 12 - .gemini/settings.json | 12 - .oxfmtrc.json | 17 +- .oxlintrc.json | 15 +- package-lock.json | 6237 ++++++++++++++++++++--------------------- package.json | 18 +- 8 files changed, 3120 insertions(+), 3202 deletions(-) delete mode 100644 .amp/settings.json delete mode 100644 .codex/config.toml delete mode 100644 .cursor/mcp.json delete mode 100644 .gemini/settings.json diff --git a/.amp/settings.json b/.amp/settings.json deleted file mode 100644 index 44fd5c50a..000000000 --- a/.amp/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "amp.mcpServers": { - "agentation": { - "command": "npx", - "args": ["-y", "agentation-mcp", "server"] - } - } -} diff --git a/.codex/config.toml b/.codex/config.toml deleted file mode 100644 index 34fd49244..000000000 --- a/.codex/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[mcp_servers.agentation] -command = "npx" -args = [ "-y", "agentation-mcp", "server" ] diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index 310665cb0..000000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "agentation": { - "command": "npx", - "args": [ - "-y", - "agentation-mcp", - "server" - ] - } - } -} \ No newline at end of file diff --git a/.gemini/settings.json b/.gemini/settings.json deleted file mode 100644 index 310665cb0..000000000 --- a/.gemini/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "agentation": { - "command": "npx", - "args": [ - "-y", - "agentation-mcp", - "server" - ] - } - } -} \ No newline at end of file diff --git a/.oxfmtrc.json b/.oxfmtrc.json index ac19e92d0..8e8f9eafc 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,5 +3,20 @@ "printWidth": 110, "singleQuote": true, "sortImports": true, - "ignorePatterns": ["*.md", "docs/**", "memory/**", "archive/**", ".fixtures/**", "dist/**"] + "sortPackageJson": true, + "sortTailwindcss": true, + "ignorePatterns": [ + ".agents/**", + ".github/**", + ".claude/**", + "*.md", + "docs/**", + "memory/**", + ".fixtures/**", + "@types/**", + "tmp/**", + "dist-web/**", + "bin/**", + "dist/**" + ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 7c1b225e2..0a40e9e03 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -7,5 +7,18 @@ "rules": { "typescript/no-deprecated": "error" }, - "ignorePatterns": ["dist", "archive", "docs", "memory", ".fixtures", "scripts"] + "ignorePatterns": [ + ".agents/**", + ".github/**", + ".claude/**", + "*.md", + "docs/**", + "memory/**", + ".fixtures/**", + "@types/**", + "tmp/**", + "dist-web/**", + "bin/**", + "dist/**" + ] } diff --git a/package-lock.json b/package-lock.json index 475df253b..c77098ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,16 +31,16 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.7.0", "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.31.10", + "drizzle-kit": "^0.18.1", "drizzle-orm": "^0.45.2", "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", - "oxfmt": "^0.2.0", - "oxlint": "^0.18.0", + "oxfmt": "latest", + "oxlint": "latest", "tsx": "^4.19.0", "typescript": "^5.7.0", - "vite": "^5.4.21", - "vitest": "^2.1.0" + "vite": "^8.0.16", + "vitest": "^4.1.8" }, "engines": { "node": ">=20" @@ -994,13 +994,6 @@ "node": ">=20.19.0" } }, - "node_modules/@drizzle-team/brocli": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", - "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@earendil-works/pi-agent-core": { "version": "0.75.3", "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", @@ -1089,22 +1082,61 @@ "koffi": "2.16.2" } }, - "node_modules/@esbuild-kit/core-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", - "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", - "deprecated": "Merged into tsx: https://tsx.is", + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "esbuild": "~0.18.20", - "source-map-support": "^0.5.21" + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -1115,13 +1147,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -1132,13 +1164,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -1149,13 +1181,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -1166,13 +1198,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -1183,13 +1215,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -1200,13 +1232,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -1217,13 +1249,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -1234,13 +1266,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -1251,13 +1283,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -1268,13 +1300,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -1285,13 +1317,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -1302,13 +1334,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -1319,13 +1351,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -1336,13 +1368,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -1353,13 +1385,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -1370,15 +1402,15 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -1387,30 +1419,13 @@ "netbsd" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -1418,16 +1433,16 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -1435,33 +1450,16 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1469,100 +1467,50 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "openbsd" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/@esbuild-kit/esm-loader": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", - "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", - "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, - "license": "MIT", - "dependencies": { - "@esbuild-kit/core-utils": "^3.3.2", - "get-tsconfig": "^4.7.0" + "node": ">=18" } }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "openharmony" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { + "node_modules/@esbuild/sunos-x64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "sunos" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm64": { + "node_modules/@esbuild/win32-arm64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -1570,322 +1518,422 @@ "license": "MIT", "optional": true, "os": [ - "android" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { + "node_modules/@esbuild/win32-ia32": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@esbuild/win32-x64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "license": "MIT", "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", "cpu": [ - "loong64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", "cpu": [ - "mips64el" + "arm64" + ], + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", "cpu": [ - "ppc64" + "arm64" + ], + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", "cpu": [ "riscv64" ], - "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", "cpu": [ - "s390x" + "x64" + ], + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", "cpu": [ "x64" ], - "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", + "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", + "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", + "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", "cpu": [ "arm64" ], @@ -1893,16 +1941,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", + "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", "cpu": [ "x64" ], @@ -1910,286 +1958,191 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", + "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", + "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", "cpu": [ - "ia32" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", + "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", + "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", + "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", + "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", + "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", + "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", + "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", - "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.6", - "@mariozechner/clipboard-darwin-universal": "0.3.6", - "@mariozechner/clipboard-darwin-x64": "0.3.6", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-musl": "0.3.6", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" - } - }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", - "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", - "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", - "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", - "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", - "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", - "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", - "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", + "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -2199,16 +2152,17 @@ "linux" ], "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", - "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", + "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -2218,187 +2172,76 @@ "linux" ], "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", - "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", + "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "openharmony" ], "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", - "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", + "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", "cpu": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@oxfmt/darwin-arm64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/darwin-arm64/-/darwin-arm64-0.2.0.tgz", - "integrity": "sha512-NK7iEPqRovUvKac+4dn2ui8v5Y5q6UJ9v4z5Zjr5lmEzTlBwXToP3TwY75IAaCYeu0g8Es7ToJpS7qCQuxUhuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxfmt/darwin-x64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/darwin-x64/-/darwin-x64-0.2.0.tgz", - "integrity": "sha512-eXDgT+DbIMnA3sWE+w38rOvXaxkP4RvHi41rPpWb9XoRGbM+tOo0c8RYqCIX39n7PizOlbF/xUBh9nMhiW01wQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxfmt/linux-arm64-gnu": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-gnu/-/linux-arm64-gnu-0.2.0.tgz", - "integrity": "sha512-h4mw9/Lck5X/SU1RPE26VraoBJgn9SOoLl8GnaYvtmRBFHK0v+2KpnBSwMWeiaIxCdJSsrVc9mpKSOcPsen7Cg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxfmt/linux-arm64-musl": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-arm64-musl/-/linux-arm64-musl-0.2.0.tgz", - "integrity": "sha512-kDZ/kVCgh9kSczMH2gPXZs+rsY/VcCQ1BoQnND8D3v1Le91LceJ5s/a4XBBA3ADI3NiPdqNmK2ssaxY4BfgXDg==", + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", + "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", "cpu": [ - "arm64" + "ia32" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@oxfmt/linux-x64-gnu": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-gnu/-/linux-x64-gnu-0.2.0.tgz", - "integrity": "sha512-qvf6cNuf4z3yjzjSvg4Jwr88jDyeFaYPU+4dBWVr6fdWhAA+BIuIvurJWYQGvWNNSkobVWH3A7m+wp+h5fNsSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" + "win32" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxfmt/linux-x64-musl": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/linux-x64-musl/-/linux-x64-musl-0.2.0.tgz", - "integrity": "sha512-D0rw4BVLHEbtMB6p9e+Ph7Y/56oD1DRP4Qcbbv1B1w0kJanWBB3vMH+v2rqgeYsErqJOtLpYRkPFmJSU9In2HA==", + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.53.0.tgz", + "integrity": "sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxfmt/win32-arm64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/win32-arm64/-/win32-arm64-0.2.0.tgz", - "integrity": "sha512-QM0fP8YUvBNyuNa6d+jNonV34A6zGvvQ2mX93wnERpX25jIbFKljKiKDiWo2M1nUsW8zL2z4ESEeaLO9bx8djg==", - "cpu": [ - "arm64" - ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@oxfmt/win32-x64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oxfmt/win32-x64/-/win32-x64-0.2.0.tgz", - "integrity": "sha512-WkFXliDbwFiziBSfvWXeM0Y3x07Kxxrl39kgsp+N3n5QgzsM8q6qe094+OW+hItDQ+9SSd2y9u231sMCsKxlog==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@oxlint/darwin-arm64": { "version": "0.18.1", @@ -2587,31 +2430,10 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -2620,12 +2442,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -2634,12 +2459,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -2648,26 +2476,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -2676,84 +2493,36 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" - ], - "dev": true, "libc": [ "glibc" ], @@ -2761,14 +2530,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "libc": [ @@ -2778,50 +2550,19 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, "libc": [ "glibc" ], @@ -2829,29 +2570,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -2863,12 +2590,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -2880,12 +2610,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -2897,26 +2630,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -2925,40 +2647,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -2967,21 +2700,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", @@ -3116,6 +2845,13 @@ "node": ">=14.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tanstack/history": { "version": "1.162.0", "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", @@ -3273,6 +3009,17 @@ } } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3335,6 +3082,24 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -3409,38 +3174,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -3452,70 +3219,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -3754,21 +3519,17 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { @@ -3793,18 +3554,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -3821,16 +3575,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -3838,6 +3582,33 @@ "dev": true, "license": "ISC" }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3886,6 +3657,20 @@ "dev": true, "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3949,20 +3734,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", "engines": { @@ -3998,6 +3773,18 @@ "node": ">=0.3.1" } }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -4005,43 +3792,44 @@ "dev": true, "license": "MIT" }, - "node_modules/drizzle-kit": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", - "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "node_modules/dreamopt": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", + "integrity": "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==", "dev": true, - "license": "MIT", "dependencies": { - "@drizzle-team/brocli": "^0.10.2", - "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.25.4", - "tsx": "^4.21.0" + "wordwrap": ">=0.0.2" }, - "bin": { - "drizzle-kit": "bin.cjs" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/drizzle-kit": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.18.1.tgz", + "integrity": "sha512-Oqie227W2Dd7FuqX4pvQWeClSvnoPCIn2cO9JueeLWZqj3tpdBhnbgt4nLHhBbOdWRlTLYwXnkTDW3hYym/gGQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "commander": "^9.4.1", + "esbuild": "^0.15.18", + "esbuild-register": "^3.4.2", + "glob": "^8.1.0", + "hanji": "^0.0.5", + "json-diff": "0.9.0", + "minimatch": "^7.4.3", + "zod": "^3.20.2" + }, + "bin": { + "drizzle-kit": "index.js" } }, "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", "cpu": [ "arm" ], @@ -4052,166 +3840,13 @@ "android" ], "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", "cpu": [ "loong64" ], @@ -4222,287 +3857,122 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], + "node_modules/drizzle-kit/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], + "node_modules/drizzle-kit/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/drizzle-kit/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/drizzle-kit/node_modules/glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], + "node_modules/drizzle-kit/node_modules/minimatch": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], + "node_modules/drizzle-kit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/drizzle-orm": { @@ -4682,12 +4152,68 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", @@ -4730,626 +4256,634 @@ "@esbuild/win32-x64": "0.28.0" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "(MIT OR WTFPL)", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" ], + "dev": true, "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=12" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.20.0" + "node": ">=12" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=12" } }, - "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=12" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "debug": "^4.3.4" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "peerDependencies": { + "esbuild": ">=0.12 <1" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": ">=12" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=6" } }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@exodus/bytes": "^1.6.0" + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=0.10" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "@types/estree": "^1.0.0" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "d": "1", + "es5-ext": "~0.10.14" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", + "license": "(MIT OR WTFPL)", "engines": { - "node": ">= 4" + "node": ">=6" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/isbot": { - "version": "5.1.40", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", - "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", - "license": "Unlicense", - "engines": { - "node": ">=18" + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, "bin": { - "jiti": "lib/jiti-cli.mjs" + "fxparser": "src/cli/cli.js" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": ">=12.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "canvas": { + "picomatch": { "optional": true } } }, - "node_modules/jsdom/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, "engines": { - "node": ">=6" + "node": "^12.20 || >= 14.13" } }, - "node_modules/json-bigint": { + "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">=16" + "node": ">=12.20.0" } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } + "license": "MIT" }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", + "os": [ + "darwin" + ], "engines": { - "node": "20 || >=22" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" }, "engines": { - "node": ">= 18" + "node": ">=18" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "CC0-1.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { "node": "18 || 20 || >=22" @@ -5358,1652 +4892,2033 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=14" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "node_modules/hanji": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", + "integrity": "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "license": "ISC", + "dependencies": { + "lodash.throttle": "^4.1.1", + "sisteransi": "^1.0.5" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", "dev": true, "license": "MIT" }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "engines": { - "node": ">=10" + "node": "*" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" }, "engines": { - "node": ">=10" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, "engines": { - "node": ">=10.5.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "engines": { + "node": ">= 14" } }, - "node_modules/node-releases": { - "version": "2.0.45", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", - "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 4" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { + "once": "^1.3.0", "wrappy": "1" } }, - "node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", - "license": "Apache-2.0", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbot": { + "version": "5.1.40", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", + "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", "bin": { - "openai": "bin/cli" + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { + "canvas": { "optional": true } } }, - "node_modules/oxfmt": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.2.0.tgz", - "integrity": "sha512-99bPAiYDiYikUej8U0Slv8pcFVbMe38Ulm+T6bJ4KQgm+UsbgTKCBpWxbD/LRz1CuYw3PRpqfPv6AkQsIAo3Ag==", + "node_modules/jsdom/node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", - "bin": { - "oxfmt": "bin/oxfmt" - }, "engines": { - "node": ">=8.*" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxfmt/darwin-arm64": "0.2.0", - "@oxfmt/darwin-x64": "0.2.0", - "@oxfmt/linux-arm64-gnu": "0.2.0", - "@oxfmt/linux-arm64-musl": "0.2.0", - "@oxfmt/linux-x64-gnu": "0.2.0", - "@oxfmt/linux-x64-musl": "0.2.0", - "@oxfmt/win32-arm64": "0.2.0", - "@oxfmt/win32-x64": "0.2.0" + "node": ">=20.18.1" } }, - "node_modules/oxlint": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-0.18.1.tgz", - "integrity": "sha512-JGcQvbhd00Qb+nq4f9sYYRh7mZIb0K/7rbMepNdJDMzo8pbmBpx1N2XOG61RjHDsNnY6ImAmVk3h4QVwFenwUQ==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { - "oxc_language_server": "bin/oxc_language_server", - "oxlint": "bin/oxlint" + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=8.*" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxlint/darwin-arm64": "0.18.1", - "@oxlint/darwin-x64": "0.18.1", - "@oxlint/linux-arm64-gnu": "0.18.1", - "@oxlint/linux-arm64-musl": "0.18.1", - "@oxlint/linux-x64-gnu": "0.18.1", - "@oxlint/linux-x64-musl": "0.18.1", - "@oxlint/win32-arm64": "0.18.1", - "@oxlint/win32-x64": "0.18.1" + "node": ">=6" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" + "bignumber.js": "^9.0.0" } }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "node_modules/json-diff": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.9.0.tgz", + "integrity": "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "cli-color": "^2.0.0", + "difflib": "~0.2.1", + "dreamopt": "~0.8.0" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "bin": { + "json-diff": "bin/json-diff.js" + }, + "engines": { + "node": "*" } }, - "node_modules/partial-json": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT" - }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, "engines": { - "node": ">=14.0.0" + "node": ">=16" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/koffi": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 14.16" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "prebuild-install": "bin.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "bin": { + "lz-string": "bin/bin.js" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, "engines": { - "node": ">= 4" + "node": ">= 18" } }, - "node_modules/protobufjs": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", - "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" }, "engines": { - "node": ">=12.0.0" + "node": ">=0.12" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": ">=6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rc": { + "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", - "license": "MIT", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, - "peerDependencies": { - "react": "^19.2.6" + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT" }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": ">= 6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + }, + "node_modules/node-releases": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", + "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/oxfmt": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.53.0.tgz", + "integrity": "sha512-9cB5glS3Ip6NMuZ+6NYTao9FCWkDhRtPYCtR3QBu/NxHoFbgzzTvi41N4jxz/GqGfuLKspui1qb/LlSu2IbMcw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "tinypool": "2.1.0" }, "bin": { - "rollup": "dist/bin/rollup" + "oxfmt": "bin/oxfmt" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "@oxfmt/binding-android-arm-eabi": "0.53.0", + "@oxfmt/binding-android-arm64": "0.53.0", + "@oxfmt/binding-darwin-arm64": "0.53.0", + "@oxfmt/binding-darwin-x64": "0.53.0", + "@oxfmt/binding-freebsd-x64": "0.53.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.53.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.53.0", + "@oxfmt/binding-linux-arm64-gnu": "0.53.0", + "@oxfmt/binding-linux-arm64-musl": "0.53.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-musl": "0.53.0", + "@oxfmt/binding-linux-s390x-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-musl": "0.53.0", + "@oxfmt/binding-openharmony-arm64": "0.53.0", + "@oxfmt/binding-win32-arm64-msvc": "0.53.0", + "@oxfmt/binding-win32-ia32-msvc": "0.53.0", + "@oxfmt/binding-win32-x64-msvc": "0.53.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true }, - { - "type": "consulting", - "url": "https://feross.org/support" + "vite-plus": { + "optional": true } - ], - "license": "MIT" + } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "node_modules/oxfmt/node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, + "license": "MIT", "engines": { - "node": ">=v12.22.7" + "node": "^20.0.0 || >=22.0.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/oxlint": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-0.18.1.tgz", + "integrity": "sha512-JGcQvbhd00Qb+nq4f9sYYRh7mZIb0K/7rbMepNdJDMzo8pbmBpx1N2XOG61RjHDsNnY6ImAmVk3h4QVwFenwUQ==", "dev": true, - "license": "ISC", + "license": "MIT", "bin": { - "semver": "bin/semver.js" + "oxc_language_server": "bin/oxc_language_server", + "oxlint": "bin/oxlint" + }, + "engines": { + "node": ">=8.*" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/darwin-arm64": "0.18.1", + "@oxlint/darwin-x64": "0.18.1", + "@oxlint/linux-arm64-gnu": "0.18.1", + "@oxlint/linux-arm64-musl": "0.18.1", + "@oxlint/linux-x64-gnu": "0.18.1", + "@oxlint/linux-x64-musl": "0.18.1", + "@oxlint/win32-arm64": "0.18.1", + "@oxlint/win32-x64": "0.18.1" } }, - "node_modules/seroval": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", - "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/seroval-plugins": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", - "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "entities": "^8.0.0" }, - "peerDependencies": { - "seroval": "^1.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 4" } }, - "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", + "node_modules/protobufjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=6" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=0.10.0" } }, - "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "dev": true, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "dependencies": { - "tldts-core": "^7.0.30" + "scheduler": "^0.27.0" }, - "bin": { - "tldts": "bin/cli.js" + "peerDependencies": { + "react": "^19.2.6" } }, - "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT" }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", "dependencies": { - "tldts": "^7.0.5" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=16" + "node": ">= 6" } }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, "engines": { - "node": ">=20" + "node": ">=0.10.0" } }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } }, - "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.28.0" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "tsx": "dist/cli.mjs" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "fsevents": "~2.3.3" - } + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", "dependencies": { - "safe-buffer": "^5.0.1" + "xmlchars": "^2.2.0" }, "engines": { - "node": "*" + "node": ">=v12.22.7" } }, - "node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=14.17" + "node": ">=8" } }, - "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { - "node": ">=22.19.0" + "node": ">=8" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "sugarss": { - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "terser": { - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" } - } - }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, + ], "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "safe-buffer": "~5.2.0" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } ], + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, "engines": { - "node": ">=12" + "node": ">=0.12" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, "engines": { - "node": ">=12" + "node": ">=16" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "punycode": "^2.3.1" + }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, "engines": { - "node": ">=12" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } + "license": "ISC" }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=12" + "node": ">=14.17" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=12" + "node": ">=22.19.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, "bin": { - "esbuild": "bin/esbuild" + "vite": "bin/vite.js" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -7014,6 +6929,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -7106,6 +7024,13 @@ "node": ">=8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 59edc744c..f99257ec9 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "build:web": "vite build", "test": "vitest --run", "test:watch": "vitest", - "lint": "oxlint src .pi/extensions", - "lint:fix": "oxlint --fix src .pi/extensions", - "fmt": "oxfmt src .pi/extensions", - "fmt:check": "oxfmt --check src .pi/extensions", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", "fix": "npm run lint:fix && npm run fmt", "check": "npm run fmt:check && npm run lint && npm run typecheck", "verify": "npm run check && npm run test && npm run build", @@ -52,16 +52,16 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.7.0", "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.31.10", + "drizzle-kit": "^0.18.1", "drizzle-orm": "^0.45.2", "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", - "oxfmt": "^0.2.0", - "oxlint": "^0.18.0", + "oxfmt": "latest", + "oxlint": "latest", "tsx": "^4.19.0", "typescript": "^5.7.0", - "vite": "^5.4.21", - "vitest": "^2.1.0" + "vite": "^8.0.16", + "vitest": "^4.1.8" }, "engines": { "node": ">=20" From 2e5fa71281c3f681b7a6531599b31447e9a77bcf Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 09:19:57 +0200 Subject: [PATCH 07/34] format and lint fixes --- .mcp.json | 11 +- .oxfmtrc.json | 40 +- .oxlintrc.json | 44 +- package.json | 140 +- src/.pi/__tests__/chrome.test.ts | 354 ++- src/.pi/__tests__/extension-registry.test.ts | 245 +-- src/.pi/__tests__/graph-tools.test.ts | 247 ++- .../__tests__/mention-autocomplete.test.ts | 140 +- src/.pi/__tests__/operational-mode.test.ts | 422 ++-- src/.pi/__tests__/prompting.test.ts | 307 ++- .../structured-exchange-extension.test.ts | 73 +- ...tructured-exchange-present-request.test.ts | 293 ++- .../structured-exchange-schemas.test.ts | 571 +++-- src/.pi/__tests__/structured-exchange.test.ts | 102 +- src/.pi/__tests__/workspace-dialog.test.ts | 557 +++-- src/.pi/components/brunch-identity.ts | 154 +- src/.pi/components/cards.ts | 102 +- src/.pi/components/workspace-dialog.ts | 2 +- .../components/workspace-dialog/component.ts | 338 ++- src/.pi/components/workspace-dialog/index.ts | 6 +- src/.pi/components/workspace-dialog/model.ts | 258 +-- .../components/workspace-dialog/preflight.ts | 90 +- src/.pi/context/builders/graph-context.ts | 4 +- src/.pi/context/builders/readiness-context.ts | 10 +- .../builders/structured-exchange-context.ts | 8 +- src/.pi/context/compose-brunch-prompt.ts | 107 +- src/.pi/extensions/alternatives.ts | 194 +- src/.pi/extensions/chrome.ts | 199 +- src/.pi/extensions/command-policy.ts | 22 +- src/.pi/extensions/graph/command-adapter.ts | 135 +- src/.pi/extensions/graph/index.ts | 136 +- src/.pi/extensions/mention-autocomplete.ts | 142 +- src/.pi/extensions/operational-mode.ts | 554 +++-- src/.pi/extensions/prompting.ts | 43 +- src/.pi/extensions/session-lifecycle.ts | 44 +- .../extensions/structured-exchange/index.ts | 51 +- .../structured-exchange/present-candidates.ts | 4 +- .../structured-exchange/present-options.ts | 103 +- .../structured-exchange/present-question.ts | 61 +- .../structured-exchange/present-review-set.ts | 4 +- .../structured-exchange/request-answer.ts | 84 +- .../structured-exchange/request-choice.ts | 153 +- .../structured-exchange/request-choices.ts | 291 ++- .../structured-exchange/request-review.ts | 4 +- .../structured-exchange/schemas/capture.ts | 81 +- .../structured-exchange/schemas/index.ts | 8 +- .../structured-exchange/schemas/present.ts | 112 +- .../structured-exchange/schemas/request.ts | 208 +- .../structured-exchange/schemas/shared.ts | 198 +- .../shared/editor-fallback.ts | 159 +- .../structured-exchange/shared/markdown.ts | 79 +- .../structured-exchange/shared/model.ts | 80 +- .../structured-exchange/shared/recovery.ts | 126 +- src/.pi/extensions/workspace-dialog.ts | 107 +- src/.pi/pi-extension-shell.ts | 78 +- src/.pi/settings.json | 7 +- src/brunch-pi-profile.ts | 152 +- src/brunch-session-envelope.ts | 160 +- src/brunch-tui.test.ts | 1178 +++++----- src/brunch-tui.ts | 107 +- src/brunch.smoke.test.ts | 24 +- src/brunch.test.ts | 231 +- src/brunch.ts | 137 +- src/db/connection.ts | 20 +- src/db/row-schemas.ts | 30 +- src/db/schema.ts | 97 +- src/elicitation-exchange.test.ts | 974 ++++----- src/elicitation-exchange.ts | 336 ++- src/graph/architecture.test.ts | 23 +- src/graph/atoms.ts | 6 +- src/graph/command-executor.test.ts | 1214 +++++------ src/graph/command-executor.ts | 508 +++-- src/graph/index.ts | 31 +- src/graph/policy/category-policy.ts | 38 +- src/graph/schema/edges.ts | 28 +- src/graph/schema/nodes.ts | 86 +- src/graph/schema/reconciliation-need.ts | 38 +- src/graph/snapshot.test.ts | 494 +++-- src/graph/snapshot.ts | 186 +- src/jsonl-session-viability.test.ts | 344 ++- src/print-snapshot.test.ts | 77 +- src/print-snapshot.ts | 59 +- src/probes/check-workspace-session-stores.ts | 24 +- src/probes/public-rpc-parity-proof.test.ts | 145 +- src/probes/public-rpc-parity-proof.ts | 443 ++-- src/probes/startup-oracle-script.test.ts | 27 +- ...structured-exchange-ordering-proof.test.ts | 71 +- .../structured-exchange-ordering-proof.ts | 354 ++- .../structured-exchange-rpc-proof.test.ts | 80 +- src/probes/structured-exchange-rpc-proof.ts | 315 ++- src/project-identity.test.ts | 194 +- src/project-identity.ts | 155 +- src/rpc/handlers.test.ts | 1892 ++++++++--------- src/rpc/handlers.ts | 1097 +++++----- src/rpc/protocol.test.ts | 109 +- src/rpc/protocol.ts | 98 +- src/rpc/websocket.ts | 114 +- src/session-binding.ts | 59 +- src/session-projection-reader.ts | 79 +- src/session-transcript.test.ts | 122 +- src/session-transcript.ts | 207 +- src/structured-exchange.ts | 93 +- src/test-helpers.ts | 39 +- src/web-client/app.test.tsx | 264 ++- src/web-client/app.tsx | 151 +- src/web-client/main.tsx | 18 +- src/web-client/rpc-client.test.ts | 416 ++-- src/web-client/rpc-client.ts | 249 +-- src/web-host.test.ts | 638 +++--- src/web-host.ts | 187 +- src/workspace-session-coordinator.test.ts | 755 +++---- src/workspace-session-coordinator.ts | 634 +++--- tsconfig.build.json | 22 +- tsconfig.json | 59 +- vite.config.ts | 14 +- 115 files changed, 11049 insertions(+), 12745 deletions(-) diff --git a/.mcp.json b/.mcp.json index 95728a565..e71883333 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,18 +2,11 @@ "mcpServers": { "shadcn": { "command": "npx", - "args": [ - "shadcn@latest", - "mcp" - ] + "args": ["shadcn@latest", "mcp"] }, "agentation": { "command": "npx", - "args": [ - "-y", - "agentation-mcp", - "server" - ] + "args": ["-y", "agentation-mcp", "server"] } } } diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 8e8f9eafc..11282bf29 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,22 +1,22 @@ { - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "printWidth": 110, - "singleQuote": true, - "sortImports": true, - "sortPackageJson": true, - "sortTailwindcss": true, - "ignorePatterns": [ - ".agents/**", - ".github/**", - ".claude/**", - "*.md", - "docs/**", - "memory/**", - ".fixtures/**", - "@types/**", - "tmp/**", - "dist-web/**", - "bin/**", - "dist/**" - ] + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 110, + "singleQuote": true, + "sortImports": true, + "sortPackageJson": true, + "sortTailwindcss": true, + "ignorePatterns": [ + ".agents/**", + ".github/**", + ".claude/**", + "*.md", + "docs/**", + "memory/**", + ".fixtures/**", + "@types/**", + "tmp/**", + "dist-web/**", + "bin/**", + "dist/**" + ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 0a40e9e03..b821e6c8b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,24 +1,24 @@ { - "$schema": "./node_modules/oxlint/configuration_schema.json", - "options": { - "typeAware": true, - "typeCheck": true - }, - "rules": { - "typescript/no-deprecated": "error" - }, - "ignorePatterns": [ - ".agents/**", - ".github/**", - ".claude/**", - "*.md", - "docs/**", - "memory/**", - ".fixtures/**", - "@types/**", - "tmp/**", - "dist-web/**", - "bin/**", - "dist/**" - ] + "$schema": "./node_modules/oxlint/configuration_schema.json", + "options": { + "typeAware": true, + "typeCheck": true + }, + "rules": { + "typescript/no-deprecated": "error" + }, + "ignorePatterns": [ + ".agents/**", + ".github/**", + ".claude/**", + "*.md", + "docs/**", + "memory/**", + ".fixtures/**", + "@types/**", + "tmp/**", + "dist-web/**", + "bin/**", + "dist/**" + ] } diff --git a/package.json b/package.json index f99257ec9..afdd8e9f4 100644 --- a/package.json +++ b/package.json @@ -1,72 +1,72 @@ { - "name": "brunch-next", - "version": "0.0.0", - "description": "Brunch — opinionated specification-workspace product over pi-coding-agent.", - "private": true, - "type": "module", - "main": "./dist/brunch.js", - "types": "./dist/brunch.d.ts", - "bin": { - "brunch-next": "./bin/brunch.js" - }, - "files": [ - "dist", - "dist-web", - "bin", - "assets" - ], - "scripts": { - "dev": "tsx src/brunch.ts", - "build": "tsc -p tsconfig.build.json && npm run build:pi-assets && npm run build:web", - "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/.pi/context/prompt-packs && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp src/.pi/context/prompt-packs/*.md dist/.pi/context/prompt-packs/", - "build:web": "vite build", - "test": "vitest --run", - "test:watch": "vitest", - "lint": "oxlint", - "lint:fix": "oxlint --fix", - "fmt": "oxfmt", - "fmt:check": "oxfmt --check", - "fix": "npm run lint:fix && npm run fmt", - "check": "npm run fmt:check && npm run lint && npm run typecheck", - "verify": "npm run check && npm run test && npm run build", - "typecheck": "tsc -p tsconfig.json" - }, - "dependencies": { - "@earendil-works/pi-coding-agent": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.4", - "@tanstack/react-query": "^5.100.11", - "@tanstack/react-router": "^1.170.6", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.20.1", - "zod": "^4.4.3" - }, - "devDependencies": { - "@sinclair/typebox": "^0.34.14", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", - "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "@types/react": "^19.2.15", - "@types/react-dom": "^19.2.3", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.7.0", - "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.18.1", - "drizzle-orm": "^0.45.2", - "drizzle-typebox": "^0.3.3", - "jsdom": "^29.1.1", - "oxfmt": "latest", - "oxlint": "latest", - "tsx": "^4.19.0", - "typescript": "^5.7.0", - "vite": "^8.0.16", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=20" - }, - "allowScripts": { - "better-sqlite3@12.8.0": true - } + "name": "brunch-next", + "version": "0.0.0", + "private": true, + "description": "Brunch — opinionated specification-workspace product over pi-coding-agent.", + "bin": { + "brunch-next": "./bin/brunch.js" + }, + "files": [ + "dist", + "dist-web", + "bin", + "assets" + ], + "type": "module", + "main": "./dist/brunch.js", + "types": "./dist/brunch.d.ts", + "scripts": { + "dev": "tsx src/brunch.ts", + "build": "tsc -p tsconfig.build.json && npm run build:pi-assets && npm run build:web", + "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/.pi/context/prompt-packs && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp src/.pi/context/prompt-packs/*.md dist/.pi/context/prompt-packs/", + "build:web": "vite build", + "test": "vitest --run", + "test:watch": "vitest", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", + "fix": "npm run lint:fix && npm run fmt", + "check": "npm run fmt:check && npm run lint && npm run typecheck", + "verify": "npm run check && npm run test && npm run build", + "typecheck": "tsc -p tsconfig.json" + }, + "dependencies": { + "@earendil-works/pi-coding-agent": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.4", + "@tanstack/react-query": "^5.100.11", + "@tanstack/react-router": "^1.170.6", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "ws": "^8.20.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.14", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.10.0", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^4.7.0", + "better-sqlite3": "^12.8.0", + "drizzle-kit": "^0.18.1", + "drizzle-orm": "^0.45.2", + "drizzle-typebox": "^0.3.3", + "jsdom": "^29.1.1", + "oxfmt": "latest", + "oxlint": "latest", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vite": "^8.0.16", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=20" + }, + "allowScripts": { + "better-sqlite3@12.8.0": true + } } diff --git a/src/.pi/__tests__/chrome.test.ts b/src/.pi/__tests__/chrome.test.ts index a99f43574..666cb876f 100644 --- a/src/.pi/__tests__/chrome.test.ts +++ b/src/.pi/__tests__/chrome.test.ts @@ -1,248 +1,220 @@ -import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent" +import type { ExtensionUIContext } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { describe, expect, it } from "vitest" - -import type { WorkspaceSessionReadyState } from "../../../workspace-session-coordinator.js" +import type { WorkspaceSessionReadyState } from '../../../workspace-session-coordinator.js'; import { chromeStateForWorkspace, formatBrunchChromeHeaderLines, formatChromeWidgetLines, projectBrunchChromeFooterLines, renderBrunchChrome, -} from "../extensions/chrome.js" - -describe("Brunch chrome projection", () => { - it("uses activated session state instead of fabricating unbound", async () => { - const state = chromeStateForWorkspace( - readyWorkspace("/tmp/project", "session-real"), - ) - - expect(formatBrunchChromeHeaderLines(state).join("\n")).toContain( - "session-real", - ) - }) - - it("populates session.label from workspace session name when available", () => { - const workspace = readyWorkspace( - "/tmp/project", - "session-abc", - "My spec — session 1", - ) - const state = chromeStateForWorkspace(workspace) - - expect(state.session.label).toBe("My spec — session 1") - expect(formatBrunchChromeHeaderLines(state).join("\n")).toContain( - "My spec — session 1", - ) - }) - - it("formats chrome header as wordmark plus runtime-state summary", async () => { +} from '../extensions/chrome.js'; + +describe('Brunch chrome projection', () => { + it('uses activated session state instead of fabricating unbound', async () => { + const state = chromeStateForWorkspace(readyWorkspace('/tmp/project', 'session-real')); + + expect(formatBrunchChromeHeaderLines(state).join('\n')).toContain('session-real'); + }); + + it('populates session.label from workspace session name when available', () => { + const workspace = readyWorkspace('/tmp/project', 'session-abc', 'My spec — session 1'); + const state = chromeStateForWorkspace(workspace); + + expect(state.session.label).toBe('My spec — session 1'); + expect(formatBrunchChromeHeaderLines(state).join('\n')).toContain('My spec — session 1'); + }); + + it('formats chrome header as wordmark plus runtime-state summary', async () => { const state = { - cwd: "/tmp/project", - spec: { id: "spec-1", title: "Spec One" }, - session: { id: "session-1", label: "Interview #1" }, - phase: "elicitation" as const, - chatMode: "responding-to-elicitation" as const, + cwd: '/tmp/project', + spec: { id: 'spec-1', title: 'Spec One' }, + session: { id: 'session-1', label: 'Interview #1' }, + phase: 'elicitation' as const, + chatMode: 'responding-to-elicitation' as const, runtime: { - bundle: "elicit-default", - role: "elicitor", - model: "claude-sonnet", - thinking: "medium", - lens: "step-by-step", + bundle: 'elicit-default', + role: 'elicitor', + model: 'claude-sonnet', + thinking: 'medium', + lens: 'step-by-step', }, - } + }; expect(formatBrunchChromeHeaderLines(state)).toEqual([ - "█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █", - "█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", - "runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step", - "spec: Spec One · session: Interview #1 · phase: elicitation", - ]) - }) - - it("formats honest Brunch chrome from one product-state snapshot", async () => { + '█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █', + '█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', + 'runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step', + 'spec: Spec One · session: Interview #1 · phase: elicitation', + ]); + }); + + it('formats honest Brunch chrome from one product-state snapshot', async () => { const state = { - cwd: "/tmp/project", - spec: { id: "spec-1", title: "Spec One" }, - session: { id: "session-1", label: "Interview #1" }, - phase: "elicitation" as const, - chatMode: "responding-to-elicitation" as const, - } + cwd: '/tmp/project', + spec: { id: 'spec-1', title: 'Spec One' }, + session: { id: 'session-1', label: 'Interview #1' }, + phase: 'elicitation' as const, + chatMode: 'responding-to-elicitation' as const, + }; expect(formatBrunchChromeHeaderLines(state)).toEqual([ - "█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █", - "█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", - "runtime: not reported", - "spec: Spec One · session: Interview #1 · phase: elicitation", - ]) + '█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █', + '█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', + 'runtime: not reported', + 'spec: Spec One · session: Interview #1 · phase: elicitation', + ]); expect(projectBrunchChromeFooterLines(state)).toEqual([ - "brunch · runtime: not reported · build: not reported", - "context: not reported", - "state: responding-to-elicitation · coherence: unknown · worker: not reported", - "spec: Spec One · session: Interview #1", - "", - ]) + 'brunch · runtime: not reported · build: not reported', + 'context: not reported', + 'state: responding-to-elicitation · coherence: unknown · worker: not reported', + 'spec: Spec One · session: Interview #1', + '', + ]); expect(formatChromeWidgetLines(state)).toEqual([ - "brunch: █▄▄ █▀█ █ █ █▄ █ █▀▀ █ █ / █▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", - "cwd: /tmp/project", - "spec: Spec One", - "session: Interview #1", - "runtime: not reported", - "context: not reported", - "chat mode: responding-to-elicitation", - ]) - }) - - it("formats rich optional runtime and context metadata without fabricating missing fields", () => { + 'brunch: █▄▄ █▀█ █ █ █▄ █ █▀▀ █ █ / █▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', + 'cwd: /tmp/project', + 'spec: Spec One', + 'session: Interview #1', + 'runtime: not reported', + 'context: not reported', + 'chat mode: responding-to-elicitation', + ]); + }); + + it('formats rich optional runtime and context metadata without fabricating missing fields', () => { const state = { - cwd: "/tmp/project", - spec: { id: "spec-1", title: "Spec One" }, - session: { id: "session-1", label: "Interview #1" }, - phase: "elicitation" as const, - chatMode: "responding-to-elicitation" as const, + cwd: '/tmp/project', + spec: { id: 'spec-1', title: 'Spec One' }, + session: { id: 'session-1', label: 'Interview #1' }, + phase: 'elicitation' as const, + chatMode: 'responding-to-elicitation' as const, runtime: { - bundle: "elicit-default", - role: "elicitor", - model: "claude-sonnet", - thinking: "medium", - lens: "step-by-step", + bundle: 'elicit-default', + role: 'elicitor', + model: 'claude-sonnet', + thinking: 'medium', + lens: 'step-by-step', }, - build: { version: "v0.0.0", dev: "dev abc123" }, + build: { version: 'v0.0.0', dev: 'dev abc123' }, contextUsage: { usedTokens: 1024, maxTokens: 2048 }, - worker: { stage: "observer-review" as const, status: "queued" as const }, - coherence: "needs_review" as const, - } + worker: { stage: 'observer-review' as const, status: 'queued' as const }, + coherence: 'needs_review' as const, + }; expect(projectBrunchChromeFooterLines(state)).toEqual([ - "brunch · runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step · build: v0.0.0 dev abc123", - "context: [█████░░░░░] 1,024/2,048 tokens (50%)", - "state: responding-to-elicitation · coherence: needs_review · worker: observer-review/queued", - "spec: Spec One · session: Interview #1", - "", - ]) - expect(formatChromeWidgetLines(state)).toContain( - "context: [█████░░░░░] 1,024/2,048 tokens (50%)", - ) - }) - - it("projects footer telemetry and foreign statuses without publishing a chrome status key", async () => { + 'brunch · runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step · build: v0.0.0 dev abc123', + 'context: [█████░░░░░] 1,024/2,048 tokens (50%)', + 'state: responding-to-elicitation · coherence: needs_review · worker: observer-review/queued', + 'spec: Spec One · session: Interview #1', + '', + ]); + expect(formatChromeWidgetLines(state)).toContain('context: [█████░░░░░] 1,024/2,048 tokens (50%)'); + }); + + it('projects footer telemetry and foreign statuses without publishing a chrome status key', async () => { const footer = projectBrunchChromeFooterLines( { - cwd: "/tmp/project", - spec: { id: "spec-1", title: "Spec One" }, - session: { id: "session-1", label: "Interview #1" }, - phase: "elicitation", - chatMode: "responding-to-elicitation", + cwd: '/tmp/project', + spec: { id: 'spec-1', title: 'Spec One' }, + session: { id: 'session-1', label: 'Interview #1' }, + phase: 'elicitation', + chatMode: 'responding-to-elicitation', runtime: { - bundle: "elicit-default", - role: "elicitor", - model: "claude-sonnet", - thinking: "medium", + bundle: 'elicit-default', + role: 'elicitor', + model: 'claude-sonnet', + thinking: 'medium', }, contextUsage: { usedTokens: 1024, maxTokens: 2048 }, }, { - gitBranch: "main", + gitBranch: 'main', statuses: new Map([ - ["brunch.reviewer", "reviewer queued"], - ["brunch.chrome", "should not echo"], + ['brunch.reviewer', 'reviewer queued'], + ['brunch.chrome', 'should not echo'], ]), }, 200, - ).join("\n") - - expect(footer).toContain("Spec One") - expect(footer).toContain("Interview #1") - expect(footer).toContain("main") - expect(footer).toContain("claude-sonnet") - expect(footer).toContain("thinking medium") - expect(footer).toContain("[█████░░░░░] 1,024/2,048 tokens (50%)") - expect(footer).toContain("reviewer queued") - expect(footer).not.toContain("should not echo") - }) - - it("renders Brunch chrome through one wrapper over Pi UI calls", async () => { - const calls: FakeUiCall[] = [] + ).join('\n'); + + expect(footer).toContain('Spec One'); + expect(footer).toContain('Interview #1'); + expect(footer).toContain('main'); + expect(footer).toContain('claude-sonnet'); + expect(footer).toContain('thinking medium'); + expect(footer).toContain('[█████░░░░░] 1,024/2,048 tokens (50%)'); + expect(footer).toContain('reviewer queued'); + expect(footer).not.toContain('should not echo'); + }); + + it('renders Brunch chrome through one wrapper over Pi UI calls', async () => { + const calls: FakeUiCall[] = []; const ui: FakeExtensionUi = { - setHeader: (...args: unknown[]) => - calls.push({ method: "setHeader", args }), - setFooter: (...args: unknown[]) => - calls.push({ method: "setFooter", args }), - setStatus: (...args: unknown[]) => - calls.push({ method: "setStatus", args }), - setWidget: (...args: unknown[]) => - calls.push({ method: "setWidget", args }), + setHeader: (...args: unknown[]) => calls.push({ method: 'setHeader', args }), + setFooter: (...args: unknown[]) => calls.push({ method: 'setFooter', args }), + setStatus: (...args: unknown[]) => calls.push({ method: 'setStatus', args }), + setWidget: (...args: unknown[]) => calls.push({ method: 'setWidget', args }), setWorkingIndicator: (_options) => {}, - setTitle: (...args: unknown[]) => - calls.push({ method: "setTitle", args }), - notify: (_message: string, _type?: "info" | "warning" | "error") => {}, - } + setTitle: (...args: unknown[]) => calls.push({ method: 'setTitle', args }), + notify: (_message: string, _type?: 'info' | 'warning' | 'error') => {}, + }; renderBrunchChrome(ui, { - cwd: "/tmp/project", - spec: { id: "spec-1", title: "Spec One" }, - session: { id: "session-1" }, - phase: "elicitation", - chatMode: "responding-to-elicitation", - }) - - expect(calls.map((call) => call.method)).toEqual([ - "setHeader", - "setFooter", - "setWidget", - "setTitle", - ]) - expect(calls.find((call) => call.method === "setFooter")?.args[0]).toEqual( - expect.any(Function), - ) - expect(calls.some((call) => call.method === "setStatus")).toBe(false) - expect(calls.find((call) => call.method === "setWidget")?.args).toEqual([ - "brunch.chrome", + cwd: '/tmp/project', + spec: { id: 'spec-1', title: 'Spec One' }, + session: { id: 'session-1' }, + phase: 'elicitation', + chatMode: 'responding-to-elicitation', + }); + + expect(calls.map((call) => call.method)).toEqual(['setHeader', 'setFooter', 'setWidget', 'setTitle']); + expect(calls.find((call) => call.method === 'setFooter')?.args[0]).toEqual(expect.any(Function)); + expect(calls.some((call) => call.method === 'setStatus')).toBe(false); + expect(calls.find((call) => call.method === 'setWidget')?.args).toEqual([ + 'brunch.chrome', [ - "brunch: █▄▄ █▀█ █ █ █▄ █ █▀▀ █ █ / █▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", - "cwd: /tmp/project", - "spec: Spec One", - "session: session-1", - "runtime: not reported", - "context: not reported", - "chat mode: responding-to-elicitation", + 'brunch: █▄▄ █▀█ █ █ █▄ █ █▀▀ █ █ / █▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', + 'cwd: /tmp/project', + 'spec: Spec One', + 'session: session-1', + 'runtime: not reported', + 'context: not reported', + 'chat mode: responding-to-elicitation', ], - { placement: "aboveEditor" }, - ]) - expect(calls.find((call) => call.method === "setTitle")?.args).toEqual([ - "brunch — Spec One", - ]) - }) -}) - -function readyWorkspace( - cwd: string, - sessionId: string, - sessionName?: string, -): WorkspaceSessionReadyState { - const spec = { id: "spec-1", title: "Spec One" } + { placement: 'aboveEditor' }, + ]); + expect(calls.find((call) => call.method === 'setTitle')?.args).toEqual(['brunch — Spec One']); + }); +}); + +function readyWorkspace(cwd: string, sessionId: string, sessionName?: string): WorkspaceSessionReadyState { + const spec = { id: 'spec-1', title: 'Spec One' }; return { - status: "ready", + status: 'ready', cwd, spec, session: { id: sessionId, file: `/sessions/${sessionId}.jsonl`, name: sessionName, - manager: {} as WorkspaceSessionReadyState["session"]["manager"], + manager: {} as WorkspaceSessionReadyState['session']['manager'], }, chrome: { cwd, spec, - phase: "elicitation", - chatMode: "responding-to-elicitation", + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - } + }; } interface FakeUiCall { - method: string - args: unknown[] + method: string; + args: unknown[]; } -type FakeExtensionUi = Pick +type FakeExtensionUi = Pick< + ExtensionUIContext, + 'setFooter' | 'setHeader' | 'setStatus' | 'setWidget' | 'setWorkingIndicator' | 'setTitle' | 'notify' +>; diff --git a/src/.pi/__tests__/extension-registry.test.ts b/src/.pi/__tests__/extension-registry.test.ts index 0f4eb85b5..84724beb6 100644 --- a/src/.pi/__tests__/extension-registry.test.ts +++ b/src/.pi/__tests__/extension-registry.test.ts @@ -1,179 +1,168 @@ -import { access, readFile, readdir } from "node:fs/promises" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" +import { access, readFile, readdir } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { createBrunchPiExtensionShell } from "../pi-extension-shell.js" -import alternatives from "../extensions/alternatives.js" -import chrome from "../extensions/chrome.js" -import commandPolicy from "../extensions/command-policy.js" -import mentionAutocomplete from "../extensions/mention-autocomplete.js" -import operationalMode from "../extensions/operational-mode.js" -import prompting from "../extensions/prompting.js" -import sessionLifecycle from "../extensions/session-lifecycle.js" +import alternatives from '../extensions/alternatives.js'; +import chrome from '../extensions/chrome.js'; +import commandPolicy from '../extensions/command-policy.js'; +import mentionAutocomplete from '../extensions/mention-autocomplete.js'; +import operationalMode from '../extensions/operational-mode.js'; +import prompting from '../extensions/prompting.js'; +import sessionLifecycle from '../extensions/session-lifecycle.js'; import structuredExchange, { PRESENT_OPTIONS_TOOL, PRESENT_QUESTION_TOOL, REQUEST_ANSWER_TOOL, REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, -} from "../extensions/structured-exchange/index.js" -import workspaceDialog, { - BRUNCH_WORKSPACE_COMMAND, -} from "../extensions/workspace-dialog.js" +} from '../extensions/structured-exchange/index.js'; +import workspaceDialog, { BRUNCH_WORKSPACE_COMMAND } from '../extensions/workspace-dialog.js'; +import { createBrunchPiExtensionShell } from '../pi-extension-shell.js'; const extensionDefaults = { - "alternatives.ts": alternatives, - "chrome.ts": chrome, - "command-policy.ts": commandPolicy, - "mention-autocomplete.ts": mentionAutocomplete, - "operational-mode.ts": operationalMode, - "prompting.ts": prompting, - "session-lifecycle.ts": sessionLifecycle, - "structured-exchange/index.ts": structuredExchange, - "workspace-dialog.ts": workspaceDialog, -} + 'alternatives.ts': alternatives, + 'chrome.ts': chrome, + 'command-policy.ts': commandPolicy, + 'mention-autocomplete.ts': mentionAutocomplete, + 'operational-mode.ts': operationalMode, + 'prompting.ts': prompting, + 'session-lifecycle.ts': sessionLifecycle, + 'structured-exchange/index.ts': structuredExchange, + 'workspace-dialog.ts': workspaceDialog, +}; -describe("Brunch explicit Pi extension registry", () => { - it("keeps default factory exports for src/.pi iteration", () => { +describe('Brunch explicit Pi extension registry', () => { + it('keeps default factory exports for src/.pi iteration', () => { for (const [path, factory] of Object.entries(extensionDefaults)) { - expect(factory, path).toEqual(expect.any(Function)) + expect(factory, path).toEqual(expect.any(Function)); } - }) + }); - it("registers product extensions from the shell in explicit order", async () => { - const recording = createRecordingExtensionApi() + it('registers product extensions from the shell in explicit order', async () => { + const recording = createRecordingExtensionApi(); - await createBrunchPiExtensionShell( - brunchChromeFixture, - recording.onSessionBoundary, - { - coordinator: {} as never, - graphMentionSource: { listMentionCandidates: () => [] }, - }, - )(recording.api) + await createBrunchPiExtensionShell(brunchChromeFixture, recording.onSessionBoundary, { + coordinator: {} as never, + graphMentionSource: { listMentionCandidates: () => [] }, + })(recording.api); expect(recording.toolNames).toEqual([ - "read", - "grep", - "find", - "ls", - "present_alternatives", + 'read', + 'grep', + 'find', + 'ls', + 'present_alternatives', PRESENT_QUESTION_TOOL, PRESENT_OPTIONS_TOOL, REQUEST_ANSWER_TOOL, REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, - ]) - expect(recording.commandNames).toEqual([BRUNCH_WORKSPACE_COMMAND]) - expect(recording.messageRenderers).toEqual(["alternatives-card-set"]) - expect(recording.shortcuts).toEqual(["ctrl+shift+b"]) + ]); + expect(recording.commandNames).toEqual([BRUNCH_WORKSPACE_COMMAND]); + expect(recording.messageRenderers).toEqual(['alternatives-card-set']); + expect(recording.shortcuts).toEqual(['ctrl+shift+b']); expect(recording.eventNames).toEqual([ - "session_start", - "before_agent_start", - "message_start", - "session_start", - "session_before_tree", - "session_before_fork", - "session_start", - "before_agent_start", - "tool_call", - "user_bash", - "before_agent_start", - "before_agent_start", - "session_start", - ]) + 'session_start', + 'before_agent_start', + 'message_start', + 'session_start', + 'session_before_tree', + 'session_before_fork', + 'session_start', + 'before_agent_start', + 'tool_call', + 'user_bash', + 'before_agent_start', + 'before_agent_start', + 'session_start', + ]); const sessionStartIndexes = recording.eventNames.flatMap((event, index) => - event === "session_start" ? [index] : [], - ) - expect(sessionStartIndexes[0]).toBeLessThan(sessionStartIndexes[1] ?? -1) - }) + event === 'session_start' ? [index] : [], + ); + expect(sessionStartIndexes[0]).toBeLessThan(sessionStartIndexes[1] ?? -1); + }); - it("does not retain the filesystem-discovery product-extension protocol", async () => { - const shell = await readFile( - join(projectRoot(), "src/.pi/pi-extension-shell.ts"), - "utf8", - ) - const discoveryExport = ["discover", "BrunchProductExtensionEntries"].join( - "", - ) - expect(shell).not.toContain(`export async function ${discoveryExport}`) - expect(shell).not.toContain("node:fs/promises") - expect(shell).not.toContain("pathToFileURL") + it('does not retain the filesystem-discovery product-extension protocol', async () => { + const shell = await readFile(join(projectRoot(), 'src/.pi/pi-extension-shell.ts'), 'utf8'); + const discoveryExport = ['discover', 'BrunchProductExtensionEntries'].join(''); + expect(shell).not.toContain(`export async function ${discoveryExport}`); + expect(shell).not.toContain('node:fs/promises'); + expect(shell).not.toContain('pathToFileURL'); const forbiddenExportNames = [ - ["brunch", "ExtensionMeta"].join(""), - ["register", "BrunchProductExtension"].join(""), - ] - const files = await listExtensionEntrypoints() + ['brunch', 'ExtensionMeta'].join(''), + ['register', 'BrunchProductExtension'].join(''), + ]; + const files = await listExtensionEntrypoints(); for (const file of files) { - const source = await readFile(file, "utf8") + const source = await readFile(file, 'utf8'); for (const exportName of forbiddenExportNames) { - expect(source, file).not.toContain(`export const ${exportName}`) - expect(source, file).not.toContain(`export function ${exportName}`) + expect(source, file).not.toContain(`export const ${exportName}`); + expect(source, file).not.toContain(`export function ${exportName}`); } } - }) -}) + }); +}); const brunchChromeFixture = { - cwd: "/tmp/brunch", - chatMode: "interactive" as const, - phase: "ready" as const, + cwd: '/tmp/brunch', + chatMode: 'interactive' as const, + phase: 'ready' as const, spec: { - id: "spec-1", - title: "Fixture spec", + id: 'spec-1', + title: 'Fixture spec', }, session: { - id: "session-1", - label: "Fixture session", + id: 'session-1', + label: 'Fixture session', }, -} +}; function createRecordingExtensionApi() { - const eventNames: string[] = [] - const toolNames: string[] = [] - const commandNames: string[] = [] - const shortcuts: string[] = [] - const messageRenderers: string[] = [] - const onSessionBoundary = async () => {} + const eventNames: string[] = []; + const toolNames: string[] = []; + const commandNames: string[] = []; + const shortcuts: string[] = []; + const messageRenderers: string[] = []; + const onSessionBoundary = async () => {}; const api = { on(eventName: string) { - eventNames.push(eventName) + eventNames.push(eventName); }, registerTool(tool: { name: string }) { - toolNames.push(tool.name) + toolNames.push(tool.name); }, registerCommand(name: string) { - commandNames.push(name) + commandNames.push(name); }, registerShortcut(name: string) { - shortcuts.push(name) + shortcuts.push(name); }, registerMessageRenderer(type: string) { - messageRenderers.push(type) + messageRenderers.push(type); }, sendMessage() {}, getAllTools: () => [ - "read", - "grep", - "find", - "ls", - "present_alternatives", + 'read', + 'grep', + 'find', + 'ls', + 'present_alternatives', PRESENT_QUESTION_TOOL, PRESENT_OPTIONS_TOOL, REQUEST_ANSWER_TOOL, REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, - "bash", - "edit", - "write", + 'bash', + 'edit', + 'write', ].map((name) => ({ name })), setActiveTools() {}, - } + }; return { api: api as never, eventNames, @@ -182,33 +171,33 @@ function createRecordingExtensionApi() { shortcuts, messageRenderers, onSessionBoundary, - } + }; } async function listExtensionEntrypoints(): Promise { - const extensionsDir = join(projectRoot(), "src/.pi/extensions") - const entries = await readdir(extensionsDir, { withFileTypes: true }) - const files: string[] = [] + const extensionsDir = join(projectRoot(), 'src/.pi/extensions'); + const entries = await readdir(extensionsDir, { withFileTypes: true }); + const files: string[] = []; for (const entry of entries) { - const path = join(extensionsDir, entry.name) - if (entry.isFile() && entry.name.endsWith(".ts")) files.push(path) + const path = join(extensionsDir, entry.name); + if (entry.isFile() && entry.name.endsWith('.ts')) files.push(path); if (entry.isDirectory()) { - const indexFile = join(path, "index.ts") - if (await fileExists(indexFile)) files.push(indexFile) + const indexFile = join(path, 'index.ts'); + if (await fileExists(indexFile)) files.push(indexFile); } } - return files + return files; } async function fileExists(file: string): Promise { try { - await access(file) - return true + await access(file); + return true; } catch { - return false + return false; } } function projectRoot(): string { - return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))) + return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))); } diff --git a/src/.pi/__tests__/graph-tools.test.ts b/src/.pi/__tests__/graph-tools.test.ts index f22c6439e..b1c8f31c5 100644 --- a/src/.pi/__tests__/graph-tools.test.ts +++ b/src/.pi/__tests__/graph-tools.test.ts @@ -7,229 +7,228 @@ * SPEC: D4-L, D20-L, D52-L, D53-L, I26-L, I34-L, A14-L */ -import { describe, beforeEach, it, expect } from "vitest" +import { describe, beforeEach, it, expect } from 'vitest'; -import { CommandExecutor } from "../../graph/command-executor.js" -import { getGraphOverview, getNodeNeighborhood } from "../../graph/snapshot.js" -import { createDb } from "../../db/connection.js" -import type { BrunchDb } from "../../db/connection.js" +import { createDb } from '../../db/connection.js'; +import type { BrunchDb } from '../../db/connection.js'; +import { CommandExecutor } from '../../graph/command-executor.js'; +import { getGraphOverview, getNodeNeighborhood } from '../../graph/snapshot.js'; import { translateCommitGraph, formatCommitGraphResult, formatGraphOverview, formatNeighborhoodResult, -} from "../extensions/graph/command-adapter.js" -import type { GraphSnapshotReaders } from "../extensions/graph/index.js" +} from '../extensions/graph/command-adapter.js'; +import type { GraphSnapshotReaders } from '../extensions/graph/index.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function createTestDb(): BrunchDb { - return createDb(":memory:") + return createDb(':memory:'); } function createSnapshots(db: BrunchDb): GraphSnapshotReaders { return { getGraphOverview: () => getGraphOverview(db), - getNodeNeighborhood: (nodeId, options) => - getNodeNeighborhood(db, nodeId, options), - } + getNodeNeighborhood: (nodeId, options) => getNodeNeighborhood(db, nodeId, options), + }; } // --------------------------------------------------------------------------- // command-adapter: translateCommitGraph // --------------------------------------------------------------------------- -describe("translateCommitGraph", () => { - it("translates flat tool params into CommitGraphInput", () => { +describe('translateCommitGraph', () => { + it('translates flat tool params into CommitGraphInput', () => { const input = translateCommitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "Test goal" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'Test goal' }, { - ref: "n2", - plane: "intent", - kind: "requirement", - title: "Test req", - body: "details", + ref: 'n2', + plane: 'intent', + kind: 'requirement', + title: 'Test req', + body: 'details', }, ], edges: [ - { category: "dependency", source: "n2", target: "n1" }, + { category: 'dependency', source: 'n2', target: 'n1' }, { - category: "support", + category: 'support', source: { existing: 42 }, - target: "n1", - stance: "for", + target: 'n1', + stance: 'for', }, ], - }) + }); - expect(input.nodes).toHaveLength(2) - expect(input.nodes[0]!.ref).toBe("n1") - expect(input.edges).toHaveLength(2) - expect(input.edges[0]!.source).toBe("n2") - expect(input.edges[1]!.source).toEqual({ existing: 42 }) - }) -}) + expect(input.nodes).toHaveLength(2); + expect(input.nodes[0]!.ref).toBe('n1'); + expect(input.edges).toHaveLength(2); + expect(input.edges[0]!.source).toBe('n2'); + expect(input.edges[1]!.source).toEqual({ existing: 42 }); + }); +}); // --------------------------------------------------------------------------- // command-adapter: formatCommitGraphResult // --------------------------------------------------------------------------- -describe("formatCommitGraphResult", () => { - it("formats success with node refs and edge ids", () => { +describe('formatCommitGraphResult', () => { + it('formats success with node refs and edge ids', () => { const text = formatCommitGraphResult({ - status: "success", + status: 'success', lsn: 5, nodes: { n1: 1, n2: 2 }, edges: [10, 11], - }) + }); - expect(text).toContain("Graph committed successfully") - expect(text).toContain("LSN 5") - expect(text).toContain("n1 → #1") - expect(text).toContain("#10") - }) + expect(text).toContain('Graph committed successfully'); + expect(text).toContain('LSN 5'); + expect(text).toContain('n1 → #1'); + expect(text).toContain('#10'); + }); - it("formats structural_illegal with diagnostics", () => { + it('formats structural_illegal with diagnostics', () => { const text = formatCommitGraphResult({ - status: "structural_illegal", + status: 'structural_illegal', diagnostics: [ - { field: "nodes[0].kind", message: '"invalid" is not a valid kind' }, - { field: "edges[0].stance", message: "stance required for proof" }, + { field: 'nodes[0].kind', message: '"invalid" is not a valid kind' }, + { field: 'edges[0].stance', message: 'stance required for proof' }, ], - }) + }); - expect(text).toContain("STRUCTURAL_ILLEGAL") - expect(text).toContain("nodes[0].kind") - expect(text).toContain("edges[0].stance") - }) -}) + expect(text).toContain('STRUCTURAL_ILLEGAL'); + expect(text).toContain('nodes[0].kind'); + expect(text).toContain('edges[0].stance'); + }); +}); // --------------------------------------------------------------------------- // command-adapter: formatGraphOverview // --------------------------------------------------------------------------- -describe("formatGraphOverview", () => { - it("reports empty graph", () => { +describe('formatGraphOverview', () => { + it('reports empty graph', () => { const text = formatGraphOverview({ nodes: [], edges: [], nodeCount: 0, edgeCount: 0, lsn: 0, - }) + }); - expect(text).toContain("empty") - }) -}) + expect(text).toContain('empty'); + }); +}); // --------------------------------------------------------------------------- // End-to-end: commit then read // --------------------------------------------------------------------------- -describe("graph tools end-to-end", () => { - let db: BrunchDb - let executor: CommandExecutor - let snapshots: GraphSnapshotReaders +describe('graph tools end-to-end', () => { + let db: BrunchDb; + let executor: CommandExecutor; + let snapshots: GraphSnapshotReaders; beforeEach(() => { - db = createTestDb() - executor = new CommandExecutor(db) - snapshots = createSnapshots(db) - }) + db = createTestDb(); + executor = new CommandExecutor(db); + snapshots = createSnapshots(db); + }); - it("commit_graph creates nodes and edges readable by read_graph", () => { + it('commit_graph creates nodes and edges readable by read_graph', () => { // Commit a small graph const input = translateCommitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "Build auth" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'Build auth' }, { - ref: "n2", - plane: "intent", - kind: "requirement", - title: "JWT tokens", + ref: 'n2', + plane: 'intent', + kind: 'requirement', + title: 'JWT tokens', }, ], - edges: [{ category: "dependency", source: "n2", target: "n1" }], - }) - const result = executor.commitGraph(input) - expect(result.status).toBe("success") + edges: [{ category: 'dependency', source: 'n2', target: 'n1' }], + }); + const result = executor.commitGraph(input); + expect(result.status).toBe('success'); // Read the graph - const overview = snapshots.getGraphOverview() - const text = formatGraphOverview(overview) + const overview = snapshots.getGraphOverview(); + const text = formatGraphOverview(overview); - expect(overview.nodeCount).toBe(2) - expect(overview.edgeCount).toBe(1) - expect(text).toContain("Build auth") - expect(text).toContain("JWT tokens") - expect(text).toContain("dependency") - }) + expect(overview.nodeCount).toBe(2); + expect(overview.edgeCount).toBe(1); + expect(text).toContain('Build auth'); + expect(text).toContain('JWT tokens'); + expect(text).toContain('dependency'); + }); - it("commit_graph returns diagnostics on invalid batch", () => { + it('commit_graph returns diagnostics on invalid batch', () => { const input = translateCommitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "not_a_kind", title: "Bad" }], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'not_a_kind', title: 'Bad' }], edges: [], - }) - const result = executor.commitGraph(input) - expect(result.status).toBe("structural_illegal") - - if (result.status === "structural_illegal") { - const text = formatCommitGraphResult(result) - expect(text).toContain("STRUCTURAL_ILLEGAL") - expect(text).toContain("not_a_kind") + }); + const result = executor.commitGraph(input); + expect(result.status).toBe('structural_illegal'); + + if (result.status === 'structural_illegal') { + const text = formatCommitGraphResult(result); + expect(text).toContain('STRUCTURAL_ILLEGAL'); + expect(text).toContain('not_a_kind'); } - }) + }); - it("commit_graph with edge validation failure rolls back nodes (I34-L)", () => { + it('commit_graph with edge validation failure rolls back nodes (I34-L)', () => { const input = translateCommitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "A goal" }], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'goal', title: 'A goal' }], edges: [ // stance required for proof but missing - { category: "proof", source: "n1", target: "n1" }, + { category: 'proof', source: 'n1', target: 'n1' }, ], - }) - const result = executor.commitGraph(input) - expect(result.status).toBe("structural_illegal") + }); + const result = executor.commitGraph(input); + expect(result.status).toBe('structural_illegal'); // Node should NOT have been created (all-or-nothing) - const overview = snapshots.getGraphOverview() - expect(overview.nodeCount).toBe(0) - }) + const overview = snapshots.getGraphOverview(); + expect(overview.nodeCount).toBe(0); + }); - it("read_graph neighborhood returns node details", () => { + it('read_graph neighborhood returns node details', () => { // Create a node first const input = translateCommitGraph({ nodes: [ { - ref: "n1", - plane: "intent", - kind: "goal", - title: "Main goal", - body: "A detailed goal", + ref: 'n1', + plane: 'intent', + kind: 'goal', + title: 'Main goal', + body: 'A detailed goal', }, ], edges: [], - }) - const commitResult = executor.commitGraph(input) - expect(commitResult.status).toBe("success") + }); + const commitResult = executor.commitGraph(input); + expect(commitResult.status).toBe('success'); - if (commitResult.status === "success") { - const nodeId = commitResult.nodes["n1"]! - const result = snapshots.getNodeNeighborhood(nodeId) - const text = formatNeighborhoodResult(result) + if (commitResult.status === 'success') { + const nodeId = commitResult.nodes['n1']!; + const result = snapshots.getNodeNeighborhood(nodeId); + const text = formatNeighborhoodResult(result); - expect(text).toContain("Main goal") - expect(text).toContain("A detailed goal") + expect(text).toContain('Main goal'); + expect(text).toContain('A detailed goal'); } - }) + }); - it("read_graph neighborhood for missing node returns not_found", () => { - const result = snapshots.getNodeNeighborhood(999) - const text = formatNeighborhoodResult(result) + it('read_graph neighborhood for missing node returns not_found', () => { + const result = snapshots.getNodeNeighborhood(999); + const text = formatNeighborhoodResult(result); - expect(text).toContain("not found") - }) -}) + expect(text).toContain('not found'); + }); +}); diff --git a/src/.pi/__tests__/mention-autocomplete.test.ts b/src/.pi/__tests__/mention-autocomplete.test.ts index f4f419b9d..70a1e8c00 100644 --- a/src/.pi/__tests__/mention-autocomplete.test.ts +++ b/src/.pi/__tests__/mention-autocomplete.test.ts @@ -1,143 +1,123 @@ -import type { ExtensionContext } from "@earendil-works/pi-coding-agent" - -import { describe, expect, it } from "vitest" +import type { ExtensionContext } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; import { extractHashPrefix, registerBrunchMentionAutocomplete, type GraphMentionSource, -} from "../extensions/mention-autocomplete.js" +} from '../extensions/mention-autocomplete.js'; -describe("Brunch mention autocomplete", () => { - it("adds graph mention prompt guidance", async () => { - const beforeAgentStart: Array<( - event: { systemPrompt: string }, - ctx: FakeExtensionContext, - ) => Promise | unknown> = [] +describe('Brunch mention autocomplete', () => { + it('adds graph mention prompt guidance', async () => { + const beforeAgentStart: Array< + (event: { systemPrompt: string }, ctx: FakeExtensionContext) => Promise | unknown + > = []; registerBrunchMentionAutocomplete({ on: (event: string, handler: never) => { - if (event === "before_agent_start") beforeAgentStart.push(handler) + if (event === 'before_agent_start') beforeAgentStart.push(handler); }, - } as never) + } as never); const promptUpdates = await Promise.all( - beforeAgentStart.map((handler) => - Promise.resolve(handler({ systemPrompt: "base" }, fakeContext())), - ), - ) + beforeAgentStart.map((handler) => Promise.resolve(handler({ systemPrompt: 'base' }, fakeContext()))), + ); expect( promptUpdates.some( (update) => - typeof update === "object" && + typeof update === 'object' && update !== null && - "systemPrompt" in update && - String(update.systemPrompt).includes("Brunch graph mention handles"), + 'systemPrompt' in update && + String(update.systemPrompt).includes('Brunch graph mention handles'), ), - ).toBe(true) - }) + ).toBe(true); + }); - it("registers graph-code mention autocomplete without fixture tag JSON", async () => { - let providerFactory: (( - current: FakeAutocompleteProvider, - ) => FakeAutocompleteProvider) | undefined + it('registers graph-code mention autocomplete without fixture tag JSON', async () => { + let providerFactory: ((current: FakeAutocompleteProvider) => FakeAutocompleteProvider) | undefined; const source: GraphMentionSource = { listMentionCandidates: () => [ { - code: "D12", - title: "Command containment", - description: "Blocks branchy Pi flows", - plane: "design", + code: 'D12', + title: 'Command containment', + description: 'Blocks branchy Pi flows', + plane: 'design', }, - { code: "I9", title: "Mention ledger", plane: "intent" }, + { code: 'I9', title: 'Mention ledger', plane: 'intent' }, ], - } + }; registerBrunchMentionAutocomplete( { on: (event: string, handler: (event: never, ctx: never) => unknown) => { - if (event === "session_start") { - void handler({} as never, { - ui: { - addAutocompleteProvider: (factory: typeof providerFactory) => { - providerFactory = factory + if (event === 'session_start') { + void handler( + {} as never, + { + ui: { + addAutocompleteProvider: (factory: typeof providerFactory) => { + providerFactory = factory; + }, }, - }, - } as never) + } as never, + ); } }, } as never, source, - ) + ); const fallback: FakeAutocompleteProvider = { - getSuggestions: async () => ({ items: [], prefix: "" }), + getSuggestions: async () => ({ items: [], prefix: '' }), applyCompletion: (lines) => ({ lines, cursorLine: 0, cursorCol: 0 }), shouldTriggerFileCompletion: () => true, - } - const provider = providerFactory?.(fallback) + }; + const provider = providerFactory?.(fallback); - expect(extractHashPrefix("See #D1", 7)).toBe("#D1") - await expect( - provider?.getSuggestions(["See #D1"], 0, 7, {} as never), - ).resolves.toEqual({ - prefix: "#D1", + expect(extractHashPrefix('See #D1', 7)).toBe('#D1'); + await expect(provider?.getSuggestions(['See #D1'], 0, 7, {} as never)).resolves.toEqual({ + prefix: '#D1', items: [ { - value: "#D12", - label: "#D12 Command containment", - description: "Blocks branchy Pi flows", + value: '#D12', + label: '#D12 Command containment', + description: 'Blocks branchy Pi flows', }, ], - }) + }); expect( - provider?.applyCompletion( - ["See #D"], - 0, - 6, - { value: "#D12", label: "#D12 Command containment" }, - "#D", - ), - ).toEqual({ lines: ["See #D12"], cursorLine: 0, cursorCol: 8 }) - }) -}) + provider?.applyCompletion(['See #D'], 0, 6, { value: '#D12', label: '#D12 Command containment' }, '#D'), + ).toEqual({ lines: ['See #D12'], cursorLine: 0, cursorCol: 8 }); + }); +}); function fakeContext(): FakeExtensionContext { return { sessionManager: { getEntries: () => [], - } as unknown as FakeExtensionContext["sessionManager"], + } as unknown as FakeExtensionContext['sessionManager'], ui: {} as never, - } + }; } -type FakeExtensionContext = Pick & { - ui: unknown -} +type FakeExtensionContext = Pick & { + ui: unknown; +}; interface FakeAutocompleteItem { - value: string - label: string + value: string; + label: string; } interface FakeAutocompleteProvider { - getSuggestions( - lines: string[], - cursorLine: number, - cursorCol: number, - options: never, - ): Promise + getSuggestions(lines: string[], cursorLine: number, cursorCol: number, options: never): Promise; applyCompletion( lines: string[], cursorLine: number, cursorCol: number, item: FakeAutocompleteItem, prefix: string, - ): unknown - shouldTriggerFileCompletion( - lines: string[], - cursorLine: number, - cursorCol: number, - ): boolean + ): unknown; + shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean; } diff --git a/src/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts index 11f86ce14..d610167d8 100644 --- a/src/.pi/__tests__/operational-mode.test.ts +++ b/src/.pi/__tests__/operational-mode.test.ts @@ -1,10 +1,9 @@ -import { mkdtemp } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" +import { mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { describe, expect, it } from "vitest" - -import { SessionManager } from "@earendil-works/pi-coding-agent" +import { SessionManager } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; import { BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, @@ -15,337 +14,318 @@ import { registerBrunchOperationalModePolicy, type BrunchAgentState, type BrunchAgentStateEntryData, -} from "../extensions/operational-mode.js" +} from '../extensions/operational-mode.js'; -function runtimeEntry( - state: BrunchAgentState, - data: Record = {}, -) { +function runtimeEntry(state: BrunchAgentState, data: Record = {}) { return { - type: "custom", + type: 'custom', customType: BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, data: { schemaVersion: 1, - reason: "switch", + reason: 'switch', state, - source: "user", + source: 'user', ...data, }, - } + }; } class FakeRuntimeStateSessionManager { entries: Array<{ - type: "custom" - customType: string - data: BrunchAgentStateEntryData - }> = [] + type: 'custom'; + customType: string; + data: BrunchAgentStateEntryData; + }> = []; getEntries() { - return this.entries + return this.entries; } appendCustomEntry(customType: string, data: BrunchAgentStateEntryData) { - this.entries.push({ type: "custom", customType, data }) - return `entry-${this.entries.length}` + this.entries.push({ type: 'custom', customType, data }); + return `entry-${this.entries.length}`; } } -describe("Brunch agent runtime-state projection", () => { - it("projects the deterministic elicit/elicitor default when no runtime entries exist", () => { +describe('Brunch agent runtime-state projection', () => { + it('projects the deterministic elicit/elicitor default when no runtime entries exist', () => { expect(projectBrunchAgentState([])).toMatchObject({ ...DEFAULT_BRUNCH_AGENT_STATE, operationalModeDefinition: { - id: "elicit", - defaultRole: "elicitor", - toolPolicyId: "elicit-read-only", + id: 'elicit', + defaultRole: 'elicitor', + toolPolicyId: 'elicit-read-only', }, agentRoleDefinition: { - id: "elicitor", - operationalMode: "elicit", + id: 'elicitor', + operationalMode: 'elicit', defaultStrategy: DEFAULT_BRUNCH_AGENT_STATE.agentStrategy, defaultLens: DEFAULT_BRUNCH_AGENT_STATE.agentLens, }, - }) - }) + }); + }); - it("uses the last valid runtime-state snapshot without mutating earlier transcript entries", () => { - const first = runtimeEntry(DEFAULT_BRUNCH_AGENT_STATE) + it('uses the last valid runtime-state snapshot without mutating earlier transcript entries', () => { + const first = runtimeEntry(DEFAULT_BRUNCH_AGENT_STATE); const latestState: BrunchAgentState = { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } - const latest = runtimeEntry(latestState) - - expect(projectBrunchAgentState([first, latest])).toMatchObject(latestState) - expect(first.data.state).toEqual(DEFAULT_BRUNCH_AGENT_STATE) - }) - - it("ignores malformed and invalid runtime entries instead of guessing", () => { - const valid = runtimeEntry(DEFAULT_BRUNCH_AGENT_STATE) + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; + const latest = runtimeEntry(latestState); + + expect(projectBrunchAgentState([first, latest])).toMatchObject(latestState); + expect(first.data.state).toEqual(DEFAULT_BRUNCH_AGENT_STATE); + }); + + it('ignores malformed and invalid runtime entries instead of guessing', () => { + const valid = runtimeEntry(DEFAULT_BRUNCH_AGENT_STATE); const invalidCombination = runtimeEntry({ schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "not-a-strategy", - agentLens: "step-by-step", - } as unknown as BrunchAgentState) + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'not-a-strategy', + agentLens: 'step-by-step', + } as unknown as BrunchAgentState); const malformed = { - type: "custom", + type: 'custom', customType: BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, - data: { schemaVersion: 1, reason: "switch", source: "user" }, - } + data: { schemaVersion: 1, reason: 'switch', source: 'user' }, + }; - expect( - projectBrunchAgentState([valid, invalidCombination, malformed]), - ).toMatchObject(DEFAULT_BRUNCH_AGENT_STATE) - }) + expect(projectBrunchAgentState([valid, invalidCombination, malformed])).toMatchObject( + DEFAULT_BRUNCH_AGENT_STATE, + ); + }); - it("applies resolved elicit state to active tools, prompt, and blockers", async () => { + it('applies resolved elicit state to active tools, prompt, and blockers', async () => { const latestState: BrunchAgentState = { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } - const events: Record unknown> = {} - const activeTools: string[][] = [] + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; + const events: Record unknown> = {}; + const activeTools: string[][] = []; registerBrunchOperationalModePolicy({ registerTool: (_tool: { name: string }) => {}, getAllTools: () => [ - "read", - "grep", - "find", - "ls", - "structured_exchange", - "present_alternatives", - "bash", - "edit", - "write", + 'read', + 'grep', + 'find', + 'ls', + 'structured_exchange', + 'present_alternatives', + 'bash', + 'edit', + 'write', ].map((name) => ({ name, })), setActiveTools: (tools: string[]) => activeTools.push(tools), on: (event: string, handler: (event: never, ctx?: never) => unknown) => { - events[event] = handler + events[event] = handler; }, - } as never) + } as never); const promptResult = await Promise.resolve( - events.before_agent_start?.({ systemPrompt: "base" } as never, { - sessionManager: { - getEntries: () => [runtimeEntry(latestState)], - }, - } as never), - ) + events.before_agent_start?.( + { systemPrompt: 'base' } as never, + { + sessionManager: { + getEntries: () => [runtimeEntry(latestState)], + }, + } as never, + ), + ); expect(activeTools).toEqual([ - [ - "read", - "grep", - "find", - "ls", - "structured_exchange", - "present_alternatives", - ], - ]) - expect(promptResult).toBeUndefined() - for (const toolName of ["bash", "edit", "write"]) { - await expect( - Promise.resolve(events.tool_call?.({ toolName } as never)), - ).resolves.toMatchObject({ + ['read', 'grep', 'find', 'ls', 'structured_exchange', 'present_alternatives'], + ]); + expect(promptResult).toBeUndefined(); + for (const toolName of ['bash', 'edit', 'write']) { + await expect(Promise.resolve(events.tool_call?.({ toolName } as never))).resolves.toMatchObject({ block: true, - reason: expect.stringContaining( - `Brunch tool policy blocks "${toolName}"`, - ), - }) + reason: expect.stringContaining(`Brunch tool policy blocks "${toolName}"`), + }); } await expect( - Promise.resolve( - events.tool_call?.({ toolName: "structured_exchange" } as never), - ), - ).resolves.toBeUndefined() - expect(events.user_bash?.({ command: "rm -rf ." } as never)).toMatchObject({ + Promise.resolve(events.tool_call?.({ toolName: 'structured_exchange' } as never)), + ).resolves.toBeUndefined(); + expect(events.user_bash?.({ command: 'rm -rf .' } as never)).toMatchObject({ result: { exitCode: 1, - output: "Brunch tool policy blocks shell commands: rm -rf .", + output: 'Brunch tool policy blocks shell commands: rm -rf .', }, - }) - }) + }); + }); - it("appends init only when the transcript has no valid runtime state", () => { - const manager = new FakeRuntimeStateSessionManager() + it('appends init only when the transcript has no valid runtime state', () => { + const manager = new FakeRuntimeStateSessionManager(); - expect(appendBrunchAgentRuntimeInit(manager)).toBe("entry-1") - expect(appendBrunchAgentRuntimeInit(manager)).toBeUndefined() - expect(manager.entries).toHaveLength(1) + expect(appendBrunchAgentRuntimeInit(manager)).toBe('entry-1'); + expect(appendBrunchAgentRuntimeInit(manager)).toBeUndefined(); + expect(manager.entries).toHaveLength(1); expect(manager.entries[0]?.data).toEqual({ schemaVersion: 1, - reason: "init", + reason: 'init', state: DEFAULT_BRUNCH_AGENT_STATE, - source: "extension", - }) - }) + source: 'extension', + }); + }); - it("appends validated runtime switches as full state snapshots", () => { - const manager = new FakeRuntimeStateSessionManager() - appendBrunchAgentRuntimeInit(manager) + it('appends validated runtime switches as full state snapshots', () => { + const manager = new FakeRuntimeStateSessionManager(); + appendBrunchAgentRuntimeInit(manager); const latestState: BrunchAgentState = { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; - expect(appendBrunchAgentRuntimeSwitch(manager, latestState, "user")).toBe( - "entry-2", - ) + expect(appendBrunchAgentRuntimeSwitch(manager, latestState, 'user')).toBe('entry-2'); expect(manager.entries[1]?.data).toEqual({ schemaVersion: 1, - reason: "switch", + reason: 'switch', state: latestState, previous: DEFAULT_BRUNCH_AGENT_STATE, - source: "user", - }) - expect(projectBrunchAgentState(manager.getEntries())).toMatchObject( - latestState, - ) - }) + source: 'user', + }); + expect(projectBrunchAgentState(manager.getEntries())).toMatchObject(latestState); + }); - it("rejects invalid runtime switch combinations before appending", () => { - const manager = new FakeRuntimeStateSessionManager() + it('rejects invalid runtime switch combinations before appending', () => { + const manager = new FakeRuntimeStateSessionManager(); for (const invalidState of [ { schemaVersion: 1, - operationalMode: "execute", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "step-by-step", + operationalMode: 'execute', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "reviewer", - agentStrategy: "step-by-step", - agentLens: "step-by-step", + operationalMode: 'elicit', + agentRole: 'reviewer', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "not-a-strategy", - agentLens: "step-by-step", + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'not-a-strategy', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "not-a-lens", + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'not-a-lens', }, ]) { expect(() => - appendBrunchAgentRuntimeSwitch( - manager, - invalidState as unknown as BrunchAgentState, - ), - ).toThrow("Invalid BrunchAgentState runtime selection.") + appendBrunchAgentRuntimeSwitch(manager, invalidState as unknown as BrunchAgentState), + ).toThrow('Invalid BrunchAgentState runtime selection.'); } - expect(manager.entries).toEqual([]) - }) + expect(manager.entries).toEqual([]); + }); - it("does not project invalid runtime mode, role, strategy, or lens entries", () => { + it('does not project invalid runtime mode, role, strategy, or lens entries', () => { for (const invalidState of [ { schemaVersion: 1, - operationalMode: "execute", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "step-by-step", + operationalMode: 'execute', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "reviewer", - agentStrategy: "step-by-step", - agentLens: "step-by-step", + operationalMode: 'elicit', + agentRole: 'reviewer', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "not-a-strategy", - agentLens: "step-by-step", + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'not-a-strategy', + agentLens: 'step-by-step', }, { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "not-a-lens", + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'not-a-lens', }, ]) { expect( - projectBrunchAgentState([ - runtimeEntry(invalidState as unknown as BrunchAgentState), - ]), - ).toMatchObject(DEFAULT_BRUNCH_AGENT_STATE) + projectBrunchAgentState([runtimeEntry(invalidState as unknown as BrunchAgentState)]), + ).toMatchObject(DEFAULT_BRUNCH_AGENT_STATE); } - }) + }); - it("appends runtime init from the extension session-start hook", async () => { - const manager = new FakeRuntimeStateSessionManager() - const events: Record unknown> = {} + it('appends runtime init from the extension session-start hook', async () => { + const manager = new FakeRuntimeStateSessionManager(); + const events: Record unknown> = {}; registerBrunchOperationalModePolicy({ registerTool: (_tool: { name: string }) => {}, - getAllTools: () => ["read"].map((name) => ({ name })), + getAllTools: () => ['read'].map((name) => ({ name })), setActiveTools: (_tools: string[]) => {}, on: (event: string, handler: (event: never, ctx?: never) => unknown) => { - events[event] = handler + events[event] = handler; }, - } as never) + } as never); - await events.session_start?.({} as never, { - sessionManager: manager, - } as never) + await events.session_start?.( + {} as never, + { + sessionManager: manager, + } as never, + ); - expect(manager.entries[0]?.data.reason).toBe("init") - }) + expect(manager.entries[0]?.data.reason).toBe('init'); + }); - it("reprojects runtime-state snapshots after Pi JSONL reload", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-agent-state-")) - const sessionDir = join(cwd, ".brunch", "sessions") - const manager = SessionManager.create(cwd, sessionDir) + it('reprojects runtime-state snapshots after Pi JSONL reload', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-agent-state-')); + const sessionDir = join(cwd, '.brunch', 'sessions'); + const manager = SessionManager.create(cwd, sessionDir); const latestState: BrunchAgentState = { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; manager.appendCustomEntry(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, { schemaVersion: 1, - reason: "init", + reason: 'init', state: DEFAULT_BRUNCH_AGENT_STATE, - source: "extension", - }) + source: 'extension', + }); manager.appendMessage({ - role: "assistant", - content: [{ type: "text", text: "runtime initialized" }], - api: "test", - provider: "test", - model: "test", + role: 'assistant', + content: [{ type: 'text', text: 'runtime initialized' }], + api: 'test', + provider: 'test', + model: 'test', usage: { input: 0, output: 0, @@ -360,21 +340,19 @@ describe("Brunch agent runtime-state projection", () => { total: 0, }, }, - stopReason: "stop", + stopReason: 'stop', timestamp: Date.now(), - } as never) + } as never); manager.appendCustomEntry(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, { schemaVersion: 1, - reason: "switch", + reason: 'switch', state: latestState, previous: DEFAULT_BRUNCH_AGENT_STATE, - source: "user", - }) + source: 'user', + }); - const reloaded = SessionManager.open(manager.getSessionFile()!, sessionDir) + const reloaded = SessionManager.open(manager.getSessionFile()!, sessionDir); - expect(projectBrunchAgentState(reloaded.getEntries())).toMatchObject( - latestState, - ) - }) -}) + expect(projectBrunchAgentState(reloaded.getEntries())).toMatchObject(latestState); + }); +}); diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index c1d4b92a3..610bda7ba 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -1,10 +1,10 @@ -import { readFile } from "node:fs/promises" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { composeBrunchPrompt } from "../context/compose-brunch-prompt.js" +import { composeBrunchPrompt } from '../context/compose-brunch-prompt.js'; import { BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, DEFAULT_BRUNCH_AGENT_STATE, @@ -12,204 +12,196 @@ import { type BrunchAgentState, type BrunchAgentStateEntryData, registerBrunchOperationalModePolicy, -} from "../extensions/operational-mode.js" -import { registerBrunchPrompting } from "../extensions/prompting.js" -import { createBrunchPiExtensionShell } from "../pi-extension-shell.js" +} from '../extensions/operational-mode.js'; +import { registerBrunchPrompting } from '../extensions/prompting.js'; +import { createBrunchPiExtensionShell } from '../pi-extension-shell.js'; function runtimeEntry(state: BrunchAgentState) { return { - type: "custom", - customType: "brunch.agent_runtime_state", + type: 'custom', + customType: 'brunch.agent_runtime_state', data: { schemaVersion: 1, - reason: "switch", + reason: 'switch', state, - source: "user", + source: 'user', }, - } + }; } class FakeRuntimeStateSessionManager { entries: Array<{ - type: "custom" - customType: string - data: BrunchAgentStateEntryData - }> = [] + type: 'custom'; + customType: string; + data: BrunchAgentStateEntryData; + }> = []; getEntries() { - return this.entries + return this.entries; } appendCustomEntry(customType: string, data: BrunchAgentStateEntryData) { - this.entries.push({ type: "custom", customType, data }) - return `entry-${this.entries.length}` + this.entries.push({ type: 'custom', customType, data }); + return `entry-${this.entries.length}`; } } -describe("Brunch prompt-pack topology", () => { - it("composes deterministic private prompt packs in stable order", () => { +describe('Brunch prompt-pack topology', () => { + it('composes deterministic private prompt packs in stable order', () => { const result = composeBrunchPrompt({ - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "step-by-step", - activeTools: ["read", "grep", "present_options"], - }) + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', + activeTools: ['read', 'grep', 'present_options'], + }); expect(result.packIds).toEqual([ - "brunch-base", - "elicit", - "elicitor", - "structured-exchange", - "candidate-proposals", - "capture-analysis", - ]) - expect(result.prompt).toContain("[Brunch agent state]") - expect(result.prompt).toContain("Operational mode: elicit.") - expect(result.prompt).toContain("Agent role: elicitor.") - expect(result.prompt).toContain( - "Brunch exposes only elicit-safe tools: read, grep, present_options.", - ) - expect(result.prompt.indexOf("# Brunch base")).toBeLessThan( - result.prompt.indexOf("# Operational mode: elicit"), - ) - expect(result.prompt.indexOf("# Structured exchanges")).toBeLessThan( - result.prompt.indexOf("# Candidate proposals"), - ) - expect(result.prompt).toContain( - "Request outcomes are an exactly-one property-presence union", - ) - expect(result.prompt).toContain( - "`graph_refs` are per-candidate and strictly existing graph node references", - ) + 'brunch-base', + 'elicit', + 'elicitor', + 'structured-exchange', + 'candidate-proposals', + 'capture-analysis', + ]); + expect(result.prompt).toContain('[Brunch agent state]'); + expect(result.prompt).toContain('Operational mode: elicit.'); + expect(result.prompt).toContain('Agent role: elicitor.'); + expect(result.prompt).toContain('Brunch exposes only elicit-safe tools: read, grep, present_options.'); + expect(result.prompt.indexOf('# Brunch base')).toBeLessThan( + result.prompt.indexOf('# Operational mode: elicit'), + ); + expect(result.prompt.indexOf('# Structured exchanges')).toBeLessThan( + result.prompt.indexOf('# Candidate proposals'), + ); + expect(result.prompt).toContain('Request outcomes are an exactly-one property-presence union'); expect(result.prompt).toContain( - "Capture is transcript-native analysis, not graph mutation.", - ) - expect(result.prompt).not.toContain("CommandExecutor result shapes") - }) + '`graph_refs` are per-candidate and strictly existing graph node references', + ); + expect(result.prompt).toContain('Capture is transcript-native analysis, not graph mutation.'); + expect(result.prompt).not.toContain('CommandExecutor result shapes'); + }); - it("appends composed Brunch prompting from runtime-state projection", async () => { + it('appends composed Brunch prompting from runtime-state projection', async () => { const latestState: BrunchAgentState = { ...DEFAULT_BRUNCH_AGENT_STATE, - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } - const events: Record unknown> = {} + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; + const events: Record unknown> = {}; registerBrunchPrompting({ on: (event: string, handler: (event: never, ctx?: never) => unknown) => { - events[event] = handler + events[event] = handler; }, getAllTools: () => - ["read", "grep", "bash", "write", "present_options"].map((name) => ({ + ['read', 'grep', 'bash', 'write', 'present_options'].map((name) => ({ name, })), - } as never) + } as never); const result = await Promise.resolve( - events.before_agent_start?.({ systemPrompt: "base" } as never, { - sessionManager: { - getEntries: () => [runtimeEntry(latestState)], - }, - } as never), - ) + events.before_agent_start?.( + { systemPrompt: 'base' } as never, + { + sessionManager: { + getEntries: () => [runtimeEntry(latestState)], + }, + } as never, + ), + ); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining("base\n\n[Brunch agent state]"), - }) + systemPrompt: expect.stringContaining('base\n\n[Brunch agent state]'), + }); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining( - "Agent strategy: disambiguate-via-examples.", - ), - }) + systemPrompt: expect.stringContaining('Agent strategy: disambiguate-via-examples.'), + }); expect(result).toMatchObject({ systemPrompt: expect.stringContaining( - "Brunch exposes only elicit-safe tools: read, grep, present_options.", + 'Brunch exposes only elicit-safe tools: read, grep, present_options.', ), - }) - }) + }); + }); - it("derives prompt and active tools from the same transcript-backed runtime state", async () => { - const manager = new FakeRuntimeStateSessionManager() - const events: Record unknown>> = {} - const activeTools: string[][] = [] + it('derives prompt and active tools from the same transcript-backed runtime state', async () => { + const manager = new FakeRuntimeStateSessionManager(); + const events: Record unknown>> = {}; + const activeTools: string[][] = []; const pi = { on: (event: string, handler: (event: never, ctx?: never) => unknown) => { - events[event] ??= [] - events[event].push(handler) + events[event] ??= []; + events[event].push(handler); }, registerTool: (_tool: { name: string }) => {}, getAllTools: () => - ["read", "grep", "bash", "edit", "write", "present_options"].map( - (name) => ({ name }), - ), + ['read', 'grep', 'bash', 'edit', 'write', 'present_options'].map((name) => ({ name })), setActiveTools: (tools: string[]) => activeTools.push(tools), - } - registerBrunchOperationalModePolicy(pi as never) - registerBrunchPrompting(pi as never) + }; + registerBrunchOperationalModePolicy(pi as never); + registerBrunchPrompting(pi as never); for (const handler of events.session_start ?? []) { - await handler({} as never, { sessionManager: manager } as never) + await handler({} as never, { sessionManager: manager } as never); } const defaultPromptResults = await Promise.all( (events.before_agent_start ?? []).map((handler) => Promise.resolve( - handler({ systemPrompt: "base" } as never, { - sessionManager: manager, - } as never), + handler( + { systemPrompt: 'base' } as never, + { + sessionManager: manager, + } as never, + ), ), ), - ) + ); const latestState: BrunchAgentState = { ...DEFAULT_BRUNCH_AGENT_STATE, - agentStrategy: "disambiguate-via-examples", - agentLens: "disambiguate-via-examples", - } - appendBrunchAgentRuntimeSwitch(manager, latestState, "user") + agentStrategy: 'disambiguate-via-examples', + agentLens: 'disambiguate-via-examples', + }; + appendBrunchAgentRuntimeSwitch(manager, latestState, 'user'); const switchedPromptResults = await Promise.all( (events.before_agent_start ?? []).map((handler) => Promise.resolve( - handler({ systemPrompt: "base" } as never, { - sessionManager: manager, - } as never), + handler( + { systemPrompt: 'base' } as never, + { + sessionManager: manager, + } as never, + ), ), ), - ) - const defaultPrompt = defaultPromptResults.find(Boolean) - const switchedPrompt = switchedPromptResults.find(Boolean) + ); + const defaultPrompt = defaultPromptResults.find(Boolean); + const switchedPrompt = switchedPromptResults.find(Boolean); - expect(manager.entries[0]?.customType).toBe( - BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, - ) + expect(manager.entries[0]?.customType).toBe(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE); expect(activeTools).toEqual([ - ["read", "grep", "present_options"], - ["read", "grep", "present_options"], - ["read", "grep", "present_options"], - ]) + ['read', 'grep', 'present_options'], + ['read', 'grep', 'present_options'], + ['read', 'grep', 'present_options'], + ]); expect(defaultPrompt).toMatchObject({ - systemPrompt: expect.stringContaining("Agent strategy: step-by-step."), - }) + systemPrompt: expect.stringContaining('Agent strategy: step-by-step.'), + }); expect(switchedPrompt).toMatchObject({ - systemPrompt: expect.stringContaining( - "Agent strategy: disambiguate-via-examples.", - ), - }) - }) + systemPrompt: expect.stringContaining('Agent strategy: disambiguate-via-examples.'), + }); + }); - it("is registered by the explicit shell after operational-mode policy", async () => { - const eventNames: string[] = [] + it('is registered by the explicit shell after operational-mode policy', async () => { + const eventNames: string[] = []; await createBrunchPiExtensionShell( { - cwd: "/tmp/brunch", - chatMode: "interactive", - phase: "ready", - spec: { id: "spec-1", title: "Spec" }, - session: { id: "session-1", label: "Session" }, + cwd: '/tmp/brunch', + chatMode: 'interactive', + phase: 'ready', + spec: { id: 'spec-1', title: 'Spec' }, + session: { id: 'session-1', label: 'Session' }, }, undefined, { @@ -223,43 +215,34 @@ describe("Brunch prompt-pack topology", () => { registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, - getAllTools: () => ["read", "bash", "write"].map((name) => ({ name })), + getAllTools: () => ['read', 'bash', 'write'].map((name) => ({ name })), setActiveTools() {}, - } as never) + } as never); - const operationalToolPolicyIndex = eventNames.indexOf("tool_call") - const userBashPolicyIndex = eventNames.indexOf("user_bash") - const promptingIndex = eventNames.indexOf( - "before_agent_start", - userBashPolicyIndex + 1, - ) - const nextBeforeAgentStartIndex = eventNames.indexOf( - "before_agent_start", - promptingIndex + 1, - ) + const operationalToolPolicyIndex = eventNames.indexOf('tool_call'); + const userBashPolicyIndex = eventNames.indexOf('user_bash'); + const promptingIndex = eventNames.indexOf('before_agent_start', userBashPolicyIndex + 1); + const nextBeforeAgentStartIndex = eventNames.indexOf('before_agent_start', promptingIndex + 1); - expect(operationalToolPolicyIndex).toBeGreaterThan(-1) - expect(userBashPolicyIndex).toBeGreaterThan(operationalToolPolicyIndex) - expect(promptingIndex).toBeGreaterThan(userBashPolicyIndex) - expect(promptingIndex).toBeLessThan(nextBeforeAgentStartIndex) - }) + expect(operationalToolPolicyIndex).toBeGreaterThan(-1); + expect(userBashPolicyIndex).toBeGreaterThan(operationalToolPolicyIndex); + expect(promptingIndex).toBeGreaterThan(userBashPolicyIndex); + expect(promptingIndex).toBeLessThan(nextBeforeAgentStartIndex); + }); - it("does not expose private prompt packs through Pi resource discovery", async () => { + it('does not expose private prompt packs through Pi resource discovery', async () => { const [promptingSource, composerSource] = await Promise.all([ - readFile(join(projectRoot(), "src/.pi/extensions/prompting.ts"), "utf8"), - readFile( - join(projectRoot(), "src/.pi/context/compose-brunch-prompt.ts"), - "utf8", - ), - ]) + readFile(join(projectRoot(), 'src/.pi/extensions/prompting.ts'), 'utf8'), + readFile(join(projectRoot(), 'src/.pi/context/compose-brunch-prompt.ts'), 'utf8'), + ]); - expect(promptingSource).not.toContain("resources_discover") - expect(promptingSource).not.toContain("promptPaths") - expect(composerSource).not.toContain("resources_discover") - expect(composerSource).not.toContain("promptPaths") - }) -}) + expect(promptingSource).not.toContain('resources_discover'); + expect(promptingSource).not.toContain('promptPaths'); + expect(composerSource).not.toContain('resources_discover'); + expect(composerSource).not.toContain('promptPaths'); + }); +}); function projectRoot(): string { - return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))) + return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))); } diff --git a/src/.pi/__tests__/structured-exchange-extension.test.ts b/src/.pi/__tests__/structured-exchange-extension.test.ts index 1639d7678..5cc5442a5 100644 --- a/src/.pi/__tests__/structured-exchange-extension.test.ts +++ b/src/.pi/__tests__/structured-exchange-extension.test.ts @@ -1,70 +1,61 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; import registerStructuredExchange, { PRESENT_OPTIONS_TOOL, REQUEST_CHOICE_TOOL, -} from "../extensions/structured-exchange/index.js" +} from '../extensions/structured-exchange/index.js'; -const ansiPattern = new RegExp( - `${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, - "g", -) +const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g'); function stripAnsi(text: string): string { - return text.replace(ansiPattern, "") + return text.replace(ansiPattern, ''); } function registerTools() { - const tools = new Map() + const tools = new Map(); registerStructuredExchange({ registerTool(definition: any) { - tools.set(definition.name, definition) + tools.set(definition.name, definition); }, - } as any) - return tools + } as any); + return tools; } const theme = { fg: (_color: string, text: string) => text, bg: (_color: string, text: string) => text, bold: (text: string) => text, -} +}; -describe("structured exchange renderers", () => { - it("keeps renderCall non-semantic for present/request tools", () => { - const tools = registerTools() - const present = tools.get(PRESENT_OPTIONS_TOOL) - const request = tools.get(REQUEST_CHOICE_TOOL) +describe('structured exchange renderers', () => { + it('keeps renderCall non-semantic for present/request tools', () => { + const tools = registerTools(); + const present = tools.get(PRESENT_OPTIONS_TOOL); + const request = tools.get(REQUEST_CHOICE_TOOL); - expect( - stripAnsi(present.renderCall({}, theme, {}).render(80).join("\n")), - ).toBe("") - expect( - stripAnsi(request.renderCall({}, theme, {}).render(80).join("\n")), - ).toBe("") - }) + expect(stripAnsi(present.renderCall({}, theme, {}).render(80).join('\n'))).toBe(''); + expect(stripAnsi(request.renderCall({}, theme, {}).render(80).join('\n'))).toBe(''); + }); - it("renders present_options from tool result markdown content", async () => { - const present = registerTools().get(PRESENT_OPTIONS_TOOL) + it('renders present_options from tool result markdown content', async () => { + const present = registerTools().get(PRESENT_OPTIONS_TOOL); const result = await present.execute( - "call-1", + 'call-1', { - exchangeId: "x-1", - heading: "Choose", - body: "Body text", - options: [{ id: "a", content: "Alpha", rationale: "First" }], + exchangeId: 'x-1', + heading: 'Choose', + body: 'Body text', + options: [{ id: 'a', content: 'Alpha', rationale: 'First' }], }, undefined, undefined, {} as never, - ) - - const rendered = stripAnsi( - present.renderResult(result, {}, theme, {}).render(80).join("\n"), - ) - expect(rendered).toContain("Choose") - expect(rendered).toContain("Alpha") - expect(rendered).toContain("First") - }) -}) + ); + + const rendered = stripAnsi(present.renderResult(result, {}, theme, {}).render(80).join('\n')); + expect(rendered).toContain('Choose'); + expect(rendered).toContain('Alpha'); + expect(rendered).toContain('First'); + }); +}); diff --git a/src/.pi/__tests__/structured-exchange-present-request.test.ts b/src/.pi/__tests__/structured-exchange-present-request.test.ts index 605b11000..42339ba5a 100644 --- a/src/.pi/__tests__/structured-exchange-present-request.test.ts +++ b/src/.pi/__tests__/structured-exchange-present-request.test.ts @@ -1,100 +1,100 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; import registerStructuredExchange, { PRESENT_OPTIONS_TOOL, REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, -} from "../extensions/structured-exchange/index.js" +} from '../extensions/structured-exchange/index.js'; import { findIncompleteStructuredExchangePresents, isStructuredExchangePresentDetails, isStructuredExchangeRequestDetails, -} from "../extensions/structured-exchange/shared/recovery.js" +} from '../extensions/structured-exchange/shared/recovery.js'; interface ToolTextContent { - type: "text" - text: string + type: 'text'; + text: string; } interface ToolExecutionResult { - content: ToolTextContent[] - details: any + content: ToolTextContent[]; + details: any; } interface RegisteredTool { - name: string - executionMode?: string + name: string; + executionMode?: string; execute: ( toolCallId: string, params: Record, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown, - ) => Promise + ) => Promise; renderResult: ( result: ToolExecutionResult, options: unknown, theme: FakeTheme, context?: unknown, - ) => { render?: (width: number) => string[] } + ) => { render?: (width: number) => string[] }; } interface FakeTheme { - fg: (_color: string, text: string) => string - bold?: (text: string) => string + fg: (_color: string, text: string) => string; + bold?: (text: string) => string; } const theme: FakeTheme = { fg: (_color, text) => text, bold: (text) => text, -} +}; function registeredTools(): Map { - const tools = new Map() + const tools = new Map(); registerStructuredExchange({ registerTool(tool: RegisteredTool) { - tools.set(tool.name, tool) + tools.set(tool.name, tool); }, - } as never) - return tools + } as never); + return tools; } -describe("structured exchange present/request tools", () => { - it("registers implemented present/request tools as sequential", () => { - const tools = registeredTools() +describe('structured exchange present/request tools', () => { + it('registers implemented present/request tools as sequential', () => { + const tools = registeredTools(); expect([...tools.keys()]).toEqual([ - "present_question", + 'present_question', PRESENT_OPTIONS_TOOL, - "request_answer", + 'request_answer', REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, - ]) - expect(tools.get(PRESENT_OPTIONS_TOOL)?.executionMode).toBe("sequential") - expect(tools.get(REQUEST_CHOICE_TOOL)?.executionMode).toBe("sequential") - expect(tools.get(REQUEST_CHOICES_TOOL)?.executionMode).toBe("sequential") - }) + ]); + expect(tools.get(PRESENT_OPTIONS_TOOL)?.executionMode).toBe('sequential'); + expect(tools.get(REQUEST_CHOICE_TOOL)?.executionMode).toBe('sequential'); + expect(tools.get(REQUEST_CHOICES_TOOL)?.executionMode).toBe('sequential'); + }); - it("persists a present_options result as markdown content plus recoverable details", async () => { - const present = registeredTools().get(PRESENT_OPTIONS_TOOL) - if (!present) throw new Error("present_options was not registered") + it('persists a present_options result as markdown content plus recoverable details', async () => { + const present = registeredTools().get(PRESENT_OPTIONS_TOOL); + if (!present) throw new Error('present_options was not registered'); const result = await present.execute( - "present-call-1", + 'present-call-1', { - exchangeId: "shell-location", - heading: "Where should the shell live?", - body: "Choose the module boundary for Brunch Pi extensions.", + exchangeId: 'shell-location', + heading: 'Where should the shell live?', + body: 'Choose the module boundary for Brunch Pi extensions.', options: [ { - id: "root", - content: "Keep src/pi-extensions.ts", - rationale: "Smallest diff.", + id: 'root', + content: 'Keep src/pi-extensions.ts', + rationale: 'Smallest diff.', }, { - id: "tui", - content: "Move under src/tui-client", - rationale: "Clearer ownership.", + id: 'tui', + content: 'Move under src/tui-client', + rationale: 'Clearer ownership.', }, ], expectedRequestTool: REQUEST_CHOICE_TOOL, @@ -102,88 +102,86 @@ describe("structured exchange present/request tools", () => { undefined, undefined, {} as never, - ) + ); - expect(result.content[0]?.text).toContain("## Where should the shell live?") - expect(result.content[0]?.text).toContain("Clearer ownership.") - expect(isStructuredExchangePresentDetails(result.details)).toBe(true) + expect(result.content[0]?.text).toContain('## Where should the shell live?'); + expect(result.content[0]?.text).toContain('Clearer ownership.'); + expect(isStructuredExchangePresentDetails(result.details)).toBe(true); expect(result.details).toMatchObject({ - exchangeId: "shell-location", + exchangeId: 'shell-location', presentTool: PRESENT_OPTIONS_TOOL, - kind: "options", - status: "presented", + kind: 'options', + status: 'presented', expectedRequest: { tool: REQUEST_CHOICE_TOOL, required: true }, - createdAtToolCallId: "present-call-1", - }) + createdAtToolCallId: 'present-call-1', + }); - const rendered = result.content[0] - ? present.renderResult(result, {}, theme).render?.(80).join("\n") - : "" - expect(rendered).toContain("Where should the shell live?") - expect(rendered).toContain("Move under src/tui-client") - }) + const rendered = result.content[0] ? present.renderResult(result, {}, theme).render?.(80).join('\n') : ''; + expect(rendered).toContain('Where should the shell live?'); + expect(rendered).toContain('Move under src/tui-client'); + }); - it("persists a request_choice response without repeating the presented content", async () => { - const request = registeredTools().get(REQUEST_CHOICE_TOOL) - if (!request) throw new Error("request_choice was not registered") + it('persists a request_choice response without repeating the presented content', async () => { + const request = registeredTools().get(REQUEST_CHOICE_TOOL); + if (!request) throw new Error('request_choice was not registered'); const result = await request.execute( - "request-call-1", + 'request-call-1', { - exchangeId: "shell-location", + exchangeId: 'shell-location', respondsToPresentTool: PRESENT_OPTIONS_TOOL, - prompt: "Select one option.", + prompt: 'Select one option.', choices: [ - { id: "root", label: "Keep src/pi-extensions.ts" }, - { id: "tui", label: "Move under src/tui-client" }, + { id: 'root', label: 'Keep src/pi-extensions.ts' }, + { id: 'tui', label: 'Move under src/tui-client' }, ], allowOther: false, - commentPrompt: "Optional comment", + commentPrompt: 'Optional comment', }, undefined, undefined, { hasUI: true, ui: { - select: async () => "Move under src/tui-client", - input: async () => "Aligns ownership with /reload iteration.", + select: async () => 'Move under src/tui-client', + input: async () => 'Aligns ownership with /reload iteration.', }, } as never, - ) + ); - expect(result.content[0]?.text).toContain("### Response") - expect(result.content[0]?.text).toContain("Move under src/tui-client") - expect(result.content[0]?.text).not.toContain("Clearer ownership") - expect(isStructuredExchangeRequestDetails(result.details)).toBe(true) + expect(result.content[0]?.text).toContain('### Response'); + expect(result.content[0]?.text).toContain('Move under src/tui-client'); + expect(result.content[0]?.text).not.toContain('Clearer ownership'); + expect(isStructuredExchangeRequestDetails(result.details)).toBe(true); expect(result.details).toMatchObject({ - exchangeId: "shell-location", + exchangeId: 'shell-location', requestTool: REQUEST_CHOICE_TOOL, - status: "answered", + status: 'answered', respondsTo: { - exchangeId: "shell-location", + exchangeId: 'shell-location', presentTool: PRESENT_OPTIONS_TOOL, }, - choice: { id: "tui", label: "Move under src/tui-client" }, - comment: "Aligns ownership with /reload iteration.", - }) - }) + choice: { id: 'tui', label: 'Move under src/tui-client' }, + comment: 'Aligns ownership with /reload iteration.', + }); + }); - it("persists a request_choices response through the editor fallback", async () => { - const request = registeredTools().get(REQUEST_CHOICES_TOOL) - if (!request) throw new Error("request_choices was not registered") + it('persists a request_choices response through the editor fallback', async () => { + const request = registeredTools().get(REQUEST_CHOICES_TOOL); + if (!request) throw new Error('request_choices was not registered'); const result = await request.execute( - "request-choices-call-1", + 'request-choices-call-1', { - exchangeId: "priorities", + exchangeId: 'priorities', respondsToPresentTool: PRESENT_OPTIONS_TOOL, - prompt: "Select all priorities.", + prompt: 'Select all priorities.', choices: [ - { id: "speed", label: "Move quickly" }, - { id: "safety", label: "Keep the transcript safe" }, + { id: 'speed', label: 'Move quickly' }, + { id: 'safety', label: 'Keep the transcript safe' }, ], allowOther: true, - commentPrompt: "Optional comment", + commentPrompt: 'Optional comment', }, undefined, undefined, @@ -191,57 +189,55 @@ describe("structured exchange present/request tools", () => { hasUI: true, ui: { editor: async (prefill: string) => { - const payload = JSON.parse(prefill) + const payload = JSON.parse(prefill); payload.response = { - status: "answered", + status: 'answered', choices: [ - { id: "speed", label: "Move quickly" }, - { id: "other", label: "Other" }, + { id: 'speed', label: 'Move quickly' }, + { id: 'other', label: 'Other' }, ], - comment: "Also keep the proof deterministic.", - } - return JSON.stringify(payload) + comment: 'Also keep the proof deterministic.', + }; + return JSON.stringify(payload); }, }, } as never, - ) + ); - expect(result.content[0]?.text).toContain("### Response") - expect(result.content[0]?.text).toContain("Move quickly") - expect(result.content[0]?.text).toContain("Other") - expect(result.content[0]?.text).toContain( - "Also keep the proof deterministic.", - ) - expect(isStructuredExchangeRequestDetails(result.details)).toBe(true) + expect(result.content[0]?.text).toContain('### Response'); + expect(result.content[0]?.text).toContain('Move quickly'); + expect(result.content[0]?.text).toContain('Other'); + expect(result.content[0]?.text).toContain('Also keep the proof deterministic.'); + expect(isStructuredExchangeRequestDetails(result.details)).toBe(true); expect(result.details).toMatchObject({ - schema: "brunch.structured_exchange.request", - exchangeId: "priorities", + schema: 'brunch.structured_exchange.request', + exchangeId: 'priorities', requestTool: REQUEST_CHOICES_TOOL, - status: "answered", + status: 'answered', respondsTo: { - exchangeId: "priorities", + exchangeId: 'priorities', presentTool: PRESENT_OPTIONS_TOOL, }, choices: [ - { id: "speed", label: "Move quickly" }, - { id: "other", label: "Other" }, + { id: 'speed', label: 'Move quickly' }, + { id: 'other', label: 'Other' }, ], - comment: "Also keep the proof deterministic.", - createdAtToolCallId: "request-choices-call-1", - }) - }) + comment: 'Also keep the proof deterministic.', + createdAtToolCallId: 'request-choices-call-1', + }); + }); - it("rejects request_choices other/none selections without a comment", async () => { - const request = registeredTools().get(REQUEST_CHOICES_TOOL) - if (!request) throw new Error("request_choices was not registered") + it('rejects request_choices other/none selections without a comment', async () => { + const request = registeredTools().get(REQUEST_CHOICES_TOOL); + if (!request) throw new Error('request_choices was not registered'); const result = await request.execute( - "request-choices-call-2", + 'request-choices-call-2', { - exchangeId: "priorities", + exchangeId: 'priorities', respondsToPresentTool: PRESENT_OPTIONS_TOOL, - prompt: "Select all priorities.", - choices: [{ id: "speed", label: "Move quickly" }], + prompt: 'Select all priorities.', + choices: [{ id: 'speed', label: 'Move quickly' }], allowOther: true, allowNone: true, }, @@ -251,54 +247,51 @@ describe("structured exchange present/request tools", () => { hasUI: true, ui: { editor: async (prefill: string) => { - const payload = JSON.parse(prefill) + const payload = JSON.parse(prefill); payload.response = { - status: "answered", - choices: [{ id: "none", label: "None" }], - comment: " ", - } - return JSON.stringify(payload) + status: 'answered', + choices: [{ id: 'none', label: 'None' }], + comment: ' ', + }; + return JSON.stringify(payload); }, }, } as never, - ) + ); expect(result.details).toMatchObject({ requestTool: REQUEST_CHOICES_TOOL, - status: "unavailable", - message: - "request_choices requires a comment for Other or None selections", - }) - expect(result.content[0]?.text).toContain( - "request_choices requires a comment", - ) - }) + status: 'unavailable', + message: 'request_choices requires a comment for Other or None selections', + }); + expect(result.content[0]?.text).toContain('request_choices requires a comment'); + }); - it("detects an unmatched present result for recovery", () => { + it('detects an unmatched present result for recovery', () => { const incomplete = findIncompleteStructuredExchangePresents([ { - type: "message", + type: 'message', message: { - role: "toolResult", + role: 'toolResult', toolName: PRESENT_OPTIONS_TOOL, - toolCallId: "present-call-1", - content: [{ type: "text", text: "## Offer" }], + toolCallId: 'present-call-1', + content: [{ type: 'text', text: '## Offer' }], details: { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', schemaVersion: 1, - exchangeId: "shell-location", + exchangeId: 'shell-location', presentTool: PRESENT_OPTIONS_TOOL, - kind: "options", - status: "presented", + kind: 'options', + status: 'presented', expectedRequest: { tool: REQUEST_CHOICE_TOOL, required: true }, - createdAtToolCallId: "present-call-1", + createdAtToolCallId: 'present-call-1', }, isError: false, }, }, - ]) + ]); - expect(incomplete).toHaveLength(1) - expect(incomplete[0]?.details.exchangeId).toBe("shell-location") - }) -}) + expect(incomplete).toHaveLength(1); + expect(incomplete[0]?.details.exchangeId).toBe('shell-location'); + }); +}); diff --git a/src/.pi/__tests__/structured-exchange-schemas.test.ts b/src/.pi/__tests__/structured-exchange-schemas.test.ts index 2c4dd09f9..1da2c9b1e 100644 --- a/src/.pi/__tests__/structured-exchange-schemas.test.ts +++ b/src/.pi/__tests__/structured-exchange-schemas.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest" -import * as z from "zod" +import { describe, expect, it } from 'vitest'; +import * as z from 'zod'; import { zCaptureAnswerDetails, @@ -26,188 +26,186 @@ import { zRequestDetailsHeader, zRequestReviewDetails, zRequestToolMeta, -} from "../extensions/structured-exchange/schemas/index.js" +} from '../extensions/structured-exchange/schemas/index.js'; function expectJsonSchemaExport(schema: z.ZodType) { - expect(() => - z.toJSONSchema(schema, { unrepresentable: "throw" }), - ).not.toThrow() + expect(() => z.toJSONSchema(schema, { unrepresentable: 'throw' })).not.toThrow(); } -describe("structured exchange shared schemas", () => { - it("parses checked details headers and rejects unsupported versions", () => { +describe('structured exchange shared schemas', () => { + it('parses checked details headers and rejects unsupported versions', () => { expect( zPresentDetailsHeader.parse({ - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 1, - exchange_id: "problem-frame", + exchange_id: 'problem-frame', }), - ).toMatchObject({ exchange_id: "problem-frame" }) + ).toMatchObject({ exchange_id: 'problem-frame' }); expect(() => zPresentDetailsHeader.parse({ - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 2, - exchange_id: "problem-frame", + exchange_id: 'problem-frame', }), - ).toThrow() + ).toThrow(); expect(() => zRequestDetailsHeader.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 2, - exchange_id: "problem-frame", + exchange_id: 'problem-frame', }), - ).toThrow() + ).toThrow(); expect(() => zCaptureDetailsHeader.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 2, - exchange_id: "problem-frame", + exchange_id: 'problem-frame', }), - ).toThrow() - }) + ).toThrow(); + }); - it("parses shared markdown, graph refs, and tool sequencing metadata", () => { - expect(zMarkdown.parse("**markdown**")).toBe("**markdown**") - expect(zGraphNodeRef.parse({ node_id: "node-1" })).toEqual({ - node_id: "node-1", - }) + it('parses shared markdown, graph refs, and tool sequencing metadata', () => { + expect(zMarkdown.parse('**markdown**')).toBe('**markdown**'); + expect(zGraphNodeRef.parse({ node_id: 'node-1' })).toEqual({ + node_id: 'node-1', + }); expect( zPresentToolMeta.parse({ - curr: "present_options", - next: "request_choices", + curr: 'present_options', + next: 'request_choices', }), - ).toEqual({ curr: "present_options", next: "request_choices" }) + ).toEqual({ curr: 'present_options', next: 'request_choices' }); expect( zRequestToolMeta.parse({ - prev: "present_candidates", - curr: "request_choice", - next: "capture_candidate", + prev: 'present_candidates', + curr: 'request_choice', + next: 'capture_candidate', }), ).toEqual({ - prev: "present_candidates", - curr: "request_choice", - next: "capture_candidate", - }) + prev: 'present_candidates', + curr: 'request_choice', + next: 'capture_candidate', + }); expect( zCaptureToolMeta.parse({ - prev: "request_choice", - curr: "capture_candidate", + prev: 'request_choice', + curr: 'capture_candidate', }), - ).toEqual({ prev: "request_choice", curr: "capture_candidate" }) - }) - - it("exports representative shared schemas to JSON Schema", () => { - expectJsonSchemaExport(zPresentDetailsHeader) - expectJsonSchemaExport(zRequestDetailsHeader) - expectJsonSchemaExport(zCaptureDetailsHeader) - expectJsonSchemaExport(zGraphNodeRef) - expectJsonSchemaExport(zPresentToolMeta) - expectJsonSchemaExport(zRequestToolMeta) - expectJsonSchemaExport(zCaptureToolMeta) - }) -}) - -describe("structured exchange present schemas", () => { + ).toEqual({ prev: 'request_choice', curr: 'capture_candidate' }); + }); + + it('exports representative shared schemas to JSON Schema', () => { + expectJsonSchemaExport(zPresentDetailsHeader); + expectJsonSchemaExport(zRequestDetailsHeader); + expectJsonSchemaExport(zCaptureDetailsHeader); + expectJsonSchemaExport(zGraphNodeRef); + expectJsonSchemaExport(zPresentToolMeta); + expectJsonSchemaExport(zRequestToolMeta); + expectJsonSchemaExport(zCaptureToolMeta); + }); +}); + +describe('structured exchange present schemas', () => { const candidateDetails = { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 1, - exchange_id: "candidate-direction", - tool_meta: { curr: "present_candidates", next: "request_choice" }, + exchange_id: 'candidate-direction', + tool_meta: { curr: 'present_candidates', next: 'request_choice' }, display: { - heading: "Which direction should we take?", - body: "Pick one candidate.", + heading: 'Which direction should we take?', + body: 'Pick one candidate.', }, candidates: [ { - id: "candidate-local-workbench", - title: "Local workbench for graph-native specs", + id: 'candidate-local-workbench', + title: 'Local workbench for graph-native specs', user_rubric: { - core_bet: "Make local graph work the thesis.", - best_fit: "Keeps the POC focused.", - cost_complexity: "Requires owning local state clearly.", - covers_well: "Covers chrome, transcript, and graph coherence.", - main_risks: "Does not solve cloud collaboration.", - lock_in_constraints: "Commits to local-first semantics.", - recommendation: "Choose this for the POC.", + core_bet: 'Make local graph work the thesis.', + best_fit: 'Keeps the POC focused.', + cost_complexity: 'Requires owning local state clearly.', + covers_well: 'Covers chrome, transcript, and graph coherence.', + main_risks: 'Does not solve cloud collaboration.', + lock_in_constraints: 'Commits to local-first semantics.', + recommendation: 'Choose this for the POC.', }, meta_rubric: { - legibility_cost_of_knowing: "Easy to inspect locally.", - failure_modes: "May under-test multi-user cases.", - coverage_range: "Strong for current assumptions.", - commitment: "Defers cloud concerns.", + legibility_cost_of_knowing: 'Easy to inspect locally.', + failure_modes: 'May under-test multi-user cases.', + coverage_range: 'Strong for current assumptions.', + commitment: 'Defers cloud concerns.', }, - graph_refs: [{ node_id: "node-1" }], + graph_refs: [{ node_id: 'node-1' }], }, ], - } + }; - it("parses conservative present variants and exact candidate details", () => { + it('parses conservative present variants and exact candidate details', () => { expect( zPresentQuestionDetails.parse({ - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 1, - exchange_id: "problem-frame", - tool_meta: { curr: "present_question", next: "request_answer" }, + exchange_id: 'problem-frame', + tool_meta: { curr: 'present_question', next: 'request_answer' }, display: { - heading: "What problem are we solving first?", - body: "Name the pain.", - preface: "We need the user-facing pull.", + heading: 'What problem are we solving first?', + body: 'Name the pain.', + preface: 'We need the user-facing pull.', }, }), - ).toMatchObject({ tool_meta: { curr: "present_question" } }) + ).toMatchObject({ tool_meta: { curr: 'present_question' } }); expect( zPresentOptionsDetails.parse({ - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 1, - exchange_id: "domain-shape", - tool_meta: { curr: "present_options", next: "request_choices" }, - display: { heading: "Which risks should stay visible?" }, + exchange_id: 'domain-shape', + tool_meta: { curr: 'present_options', next: 'request_choices' }, + display: { heading: 'Which risks should stay visible?' }, options: [ { - id: "transport", - content: "Transport contract", - rationale: "Public RPC is a product seam.", + id: 'transport', + content: 'Transport contract', + rationale: 'Public RPC is a product seam.', }, ], }), - ).toMatchObject({ tool_meta: { next: "request_choices" } }) + ).toMatchObject({ tool_meta: { next: 'request_choices' } }); expect( zPresentReviewSetDetails.parse({ - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', v: 1, - exchange_id: "review-set-17", - tool_meta: { curr: "present_review_set", next: "request_review" }, - display: { heading: "Review proposed requirements" }, - review_set: { proposal_entry_id: "entry-review-proposal-17" }, + exchange_id: 'review-set-17', + tool_meta: { curr: 'present_review_set', next: 'request_review' }, + display: { heading: 'Review proposed requirements' }, + review_set: { proposal_entry_id: 'entry-review-proposal-17' }, }), ).toMatchObject({ - review_set: { proposal_entry_id: "entry-review-proposal-17" }, - }) + review_set: { proposal_entry_id: 'entry-review-proposal-17' }, + }); expect(zPresentCandidatesDetails.parse(candidateDetails)).toMatchObject({ - candidates: [{ graph_refs: [{ node_id: "node-1" }] }], - }) + candidates: [{ graph_refs: [{ node_id: 'node-1' }] }], + }); expect(zPresentDetails.parse(candidateDetails)).toMatchObject({ - tool_meta: { curr: "present_candidates" }, - }) - }) + tool_meta: { curr: 'present_candidates' }, + }); + }); - it("rejects candidate graph refs and rubric drift fields", () => { + it('rejects candidate graph refs and rubric drift fields', () => { expect(() => zPresentCandidatesDetails.parse({ ...candidateDetails, candidates: [ { ...candidateDetails.candidates[0], - graph_refs: [{ node_id: "node-1", role: "supporting" }], + graph_refs: [{ node_id: 'node-1', role: 'supporting' }], }, ], }), - ).toThrow() + ).toThrow(); expect(() => zPresentCandidatesDetails.parse({ @@ -217,337 +215,332 @@ describe("structured exchange present schemas", () => { ...candidateDetails.candidates[0], user_rubric: { ...candidateDetails.candidates[0].user_rubric, - confidence: "high", + confidence: 'high', }, }, ], }), - ).toThrow() - }) + ).toThrow(); + }); - it("rejects retired present-side control fields", () => { - for (const field of [ - "phase", - "status", - "next_required", - "schema_version", - ] as const) { + it('rejects retired present-side control fields', () => { + for (const field of ['phase', 'status', 'next_required', 'schema_version'] as const) { expect(() => zPresentCandidatesDetails.parse({ ...candidateDetails, - [field]: field === "status" ? "presented" : true, + [field]: field === 'status' ? 'presented' : true, }), - ).toThrow() + ).toThrow(); } - }) - - it("exports present schemas to JSON Schema", () => { - expectJsonSchemaExport(zPresentQuestionDetails) - expectJsonSchemaExport(zPresentOptionsDetails) - expectJsonSchemaExport(zPresentReviewSetDetails) - expectJsonSchemaExport(zPresentCandidatesDetails) - expectJsonSchemaExport(zPresentDetails) - }) -}) - -describe("structured exchange request schemas", () => { + }); + + it('exports present schemas to JSON Schema', () => { + expectJsonSchemaExport(zPresentQuestionDetails); + expectJsonSchemaExport(zPresentOptionsDetails); + expectJsonSchemaExport(zPresentReviewSetDetails); + expectJsonSchemaExport(zPresentCandidatesDetails); + expectJsonSchemaExport(zPresentDetails); + }); +}); + +describe('structured exchange request schemas', () => { const answerBase = { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "problem-frame", + exchange_id: 'problem-frame', tool_meta: { - prev: "present_question", - curr: "request_answer", - next: "capture_answer", + prev: 'present_question', + curr: 'request_answer', + next: 'capture_answer', }, - } + }; - it("parses answered, cancelled, and unavailable outcomes", () => { + it('parses answered, cancelled, and unavailable outcomes', () => { expect( zRequestAnswerDetails.parse({ ...answerBase, answered: { - text: "The hard part is coherence across sessions.", + text: 'The hard part is coherence across sessions.', }, }), ).toMatchObject({ - answered: { text: "The hard part is coherence across sessions." }, - }) + answered: { text: 'The hard part is coherence across sessions.' }, + }); expect( zRequestAnswerDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "problem-frame", - tool_meta: { prev: "present_question", curr: "request_answer" }, - cancelled: { message: "User cancelled." }, + exchange_id: 'problem-frame', + tool_meta: { prev: 'present_question', curr: 'request_answer' }, + cancelled: { message: 'User cancelled.' }, }), - ).toMatchObject({ cancelled: { message: "User cancelled." } }) + ).toMatchObject({ cancelled: { message: 'User cancelled.' } }); expect( zRequestAnswerDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "problem-frame", - tool_meta: { prev: "present_question", curr: "request_answer" }, - unavailable: { message: "request_answer requires interactive UI." }, + exchange_id: 'problem-frame', + tool_meta: { prev: 'present_question', curr: 'request_answer' }, + unavailable: { message: 'request_answer requires interactive UI.' }, }), ).toMatchObject({ - unavailable: { message: "request_answer requires interactive UI." }, - }) - }) + unavailable: { message: 'request_answer requires interactive UI.' }, + }); + }); - it("rejects missing or multiple terminal outcomes", () => { - expect(() => zRequestAnswerDetails.parse(answerBase)).toThrow() + it('rejects missing or multiple terminal outcomes', () => { + expect(() => zRequestAnswerDetails.parse(answerBase)).toThrow(); expect(() => zRequestAnswerDetails.parse({ ...answerBase, - answered: { text: "Yes." }, - cancelled: { message: "User cancelled." }, + answered: { text: 'Yes.' }, + cancelled: { message: 'User cancelled.' }, }), - ).toThrow() - }) + ).toThrow(); + }); - it("keeps comment on answered payloads and message on terminal runtime payloads", () => { + it('keeps comment on answered payloads and message on terminal runtime payloads', () => { expect( zRequestChoiceDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "domain-shape", + exchange_id: 'domain-shape', tool_meta: { - prev: "present_options", - curr: "request_choice", - next: "capture_choice", + prev: 'present_options', + curr: 'request_choice', + next: 'capture_choice', }, answered: { choice: { - id: "local-first", - label: "Local-first app", - kind: "listed", + id: 'local-first', + label: 'Local-first app', + kind: 'listed', }, - comment: "This fits the POC constraints.", + comment: 'This fits the POC constraints.', }, }), - ).toMatchObject({ answered: { comment: "This fits the POC constraints." } }) + ).toMatchObject({ answered: { comment: 'This fits the POC constraints.' } }); expect(() => zRequestChoiceDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "domain-shape", - tool_meta: { prev: "present_options", curr: "request_choice" }, - cancelled: { message: "User cancelled." }, - comment: "human text in the wrong place", + exchange_id: 'domain-shape', + tool_meta: { prev: 'present_options', curr: 'request_choice' }, + cancelled: { message: 'User cancelled.' }, + comment: 'human text in the wrong place', }), - ).toThrow() + ).toThrow(); expect(() => zRequestChoiceDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "domain-shape", - tool_meta: { prev: "present_options", curr: "request_choice" }, + exchange_id: 'domain-shape', + tool_meta: { prev: 'present_options', curr: 'request_choice' }, answered: { choice: { - id: "local-first", - label: "Local-first app", - kind: "listed", + id: 'local-first', + label: 'Local-first app', + kind: 'listed', }, - message: "runtime text in the wrong place", + message: 'runtime text in the wrong place', }, }), - ).toThrow() - }) + ).toThrow(); + }); - it("supports candidate choices and requires comments for other or none choices", () => { + it('supports candidate choices and requires comments for other or none choices', () => { expect( zRequestChoiceDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "candidate-direction", + exchange_id: 'candidate-direction', tool_meta: { - prev: "present_candidates", - curr: "request_choice", - next: "capture_candidate", + prev: 'present_candidates', + curr: 'request_choice', + next: 'capture_candidate', }, answered: { choice: { - id: "candidate-local-workbench", - label: "Local workbench for graph-native specs", - kind: "listed", + id: 'candidate-local-workbench', + label: 'Local workbench for graph-native specs', + kind: 'listed', }, }, }), - ).toMatchObject({ tool_meta: { prev: "present_candidates" } }) + ).toMatchObject({ tool_meta: { prev: 'present_candidates' } }); expect(() => zRequestChoiceDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "domain-shape", - tool_meta: { prev: "present_options", curr: "request_choice" }, + exchange_id: 'domain-shape', + tool_meta: { prev: 'present_options', curr: 'request_choice' }, answered: { - choice: { id: "none", label: "None of these", kind: "none" }, + choice: { id: 'none', label: 'None of these', kind: 'none' }, }, }), - ).toThrow() - }) + ).toThrow(); + }); - it("parses multiple choices and requires comments for other or none selections", () => { + it('parses multiple choices and requires comments for other or none selections', () => { expect( zRequestChoicesDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "open-risks", + exchange_id: 'open-risks', tool_meta: { - prev: "present_options", - curr: "request_choices", - next: "capture_choices", + prev: 'present_options', + curr: 'request_choices', + next: 'capture_choices', }, answered: { choices: [ - { id: "transport", label: "Transport contract", kind: "listed" }, + { id: 'transport', label: 'Transport contract', kind: 'listed' }, { - id: "other", - label: "Schema source-of-truth drift", - kind: "other", + id: 'other', + label: 'Schema source-of-truth drift', + kind: 'other', }, ], - comment: "Keep schema drift visible.", + comment: 'Keep schema drift visible.', }, }), ).toMatchObject({ - answered: { choices: [{ id: "transport" }, { id: "other" }] }, - }) + answered: { choices: [{ id: 'transport' }, { id: 'other' }] }, + }); expect(() => zRequestChoicesDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "open-risks", - tool_meta: { prev: "present_options", curr: "request_choices" }, + exchange_id: 'open-risks', + tool_meta: { prev: 'present_options', curr: 'request_choices' }, answered: { - choices: [{ id: "none", label: "None of these", kind: "none" }], + choices: [{ id: 'none', label: 'None of these', kind: 'none' }], }, }), - ).toThrow() - }) + ).toThrow(); + }); - it("requires a comment for request_changes review decisions", () => { + it('requires a comment for request_changes review decisions', () => { expect( zRequestReviewDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "review-set-17", + exchange_id: 'review-set-17', tool_meta: { - prev: "present_review_set", - curr: "request_review", - next: "capture_review", + prev: 'present_review_set', + curr: 'request_review', + next: 'capture_review', }, answered: { - decision: "approve", - comment: "This is ready to commit.", + decision: 'approve', + comment: 'This is ready to commit.', }, }), - ).toMatchObject({ answered: { decision: "approve" } }) + ).toMatchObject({ answered: { decision: 'approve' } }); expect(() => zRequestReviewDetails.parse({ - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', v: 1, - exchange_id: "review-set-17", - tool_meta: { prev: "present_review_set", curr: "request_review" }, - answered: { decision: "request_changes" }, + exchange_id: 'review-set-17', + tool_meta: { prev: 'present_review_set', curr: 'request_review' }, + answered: { decision: 'request_changes' }, }), - ).toThrow() - }) - - it("exports request schemas to JSON Schema", () => { - expectJsonSchemaExport(zRequestAnswerDetails) - expectJsonSchemaExport(zRequestChoiceDetails) - expectJsonSchemaExport(zRequestChoicesDetails) - expectJsonSchemaExport(zRequestReviewDetails) - expectJsonSchemaExport(zRequestDetails) - }) -}) - -describe("structured exchange capture schemas", () => { - it("parses the agreed minimal capture variants", () => { + ).toThrow(); + }); + + it('exports request schemas to JSON Schema', () => { + expectJsonSchemaExport(zRequestAnswerDetails); + expectJsonSchemaExport(zRequestChoiceDetails); + expectJsonSchemaExport(zRequestChoicesDetails); + expectJsonSchemaExport(zRequestReviewDetails); + expectJsonSchemaExport(zRequestDetails); + }); +}); + +describe('structured exchange capture schemas', () => { + it('parses the agreed minimal capture variants', () => { expect( zCaptureAnswerDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "problem-frame", - tool_meta: { prev: "request_answer", curr: "capture_answer" }, + exchange_id: 'problem-frame', + tool_meta: { prev: 'request_answer', curr: 'capture_answer' }, }), - ).toMatchObject({ tool_meta: { curr: "capture_answer" } }) + ).toMatchObject({ tool_meta: { curr: 'capture_answer' } }); expect( zCaptureChoiceDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "domain-shape", - tool_meta: { prev: "request_choice", curr: "capture_choice" }, + exchange_id: 'domain-shape', + tool_meta: { prev: 'request_choice', curr: 'capture_choice' }, }), - ).toMatchObject({ tool_meta: { curr: "capture_choice" } }) + ).toMatchObject({ tool_meta: { curr: 'capture_choice' } }); expect( zCaptureChoicesDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "open-risks", - tool_meta: { prev: "request_choices", curr: "capture_choices" }, + exchange_id: 'open-risks', + tool_meta: { prev: 'request_choices', curr: 'capture_choices' }, }), - ).toMatchObject({ tool_meta: { curr: "capture_choices" } }) + ).toMatchObject({ tool_meta: { curr: 'capture_choices' } }); expect( zCaptureReviewDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "review-set-17", - tool_meta: { prev: "request_review", curr: "capture_review" }, + exchange_id: 'review-set-17', + tool_meta: { prev: 'request_review', curr: 'capture_review' }, }), - ).toMatchObject({ tool_meta: { curr: "capture_review" } }) + ).toMatchObject({ tool_meta: { curr: 'capture_review' } }); expect( zCaptureCandidateDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "candidate-direction", - tool_meta: { prev: "request_choice", curr: "capture_candidate" }, + exchange_id: 'candidate-direction', + tool_meta: { prev: 'request_choice', curr: 'capture_candidate' }, }), - ).toMatchObject({ tool_meta: { curr: "capture_candidate" } }) - }) + ).toMatchObject({ tool_meta: { curr: 'capture_candidate' } }); + }); - it("rejects graph payloads and analysis/provenance fields", () => { + it('rejects graph payloads and analysis/provenance fields', () => { for (const field of [ - "committed_graph_nodes", - "graph_edges", - "lsn", - "command_result", - "assumptions", - "caveats", - "observations", - "selected_candidate_id", + 'committed_graph_nodes', + 'graph_edges', + 'lsn', + 'command_result', + 'assumptions', + 'caveats', + 'observations', + 'selected_candidate_id', ] as const) { expect(() => zCaptureCandidateDetails.parse({ - schema: "brunch.structured_exchange.capture", + schema: 'brunch.structured_exchange.capture', v: 1, - exchange_id: "candidate-direction", - tool_meta: { prev: "request_choice", curr: "capture_candidate" }, + exchange_id: 'candidate-direction', + tool_meta: { prev: 'request_choice', curr: 'capture_candidate' }, [field]: field, }), - ).toThrow() + ).toThrow(); } - }) - - it("exports capture schemas to JSON Schema", () => { - expectJsonSchemaExport(zCaptureAnswerDetails) - expectJsonSchemaExport(zCaptureChoiceDetails) - expectJsonSchemaExport(zCaptureChoicesDetails) - expectJsonSchemaExport(zCaptureReviewDetails) - expectJsonSchemaExport(zCaptureCandidateDetails) - expectJsonSchemaExport(zCaptureDetails) - }) -}) + }); + + it('exports capture schemas to JSON Schema', () => { + expectJsonSchemaExport(zCaptureAnswerDetails); + expectJsonSchemaExport(zCaptureChoiceDetails); + expectJsonSchemaExport(zCaptureChoicesDetails); + expectJsonSchemaExport(zCaptureReviewDetails); + expectJsonSchemaExport(zCaptureCandidateDetails); + expectJsonSchemaExport(zCaptureDetails); + }); +}); diff --git a/src/.pi/__tests__/structured-exchange.test.ts b/src/.pi/__tests__/structured-exchange.test.ts index f232f1b62..b6440633c 100644 --- a/src/.pi/__tests__/structured-exchange.test.ts +++ b/src/.pi/__tests__/structured-exchange.test.ts @@ -1,85 +1,85 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; import { buildStructuredExchangeEditorPrefill, parseStructuredExchangeEditorResponse, structuredExchangeResultFromEditor, -} from "../extensions/structured-exchange/index.js" +} from '../extensions/structured-exchange/index.js'; -describe("structured exchange JSON-editor fallback compatibility helpers", () => { - it("builds schema-tagged editor prefill for the raw Pi RPC fallback proof", () => { +describe('structured exchange JSON-editor fallback compatibility helpers', () => { + it('builds schema-tagged editor prefill for the raw Pi RPC fallback proof', () => { const prefill = buildStructuredExchangeEditorPrefill({ - question: "Pick paths", - context: "Use the fallback.", - mode: "multi-select", + question: 'Pick paths', + context: 'Use the fallback.', + mode: 'multi-select', options: [ - { label: "Alpha", value: "a" }, - { label: "Beta", value: "b", description: "Second" }, + { label: 'Alpha', value: 'a' }, + { label: 'Beta', value: 'b', description: 'Second' }, ], - }) + }); expect(JSON.parse(prefill)).toMatchObject({ - schema: "brunch.structured_exchange.editor", + schema: 'brunch.structured_exchange.editor', schemaVersion: 1, - question: "Pick paths", - context: "Use the fallback.", - mode: "multi-select", + question: 'Pick paths', + context: 'Use the fallback.', + mode: 'multi-select', options: [ - { index: 1, label: "Alpha", value: "a" }, - { index: 2, label: "Beta", value: "b", description: "Second" }, + { index: 1, label: 'Alpha', value: 'a' }, + { index: 2, label: 'Beta', value: 'b', description: 'Second' }, ], - response: { status: "cancelled", answers: [], note: "" }, - }) - }) + response: { status: 'cancelled', answers: [], note: '' }, + }); + }); - it("parses answered editor JSON with explicit empty notes", () => { + it('parses answered editor JSON with explicit empty notes', () => { const parsed = parseStructuredExchangeEditorResponse( JSON.stringify({ response: { - status: "answered", - answers: [{ type: "option", label: "Beta", value: "b", index: 2 }], - note: "", + status: 'answered', + answers: [{ type: 'option', label: 'Beta', value: 'b', index: 2 }], + note: '', }, }), - ) + ); expect(parsed).toEqual({ - status: "answered", - answers: [{ type: "option", label: "Beta", value: "b", index: 2 }], - note: "", - }) - }) + status: 'answered', + answers: [{ type: 'option', label: 'Beta', value: 'b', index: 2 }], + note: '', + }); + }); - it("returns legacy structured result details for the existing RPC proof", () => { + it('returns legacy structured result details for the existing RPC proof', () => { const prefill = JSON.parse( buildStructuredExchangeEditorPrefill({ - question: "Pick paths", - mode: "single-select", - options: [{ label: "Alpha", value: "a" }], + question: 'Pick paths', + mode: 'single-select', + options: [{ label: 'Alpha', value: 'a' }], }), - ) + ); prefill.response = { - status: "answered", - answers: [{ type: "option", label: "Alpha", value: "a", index: 1 }], - note: "Add context", - } + status: 'answered', + answers: [{ type: 'option', label: 'Alpha', value: 'a', index: 1 }], + note: 'Add context', + }; const result = structuredExchangeResultFromEditor( { - question: "Pick paths", - mode: "single-select", - options: [{ label: "Alpha", value: "a" }], + question: 'Pick paths', + mode: 'single-select', + options: [{ label: 'Alpha', value: 'a' }], }, JSON.stringify(prefill), - ) + ); expect(result.details).toMatchObject({ - schema: "brunch.structured_exchange.result", - status: "answered", - mode: "single-select", - answers: [{ type: "option", label: "Alpha", value: "a", index: 1 }], - note: "Add context", - transport: { surface: "rpc-editor" }, - }) - }) -}) + schema: 'brunch.structured_exchange.result', + status: 'answered', + mode: 'single-select', + answers: [{ type: 'option', label: 'Alpha', value: 'a', index: 1 }], + note: 'Add context', + transport: { surface: 'rpc-editor' }, + }); + }); +}); diff --git a/src/.pi/__tests__/workspace-dialog.test.ts b/src/.pi/__tests__/workspace-dialog.test.ts index b9307a9b6..582f1ce25 100644 --- a/src/.pi/__tests__/workspace-dialog.test.ts +++ b/src/.pi/__tests__/workspace-dialog.test.ts @@ -1,411 +1,376 @@ -import { readFile } from "node:fs/promises" +import { readFile } from 'node:fs/promises'; -import { type Terminal } from "@earendil-works/pi-tui" - -import { describe, expect, it } from "vitest" +import { type Terminal } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; +import type { WorkspaceLaunchInventory } from '../../../workspace-session-coordinator.js'; +import { formatBrunchProductIdentity, readBrunchAnsiLogo } from '../components/brunch-identity.js'; import { buildWorkspaceSelectionView, createWorkspaceDialogComponent, selectWorkspaceSelectionOption, runWorkspaceDialogPreflight, -} from "../components/workspace-dialog/index.js" -import { - formatBrunchProductIdentity, - readBrunchAnsiLogo, -} from "../components/brunch-identity.js" -import type { WorkspaceLaunchInventory } from "../../../workspace-session-coordinator.js" +} from '../components/workspace-dialog/index.js'; -describe("spec/session picker", () => { - it("builds a hierarchical spec/session selection home without per-spec top-level actions", () => { - const view = buildWorkspaceSelectionView(inventory()) +describe('spec/session picker', () => { + it('builds a hierarchical spec/session selection home without per-spec top-level actions', () => { + const view = buildWorkspaceSelectionView(inventory()); - expect(view.stage).toBe("home") + expect(view.stage).toBe('home'); expect(view.options.map((option) => option.kind)).toEqual([ - "continue", - "resumeSpec", - "newSpec", - "cancel", - ]) + 'continue', + 'resumeSpec', + 'newSpec', + 'cancel', + ]); expect(view.options.map((option) => option.label)).toEqual([ - "Continue your latest spec and session", - "Continue another existing specification", - "Start a new specification", - "Cancel", - ]) - expect(view.options.map((option) => option.label).join("\n")).not.toMatch( + 'Continue your latest spec and session', + 'Continue another existing specification', + 'Start a new specification', + 'Cancel', + ]); + expect(view.options.map((option) => option.label).join('\n')).not.toMatch( /Resume Alpha|Open Alpha|Start new session in Alpha/, - ) + ); expect(selectWorkspaceSelectionOption(view, 0)).toEqual({ decision: { - action: "continue", - specId: "spec-alpha", - sessionFile: "/sessions/alpha-current.jsonl", + action: 'continue', + specId: 'spec-alpha', + sessionFile: '/sessions/alpha-current.jsonl', }, - }) - }) - - it("navigates resume-existing-spec to spec actions without emitting activation early", () => { - const currentInventory = inventory() - const home = buildWorkspaceSelectionView(currentInventory) - const specList = selectWorkspaceSelectionOption(home, 1, currentInventory) - - expect(specList).toMatchObject({ view: { stage: "specList" } }) - if (!("view" in specList)) throw new Error("expected spec list") - expect(specList.view.options.map((option) => option.label)).toEqual([ - "Alpha", - "Beta", - ]) - - const specAction = selectWorkspaceSelectionOption( - specList.view, - 0, - currentInventory, - ) - - expect(specAction).toMatchObject({ view: { stage: "specAction" } }) - if (!("view" in specAction)) throw new Error("expected spec action") + }); + }); + + it('navigates resume-existing-spec to spec actions without emitting activation early', () => { + const currentInventory = inventory(); + const home = buildWorkspaceSelectionView(currentInventory); + const specList = selectWorkspaceSelectionOption(home, 1, currentInventory); + + expect(specList).toMatchObject({ view: { stage: 'specList' } }); + if (!('view' in specList)) throw new Error('expected spec list'); + expect(specList.view.options.map((option) => option.label)).toEqual(['Alpha', 'Beta']); + + const specAction = selectWorkspaceSelectionOption(specList.view, 0, currentInventory); + + expect(specAction).toMatchObject({ view: { stage: 'specAction' } }); + if (!('view' in specAction)) throw new Error('expected spec action'); expect(specAction.view.options.map((option) => option.label)).toEqual([ - "Create new session", - "Resume existing session", - ]) + 'Create new session', + 'Resume existing session', + ]); expect(selectWorkspaceSelectionOption(specAction.view, 0)).toEqual({ - decision: { action: "newSession", specId: "spec-alpha" }, - }) - }) + decision: { action: 'newSession', specId: 'spec-alpha' }, + }); + }); - it("emits open-session only after a session is selected", () => { + it('emits open-session only after a session is selected', () => { const sessionList = buildWorkspaceSelectionView(inventory(), { - stage: "sessionList", - specId: "spec-alpha", - }) + stage: 'sessionList', + specId: 'spec-alpha', + }); expect(sessionList.options.map((option) => option.label)).toEqual([ - "session-alpha-current", - "session-alpha-older", - ]) + 'session-alpha-current', + 'session-alpha-older', + ]); expect(selectWorkspaceSelectionOption(sessionList, 1)).toEqual({ decision: { - action: "openSession", - specId: "spec-alpha", - sessionFile: "/sessions/alpha-older.jsonl", + action: 'openSession', + specId: 'spec-alpha', + sessionFile: '/sessions/alpha-older.jsonl', }, - }) - }) + }); + }); - it("enters new-spec title state before emitting a new-spec decision", () => { - const home = buildWorkspaceSelectionView(inventory()) + it('enters new-spec title state before emitting a new-spec decision', () => { + const home = buildWorkspaceSelectionView(inventory()); expect(selectWorkspaceSelectionOption(home, 2)).toMatchObject({ - view: { stage: "newSpecTitle", title: "", options: [] }, - }) - }) + view: { stage: 'newSpecTitle', title: '', options: [] }, + }); + }); - it("only shows logical home options in an empty workspace", () => { - const view = buildWorkspaceSelectionView(emptyInventory()) + it('only shows logical home options in an empty workspace', () => { + const view = buildWorkspaceSelectionView(emptyInventory()); - expect(view.options.map((option) => option.label)).toEqual([ - "Start a new specification", - "Cancel", - ]) - }) + expect(view.options.map((option) => option.label)).toEqual(['Start a new specification', 'Cancel']); + }); - it("only shows resume-existing-session when the chosen spec has sessions", () => { + it('only shows resume-existing-session when the chosen spec has sessions', () => { const view = buildWorkspaceSelectionView(emptySessionInventory(), { - stage: "specAction", - specId: "spec-empty", - }) + stage: 'specAction', + specId: 'spec-empty', + }); - expect(view.options.map((option) => option.label)).toEqual([ - "Create new session", - ]) - }) + expect(view.options.map((option) => option.label)).toEqual(['Create new session']); + }); - it("renders specification copy without user-created workspace wording", () => { + it('renders specification copy without user-created workspace wording', () => { const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: () => {}, - }) + }); - const text = component.render(80).join("\n") + const text = component.render(80).join('\n'); - expect(text).toContain("Choose a specification") - expect(text).toContain("Start a new specification") - expect(text).toContain("Continue another existing specification") - expect(text).not.toContain("Brunch workspace") - expect(text).not.toContain("Create workspace") - expect(text).not.toContain("Open workspace") - }) + expect(text).toContain('Choose a specification'); + expect(text).toContain('Start a new specification'); + expect(text).toContain('Continue another existing specification'); + expect(text).not.toContain('Brunch workspace'); + expect(text).not.toContain('Create workspace'); + expect(text).not.toContain('Open workspace'); + }); - it("omits continue-latest from in-session picker contexts", () => { + it('omits continue-latest from in-session picker contexts', () => { const component = createWorkspaceDialogComponent({ inventory: inventory(), includeContinue: false, onDecision: () => {}, - }) + }); - const text = component.render(80).join("\n") + const text = component.render(80).join('\n'); - expect(text).not.toContain("Continue your latest spec and session") - expect(text).toContain("Switch to another specification") - expect(text).toContain("Start a new specification") - expect(text.indexOf("Switch to another specification")).toBeLessThan( - text.indexOf("Start a new specification"), - ) - }) + expect(text).not.toContain('Continue your latest spec and session'); + expect(text).toContain('Switch to another specification'); + expect(text).toContain('Start a new specification'); + expect(text.indexOf('Switch to another specification')).toBeLessThan( + text.indexOf('Start a new specification'), + ); + }); - it("selects current continue as a typed decision", () => { - const decisions: unknown[] = [] + it('selects current continue as a typed decision', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\r") + component.handleInput!('\r'); expect(decisions).toEqual([ { - action: "continue", - specId: "spec-alpha", - sessionFile: "/sessions/alpha-current.jsonl", + action: 'continue', + specId: 'spec-alpha', + sessionFile: '/sessions/alpha-current.jsonl', }, - ]) - }) + ]); + }); - it("returns new-session through the hierarchical keyboard path", () => { - const decisions: unknown[] = [] + it('returns new-session through the hierarchical keyboard path', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\x1B[B") - component.handleInput!("\r") - component.handleInput!("\r") - component.handleInput!("\r") + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + component.handleInput!('\r'); + component.handleInput!('\r'); - expect(decisions).toEqual([{ action: "newSession", specId: "spec-alpha" }]) - }) + expect(decisions).toEqual([{ action: 'newSession', specId: 'spec-alpha' }]); + }); - it("returns open-session through the hierarchical keyboard path", () => { - const decisions: unknown[] = [] + it('returns open-session through the hierarchical keyboard path', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\x1B[B") - component.handleInput!("\r") - component.handleInput!("\r") - component.handleInput!("\x1B[B") - component.handleInput!("\r") - component.handleInput!("\x1B[B") - component.handleInput!("\r") + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + component.handleInput!('\r'); + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); expect(decisions).toEqual([ { - action: "openSession", - specId: "spec-alpha", - sessionFile: "/sessions/alpha-older.jsonl", + action: 'openSession', + specId: 'spec-alpha', + sessionFile: '/sessions/alpha-older.jsonl', }, - ]) - }) + ]); + }); - it("returns new-spec decisions from title entry and cancel on escape", () => { - const decisions: unknown[] = [] + it('returns new-spec decisions from title entry and cancel on escape', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\x1B[B") - component.handleInput!("\x1B[B") - component.handleInput!("\r") - for (const char of "Gamma") { - component.handleInput!(char) + component.handleInput!('\x1B[B'); + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + for (const char of 'Gamma') { + component.handleInput!(char); } - component.handleInput!("\r") + component.handleInput!('\r'); const cancelComponent = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) - cancelComponent.handleInput!("\x1B") + }); + cancelComponent.handleInput!('\x1B'); - expect(decisions).toEqual([ - { action: "newSpec", title: "Gamma" }, - { action: "cancel" }, - ]) - }) + expect(decisions).toEqual([{ action: 'newSpec', title: 'Gamma' }, { action: 'cancel' }]); + }); - it("accepts chunked title input from terminal automation", () => { - const decisions: unknown[] = [] + it('accepts chunked title input from terminal automation', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\x1B[B") - component.handleInput!("\x1B[B") - component.handleInput!("\r") - component.handleInput!("Gamma") - component.handleInput!("\r") + component.handleInput!('\x1B[B'); + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + component.handleInput!('Gamma'); + component.handleInput!('\r'); - expect(decisions).toEqual([{ action: "newSpec", title: "Gamma" }]) - }) + expect(decisions).toEqual([{ action: 'newSpec', title: 'Gamma' }]); + }); - it("backs out one picker stage on escape and cancels from the home stage", () => { - const decisions: unknown[] = [] + it('backs out one picker stage on escape and cancels from the home stage', () => { + const decisions: unknown[] = []; const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: (decision) => decisions.push(decision), - }) + }); - component.handleInput!("\x1B[B") - component.handleInput!("\r") - expect(component.render(80).join("\n")).toContain("Choose a specification") - component.handleInput!("\x1B") - expect(component.render(80).join("\n")).toContain( - "Continue your latest spec and session", - ) - component.handleInput!("\x1B") + component.handleInput!('\x1B[B'); + component.handleInput!('\r'); + expect(component.render(80).join('\n')).toContain('Choose a specification'); + component.handleInput!('\x1B'); + expect(component.render(80).join('\n')).toContain('Continue your latest spec and session'); + component.handleInput!('\x1B'); - expect(decisions).toEqual([{ action: "cancel" }]) - }) + expect(decisions).toEqual([{ action: 'cancel' }]); + }); - it("cancels from startup preflight on ctrl-c", async () => { - const terminal = new FakeTerminal() - const decision = runWorkspaceDialogPreflight(inventory(), { terminal }) + it('cancels from startup preflight on ctrl-c', async () => { + const terminal = new FakeTerminal(); + const decision = runWorkspaceDialogPreflight(inventory(), { terminal }); - terminal.emit("\x03") + terminal.emit('\x03'); - await expect(decision).resolves.toEqual({ action: "cancel" }) - expect(terminal.events.at(-2)).toBe("stop") - expect(terminal.events.at(-1)).toBe("clearScreen") - }) + await expect(decision).resolves.toEqual({ action: 'cancel' }); + expect(terminal.events.at(-2)).toBe('stop'); + expect(terminal.events.at(-1)).toBe('clearScreen'); + }); - it("renders a branded centered-dialog frame with separately styled version metadata", () => { + it('renders a branded centered-dialog frame with separately styled version metadata', () => { const component = createWorkspaceDialogComponent({ inventory: inventory(), onDecision: () => {}, theme: { fg: (color, text) => `[${color}]${text}[/${color}]`, }, - }) + }); - const lines = component.render(80) + const lines = component.render(80); - expect(lines[0]).toContain("╭") - expect(lines[1]).toMatch( - /^\[borderMuted\]│\[\/borderMuted\]\s+\[borderMuted\]│\[\/borderMuted\]$/, - ) - expect(lines.some((line) => line.includes("Choose a specification"))).toBe( - true, - ) - expect( - lines.some((line) => line.includes("[accent]brunch v0.0.0[/accent]")), - ).toBe(true) - expect(lines.some((line) => line.includes("[success](dev"))).toBe(true) - expect(lines.some((line) => line.includes("built on Pi v"))).toBe(true) - }) - - it("provides deterministic shared Brunch identity primitives", async () => { - const assetUrl = new URL( - "../components/workspace-dialog/assets/", - import.meta.url, - ) + expect(lines[0]).toContain('╭'); + expect(lines[1]).toMatch(/^\[borderMuted\]│\[\/borderMuted\]\s+\[borderMuted\]│\[\/borderMuted\]$/); + expect(lines.some((line) => line.includes('Choose a specification'))).toBe(true); + expect(lines.some((line) => line.includes('[accent]brunch v0.0.0[/accent]'))).toBe(true); + expect(lines.some((line) => line.includes('[success](dev'))).toBe(true); + expect(lines.some((line) => line.includes('built on Pi v'))).toBe(true); + }); - expect( - readBrunchAnsiLogo({ assetUrl, truecolor: false }).join("\n"), - ).toContain("\x1B[") + it('provides deterministic shared Brunch identity primitives', async () => { + const assetUrl = new URL('../components/workspace-dialog/assets/', import.meta.url); + + expect(readBrunchAnsiLogo({ assetUrl, truecolor: false }).join('\n')).toContain('\x1B['); expect( formatBrunchProductIdentity({ logoLines: [], - colorMode: "plain", - version: { version: "v-test", dev: null }, - piVersion: "test-pi", + colorMode: 'plain', + version: { version: 'v-test', dev: null }, + piVersion: 'test-pi', }), ).toEqual([ - "█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █", - "█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", - "", - "brunch v-test", - "built on Pi vtest-pi", - ]) + '█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █', + '█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', + '', + 'brunch v-test', + 'built on Pi vtest-pi', + ]); expect( formatBrunchProductIdentity({ - logoLines: ["logo"], - colorMode: "dark", - version: { version: "v-test", dev: "(dev abc)" }, + logoLines: ['logo'], + colorMode: 'dark', + version: { version: 'v-test', dev: '(dev abc)' }, theme: { fg: (color, text) => `[${color}]${text}[/${color}]` }, - piVersion: "test-pi", + piVersion: 'test-pi', }), ).toEqual([ - "logo", - "", - "[muted]█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █[/muted]", - "[muted]█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█[/muted]", - "", - "[accent]brunch v-test[/accent]", - "[success](dev abc)[/success]", - "[dim]built on Pi vtest-pi[/dim]", - ]) - }) - - it("keeps logo assets colocated with the private picker component", async () => { + 'logo', + '', + '[muted]█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █[/muted]', + '[muted]█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█[/muted]', + '', + '[accent]brunch v-test[/accent]', + '[success](dev abc)[/success]', + '[dim]built on Pi vtest-pi[/dim]', + ]); + }); + + it('keeps logo assets colocated with the private picker component', async () => { const source = await readFile( - new URL( - "../components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi", - import.meta.url, - ), - "utf8", - ) + new URL('../components/workspace-dialog/assets/brunch-logo-quad-56x18.ansi', import.meta.url), + 'utf8', + ); - expect(source).toContain("\x1B[") - }) + expect(source).toContain('\x1B['); + }); - it("declares pi-tui as a direct dependency", async () => { + it('declares pi-tui as a direct dependency', async () => { const manifest = JSON.parse( - await readFile(new URL("../../../package.json", import.meta.url), "utf8"), - ) as { dependencies?: Record } + await readFile(new URL('../../../package.json', import.meta.url), 'utf8'), + ) as { dependencies?: Record }; - expect(manifest.dependencies).toHaveProperty("@earendil-works/pi-tui") - }) + expect(manifest.dependencies).toHaveProperty('@earendil-works/pi-tui'); + }); - it("clears the startup preflight frame after a spec/session decision", async () => { - const terminal = new FakeTerminal() - const decision = runWorkspaceDialogPreflight(inventory(), { terminal }) + it('clears the startup preflight frame after a spec/session decision', async () => { + const terminal = new FakeTerminal(); + const decision = runWorkspaceDialogPreflight(inventory(), { terminal }); - terminal.emit("\r") + terminal.emit('\r'); - await expect(decision).resolves.toMatchObject({ action: "continue" }) - expect(terminal.events.at(-2)).toBe("stop") - expect(terminal.events.at(-1)).toBe("clearScreen") - }) -}) + await expect(decision).resolves.toMatchObject({ action: 'continue' }); + expect(terminal.events.at(-2)).toBe('stop'); + expect(terminal.events.at(-1)).toBe('clearScreen'); + }); +}); class FakeTerminal implements Terminal { - events: string[] = [] - #onInput: ((data: string) => void) | undefined + events: string[] = []; + #onInput: ((data: string) => void) | undefined; get columns(): number { - return 100 + return 100; } get rows(): number { - return 32 + return 32; } get kittyProtocolActive(): boolean { - return false + return false; } start(onInput: (data: string) => void): void { - this.events.push("start") - this.#onInput = onInput + this.events.push('start'); + this.#onInput = onInput; } stop(): void { - this.events.push("stop") + this.events.push('stop'); } async drainInput(): Promise {} @@ -423,7 +388,7 @@ class FakeTerminal implements Terminal { clearFromCursor(): void {} clearScreen(): void { - this.events.push("clearScreen") + this.events.push('clearScreen'); } setTitle(_title: string): void {} @@ -431,71 +396,71 @@ class FakeTerminal implements Terminal { setProgress(_active: boolean): void {} emit(data: string): void { - this.#onInput?.(data) + this.#onInput?.(data); } } function emptyInventory(): WorkspaceLaunchInventory { return { - cwd: "/project", + cwd: '/project', currentSpec: null, currentSessionFile: null, needsNewSpec: true, specs: [], unavailableSessions: [], - } + }; } function emptySessionInventory(): WorkspaceLaunchInventory { return { - cwd: "/project", - currentSpec: { id: "spec-empty", title: "Empty" }, + cwd: '/project', + currentSpec: { id: 'spec-empty', title: 'Empty' }, currentSessionFile: null, needsNewSpec: false, - specs: [{ spec: { id: "spec-empty", title: "Empty" }, sessions: [] }], + specs: [{ spec: { id: 'spec-empty', title: 'Empty' }, sessions: [] }], unavailableSessions: [], - } + }; } function inventory(): WorkspaceLaunchInventory { return { - cwd: "/project", - currentSpec: { id: "spec-alpha", title: "Alpha" }, - currentSessionFile: "/sessions/alpha-current.jsonl", + cwd: '/project', + currentSpec: { id: 'spec-alpha', title: 'Alpha' }, + currentSessionFile: '/sessions/alpha-current.jsonl', needsNewSpec: false, specs: [ { - spec: { id: "spec-alpha", title: "Alpha" }, + spec: { id: 'spec-alpha', title: 'Alpha' }, sessions: [ { - id: "session-alpha-current", - file: "/sessions/alpha-current.jsonl", - specId: "spec-alpha", - specTitle: "Alpha", + id: 'session-alpha-current', + file: '/sessions/alpha-current.jsonl', + specId: 'spec-alpha', + specTitle: 'Alpha', available: true, }, { - id: "session-alpha-older", - file: "/sessions/alpha-older.jsonl", - specId: "spec-alpha", - specTitle: "Alpha", + id: 'session-alpha-older', + file: '/sessions/alpha-older.jsonl', + specId: 'spec-alpha', + specTitle: 'Alpha', available: true, }, ], }, { - spec: { id: "spec-beta", title: "Beta" }, + spec: { id: 'spec-beta', title: 'Beta' }, sessions: [ { - id: "session-beta", - file: "/sessions/beta.jsonl", - specId: "spec-beta", - specTitle: "Beta", + id: 'session-beta', + file: '/sessions/beta.jsonl', + specId: 'spec-beta', + specTitle: 'Beta', available: true, }, ], }, ], unavailableSessions: [], - } + }; } diff --git a/src/.pi/components/brunch-identity.ts b/src/.pi/components/brunch-identity.ts index 725fe8923..58ae854f4 100644 --- a/src/.pi/components/brunch-identity.ts +++ b/src/.pi/components/brunch-identity.ts @@ -1,141 +1,107 @@ -import { readFileSync } from "node:fs" -import { fileURLToPath } from "node:url" +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; -import { VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent" -import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent" +import { VERSION as PI_VERSION } from '@earendil-works/pi-coding-agent'; +import type { Theme, ThemeColor } from '@earendil-works/pi-coding-agent'; -const ESC = String.fromCharCode(27) -const ANSI_SEQUENCE = new RegExp(`^${ESC}\\[[0-9;?]*[ -/]*[@-~]`) -const ANSI_SEQUENCE_GLOBAL = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g") -const LOGO_TRUECOLOR = "brunch-logo-quad-56x18.ansi" -const LOGO_240 = "brunch-logo-quad-56x18-240.ansi" +const ESC = String.fromCharCode(27); +const ANSI_SEQUENCE = new RegExp(`^${ESC}\\[[0-9;?]*[ -/]*[@-~]`); +const ANSI_SEQUENCE_GLOBAL = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, 'g'); +const LOGO_TRUECOLOR = 'brunch-logo-quad-56x18.ansi'; +const LOGO_240 = 'brunch-logo-quad-56x18-240.ansi'; // Letterform copied from: cfonts "brunch" -f tiny -c candy. -export const BRUNCH_COMPACT_WORDMARK = [ - "█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █", - "█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█", -] as const +export const BRUNCH_COMPACT_WORDMARK = ['█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █', '█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█'] as const; -export type BrunchIdentityColorMode = "dark" | "light" | "plain" -export type BrunchIdentityTheme = Pick +export type BrunchIdentityColorMode = 'dark' | 'light' | 'plain'; +export type BrunchIdentityTheme = Pick; export interface BrunchVersionInfo { - version: string - dev: string | null + version: string; + dev: string | null; } export interface BrunchLogoReadOptions { - assetUrl: URL - truecolor: boolean + assetUrl: URL; + truecolor: boolean; } export interface BrunchProductIdentityOptions { - logoLines?: readonly string[] - version: BrunchVersionInfo - theme?: BrunchIdentityTheme - colorMode?: BrunchIdentityColorMode - piVersion?: string + logoLines?: readonly string[]; + version: BrunchVersionInfo; + theme?: BrunchIdentityTheme; + colorMode?: BrunchIdentityColorMode; + piVersion?: string; } export function readBrunchAnsiLogo(options: BrunchLogoReadOptions): string[] { - const asset = options.truecolor ? LOGO_TRUECOLOR : LOGO_240 + const asset = options.truecolor ? LOGO_TRUECOLOR : LOGO_240; try { return cropLogo( - readFileSync(fileURLToPath(new URL(asset, options.assetUrl)), "utf8") - .replace(new RegExp(`${ESC}\\[\\?25[lh]`, "g"), "") - .replace(new RegExp(`${ESC}\\[0m$`, "g"), "") - .split("\n"), - ) + readFileSync(fileURLToPath(new URL(asset, options.assetUrl)), 'utf8') + .replace(new RegExp(`${ESC}\\[\\?25[lh]`, 'g'), '') + .replace(new RegExp(`${ESC}\\[0m$`, 'g'), '') + .split('\n'), + ); } catch { - return [] + return []; } } -export function formatBrunchProductIdentity( - options: BrunchProductIdentityOptions, -): string[] { - const logo = [...(options.logoLines ?? [])] - const wordmark = BRUNCH_COMPACT_WORDMARK.map((line) => - identityStyle(options, "muted", line), - ) - const versionLine = identityStyle( - options, - "accent", - `brunch ${options.version.version}`, - ) - const devLine = options.version.dev - ? [identityStyle(options, "success", options.version.dev)] - : [] - const piLine = identityStyle( - options, - "dim", - `built on Pi v${options.piVersion ?? PI_VERSION}`, - ) - - return [ - ...logo, - ...(logo.length > 0 ? [""] : []), - ...wordmark, - "", - versionLine, - ...devLine, - piLine, - ] +export function formatBrunchProductIdentity(options: BrunchProductIdentityOptions): string[] { + const logo = [...(options.logoLines ?? [])]; + const wordmark = BRUNCH_COMPACT_WORDMARK.map((line) => identityStyle(options, 'muted', line)); + const versionLine = identityStyle(options, 'accent', `brunch ${options.version.version}`); + const devLine = options.version.dev ? [identityStyle(options, 'success', options.version.dev)] : []; + const piLine = identityStyle(options, 'dim', `built on Pi v${options.piVersion ?? PI_VERSION}`); + + return [...logo, ...(logo.length > 0 ? [''] : []), ...wordmark, '', versionLine, ...devLine, piLine]; } -function identityStyle( - options: BrunchProductIdentityOptions, - color: ThemeColor, - text: string, -): string { - if (options.colorMode === "plain") return text - return options.theme ? options.theme.fg(color, text) : text +function identityStyle(options: BrunchProductIdentityOptions, color: ThemeColor, text: string): string { + if (options.colorMode === 'plain') return text; + return options.theme ? options.theme.fg(color, text) : text; } function cropLogo(lines: string[]): string[] { - const cropped = [...lines] - while (cropped.length > 0 && stripAnsi(cropped[0]!).trim().length === 0) - cropped.shift() - while ( - cropped.length > 0 && - stripAnsi(cropped[cropped.length - 1]!).trim().length === 0 - ) - cropped.pop() - if (cropped.length === 0) return [] - - const commonLeft = Math.min(...cropped.map(visibleLeadingSpaces)) - return cropped.map((line) => removeVisibleColumns(line, commonLeft)) + const cropped = [...lines]; + while (cropped.length > 0 && stripAnsi(cropped[0]!).trim().length === 0) cropped.shift(); + while (cropped.length > 0 && stripAnsi(cropped[cropped.length - 1]!).trim().length === 0) cropped.pop(); + if (cropped.length === 0) return []; + + const commonLeft = Math.min(...cropped.map(visibleLeadingSpaces)); + return cropped.map((line) => removeVisibleColumns(line, commonLeft)); } function stripAnsi(text: string): string { - return text.replace(ANSI_SEQUENCE_GLOBAL, "") + return text.replace(ANSI_SEQUENCE_GLOBAL, ''); } function visibleLeadingSpaces(line: string): number { - const match = stripAnsi(line).match(/^ */) - return match?.[0].length ?? 0 + const match = stripAnsi(line).match(/^ */); + return match?.[0].length ?? 0; } function removeVisibleColumns(line: string, columns: number): string { - if (columns <= 0) return line + if (columns <= 0) return line; - let output = "" - let removed = 0 + let output = ''; + let removed = 0; for (let index = 0; index < line.length; index += 1) { if (line[index] === ESC) { - const match = line.slice(index).match(ANSI_SEQUENCE) + const match = line.slice(index).match(ANSI_SEQUENCE); if (match) { - output += match[0] - index += match[0].length - 1 - continue + output += match[0]; + index += match[0].length - 1; + continue; } } if (removed < columns) { - removed += 1 - continue + removed += 1; + continue; } - output += line[index]! + output += line[index]!; } - return output + return output; } diff --git a/src/.pi/components/cards.ts b/src/.pi/components/cards.ts index b80dd0c3a..cbf4d891f 100644 --- a/src/.pi/components/cards.ts +++ b/src/.pi/components/cards.ts @@ -7,14 +7,9 @@ * Components here should remain stateless and stitch only pi-tui primitives. */ -import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent" -import { getMarkdownTheme } from "@earendil-works/pi-coding-agent" -import { - type Component, - Markdown, - visibleWidth, - truncateToWidth, -} from "@earendil-works/pi-tui" +import type { Theme, ThemeColor } from '@earendil-works/pi-coding-agent'; +import { getMarkdownTheme } from '@earendil-works/pi-coding-agent'; +import { type Component, Markdown, visibleWidth, truncateToWidth } from '@earendil-works/pi-tui'; /** * Lay components out side-by-side and fall back to vertical stacking once the @@ -30,47 +25,47 @@ export class ResponsiveColumns implements Component { invalidate(): void {} render(width: number): string[] { - if (this.children.length === 0) return [] - if (this.children.length === 1) return this.children[0]!.render(width) + if (this.children.length === 0) return []; + if (this.children.length === 1) return this.children[0]!.render(width); - const n = this.children.length - const totalGap = this.gap * (n - 1) - const perChild = Math.floor((width - totalGap) / n) + const n = this.children.length; + const totalGap = this.gap * (n - 1); + const perChild = Math.floor((width - totalGap) / n); // Too narrow for columns — stack vertically. if (perChild < this.minChildWidth) { - const lines: string[] = [] + const lines: string[] = []; this.children.forEach((c, i) => { - if (i > 0) lines.push("") - lines.push(...c.render(width)) - }) - return lines + if (i > 0) lines.push(''); + lines.push(...c.render(width)); + }); + return lines; } - const grids = this.children.map((c) => c.render(perChild)) - const rowCount = Math.max(...grids.map((g) => g.length)) + const grids = this.children.map((c) => c.render(perChild)); + const rowCount = Math.max(...grids.map((g) => g.length)); // Pad shorter columns with blank lines so all columns share rowCount. - const blank = " ".repeat(perChild) + const blank = ' '.repeat(perChild); const padded = grids.map((g) => { - const result = [...g] - while (result.length < rowCount) result.push(blank) - return result - }) + const result = [...g]; + while (result.length < rowCount) result.push(blank); + return result; + }); // Stitch rows. Each line is padded to perChild visible width before joining. - const gapStr = " ".repeat(this.gap) - const lines: string[] = [] + const gapStr = ' '.repeat(this.gap); + const lines: string[] = []; for (let r = 0; r < rowCount; r++) { const parts = padded.map((g) => { - const line = g[r] ?? blank - const vis = visibleWidth(line) - const padding = vis < perChild ? " ".repeat(perChild - vis) : "" - return line + padding - }) - lines.push(parts.join(gapStr)) + const line = g[r] ?? blank; + const vis = visibleWidth(line); + const padding = vis < perChild ? ' '.repeat(perChild - vis) : ''; + return line + padding; + }); + lines.push(parts.join(gapStr)); } - return lines + return lines; } } @@ -83,7 +78,7 @@ export class CardComponent implements Component { private title: string, private body: string, private theme: Theme, - private accent: ThemeColor = "accent", + private accent: ThemeColor = 'accent', ) {} invalidate(): void { @@ -92,39 +87,36 @@ export class CardComponent implements Component { render(width: number): string[] { // 4 = "│ " (2) + " │" (2). Markdown fills the inner column. - const innerWidth = Math.max(10, width - 4) - const bodyLines = new Markdown(this.body, 0, 0, getMarkdownTheme()).render( - innerWidth, - ) + const innerWidth = Math.max(10, width - 4); + const bodyLines = new Markdown(this.body, 0, 0, getMarkdownTheme()).render(innerWidth); - const c = (s: string) => this.theme.fg(this.accent, s) - const titleText = ` ${this.theme.bold(this.title)} ` - const titleVis = visibleWidth(titleText) + const c = (s: string) => this.theme.fg(this.accent, s); + const titleText = ` ${this.theme.bold(this.title)} `; + const titleVis = visibleWidth(titleText); // Top: ╭─ Title ──...──╮ - const topFiller = Math.max(0, width - 2 - 1 - titleVis) // border corners (2) + opening dash (1) - const top = c("╭─") + titleText + c("─".repeat(topFiller) + "╮") + const topFiller = Math.max(0, width - 2 - 1 - titleVis); // border corners (2) + opening dash (1) + const top = c('╭─') + titleText + c('─'.repeat(topFiller) + '╮'); // Bottom: ╰────────────╯ - const bottom = c("╰" + "─".repeat(Math.max(0, width - 2)) + "╯") + const bottom = c('╰' + '─'.repeat(Math.max(0, width - 2)) + '╯'); // Body: │ │ const sided = bodyLines.map((line) => { - const vis = visibleWidth(line) - const padding = vis < innerWidth ? " ".repeat(innerWidth - vis) : "" + const vis = visibleWidth(line); + const padding = vis < innerWidth ? ' '.repeat(innerWidth - vis) : ''; // If a markdown line exceeds innerWidth, truncate to avoid wrapping. - const safeLine = - vis > innerWidth ? truncateToWidth(line, innerWidth) : line + padding - return c("│ ") + safeLine + c(" │") - }) + const safeLine = vis > innerWidth ? truncateToWidth(line, innerWidth) : line + padding; + return c('│ ') + safeLine + c(' │'); + }); - return [top, ...sided, bottom] + return [top, ...sided, bottom]; } } /** Split an array into fixed-size chunks; last chunk may be shorter. */ export function chunk(arr: T[], size: number): T[][] { - const out: T[][] = [] - for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)) - return out + const out: T[][] = []; + for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); + return out; } diff --git a/src/.pi/components/workspace-dialog.ts b/src/.pi/components/workspace-dialog.ts index 003080da2..a04377084 100644 --- a/src/.pi/components/workspace-dialog.ts +++ b/src/.pi/components/workspace-dialog.ts @@ -2,4 +2,4 @@ export { createWorkspaceDialogComponent, runWorkspaceDialogPreflight, type WorkspaceDialogComponentOptions, -} from "./workspace-dialog/index.js" +} from './workspace-dialog/index.js'; diff --git a/src/.pi/components/workspace-dialog/component.ts b/src/.pi/components/workspace-dialog/component.ts index c0225858b..a205302cd 100644 --- a/src/.pi/components/workspace-dialog/component.ts +++ b/src/.pi/components/workspace-dialog/component.ts @@ -1,344 +1,296 @@ -import { execSync } from "node:child_process" -import { readFileSync } from "node:fs" -import { fileURLToPath } from "node:url" +import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; -import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent" -import { - Key, - matchesKey, - truncateToWidth, - visibleWidth, - type Component, -} from "@earendil-works/pi-tui" +import type { Theme, ThemeColor } from '@earendil-works/pi-coding-agent'; +import { Key, matchesKey, truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui'; import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from "../../../workspace-session-coordinator.js" +} from '../../../workspace-session-coordinator.js'; import { formatBrunchProductIdentity, readBrunchAnsiLogo, type BrunchVersionInfo, -} from "../brunch-identity.js" +} from '../brunch-identity.js'; import { buildWorkspaceSelectionView, selectWorkspaceSelectionOption, type WorkspaceSelectionStage, type WorkspaceSelectionView, -} from "./model.js" +} from './model.js'; -export const WORKSPACE_DIALOG_WIDTH = 80 -const CTRL_C = "\x03" -const ASSET_DIR = new URL("./assets/", import.meta.url) -const PACKAGE_JSON_URL = new URL("../../../../../package.json", import.meta.url) -const LOCAL_BUILD_TIME = formatBuildTime(new Date()) +export const WORKSPACE_DIALOG_WIDTH = 80; +const CTRL_C = '\x03'; +const ASSET_DIR = new URL('./assets/', import.meta.url); +const PACKAGE_JSON_URL = new URL('../../../../../package.json', import.meta.url); +const LOCAL_BUILD_TIME = formatBuildTime(new Date()); -export type WorkspaceDialogTheme = Pick +export type WorkspaceDialogTheme = Pick; export interface WorkspaceDialogComponentOptions { - inventory: WorkspaceLaunchInventory - onDecision: (decision: SpecSessionActivationDecision) => void - theme?: WorkspaceDialogTheme - includeContinue?: boolean + inventory: WorkspaceLaunchInventory; + onDecision: (decision: SpecSessionActivationDecision) => void; + theme?: WorkspaceDialogTheme; + includeContinue?: boolean; } -export function createWorkspaceDialogComponent( - options: WorkspaceDialogComponentOptions, -): Component { - return new WorkspaceDialogComponent(options) +export function createWorkspaceDialogComponent(options: WorkspaceDialogComponentOptions): Component { + return new WorkspaceDialogComponent(options); } class WorkspaceDialogComponent implements Component { - #inventory: WorkspaceLaunchInventory - #onDecision: (decision: SpecSessionActivationDecision) => void - #theme: WorkspaceDialogTheme | undefined - #includeContinue: boolean - #selectedIndex = 0 - #stage: WorkspaceSelectionStage = { stage: "home" } - #history: WorkspaceSelectionStage[] = [] - #title = "" + #inventory: WorkspaceLaunchInventory; + #onDecision: (decision: SpecSessionActivationDecision) => void; + #theme: WorkspaceDialogTheme | undefined; + #includeContinue: boolean; + #selectedIndex = 0; + #stage: WorkspaceSelectionStage = { stage: 'home' }; + #history: WorkspaceSelectionStage[] = []; + #title = ''; constructor(options: WorkspaceDialogComponentOptions) { - this.#inventory = options.inventory - this.#onDecision = options.onDecision - this.#theme = options.theme - this.#includeContinue = options.includeContinue ?? true + this.#inventory = options.inventory; + this.#onDecision = options.onDecision; + this.#theme = options.theme; + this.#includeContinue = options.includeContinue ?? true; } handleInput(data: string): void { if (data === CTRL_C) { - this.#onDecision({ action: "cancel" }) - return + this.#onDecision({ action: 'cancel' }); + return; } - if (this.#stage.stage === "newSpecTitle") { - this.#handleTitleInput(data) - return + if (this.#stage.stage === 'newSpecTitle') { + this.#handleTitleInput(data); + return; } - const view = this.#view() + const view = this.#view(); if (matchesKey(data, Key.up)) { - this.#selectedIndex = Math.max(0, this.#selectedIndex - 1) - return + this.#selectedIndex = Math.max(0, this.#selectedIndex - 1); + return; } if (matchesKey(data, Key.down)) { - this.#selectedIndex = Math.min( - view.options.length - 1, - this.#selectedIndex + 1, - ) - return + this.#selectedIndex = Math.min(view.options.length - 1, this.#selectedIndex + 1); + return; } if (matchesKey(data, Key.escape)) { - this.#backOrCancel() - return + this.#backOrCancel(); + return; } if (matchesKey(data, Key.enter)) { - this.#selectCurrentOption() + this.#selectCurrentOption(); } } render(width: number): string[] { - const dialogWidth = Math.max(24, Math.min(width, WORKSPACE_DIALOG_WIDTH)) - const content = this.#contentLines() - return renderFrame(content, dialogWidth, this.#theme) + const dialogWidth = Math.max(24, Math.min(width, WORKSPACE_DIALOG_WIDTH)); + const content = this.#contentLines(); + return renderFrame(content, dialogWidth, this.#theme); } invalidate(): void {} #contentLines(): string[] { - const view = this.#view() - const title = style(this.#theme, "accent", view.title) + const view = this.#view(); + const title = style(this.#theme, 'accent', view.title); const subtitle = style( this.#theme, - "dim", - "Choose or create the spec/session before the agent loop runs.", - ) + 'dim', + 'Choose or create the spec/session before the agent loop runs.', + ); const lines = [ ...formatBrunchProductIdentity({ logoLines: readLogo(), version: brunchVersion(), ...(this.#theme ? { theme: this.#theme } : {}), }), - "", + '', title, subtitle, - "", - ] + '', + ]; - if (this.#stage.stage === "newSpecTitle") { - lines.push("New specification title:", `› ${this.#title}`) - lines.push("", style(this.#theme, "dim", "enter create • esc back")) - return lines + if (this.#stage.stage === 'newSpecTitle') { + lines.push('New specification title:', `› ${this.#title}`); + lines.push('', style(this.#theme, 'dim', 'enter create • esc back')); + return lines; } for (const [index, option] of view.options.entries()) { - const selected = index === this.#selectedIndex - const prefix = selected ? style(this.#theme, "accent", "› ") : " " - const label = selected - ? style(this.#theme, "accent", option.label) - : option.label - lines.push(`${prefix}${label}`) - lines.push(` ${style(this.#theme, "dim", option.description)}`) + const selected = index === this.#selectedIndex; + const prefix = selected ? style(this.#theme, 'accent', '› ') : ' '; + const label = selected ? style(this.#theme, 'accent', option.label) : option.label; + lines.push(`${prefix}${label}`); + lines.push(` ${style(this.#theme, 'dim', option.description)}`); } - lines.push( - "", - style(this.#theme, "dim", "↑↓ navigate • enter select • esc cancel"), - ) - return lines + lines.push('', style(this.#theme, 'dim', '↑↓ navigate • enter select • esc cancel')); + return lines; } #selectCurrentOption(): void { - const result = selectWorkspaceSelectionOption( - this.#view(), - this.#selectedIndex, - this.#inventory, - { includeContinue: this.#includeContinue }, - ) - if ("decision" in result) { - this.#onDecision(result.decision) - return + const result = selectWorkspaceSelectionOption(this.#view(), this.#selectedIndex, this.#inventory, { + includeContinue: this.#includeContinue, + }); + if ('decision' in result) { + this.#onDecision(result.decision); + return; } - this.#history.push(this.#stage) - this.#stage = viewToStage(result.view) - this.#selectedIndex = 0 - if (this.#stage.stage === "newSpecTitle") this.#title = "" + this.#history.push(this.#stage); + this.#stage = viewToStage(result.view); + this.#selectedIndex = 0; + if (this.#stage.stage === 'newSpecTitle') this.#title = ''; } #handleTitleInput(data: string): void { if (matchesKey(data, Key.escape)) { - this.#backOrCancel() - return + this.#backOrCancel(); + return; } if (matchesKey(data, Key.backspace)) { - this.#title = this.#title.slice(0, -1) - return + this.#title = this.#title.slice(0, -1); + return; } if (matchesKey(data, Key.enter)) { - const title = this.#title.trim() + const title = this.#title.trim(); if (title.length > 0) { - this.#onDecision({ action: "newSpec", title }) + this.#onDecision({ action: 'newSpec', title }); } - return + return; } - const text = printableInputText(data) + const text = printableInputText(data); if (text) { - this.#title += text + this.#title += text; } } #view(): WorkspaceSelectionView { return buildWorkspaceSelectionView(this.#inventory, this.#stage, { includeContinue: this.#includeContinue, - }) + }); } #backOrCancel(): void { - const previous = this.#history.pop() + const previous = this.#history.pop(); if (!previous) { - this.#onDecision({ action: "cancel" }) - return + this.#onDecision({ action: 'cancel' }); + return; } - this.#stage = previous - this.#selectedIndex = 0 - this.#title = "" + this.#stage = previous; + this.#selectedIndex = 0; + this.#title = ''; } } function viewToStage(view: WorkspaceSelectionView): WorkspaceSelectionStage { - if (view.stage === "newSpecTitle") return { stage: "newSpecTitle", title: "" } - if (view.stage === "specAction" && view.specId) - return { stage: "specAction", specId: view.specId } - if (view.stage === "sessionList" && view.specId) - return { stage: "sessionList", specId: view.specId } - if (view.stage === "specList") return { stage: "specList" } - return { stage: "home" } + if (view.stage === 'newSpecTitle') return { stage: 'newSpecTitle', title: '' }; + if (view.stage === 'specAction' && view.specId) return { stage: 'specAction', specId: view.specId }; + if (view.stage === 'sessionList' && view.specId) return { stage: 'sessionList', specId: view.specId }; + if (view.stage === 'specList') return { stage: 'specList' }; + return { stage: 'home' }; } -function renderFrame( - content: string[], - width: number, - theme: WorkspaceDialogTheme | undefined, -): string[] { +function renderFrame(content: string[], width: number, theme: WorkspaceDialogTheme | undefined): string[] { return [ topBorderLine(width, theme), emptyLine(width, theme), ...content.map((line) => contentLine(line, width, theme)), emptyLine(width, theme), bottomBorderLine(width, theme), - ] + ]; } interface PackageJson { - version?: unknown - private?: unknown + version?: unknown; + private?: unknown; } function formatBuildTime(date: Date): string { return date .toISOString() - .replace("T", " ") - .replace(/\.\d+Z$/, " UTC") + .replace('T', ' ') + .replace(/\.\d+Z$/, ' UTC'); } function readPackage(): PackageJson { try { - return JSON.parse( - readFileSync(fileURLToPath(PACKAGE_JSON_URL), "utf8"), - ) as PackageJson + return JSON.parse(readFileSync(fileURLToPath(PACKAGE_JSON_URL), 'utf8')) as PackageJson; } catch { - return {} + return {}; } } function getGitSha(): string { try { - return execSync("git rev-parse --short=7 HEAD", { - cwd: fileURLToPath(new URL("../../../../../", import.meta.url)), - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim() + return execSync('git rev-parse --short=7 HEAD', { + cwd: fileURLToPath(new URL('../../../../../', import.meta.url)), + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); } catch { - return "" + return ''; } } function brunchVersion(): BrunchVersionInfo { - const pkg = readPackage() - const version = typeof pkg.version === "string" ? pkg.version : "0.0.0" - const isLocalDev = pkg.private === true || version === "0.0.0" - if (!isLocalDev) return { version: `v${version}`, dev: null } - - const gitSha = getGitSha() - const devMeta = [gitSha, `@ ${LOCAL_BUILD_TIME}`].filter(Boolean).join(" ") - return { version: `v${version}`, dev: devMeta ? `(dev ${devMeta})` : "(dev)" } + const pkg = readPackage(); + const version = typeof pkg.version === 'string' ? pkg.version : '0.0.0'; + const isLocalDev = pkg.private === true || version === '0.0.0'; + if (!isLocalDev) return { version: `v${version}`, dev: null }; + + const gitSha = getGitSha(); + const devMeta = [gitSha, `@ ${LOCAL_BUILD_TIME}`].filter(Boolean).join(' '); + return { version: `v${version}`, dev: devMeta ? `(dev ${devMeta})` : '(dev)' }; } -function contentLine( - content: string, - width: number, - theme: WorkspaceDialogTheme | undefined, -): string { - if (width <= 4) return truncateToWidth(content, width) - const innerWidth = width - 4 - const inner = truncateToWidth(content, innerWidth) - const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(inner))) - const vertical = style(theme, "borderMuted", "│") - return `${vertical} ${inner}${padding} ${vertical}` +function contentLine(content: string, width: number, theme: WorkspaceDialogTheme | undefined): string { + if (width <= 4) return truncateToWidth(content, width); + const innerWidth = width - 4; + const inner = truncateToWidth(content, innerWidth); + const padding = ' '.repeat(Math.max(0, innerWidth - visibleWidth(inner))); + const vertical = style(theme, 'borderMuted', '│'); + return `${vertical} ${inner}${padding} ${vertical}`; } -function emptyLine( - width: number, - theme: WorkspaceDialogTheme | undefined, -): string { - if (width <= 2) return " ".repeat(Math.max(0, width)) - const vertical = style(theme, "borderMuted", "│") - return `${vertical}${" ".repeat(width - 2)}${vertical}` +function emptyLine(width: number, theme: WorkspaceDialogTheme | undefined): string { + if (width <= 2) return ' '.repeat(Math.max(0, width)); + const vertical = style(theme, 'borderMuted', '│'); + return `${vertical}${' '.repeat(width - 2)}${vertical}`; } -function topBorderLine( - width: number, - theme: WorkspaceDialogTheme | undefined, -): string { - if (width <= 2) return " ".repeat(Math.max(0, width)) - return style(theme, "borderMuted", `╭${"─".repeat(width - 2)}╮`) +function topBorderLine(width: number, theme: WorkspaceDialogTheme | undefined): string { + if (width <= 2) return ' '.repeat(Math.max(0, width)); + return style(theme, 'borderMuted', `╭${'─'.repeat(width - 2)}╮`); } -function bottomBorderLine( - width: number, - theme: WorkspaceDialogTheme | undefined, -): string { - if (width <= 2) return " ".repeat(Math.max(0, width)) - return style(theme, "borderMuted", `╰${"─".repeat(width - 2)}╯`) +function bottomBorderLine(width: number, theme: WorkspaceDialogTheme | undefined): string { + if (width <= 2) return ' '.repeat(Math.max(0, width)); + return style(theme, 'borderMuted', `╰${'─'.repeat(width - 2)}╯`); } function readLogo(): string[] { return readBrunchAnsiLogo({ assetUrl: ASSET_DIR, truecolor: supportsTruecolor(), - }) + }); } function supportsTruecolor(): boolean { - const colorterm = process.env.COLORTERM?.toLowerCase() ?? "" - const term = process.env.TERM?.toLowerCase() ?? "" - return ( - colorterm === "truecolor" || - colorterm === "24bit" || - term.includes("truecolor") - ) + const colorterm = process.env.COLORTERM?.toLowerCase() ?? ''; + const term = process.env.TERM?.toLowerCase() ?? ''; + return colorterm === 'truecolor' || colorterm === '24bit' || term.includes('truecolor'); } -function style( - theme: WorkspaceDialogTheme | undefined, - color: ThemeColor, - text: string, -): string { - return theme ? theme.fg(color, text) : text +function style(theme: WorkspaceDialogTheme | undefined, color: ThemeColor, text: string): string { + return theme ? theme.fg(color, text) : text; } function printableInputText(data: string): string { return Array.from(data) - .filter((char) => char >= " " && char !== "\u007f") - .join("") + .filter((char) => char >= ' ' && char !== '\u007f') + .join(''); } diff --git a/src/.pi/components/workspace-dialog/index.ts b/src/.pi/components/workspace-dialog/index.ts index 98f9d4e8a..c5c332e3d 100644 --- a/src/.pi/components/workspace-dialog/index.ts +++ b/src/.pi/components/workspace-dialog/index.ts @@ -2,7 +2,7 @@ export { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, type WorkspaceDialogComponentOptions, -} from "./component.js" +} from './component.js'; export { buildWorkspaceSelectionView, selectWorkspaceSelectionOption, @@ -10,5 +10,5 @@ export { type WorkspaceSelectionResult, type WorkspaceSelectionStage, type WorkspaceSelectionView, -} from "./model.js" -export { runWorkspaceDialogPreflight } from "./preflight.js" +} from './model.js'; +export { runWorkspaceDialogPreflight } from './preflight.js'; diff --git a/src/.pi/components/workspace-dialog/model.ts b/src/.pi/components/workspace-dialog/model.ts index 005ca405a..a7ded7ea9 100644 --- a/src/.pi/components/workspace-dialog/model.ts +++ b/src/.pi/components/workspace-dialog/model.ts @@ -2,123 +2,136 @@ import type { WorkspaceLaunchInventory, WorkspaceLaunchSession, SpecSessionActivationDecision, -} from "../../../workspace-session-coordinator.js" - -export type WorkspaceSelectionStage = { stage: "home" } | { - stage: "newSpecTitle" - title: string -} | { stage: "specList" } | { - stage: "specAction" - specId: string -} | { - stage: "sessionList" - specId: string -} +} from '../../../workspace-session-coordinator.js'; + +export type WorkspaceSelectionStage = + | { stage: 'home' } + | { + stage: 'newSpecTitle'; + title: string; + } + | { stage: 'specList' } + | { + stage: 'specAction'; + specId: string; + } + | { + stage: 'sessionList'; + specId: string; + }; export interface WorkspaceSelectionOption { - id: string - label: string - description: string - kind: "continue" | "newSpec" | "resumeSpec" | "cancel" | "spec" | "newSession" | "resumeSession" | "session" - decision?: SpecSessionActivationDecision - nextStage?: WorkspaceSelectionStage + id: string; + label: string; + description: string; + kind: + | 'continue' + | 'newSpec' + | 'resumeSpec' + | 'cancel' + | 'spec' + | 'newSession' + | 'resumeSession' + | 'session'; + decision?: SpecSessionActivationDecision; + nextStage?: WorkspaceSelectionStage; } export interface WorkspaceSelectionView { - stage: WorkspaceSelectionStage["stage"] - title: string - options: WorkspaceSelectionOption[] - specId?: string + stage: WorkspaceSelectionStage['stage']; + title: string; + options: WorkspaceSelectionOption[]; + specId?: string; } export interface WorkspaceSelectionViewOptions { - includeContinue?: boolean + includeContinue?: boolean; } -export type WorkspaceSelectionResult = { - decision: SpecSessionActivationDecision -} | { - view: WorkspaceSelectionView -} +export type WorkspaceSelectionResult = + | { + decision: SpecSessionActivationDecision; + } + | { + view: WorkspaceSelectionView; + }; export function buildWorkspaceSelectionView( inventory: WorkspaceLaunchInventory, - stage: WorkspaceSelectionStage = { stage: "home" }, + stage: WorkspaceSelectionStage = { stage: 'home' }, options: WorkspaceSelectionViewOptions = {}, ): WorkspaceSelectionView { - if (stage.stage === "newSpecTitle") { + if (stage.stage === 'newSpecTitle') { return { - stage: "newSpecTitle", - title: "Create new specification", + stage: 'newSpecTitle', + title: 'Create new specification', options: [], - } + }; } - if (stage.stage === "specList") { + if (stage.stage === 'specList') { return { - stage: "specList", - title: "Choose a specification", + stage: 'specList', + title: 'Choose a specification', options: inventory.specs.map(({ spec }) => ({ id: `spec:${spec.id}`, label: spec.title, - description: "Choose how to continue this specification", - kind: "spec", - nextStage: { stage: "specAction", specId: spec.id }, + description: 'Choose how to continue this specification', + kind: 'spec', + nextStage: { stage: 'specAction', specId: spec.id }, })), - } + }; } - if (stage.stage === "specAction") { - const spec = findSpec(inventory, stage.specId) + if (stage.stage === 'specAction') { + const spec = findSpec(inventory, stage.specId); const options: WorkspaceSelectionOption[] = [ { id: `new-session:${stage.specId}`, - label: "Create new session", - description: "Start a binding-only session for this specification", - kind: "newSession", - decision: { action: "newSession", specId: stage.specId }, + label: 'Create new session', + description: 'Start a binding-only session for this specification', + kind: 'newSession', + decision: { action: 'newSession', specId: stage.specId }, }, - ] + ]; if ((spec?.sessions.length ?? 0) > 0) { options.push({ id: `resume-session:${stage.specId}`, - label: "Resume existing session", - description: "Choose a prior session transcript explicitly", - kind: "resumeSession", - nextStage: { stage: "sessionList", specId: stage.specId }, - }) + label: 'Resume existing session', + description: 'Choose a prior session transcript explicitly', + kind: 'resumeSession', + nextStage: { stage: 'sessionList', specId: stage.specId }, + }); } return { - stage: "specAction", + stage: 'specAction', specId: stage.specId, - title: spec ? `Continue ${spec.spec.title}` : "Continue specification", + title: spec ? `Continue ${spec.spec.title}` : 'Continue specification', options, - } + }; } - if (stage.stage === "sessionList") { - const spec = findSpec(inventory, stage.specId) + if (stage.stage === 'sessionList') { + const spec = findSpec(inventory, stage.specId); return { - stage: "sessionList", + stage: 'sessionList', specId: stage.specId, - title: spec - ? `Choose a session for ${spec.spec.title}` - : "Choose a session", + title: spec ? `Choose a session for ${spec.spec.title}` : 'Choose a session', options: (spec?.sessions ?? []).map((session) => ({ id: `session:${session.file}`, label: session.name ?? session.id, - description: sessionDescription(session, "Open existing session"), - kind: "session", + description: sessionDescription(session, 'Open existing session'), + kind: 'session', decision: { - action: "openSession", + action: 'openSession', specId: stage.specId, sessionFile: session.file, }, })), - } + }; } - return buildHomeSelectionView(inventory, options) + return buildHomeSelectionView(inventory, options); } export function selectWorkspaceSelectionOption( @@ -127,121 +140,110 @@ export function selectWorkspaceSelectionOption( inventory?: WorkspaceLaunchInventory, options: WorkspaceSelectionViewOptions = {}, ): WorkspaceSelectionResult { - const option = view.options[index] - if (!option) return { decision: { action: "cancel" } } - if (option.decision) return { decision: option.decision } + const option = view.options[index]; + if (!option) return { decision: { action: 'cancel' } }; + if (option.decision) return { decision: option.decision }; if (!inventory) { - return { view: stageOnlyView(option.nextStage ?? { stage: "home" }) } + return { view: stageOnlyView(option.nextStage ?? { stage: 'home' }) }; } return { view: buildWorkspaceSelectionView(inventory, option.nextStage, options), - } + }; } function stageOnlyView(stage: WorkspaceSelectionStage): WorkspaceSelectionView { return { stage: stage.stage, - title: stage.stage === "newSpecTitle" ? stage.title : "", - ...("specId" in stage ? { specId: stage.specId } : {}), + title: stage.stage === 'newSpecTitle' ? stage.title : '', + ...('specId' in stage ? { specId: stage.specId } : {}), options: [], - } + }; } function buildHomeSelectionView( inventory: WorkspaceLaunchInventory, viewOptions: WorkspaceSelectionViewOptions, ): WorkspaceSelectionView { - const selectionOptions: WorkspaceSelectionOption[] = [] - const currentSession = findCurrentSession(inventory) - - if ( - viewOptions.includeContinue !== false && - currentSession && - inventory.currentSpec - ) { + const selectionOptions: WorkspaceSelectionOption[] = []; + const currentSession = findCurrentSession(inventory); + + if (viewOptions.includeContinue !== false && currentSession && inventory.currentSpec) { selectionOptions.push({ id: `continue:${currentSession.file}`, - label: "Continue your latest spec and session", + label: 'Continue your latest spec and session', description: `${inventory.currentSpec.title} · ${currentSession.id}`, - kind: "continue", + kind: 'continue', decision: { - action: "continue", + action: 'continue', specId: inventory.currentSpec.id, sessionFile: currentSession.file, }, - }) + }); } const newSpecOption: WorkspaceSelectionOption = { - id: "new-spec", - label: "Start a new specification", - description: "Name a new spec and create its first session", - kind: "newSpec", - nextStage: { stage: "newSpecTitle", title: "" }, - } + id: 'new-spec', + label: 'Start a new specification', + description: 'Name a new spec and create its first session', + kind: 'newSpec', + nextStage: { stage: 'newSpecTitle', title: '' }, + }; const resumeSpecOption: WorkspaceSelectionOption | null = inventory.specs.length > 0 ? { - id: "resume-spec", + id: 'resume-spec', label: viewOptions.includeContinue === false - ? "Switch to another specification" - : "Continue another existing specification", - description: "Choose a spec, then create or resume a session", - kind: "resumeSpec", - nextStage: { stage: "specList" }, + ? 'Switch to another specification' + : 'Continue another existing specification', + description: 'Choose a spec, then create or resume a session', + kind: 'resumeSpec', + nextStage: { stage: 'specList' }, } - : null + : null; const cancelOption: WorkspaceSelectionOption = { - id: "cancel", - label: "Cancel", - description: "Exit without activating a spec/session", - kind: "cancel", - decision: { action: "cancel" }, - } + id: 'cancel', + label: 'Cancel', + description: 'Exit without activating a spec/session', + kind: 'cancel', + decision: { action: 'cancel' }, + }; if (viewOptions.includeContinue === false) { - if (resumeSpecOption) selectionOptions.push(resumeSpecOption) - selectionOptions.push(newSpecOption, cancelOption) + if (resumeSpecOption) selectionOptions.push(resumeSpecOption); + selectionOptions.push(newSpecOption, cancelOption); } else { - if (resumeSpecOption) selectionOptions.push(resumeSpecOption) - selectionOptions.push(newSpecOption, cancelOption) + if (resumeSpecOption) selectionOptions.push(resumeSpecOption); + selectionOptions.push(newSpecOption, cancelOption); } return { - stage: "home", - title: "Choose a specification", + stage: 'home', + title: 'Choose a specification', options: selectionOptions, - } + }; } -function findCurrentSession( - inventory: WorkspaceLaunchInventory, -): WorkspaceLaunchSession | undefined { +function findCurrentSession(inventory: WorkspaceLaunchInventory): WorkspaceLaunchSession | undefined { if (!inventory.currentSessionFile) { - return undefined + return undefined; } for (const spec of inventory.specs) { - const session = spec.sessions.find( - (candidate) => candidate.file === inventory.currentSessionFile, - ) + const session = spec.sessions.find((candidate) => candidate.file === inventory.currentSessionFile); if (session) { - return session + return session; } } - return undefined + return undefined; } function findSpec( inventory: WorkspaceLaunchInventory, specId: string, -): WorkspaceLaunchInventory["specs"][number] | undefined { - return inventory.specs.find((candidate) => candidate.spec.id === specId) +): WorkspaceLaunchInventory['specs'][number] | undefined { + return inventory.specs.find((candidate) => candidate.spec.id === specId); } -function sessionDescription( - session: WorkspaceLaunchSession, - prefix: string, -): string { - return `${prefix} · ${session.id}` +function sessionDescription(session: WorkspaceLaunchSession, prefix: string): string { + return `${prefix} · ${session.id}`; } diff --git a/src/.pi/components/workspace-dialog/preflight.ts b/src/.pi/components/workspace-dialog/preflight.ts index 2b6504705..8f1cb2ba1 100644 --- a/src/.pi/components/workspace-dialog/preflight.ts +++ b/src/.pi/components/workspace-dialog/preflight.ts @@ -1,88 +1,86 @@ -import type { ThemeColor } from "@earendil-works/pi-coding-agent" -import { ProcessTerminal, TUI, type Terminal } from "@earendil-works/pi-tui" +import type { ThemeColor } from '@earendil-works/pi-coding-agent'; +import { ProcessTerminal, TUI, type Terminal } from '@earendil-works/pi-tui'; import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from "../../../workspace-session-coordinator.js" +} from '../../../workspace-session-coordinator.js'; import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, type WorkspaceDialogTheme, -} from "./component.js" +} from './component.js'; interface WorkspaceDialogPreflightOptions { - terminal?: Terminal - theme?: WorkspaceDialogTheme + terminal?: Terminal; + theme?: WorkspaceDialogTheme; } export async function runWorkspaceDialogPreflight( inventory: WorkspaceLaunchInventory, options: WorkspaceDialogPreflightOptions = {}, ): Promise { - const terminal = options.terminal ?? new ProcessTerminal() - const tui = new TUI(terminal) - const dialogTheme = options.theme ?? resolveStartupDialogTheme() + const terminal = options.terminal ?? new ProcessTerminal(); + const tui = new TUI(terminal); + const dialogTheme = options.theme ?? resolveStartupDialogTheme(); return await new Promise((resolve) => { const finish = (decision: SpecSessionActivationDecision) => { - overlay.hide() - tui.stop() - terminal.clearScreen() - resolve(decision) - } + overlay.hide(); + tui.stop(); + terminal.clearScreen(); + resolve(decision); + }; const component = createWorkspaceDialogComponent({ inventory, theme: dialogTheme, onDecision: finish, - }) + }); const overlay = tui.showOverlay(component, { - anchor: "center", + anchor: 'center', width: WORKSPACE_DIALOG_WIDTH, - maxHeight: "90%", + maxHeight: '90%', margin: 1, - }) - terminal.clearScreen() - tui.start() - }) + }); + terminal.clearScreen(); + tui.start(); + }); } function resolveStartupDialogTheme(): WorkspaceDialogTheme { - const colors = startupPalette(detectStartupThemeName()) + const colors = startupPalette(detectStartupThemeName()); return { fg(color: ThemeColor, text: string) { - const ansi = colors[color] - return ansi ? `${ansi}${text}\x1B[39m` : text + const ansi = colors[color]; + return ansi ? `${ansi}${text}\x1B[39m` : text; }, - } + }; } -function detectStartupThemeName(): "dark" | "light" { - const colorfgbg = process.env.COLORFGBG ?? "" - const background = Number.parseInt(colorfgbg.split(";").at(-1) ?? "", 10) +function detectStartupThemeName(): 'dark' | 'light' { + const colorfgbg = process.env.COLORFGBG ?? ''; + const background = Number.parseInt(colorfgbg.split(';').at(-1) ?? '', 10); if (!Number.isNaN(background)) { - return background < 8 ? "dark" : "light" + return background < 8 ? 'dark' : 'light'; } - return "dark" + return 'dark'; } -function startupPalette( - themeName: "dark" | "light", -): Partial> { - if (themeName === "light") { +function startupPalette(themeName: 'dark' | 'light'): Partial> { + if (themeName === 'light') { return { - accent: "\x1B[38;2;90;128;128m", - borderMuted: "\x1B[38;2;176;176;176m", - dim: "\x1B[38;2;118;118;118m", - muted: "\x1B[38;2;108;108;108m", - success: "\x1B[38;2;88;132;88m", - } + accent: '\x1B[38;2;90;128;128m', + borderMuted: '\x1B[38;2;176;176;176m', + dim: '\x1B[38;2;118;118;118m', + muted: '\x1B[38;2;108;108;108m', + success: '\x1B[38;2;88;132;88m', + }; } return { - accent: "\x1B[38;2;138;190;183m", - borderMuted: "\x1B[38;2;80;80;80m", - dim: "\x1B[38;2;102;102;102m", - muted: "\x1B[38;2;128;128;128m", - success: "\x1B[38;2;181;189;104m", - } + accent: '\x1B[38;2;138;190;183m', + borderMuted: '\x1B[38;2;80;80;80m', + dim: '\x1B[38;2;102;102;102m', + muted: '\x1B[38;2;128;128;128m', + success: '\x1B[38;2;181;189;104m', + }; } diff --git a/src/.pi/context/builders/graph-context.ts b/src/.pi/context/builders/graph-context.ts index 1fe7b43ce..e49f7a9d0 100644 --- a/src/.pi/context/builders/graph-context.ts +++ b/src/.pi/context/builders/graph-context.ts @@ -1,7 +1,7 @@ export interface GraphContextSnapshot { - graphNodeCount?: number + graphNodeCount?: number; } export function renderGraphContext(_snapshot?: GraphContextSnapshot): string { - return "" + return ''; } diff --git a/src/.pi/context/builders/readiness-context.ts b/src/.pi/context/builders/readiness-context.ts index 1f8c2c57d..1741a3132 100644 --- a/src/.pi/context/builders/readiness-context.ts +++ b/src/.pi/context/builders/readiness-context.ts @@ -1,10 +1,8 @@ export interface ReadinessContextSnapshot { - readinessGrade?: string - elicitationPosture?: string + readinessGrade?: string; + elicitationPosture?: string; } -export function renderReadinessContext( - _snapshot?: ReadinessContextSnapshot, -): string { - return "" +export function renderReadinessContext(_snapshot?: ReadinessContextSnapshot): string { + return ''; } diff --git a/src/.pi/context/builders/structured-exchange-context.ts b/src/.pi/context/builders/structured-exchange-context.ts index f2183ae4d..eb6acc487 100644 --- a/src/.pi/context/builders/structured-exchange-context.ts +++ b/src/.pi/context/builders/structured-exchange-context.ts @@ -1,9 +1,7 @@ export interface StructuredExchangeContextSnapshot { - pendingExchangeId?: string + pendingExchangeId?: string; } -export function renderStructuredExchangeContext( - _snapshot?: StructuredExchangeContextSnapshot, -): string { - return "" +export function renderStructuredExchangeContext(_snapshot?: StructuredExchangeContextSnapshot): string { + return ''; } diff --git a/src/.pi/context/compose-brunch-prompt.ts b/src/.pi/context/compose-brunch-prompt.ts index 38ec5159a..fdc8889d9 100644 --- a/src/.pi/context/compose-brunch-prompt.ts +++ b/src/.pi/context/compose-brunch-prompt.ts @@ -1,104 +1,91 @@ -import { readFileSync } from "node:fs" +import { readFileSync } from 'node:fs'; -import { renderGraphContext } from "./builders/graph-context.js" -import { renderReadinessContext } from "./builders/readiness-context.js" -import { renderStructuredExchangeContext } from "./builders/structured-exchange-context.js" +import { renderGraphContext } from './builders/graph-context.js'; +import { renderReadinessContext } from './builders/readiness-context.js'; +import { renderStructuredExchangeContext } from './builders/structured-exchange-context.js'; export interface BrunchPromptCompositionState { - operationalMode: string - agentRole: string - agentStrategy: string - agentLens: string | null - activeTools: readonly string[] + operationalMode: string; + agentRole: string; + agentStrategy: string; + agentLens: string | null; + activeTools: readonly string[]; } export interface BrunchPromptPack { - id: string - title: string - markdown: string + id: string; + title: string; + markdown: string; } export interface BrunchPromptCompositionResult { - prompt: string - packIds: readonly string[] + prompt: string; + packIds: readonly string[]; } const PROMPT_PACK_ORDER = [ - "brunch-base", - "elicit", - "elicitor", - "structured-exchange", - "candidate-proposals", - "capture-analysis", -] as const + 'brunch-base', + 'elicit', + 'elicitor', + 'structured-exchange', + 'candidate-proposals', + 'capture-analysis', +] as const; -type PromptPackId = typeof PROMPT_PACK_ORDER[number] +type PromptPackId = (typeof PROMPT_PACK_ORDER)[number]; const PROMPT_PACK_TITLES: Record = { - "brunch-base": "Brunch base", - elicit: "Operational mode: elicit", - elicitor: "Agent role: elicitor", - "structured-exchange": "Structured exchanges", - "candidate-proposals": "Candidate proposals", - "capture-analysis": "Capture analysis", -} + 'brunch-base': 'Brunch base', + elicit: 'Operational mode: elicit', + elicitor: 'Agent role: elicitor', + 'structured-exchange': 'Structured exchanges', + 'candidate-proposals': 'Candidate proposals', + 'capture-analysis': 'Capture analysis', +}; function readPromptPack(id: PromptPackId): BrunchPromptPack { return { id, title: PROMPT_PACK_TITLES[id], - markdown: readFileSync( - new URL(`./prompt-packs/${id}.md`, import.meta.url), - "utf8", - ).trim(), - } + markdown: readFileSync(new URL(`./prompt-packs/${id}.md`, import.meta.url), 'utf8').trim(), + }; } -const PROMPT_PACKS = PROMPT_PACK_ORDER.map(readPromptPack) +const PROMPT_PACKS = PROMPT_PACK_ORDER.map(readPromptPack); function renderAgentState(state: BrunchPromptCompositionState): string { - const tools = state.activeTools.join(", ") || "none" - const lens = state.agentLens ?? "none" + const tools = state.activeTools.join(', ') || 'none'; + const lens = state.agentLens ?? 'none'; return [ - "[Brunch agent state]", + '[Brunch agent state]', `- Operational mode: ${state.operationalMode}.`, `- Agent role: ${state.agentRole}.`, `- Agent strategy: ${state.agentStrategy}.`, `- Agent lens: ${lens}.`, - `- Prompt packs: ${PROMPT_PACK_ORDER.join(", ")}.`, - "", - "[Brunch tool policy]", + `- Prompt packs: ${PROMPT_PACK_ORDER.join(', ')}.`, + '', + '[Brunch tool policy]', `- Brunch exposes only elicit-safe tools: ${tools}.`, - "- Do not attempt to write files, edit code, run shell commands, change git state, install dependencies, start processes, or mutate external systems.", - "- If the user asks for a side-effecting action, explain that this Brunch prototype is read-only for now.", - ].join("\n") + '- Do not attempt to write files, edit code, run shell commands, change git state, install dependencies, start processes, or mutate external systems.', + '- If the user asks for a side-effecting action, explain that this Brunch prototype is read-only for now.', + ].join('\n'); } function joinPromptSections(sections: readonly string[]): string { return sections .map((section) => section.trim()) .filter((section) => section.length > 0) - .join("\n\n") + .join('\n\n'); } -export function composeBrunchPrompt( - state: BrunchPromptCompositionState, -): BrunchPromptCompositionResult { - const packSections = PROMPT_PACKS.map((pack) => pack.markdown) - const dynamicSections = [ - renderGraphContext(), - renderReadinessContext(), - renderStructuredExchangeContext(), - ] - const prompt = joinPromptSections([ - renderAgentState(state), - ...packSections, - ...dynamicSections, - ]) +export function composeBrunchPrompt(state: BrunchPromptCompositionState): BrunchPromptCompositionResult { + const packSections = PROMPT_PACKS.map((pack) => pack.markdown); + const dynamicSections = [renderGraphContext(), renderReadinessContext(), renderStructuredExchangeContext()]; + const prompt = joinPromptSections([renderAgentState(state), ...packSections, ...dynamicSections]); return { prompt, packIds: PROMPT_PACKS.map((pack) => pack.id), - } + }; } diff --git a/src/.pi/extensions/alternatives.ts b/src/.pi/extensions/alternatives.ts index 406c33cfc..05302d2ef 100644 --- a/src/.pi/extensions/alternatives.ts +++ b/src/.pi/extensions/alternatives.ts @@ -10,47 +10,45 @@ * and visible to transcript replay/RPC clients through markdown fallback text. */ -import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent" -import { Container, Text } from "@earendil-works/pi-tui" -import { StringEnum } from "@earendil-works/pi-ai" -import { Type } from "typebox" +import { StringEnum } from '@earendil-works/pi-ai'; +import type { ExtensionAPI, ThemeColor } from '@earendil-works/pi-coding-agent'; +import { Container, Text } from '@earendil-works/pi-tui'; +import { Type } from 'typebox'; -import { CardComponent, ResponsiveColumns, chunk } from "../components/cards.js" +import { CardComponent, ResponsiveColumns, chunk } from '../components/cards.js'; // ── Types & schema ───────────────────────────────────────────────────── -const FLAVOR = StringEnum(["accent", "success", "warning", "muted"] as const) -type Flavor = "accent" | "success" | "warning" | "muted" +const FLAVOR = StringEnum(['accent', 'success', 'warning', 'muted'] as const); +type Flavor = 'accent' | 'success' | 'warning' | 'muted'; interface Alternative { - title: string - body: string - flavor?: Flavor + title: string; + body: string; + flavor?: Flavor; } -type Layout = "stack" | "columns" +type Layout = 'stack' | 'columns'; interface AlternativesDetails { - headline?: string | undefined - alternatives: Alternative[] - layout?: Layout | undefined - columnCount?: number | undefined - minColumnWidth?: number | undefined + headline?: string | undefined; + alternatives: Alternative[]; + layout?: Layout | undefined; + columnCount?: number | undefined; + minColumnWidth?: number | undefined; } const AlternativeSchema = Type.Object({ - title: Type.String({ description: "Short label for the card header" }), + title: Type.String({ description: 'Short label for the card header' }), body: Type.String({ - description: "Markdown content rendered inside the card", + description: 'Markdown content rendered inside the card', }), flavor: Type.Optional(FLAVOR), -}) +}); -const LAYOUT = StringEnum(["stack", "columns"] as const) +const LAYOUT = StringEnum(['stack', 'columns'] as const); const PresentAlternativesParams = Type.Object({ - headline: Type.Optional( - Type.String({ description: "Optional headline shown above the cards" }), - ), + headline: Type.Optional(Type.String({ description: 'Optional headline shown above the cards' })), alternatives: Type.Array(AlternativeSchema, { minItems: 1, maxItems: 6 }), layout: Type.Optional(LAYOUT), columnCount: Type.Optional( @@ -64,113 +62,95 @@ const PresentAlternativesParams = Type.Object({ Type.Integer({ minimum: 20, maximum: 200, - description: - "Minimum width per card before falling back to vertical stack. Default 40.", + description: 'Minimum width per card before falling back to vertical stack. Default 40.', }), ), -}) +}); function flavorToColor(flavor: Flavor | undefined): ThemeColor { switch (flavor) { - case "success": - return "success" - case "warning": - return "warning" - case "muted": - return "muted" + case 'success': + return 'success'; + case 'warning': + return 'warning'; + case 'muted': + return 'muted'; default: - return "accent" + return 'accent'; } } // Plain-markdown fallback so RPC clients without the renderer still see // coherent content. Also persisted as the message `content` field. function alternativesToMarkdown(details: AlternativesDetails): string { - const sections: string[] = [] - if (details.headline) sections.push(`## ${details.headline}`) + const sections: string[] = []; + if (details.headline) sections.push(`## ${details.headline}`); for (const alt of details.alternatives) { - sections.push(`### ${alt.title}\n\n${alt.body}`) + sections.push(`### ${alt.title}\n\n${alt.body}`); } - return sections.join("\n\n---\n\n") + return sections.join('\n\n---\n\n'); } function supportsAlternativesPrimitive(pi: ExtensionAPI): boolean { - const candidate = pi as Partial + const candidate = pi as Partial; return ( - typeof candidate.registerMessageRenderer === "function" && - typeof candidate.registerTool === "function" && - typeof candidate.sendMessage === "function" - ) + typeof candidate.registerMessageRenderer === 'function' && + typeof candidate.registerTool === 'function' && + typeof candidate.sendMessage === 'function' + ); } export function registerBrunchAlternatives(pi: ExtensionAPI) { if (!supportsAlternativesPrimitive(pi)) { - return + return; } // ── Renderer ──────────────────────────────────────────────────────── - pi.registerMessageRenderer( - "alternatives-card-set", - (message, _opts, theme) => { - const details = message.details as AlternativesDetails | undefined - if (!details) { - // Fallback: if details is missing, render the raw content string. - return new Text( - typeof message.content === "string" ? message.content : "", - 0, - 0, - ) - } - - const container = new Container() - if (details.headline) { - container.addChild( - new Text( - theme.fg("customMessageLabel", theme.bold(details.headline)), - 1, - 1, - ), - ) - } - - const layout = details.layout ?? "stack" - const columnCount = Math.max(1, Math.min(4, details.columnCount ?? 2)) - const minColumnWidth = details.minColumnWidth ?? 40 - - const makeCard = (alt: Alternative) => - new CardComponent(alt.title, alt.body, theme, flavorToColor(alt.flavor)) - - if (layout === "columns" && details.alternatives.length > 1) { - const groups = chunk(details.alternatives, columnCount) - groups.forEach((group, gi) => { - container.addChild( - new ResponsiveColumns(group.map(makeCard), minColumnWidth), - ) - if (gi < groups.length - 1) container.addChild(new Text("", 0, 0)) - }) - } else { - details.alternatives.forEach((alt, i) => { - container.addChild(makeCard(alt)) - if (i < details.alternatives.length - 1) - container.addChild(new Text("", 0, 0)) - }) - } - return container - }, - ) + pi.registerMessageRenderer('alternatives-card-set', (message, _opts, theme) => { + const details = message.details as AlternativesDetails | undefined; + if (!details) { + // Fallback: if details is missing, render the raw content string. + return new Text(typeof message.content === 'string' ? message.content : '', 0, 0); + } + + const container = new Container(); + if (details.headline) { + container.addChild(new Text(theme.fg('customMessageLabel', theme.bold(details.headline)), 1, 1)); + } + + const layout = details.layout ?? 'stack'; + const columnCount = Math.max(1, Math.min(4, details.columnCount ?? 2)); + const minColumnWidth = details.minColumnWidth ?? 40; + + const makeCard = (alt: Alternative) => + new CardComponent(alt.title, alt.body, theme, flavorToColor(alt.flavor)); + + if (layout === 'columns' && details.alternatives.length > 1) { + const groups = chunk(details.alternatives, columnCount); + groups.forEach((group, gi) => { + container.addChild(new ResponsiveColumns(group.map(makeCard), minColumnWidth)); + if (gi < groups.length - 1) container.addChild(new Text('', 0, 0)); + }); + } else { + details.alternatives.forEach((alt, i) => { + container.addChild(makeCard(alt)); + if (i < details.alternatives.length - 1) container.addChild(new Text('', 0, 0)); + }); + } + return container; + }); // ── Tool ──────────────────────────────────────────────────────────── pi.registerTool({ - name: "present_alternatives", - label: "Present Alternatives", + name: 'present_alternatives', + label: 'Present Alternatives', description: - "Present 1–6 alternative options to the user as bordered cards. Each alternative has a short title and a markdown body. Optional `flavor` (accent/success/warning/muted) styles the card border. Use when comparing options, surfacing draft variants, or laying out trade-offs.", - promptSnippet: - "Present comparable alternatives as bordered cards in the transcript", + 'Present 1–6 alternative options to the user as bordered cards. Each alternative has a short title and a markdown body. Optional `flavor` (accent/success/warning/muted) styles the card border. Use when comparing options, surfacing draft variants, or laying out trade-offs.', + promptSnippet: 'Present comparable alternatives as bordered cards in the transcript', promptGuidelines: [ - "Use present_alternatives when the user needs to compare 2–6 options side by side.", + 'Use present_alternatives when the user needs to compare 2–6 options side by side.', "Each alternative's body should be self-contained markdown — headings, lists, code blocks all work.", - "After present_alternatives, ask the user which one they prefer rather than picking yourself.", + 'After present_alternatives, ask the user which one they prefer rather than picking yourself.', ], parameters: PresentAlternativesParams, @@ -181,29 +161,29 @@ export function registerBrunchAlternatives(pi: ExtensionAPI) { layout: params.layout, columnCount: params.columnCount, minColumnWidth: params.minColumnWidth, - } + }; pi.sendMessage({ - customType: "alternatives-card-set", + customType: 'alternatives-card-set', content: alternativesToMarkdown(details), // fallback / replay display: true, details, - }) + }); return { content: [ { - type: "text", + type: 'text', text: `Presented ${params.alternatives.length} alternative${ - params.alternatives.length === 1 ? "" : "s" + params.alternatives.length === 1 ? '' : 's' }.`, }, ], details: { count: params.alternatives.length }, terminate: true, - } + }; }, - }) + }); } -export default registerBrunchAlternatives +export default registerBrunchAlternatives; diff --git a/src/.pi/extensions/chrome.ts b/src/.pi/extensions/chrome.ts index 4813819d0..58ac41bb5 100644 --- a/src/.pi/extensions/chrome.ts +++ b/src/.pi/extensions/chrome.ts @@ -1,67 +1,62 @@ -import type { - ExtensionAPI, - ExtensionUIContext, -} from "@earendil-works/pi-coding-agent" -import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui" +import type { ExtensionAPI, ExtensionUIContext } from '@earendil-works/pi-coding-agent'; +import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; -import { BRUNCH_COMPACT_WORDMARK } from "../components/brunch-identity.js" import type { WorkspaceSessionChromeState, WorkspaceSessionReadyState, -} from "../../workspace-session-coordinator.js" +} from '../../workspace-session-coordinator.js'; +import { BRUNCH_COMPACT_WORDMARK } from '../components/brunch-identity.js'; -export type BrunchChromeStage = "idle" | "streaming" | "observer-review" -export type BrunchChromeWorkerStatus = "idle" | "queued" | "running" | "blocked" -export type BrunchChromeCoherenceVerdict = "unknown" | "coherent" | "needs_review" | "incoherent" +export type BrunchChromeStage = 'idle' | 'streaming' | 'observer-review'; +export type BrunchChromeWorkerStatus = 'idle' | 'queued' | 'running' | 'blocked'; +export type BrunchChromeCoherenceVerdict = 'unknown' | 'coherent' | 'needs_review' | 'incoherent'; export interface BrunchChromeContextUsage { - usedTokens: number - maxTokens: number + usedTokens: number; + maxTokens: number; } export interface BrunchChromeRuntimeState { - bundle?: string - role?: string - model?: string - thinking?: string - lens?: string + bundle?: string; + role?: string; + model?: string; + thinking?: string; + lens?: string; } export interface BrunchChromeBuildState { - version?: string - dev?: string + version?: string; + dev?: string; } export interface BrunchChromeFooterTelemetry { - gitBranch?: string | null - statuses?: ReadonlyMap + gitBranch?: string | null; + statuses?: ReadonlyMap; } export interface BrunchChromeState extends WorkspaceSessionChromeState { session: { - id: string - label?: string - } - runtime?: BrunchChromeRuntimeState - build?: BrunchChromeBuildState - contextUsage?: BrunchChromeContextUsage + id: string; + label?: string; + }; + runtime?: BrunchChromeRuntimeState; + build?: BrunchChromeBuildState; + contextUsage?: BrunchChromeContextUsage; worker?: { - stage?: BrunchChromeStage - status?: BrunchChromeWorkerStatus - } - coherence?: BrunchChromeCoherenceVerdict + stage?: BrunchChromeStage; + status?: BrunchChromeWorkerStatus; + }; + coherence?: BrunchChromeCoherenceVerdict; } -export type BrunchChromeUi = Pick +export type BrunchChromeUi = Pick; -export function formatBrunchChromeHeaderLines( - chrome: BrunchChromeState, -): string[] { +export function formatBrunchChromeHeaderLines(chrome: BrunchChromeState): string[] { return [ ...BRUNCH_COMPACT_WORDMARK, `runtime: ${formatRuntime(chrome)}`, `${formatChromeIdentity(chrome)} · phase: ${chrome.phase}`, - ] + ]; } export function projectBrunchChromeFooterLines( @@ -69,20 +64,18 @@ export function projectBrunchChromeFooterLines( telemetry?: BrunchChromeFooterTelemetry, width?: number, ): string[] { - const statuses = sanitizeChromeStatuses(telemetry?.statuses) - const branch = telemetry?.gitBranch - const identity = `${formatChromeIdentity(chrome)}${ - branch ? ` · branch: ${branch}` : "" - }` - const runtime = `brunch · runtime: ${formatRuntime(chrome)} · build: ${formatBuild(chrome)}` - const context = `context: ${formatContextUsage(chrome.contextUsage)}` + const statuses = sanitizeChromeStatuses(telemetry?.statuses); + const branch = telemetry?.gitBranch; + const identity = `${formatChromeIdentity(chrome)}${branch ? ` · branch: ${branch}` : ''}`; + const runtime = `brunch · runtime: ${formatRuntime(chrome)} · build: ${formatBuild(chrome)}`; + const context = `context: ${formatContextUsage(chrome.contextUsage)}`; return [ width === undefined ? runtime : alignChromeColumns(runtime, context, width), ...(width === undefined ? [context] : []), - `state: ${chrome.chatMode} · coherence: ${chrome.coherence ?? "unknown"} · worker: ${formatWorker(chrome)}`, + `state: ${chrome.chatMode} · coherence: ${chrome.coherence ?? 'unknown'} · worker: ${formatWorker(chrome)}`, identity, - statuses.length > 0 ? `status: ${statuses.join(" · ")}` : "", - ] + statuses.length > 0 ? `status: ${statuses.join(' · ')}` : '', + ]; } export function formatChromeWidgetLines(chrome: BrunchChromeState): string[] { @@ -94,59 +87,46 @@ export function formatChromeWidgetLines(chrome: BrunchChromeState): string[] { `runtime: ${formatRuntime(chrome)}`, `context: ${formatContextUsage(chrome.contextUsage)}`, `chat mode: ${chrome.chatMode}`, - ] + ]; } function formatChromeIdentity(chrome: BrunchChromeState): string { - return `spec: ${formatSpec(chrome)} · session: ${formatSession(chrome)}` + return `spec: ${formatSpec(chrome)} · session: ${formatSession(chrome)}`; } function formatCompactWordmark(): string { - return BRUNCH_COMPACT_WORDMARK.join(" / ") + return BRUNCH_COMPACT_WORDMARK.join(' / '); } -function sanitizeChromeStatuses( - statuses: ReadonlyMap | undefined, -): string[] { +function sanitizeChromeStatuses(statuses: ReadonlyMap | undefined): string[] { return [...(statuses ?? new Map())] - .filter( - ([key, value]) => key !== "brunch.chrome" && value.trim().length > 0, - ) - .map(([, value]) => value.trim()) + .filter(([key, value]) => key !== 'brunch.chrome' && value.trim().length > 0) + .map(([, value]) => value.trim()); } -function alignChromeColumns( - left: string, - right: string, - width: number, -): string { - const available = Math.max(0, width) - const gap = Math.max(1, available - visibleWidth(left) - visibleWidth(right)) - return truncateToWidth(`${left}${" ".repeat(gap)}${right}`, available) +function alignChromeColumns(left: string, right: string, width: number): string { + const available = Math.max(0, width); + const gap = Math.max(1, available - visibleWidth(left) - visibleWidth(right)); + return truncateToWidth(`${left}${' '.repeat(gap)}${right}`, available); } -export function chromeStateForWorkspace( - workspace: WorkspaceSessionReadyState, -): BrunchChromeState { +export function chromeStateForWorkspace(workspace: WorkspaceSessionReadyState): BrunchChromeState { return { ...workspace.chrome, session: { id: workspace.session.id, label: workspace.session.name ?? workspace.session.id, }, - } + }; } -export function renderBrunchChrome( - ui: BrunchChromeUi, - chrome: BrunchChromeState, -): void { +export function renderBrunchChrome(ui: BrunchChromeUi, chrome: BrunchChromeState): void { ui.setHeader(() => ({ render: () => formatBrunchChromeHeaderLines(chrome), invalidate: () => {}, - })) + })); ui.setFooter((tui, _theme, footerData) => { - const unsubscribe = footerData.onBranchChange(() => tui.requestRender()) + const unsubscribe = footerData.onBranchChange(() => tui.requestRender()); return { render: (width: number) => projectBrunchChromeFooterLines( @@ -159,70 +139,63 @@ export function renderBrunchChrome( ), invalidate: () => {}, dispose: unsubscribe, - } - }) - ui.setWidget("brunch.chrome", formatChromeWidgetLines(chrome), { - placement: "aboveEditor", - }) - ui.setTitle(`brunch — ${chrome.spec?.title ?? chrome.cwd}`) + }; + }); + ui.setWidget('brunch.chrome', formatChromeWidgetLines(chrome), { + placement: 'aboveEditor', + }); + ui.setTitle(`brunch — ${chrome.spec?.title ?? chrome.cwd}`); } -export function registerBrunchChrome( - pi: ExtensionAPI, - chrome: BrunchChromeState, -): void { - pi.on("session_start", async (_event, ctx) => { - renderBrunchChrome(ctx.ui, chrome) - }) +export function registerBrunchChrome(pi: ExtensionAPI, chrome: BrunchChromeState): void { + pi.on('session_start', async (_event, ctx) => { + renderBrunchChrome(ctx.ui, chrome); + }); } export default function brunchChrome(_pi: ExtensionAPI): void {} function formatSpec(chrome: BrunchChromeState): string { - return chrome.spec?.title ?? "no spec selected" + return chrome.spec?.title ?? 'no spec selected'; } function formatSession(chrome: BrunchChromeState): string { - return chrome.session.label ?? chrome.session.id + return chrome.session.label ?? chrome.session.id; } function formatRuntime(chrome: BrunchChromeState): string { - const runtime = chrome.runtime - if (!runtime) return "not reported" + const runtime = chrome.runtime; + if (!runtime) return 'not reported'; const parts = [ runtime.bundle, runtime.role ? `role ${runtime.role}` : undefined, runtime.model, runtime.thinking ? `thinking ${runtime.thinking}` : undefined, runtime.lens ? `lens ${runtime.lens}` : undefined, - ].filter((part): part is string => Boolean(part)) - return parts.length > 0 ? parts.join(" · ") : "not reported" + ].filter((part): part is string => Boolean(part)); + return parts.length > 0 ? parts.join(' · ') : 'not reported'; } function formatBuild(chrome: BrunchChromeState): string { - const build = chrome.build - if (!build) return "not reported" - return [build.version, build.dev].filter(Boolean).join(" ") || "not reported" + const build = chrome.build; + if (!build) return 'not reported'; + return [build.version, build.dev].filter(Boolean).join(' ') || 'not reported'; } -function formatContextUsage( - usage: BrunchChromeContextUsage | undefined, -): string { - if (!usage) return "not reported" - const max = Math.max(0, usage.maxTokens) - const used = Math.max(0, usage.usedTokens) - if (max === 0) return `${used.toLocaleString()} tokens · no limit reported` - const ratio = Math.min(1, used / max) - const filled = Math.round(ratio * 10) - const bar = `${"█".repeat(filled)}${"░".repeat(10 - filled)}` - const percent = Math.round(ratio * 100) - return `[${bar}] ${used.toLocaleString()}/${max.toLocaleString()} tokens (${percent}%)` +function formatContextUsage(usage: BrunchChromeContextUsage | undefined): string { + if (!usage) return 'not reported'; + const max = Math.max(0, usage.maxTokens); + const used = Math.max(0, usage.usedTokens); + if (max === 0) return `${used.toLocaleString()} tokens · no limit reported`; + const ratio = Math.min(1, used / max); + const filled = Math.round(ratio * 10); + const bar = `${'█'.repeat(filled)}${'░'.repeat(10 - filled)}`; + const percent = Math.round(ratio * 100); + return `[${bar}] ${used.toLocaleString()}/${max.toLocaleString()} tokens (${percent}%)`; } function formatWorker(chrome: BrunchChromeState): string { - const worker = chrome.worker - if (!worker) return "not reported" - return ( - [worker.stage, worker.status].filter(Boolean).join("/") || "not reported" - ) + const worker = chrome.worker; + if (!worker) return 'not reported'; + return [worker.stage, worker.status].filter(Boolean).join('/') || 'not reported'; } diff --git a/src/.pi/extensions/command-policy.ts b/src/.pi/extensions/command-policy.ts index 4b8649b63..adefc9f73 100644 --- a/src/.pi/extensions/command-policy.ts +++ b/src/.pi/extensions/command-policy.ts @@ -1,17 +1,17 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; export const BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE = - "Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec." + 'Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec.'; export function registerBrunchBranchPolicyHandlers(pi: ExtensionAPI): void { - pi.on("session_before_tree", (_event, ctx) => { - ctx.ui.notify(BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, "warning") - return { cancel: true } - }) - pi.on("session_before_fork", (_event, ctx) => { - ctx.ui.notify(BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, "warning") - return { cancel: true } - }) + pi.on('session_before_tree', (_event, ctx) => { + ctx.ui.notify(BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, 'warning'); + return { cancel: true }; + }); + pi.on('session_before_fork', (_event, ctx) => { + ctx.ui.notify(BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, 'warning'); + return { cancel: true }; + }); } -export default registerBrunchBranchPolicyHandlers +export default registerBrunchBranchPolicyHandlers; diff --git a/src/.pi/extensions/graph/command-adapter.ts b/src/.pi/extensions/graph/command-adapter.ts index 69cc611b5..bf0db18b3 100644 --- a/src/.pi/extensions/graph/command-adapter.ts +++ b/src/.pi/extensions/graph/command-adapter.ts @@ -18,11 +18,8 @@ import type { CommitGraphSuccess, Diagnostic, StructuralIllegal, -} from "../../../graph/command-executor.js" -import type { - GraphOverview, - NeighborhoodResult, -} from "../../../graph/snapshot.js" +} from '../../../graph/command-executor.js'; +import type { GraphOverview, NeighborhoodResult } from '../../../graph/snapshot.js'; // --------------------------------------------------------------------------- // commit-graph: Pi params → CommitGraphInput @@ -30,29 +27,29 @@ import type { /** Shape of a node as received from the LLM tool call. */ export interface ToolCommitNode { - readonly ref: string - readonly plane: string - readonly kind: string - readonly title: string - readonly body?: string - readonly basis?: string - readonly source?: string - readonly detail?: unknown + readonly ref: string; + readonly plane: string; + readonly kind: string; + readonly title: string; + readonly body?: string; + readonly basis?: string; + readonly source?: string; + readonly detail?: unknown; } /** Shape of an edge as received from the LLM tool call. */ export interface ToolCommitEdge { - readonly category: string - readonly source: string | { readonly existing: number } - readonly target: string | { readonly existing: number } - readonly stance?: string - readonly rationale?: string + readonly category: string; + readonly source: string | { readonly existing: number }; + readonly target: string | { readonly existing: number }; + readonly stance?: string; + readonly rationale?: string; } /** Shape of the commit_graph tool params from the LLM. */ export interface ToolCommitGraphParams { - readonly nodes: readonly ToolCommitNode[] - readonly edges: readonly ToolCommitEdge[] + readonly nodes: readonly ToolCommitNode[]; + readonly edges: readonly ToolCommitEdge[]; } /** @@ -60,19 +57,17 @@ export interface ToolCommitGraphParams { * * The translation is thin — structural validation happens in the CommandExecutor. */ -export function translateCommitGraph( - params: ToolCommitGraphParams, -): CommitGraphInput { +export function translateCommitGraph(params: ToolCommitGraphParams): CommitGraphInput { const nodes: BatchNodeInput[] = params.nodes.map((n) => ({ ref: n.ref, - plane: n.plane as BatchNodeInput["plane"], + plane: n.plane as BatchNodeInput['plane'], kind: n.kind, title: n.title, body: n.body, - basis: n.basis as BatchNodeInput["basis"], + basis: n.basis as BatchNodeInput['basis'], source: n.source, detail: n.detail, - })) + })); const edges: BatchEdgeInput[] = params.edges.map((e) => ({ category: e.category, @@ -80,16 +75,14 @@ export function translateCommitGraph( target: resolveEdgeRef(e.target), stance: e.stance, rationale: e.rationale, - })) + })); - return { nodes, edges } + return { nodes, edges }; } -function resolveEdgeRef( - ref: string | { readonly existing: number }, -): BatchEdgeRef { - if (typeof ref === "string") return ref - return { existing: ref.existing } +function resolveEdgeRef(ref: string | { readonly existing: number }): BatchEdgeRef { + if (typeof ref === 'string') return ref; + return { existing: ref.existing }; } // --------------------------------------------------------------------------- @@ -103,41 +96,37 @@ function resolveEdgeRef( * On structural_illegal: diagnostic listing for agent self-correction. */ export function formatCommitGraphResult(result: CommitGraphResult): string { - if (result.status === "success") { - return formatCommitSuccess(result) + if (result.status === 'success') { + return formatCommitSuccess(result); } - return formatDiagnostics(result) + return formatDiagnostics(result); } function formatCommitSuccess(result: CommitGraphSuccess): string { - const nodeEntries = Object.entries(result.nodes) - const lines: string[] = [`Graph committed successfully (LSN ${result.lsn}).`] + const nodeEntries = Object.entries(result.nodes); + const lines: string[] = [`Graph committed successfully (LSN ${result.lsn}).`]; if (nodeEntries.length > 0) { - lines.push( - `Nodes created: ${nodeEntries.map(([ref, id]) => `${ref} → #${id}`).join(", ")}`, - ) + lines.push(`Nodes created: ${nodeEntries.map(([ref, id]) => `${ref} → #${id}`).join(', ')}`); } if (result.edges.length > 0) { - lines.push( - `Edges created: ${result.edges.map((id) => `#${id}`).join(", ")}`, - ) + lines.push(`Edges created: ${result.edges.map((id) => `#${id}`).join(', ')}`); } - return lines.join("\n") + return lines.join('\n'); } function formatDiagnostics(result: StructuralIllegal): string { const lines: string[] = [ - "STRUCTURAL_ILLEGAL: The batch was rejected. Fix the following issues and retry:", - "", - ] + 'STRUCTURAL_ILLEGAL: The batch was rejected. Fix the following issues and retry:', + '', + ]; for (const d of result.diagnostics) { - lines.push(`- ${d.field}: ${d.message}`) + lines.push(`- ${d.field}: ${d.message}`); } - return lines.join("\n") + return lines.join('\n'); } // --------------------------------------------------------------------------- @@ -149,67 +138,61 @@ function formatDiagnostics(result: StructuralIllegal): string { */ export function formatGraphOverview(overview: GraphOverview): string { if (overview.nodeCount === 0) { - return "The graph is empty (no nodes or edges)." + return 'The graph is empty (no nodes or edges).'; } const lines: string[] = [ `Graph overview (LSN ${overview.lsn}): ${overview.nodeCount} node(s), ${overview.edgeCount} edge(s).`, - "", - ] + '', + ]; for (const node of overview.nodes) { - const detail = node.detail ? ` [has detail]` : "" - lines.push( - `- [#${node.id}] ${node.plane}/${node.kind}: "${node.title}"${detail}`, - ) + const detail = node.detail ? ` [has detail]` : ''; + lines.push(`- [#${node.id}] ${node.plane}/${node.kind}: "${node.title}"${detail}`); } if (overview.edges.length > 0) { - lines.push("") + lines.push(''); for (const edge of overview.edges) { - const stance = edge.stance ? ` (${edge.stance})` : "" - lines.push( - `- Edge #${edge.id}: #${edge.sourceId} —[${edge.category}${stance}]→ #${edge.targetId}`, - ) + const stance = edge.stance ? ` (${edge.stance})` : ''; + lines.push(`- Edge #${edge.id}: #${edge.sourceId} —[${edge.category}${stance}]→ #${edge.targetId}`); } } - return lines.join("\n") + return lines.join('\n'); } /** * Format a NeighborhoodResult as readable text for the agent. */ export function formatNeighborhoodResult(result: NeighborhoodResult): string { - if (result.status === "not_found") { - return "Node not found." + if (result.status === 'not_found') { + return 'Node not found.'; } - const { anchor, neighbors, edges } = result + const { anchor, neighbors, edges } = result; const lines: string[] = [ `Neighborhood of [#${anchor.id}] ${anchor.plane}/${anchor.kind}: "${anchor.title}"`, - ] + ]; if (anchor.body) { - lines.push(`Body: ${anchor.body}`) + lines.push(`Body: ${anchor.body}`); } if (neighbors.length > 0) { - lines.push("", "Neighbors:") + lines.push('', 'Neighbors:'); for (const n of neighbors) { - lines.push(` - [#${n.id}] ${n.plane}/${n.kind}: "${n.title}"`) + lines.push(` - [#${n.id}] ${n.plane}/${n.kind}: "${n.title}"`); } } if (edges.length > 0) { - lines.push("", "Edges:") + lines.push('', 'Edges:'); for (const e of edges) { - const stance = e.stance ? ` (${e.stance})` : "" - lines.push( - ` - #${e.id}: #${e.sourceId} —[${e.category}${stance}]→ #${e.targetId}`, - ) + const stance = e.stance ? ` (${e.stance})` : ''; + lines.push(` - #${e.id}: #${e.sourceId} —[${e.category}${stance}]→ #${e.targetId}`); } } - return lines.join("\n") + return lines.join('\n'); } diff --git a/src/.pi/extensions/graph/index.ts b/src/.pi/extensions/graph/index.ts index 564c2a88a..7f4f9ae07 100644 --- a/src/.pi/extensions/graph/index.ts +++ b/src/.pi/extensions/graph/index.ts @@ -10,15 +10,11 @@ * dependencies from the extension shell. */ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" -import { StringEnum } from "@earendil-works/pi-ai" -import { Type } from "typebox" - -import type { CommandExecutor } from "../../../graph/command-executor.js" -import type { - GraphOverview, - NeighborhoodResult, -} from "../../../graph/snapshot.js" +import { StringEnum } from '@earendil-works/pi-ai'; +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; + +import type { CommandExecutor } from '../../../graph/command-executor.js'; import { INTENT_KINDS, ORACLE_KINDS, @@ -27,13 +23,14 @@ import { EDGE_CATEGORIES, EDGE_STANCES, NODE_BASES, -} from "../../../graph/index.js" +} from '../../../graph/index.js'; +import type { GraphOverview, NeighborhoodResult } from '../../../graph/snapshot.js'; import { translateCommitGraph, formatCommitGraphResult, formatGraphOverview, formatNeighborhoodResult, -} from "./command-adapter.js" +} from './command-adapter.js'; // --------------------------------------------------------------------------- // Dependencies injected by the extension shell @@ -41,37 +38,29 @@ import { /** Pre-bound snapshot readers so the extension never touches db/ directly. */ export interface GraphSnapshotReaders { - readonly getGraphOverview: () => GraphOverview - readonly getNodeNeighborhood: ( - nodeId: number, - options?: { hops?: number }, - ) => NeighborhoodResult + readonly getGraphOverview: () => GraphOverview; + readonly getNodeNeighborhood: (nodeId: number, options?: { hops?: number }) => NeighborhoodResult; } export interface BrunchGraphDeps { - readonly commandExecutor: CommandExecutor - readonly snapshots: GraphSnapshotReaders + readonly commandExecutor: CommandExecutor; + readonly snapshots: GraphSnapshotReaders; } // --------------------------------------------------------------------------- // Tool parameter schemas (TypeBox v1.x / Pi's typebox) // --------------------------------------------------------------------------- -const ALL_KINDS = [ - ...INTENT_KINDS, - ...ORACLE_KINDS, - ...DESIGN_KINDS, - ...PLAN_KINDS, -] as const +const ALL_KINDS = [...INTENT_KINDS, ...ORACLE_KINDS, ...DESIGN_KINDS, ...PLAN_KINDS] as const; const CommitNodeSchema = Type.Object({ ref: Type.String({ description: "Temporary batch reference id (e.g. 'n1', 'n2')", }), - plane: StringEnum(["intent", "oracle", "design", "plan"] as const), + plane: StringEnum(['intent', 'oracle', 'design', 'plan'] as const), kind: StringEnum([...ALL_KINDS]), - title: Type.String({ description: "Node title — must be non-empty" }), - body: Type.Optional(Type.String({ description: "Extended description" })), + title: Type.String({ description: 'Node title — must be non-empty' }), + body: Type.Optional(Type.String({ description: 'Extended description' })), basis: Type.Optional(StringEnum([...NODE_BASES])), source: Type.Optional( Type.String({ @@ -81,17 +70,17 @@ const CommitNodeSchema = Type.Object({ detail: Type.Optional( Type.Unknown({ description: - "Per-kind detail: decision requires {chosen_option, rejected, rationale}; term requires {definition, aliases?}", + 'Per-kind detail: decision requires {chosen_option, rejected, rationale}; term requires {definition, aliases?}', }), ), -}) +}); const EdgeRefSchema = Type.Union([ Type.String({ description: "Intra-batch ref (e.g. 'n1')" }), Type.Object({ - existing: Type.Number({ description: "Id of an existing node" }), + existing: Type.Number({ description: 'Id of an existing node' }), }), -]) +]); const CommitEdgeSchema = Type.Object({ category: StringEnum([...EDGE_CATEGORIES]), @@ -99,81 +88,74 @@ const CommitEdgeSchema = Type.Object({ target: EdgeRefSchema, stance: Type.Optional(StringEnum([...EDGE_STANCES])), rationale: Type.Optional(Type.String()), -}) +}); const CommitGraphParams = Type.Object({ nodes: Type.Array(CommitNodeSchema, { - description: "Nodes to create in this batch", + description: 'Nodes to create in this batch', }), edges: Type.Array(CommitEdgeSchema, { - description: "Edges to create, referencing batch refs or existing node ids", + description: 'Edges to create, referencing batch refs or existing node ids', }), -}) +}); const ReadGraphParams = Type.Object({ - mode: StringEnum(["overview", "neighborhood"] as const), + mode: StringEnum(['overview', 'neighborhood'] as const), node_id: Type.Optional( Type.Number({ - description: "Required for neighborhood mode — the anchor node id", + description: 'Required for neighborhood mode — the anchor node id', }), ), - hops: Type.Optional( - Type.Number({ description: "Neighborhood traversal depth (default: 1)" }), - ), -}) + hops: Type.Optional(Type.Number({ description: 'Neighborhood traversal depth (default: 1)' })), +}); // --------------------------------------------------------------------------- // Registrar // --------------------------------------------------------------------------- -export function registerBrunchGraph( - pi: ExtensionAPI, - deps: BrunchGraphDeps, -): void { - const { commandExecutor, snapshots } = deps +export function registerBrunchGraph(pi: ExtensionAPI, deps: BrunchGraphDeps): void { + const { commandExecutor, snapshots } = deps; // ── commit_graph ──────────────────────────────────────────────────── pi.registerTool({ - name: "commit_graph", - label: "Commit Graph", + name: 'commit_graph', + label: 'Commit Graph', description: - "Atomically create a batch of nodes and edges in the specification graph. " + + 'Atomically create a batch of nodes and edges in the specification graph. ' + "Each node gets a temporary batch ref (e.g. 'n1') that edges can reference. " + - "Edges can also reference existing nodes by id via {existing: }. " + - "The entire batch succeeds or fails atomically.", - promptSnippet: - "Atomically commit nodes and edges to the specification graph", + 'Edges can also reference existing nodes by id via {existing: }. ' + + 'The entire batch succeeds or fails atomically.', + promptSnippet: 'Atomically commit nodes and edges to the specification graph', promptGuidelines: [ - "Use commit_graph to persist specification elements (goals, requirements, decisions, etc.) after the user has accepted the concept.", - "Each node must have a unique batch `ref` string. Edges reference nodes by their `ref` or by `{existing: }` for nodes already in the graph.", - "If commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics, fix the issues, and retry. Do not show intermediate failures to the user.", - "The `stance` field is required on `proof` and `support` edges, and invalid on all other categories.", - "Node kinds `decision` and `term` require a `detail` object; all other kinds must omit `detail`.", + 'Use commit_graph to persist specification elements (goals, requirements, decisions, etc.) after the user has accepted the concept.', + 'Each node must have a unique batch `ref` string. Edges reference nodes by their `ref` or by `{existing: }` for nodes already in the graph.', + 'If commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics, fix the issues, and retry. Do not show intermediate failures to the user.', + 'The `stance` field is required on `proof` and `support` edges, and invalid on all other categories.', + 'Node kinds `decision` and `term` require a `detail` object; all other kinds must omit `detail`.', ], parameters: CommitGraphParams, async execute(_toolCallId, params) { - const input = translateCommitGraph(params) - const result = commandExecutor.commitGraph(input) - const text = formatCommitGraphResult(result) + const input = translateCommitGraph(params); + const result = commandExecutor.commitGraph(input); + const text = formatCommitGraphResult(result); return { - content: [{ type: "text" as const, text }], + content: [{ type: 'text' as const, text }], details: result, - } + }; }, - }) + }); // ── read_graph ────────────────────────────────────────────────────── pi.registerTool({ - name: "read_graph", - label: "Read Graph", + name: 'read_graph', + label: 'Read Graph', description: - "Read the current specification graph. " + + 'Read the current specification graph. ' + "Use mode 'overview' for a full graph summary, or " + "mode 'neighborhood' with a node_id to see a specific node and its neighbors.", - promptSnippet: - "Read the specification graph (overview or node neighborhood)", + promptSnippet: 'Read the specification graph (overview or node neighborhood)', promptGuidelines: [ "Use read_graph with mode 'overview' to see all nodes and edges before committing new graph elements.", "Use read_graph with mode 'neighborhood' and a node_id to inspect a specific node and its connections.", @@ -181,26 +163,26 @@ export function registerBrunchGraph( parameters: ReadGraphParams, async execute(_toolCallId, params) { - let text: string + let text: string; - if (params.mode === "overview") { - text = formatGraphOverview(snapshots.getGraphOverview()) + if (params.mode === 'overview') { + text = formatGraphOverview(snapshots.getGraphOverview()); } else { if (params.node_id == null) { - throw new Error("node_id is required for neighborhood mode") + throw new Error('node_id is required for neighborhood mode'); } text = formatNeighborhoodResult( snapshots.getNodeNeighborhood( params.node_id, params.hops != null ? { hops: params.hops } : undefined, ), - ) + ); } return { - content: [{ type: "text" as const, text }], + content: [{ type: 'text' as const, text }], details: {}, - } + }; }, - }) + }); } diff --git a/src/.pi/extensions/mention-autocomplete.ts b/src/.pi/extensions/mention-autocomplete.ts index 691762724..1f896751b 100644 --- a/src/.pi/extensions/mention-autocomplete.ts +++ b/src/.pi/extensions/mention-autocomplete.ts @@ -1,157 +1,127 @@ -import type { - ExtensionAPI, - ExtensionContext, -} from "@earendil-works/pi-coding-agent" -import type { - AutocompleteItem, - AutocompleteSuggestions, -} from "@earendil-works/pi-tui" +import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'; +import type { AutocompleteItem, AutocompleteSuggestions } from '@earendil-works/pi-tui'; export interface GraphMentionCandidate { - code: string - title: string - description?: string - plane?: "intent" | "oracle" | "design" | "plan" + code: string; + title: string; + description?: string; + plane?: 'intent' | 'oracle' | 'design' | 'plan'; } export interface GraphMentionSource { - listMentionCandidates( - ctx: ExtensionContext, - ): Promise | GraphMentionCandidate[] + listMentionCandidates(ctx: ExtensionContext): Promise | GraphMentionCandidate[]; } const EMPTY_GRAPH_MENTION_SOURCE: GraphMentionSource = { listMentionCandidates: () => [], -} +}; export const FIXTURE_GRAPH_MENTION_SOURCE: GraphMentionSource = { listMentionCandidates: () => [ { - code: "D12", - title: "Transcript-native structured prompts", - description: - "Structured elicitation prompt/response entries stay visible in Pi JSONL.", - plane: "design", + code: 'D12', + title: 'Transcript-native structured prompts', + description: 'Structured elicitation prompt/response entries stay visible in Pi JSONL.', + plane: 'design', }, { - code: "I9", - title: "Mention ledger uses stable handles", - description: - "Inserted # handles are transcript text; labels are UI-only.", - plane: "intent", + code: 'I9', + title: 'Mention ledger uses stable handles', + description: 'Inserted # handles are transcript text; labels are UI-only.', + plane: 'intent', }, { - code: "A10", - title: "Persistent TUI chrome seam", - description: - "Brunch chrome renders through Pi UI primitives without forking Pi.", - plane: "intent", + code: 'A10', + title: 'Persistent TUI chrome seam', + description: 'Brunch chrome renders through Pi UI primitives without forking Pi.', + plane: 'intent', }, ], -} +}; export function registerBrunchMentionAutocomplete( pi: ExtensionAPI, source: GraphMentionSource = EMPTY_GRAPH_MENTION_SOURCE, ): void { - pi.on("before_agent_start", async (event) => ({ + pi.on('before_agent_start', async (event) => ({ systemPrompt: event.systemPrompt + `\n\n[Brunch graph references]\n` + `- Tokens like #D12 are Brunch graph mention handles inserted as visible transcript text.\n` + `- Treat the inserted handle as the only durable reference; autocomplete labels/descriptions are UI-only and are not hidden metadata.\n` + `- Resolve deeper graph detail only through Brunch graph lookup/read tools when those are available.`, - })) + })); - pi.on("session_start", async (_event, ctx) => { - if (typeof ctx.ui.addAutocompleteProvider !== "function") { - return + pi.on('session_start', async (_event, ctx) => { + if (typeof ctx.ui.addAutocompleteProvider !== 'function') { + return; } ctx.ui.addAutocompleteProvider((current) => ({ async getSuggestions(lines, cursorLine, cursorCol, options) { - const line = lines[cursorLine] ?? "" - const prefix = extractHashPrefix(line, cursorCol) + const line = lines[cursorLine] ?? ''; + const prefix = extractHashPrefix(line, cursorCol); if (prefix === null) { - return current.getSuggestions(lines, cursorLine, cursorCol, options) + return current.getSuggestions(lines, cursorLine, cursorCol, options); } - const query = prefix.slice(1).toLowerCase() - const candidates = await source.listMentionCandidates(ctx) + const query = prefix.slice(1).toLowerCase(); + const candidates = await source.listMentionCandidates(ctx); const items: AutocompleteItem[] = candidates .filter((candidate) => candidateMatches(candidate, query)) - .map(candidateToAutocompleteItem) + .map(candidateToAutocompleteItem); - const result: AutocompleteSuggestions = { items, prefix } - return result + const result: AutocompleteSuggestions = { items, prefix }; + return result; }, applyCompletion(lines, cursorLine, cursorCol, item, prefix) { - if (!prefix.startsWith("#")) { - return current.applyCompletion( - lines, - cursorLine, - cursorCol, - item, - prefix, - ) + if (!prefix.startsWith('#')) { + return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix); } - const line = lines[cursorLine] ?? "" - const before = line.slice(0, cursorCol) - const after = line.slice(cursorCol) - const newBefore = before.slice(0, -prefix.length) + item.value + const line = lines[cursorLine] ?? ''; + const before = line.slice(0, cursorCol); + const after = line.slice(cursorCol); + const newBefore = before.slice(0, -prefix.length) + item.value; return { lines: lines.map((candidateLine, index) => index === cursorLine ? newBefore + after : candidateLine, ), cursorLine, cursorCol: newBefore.length, - } + }; }, shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { - return ( - current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? - false - ) + return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? false; }, - })) - }) + })); + }); } -export function extractHashPrefix( - line: string, - cursorCol: number, -): string | null { - const before = line.slice(0, cursorCol) - const match = before.match(/(?:^|\s)(#[\w-]*)$/) - return match?.[1] ?? null +export function extractHashPrefix(line: string, cursorCol: number): string | null { + const before = line.slice(0, cursorCol); + const match = before.match(/(?:^|\s)(#[\w-]*)$/); + return match?.[1] ?? null; } -function candidateMatches( - candidate: GraphMentionCandidate, - query: string, -): boolean { +function candidateMatches(candidate: GraphMentionCandidate, query: string): boolean { if (query.length === 0) { - return true + return true; } return [candidate.code, candidate.title, candidate.description] - .filter((value): value is string => typeof value === "string") - .some((value) => value.toLowerCase().includes(query)) + .filter((value): value is string => typeof value === 'string') + .some((value) => value.toLowerCase().includes(query)); } -function candidateToAutocompleteItem( - candidate: GraphMentionCandidate, -): AutocompleteItem { +function candidateToAutocompleteItem(candidate: GraphMentionCandidate): AutocompleteItem { return { value: `#${candidate.code}`, label: `#${candidate.code} ${candidate.title}`, - ...(candidate.description !== undefined - ? { description: candidate.description } - : {}), - } + ...(candidate.description !== undefined ? { description: candidate.description } : {}), + }; } -export default registerBrunchMentionAutocomplete +export default registerBrunchMentionAutocomplete; diff --git a/src/.pi/extensions/operational-mode.ts b/src/.pi/extensions/operational-mode.ts index 184c4c3d1..e42b993eb 100644 --- a/src/.pi/extensions/operational-mode.ts +++ b/src/.pi/extensions/operational-mode.ts @@ -8,143 +8,132 @@ * BrunchAgentState projection, but the policy remains operational-mode owned. */ -import { homedir } from "node:os" +import { homedir } from 'node:os'; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; import { createFindTool, createGrepTool, createLsTool, createReadTool, -} from "@earendil-works/pi-coding-agent" -import { Text } from "@earendil-works/pi-tui" +} from '@earendil-works/pi-coding-agent'; +import { Text } from '@earendil-works/pi-tui'; -const ELICIT_BLOCKED_TOOLS = ["bash", "edit", "write"] as const -type ElicitBlockedToolName = typeof ELICIT_BLOCKED_TOOLS[number] +const ELICIT_BLOCKED_TOOLS = ['bash', 'edit', 'write'] as const; +type ElicitBlockedToolName = (typeof ELICIT_BLOCKED_TOOLS)[number]; -export const BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE = - "brunch.agent_runtime_state" +export const BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE = 'brunch.agent_runtime_state'; -export type OperationalModeId = "elicit" -export type AgentRoleId = "elicitor" -export type AgentStrategyId = "step-by-step" | "disambiguate-via-examples" -export type AgentLensId = AgentStrategyId -export type ToolPolicyId = "elicit-read-only" -export type PromptPackId = "brunch-base" | "elicit" | "elicitor" -export type ModelPreference = "default" -export type ThinkingLevel = "low" | "medium" | "high" +export type OperationalModeId = 'elicit'; +export type AgentRoleId = 'elicitor'; +export type AgentStrategyId = 'step-by-step' | 'disambiguate-via-examples'; +export type AgentLensId = AgentStrategyId; +export type ToolPolicyId = 'elicit-read-only'; +export type PromptPackId = 'brunch-base' | 'elicit' | 'elicitor'; +export type ModelPreference = 'default'; +export type ThinkingLevel = 'low' | 'medium' | 'high'; export interface BrunchAgentState { - schemaVersion: 1 - operationalMode: OperationalModeId - agentRole: AgentRoleId - agentStrategy: AgentStrategyId - agentLens: AgentLensId | null + schemaVersion: 1; + operationalMode: OperationalModeId; + agentRole: AgentRoleId; + agentStrategy: AgentStrategyId; + agentLens: AgentLensId | null; } export interface OperationalModeDefinition { - id: OperationalModeId - defaultRole: AgentRoleId - allowedRoles: readonly AgentRoleId[] - toolPolicyId: ToolPolicyId - promptPackIds: readonly PromptPackId[] + id: OperationalModeId; + defaultRole: AgentRoleId; + allowedRoles: readonly AgentRoleId[]; + toolPolicyId: ToolPolicyId; + promptPackIds: readonly PromptPackId[]; } export interface AgentRoleDefinition { - id: AgentRoleId - operationalMode: OperationalModeId - defaultStrategy: AgentStrategyId - allowedStrategies: readonly AgentStrategyId[] - defaultLens: AgentLensId | null - allowedLenses: readonly AgentLensId[] - promptPackIds: readonly PromptPackId[] - modelPreference?: ModelPreference - thinkingLevel?: ThinkingLevel + id: AgentRoleId; + operationalMode: OperationalModeId; + defaultStrategy: AgentStrategyId; + allowedStrategies: readonly AgentStrategyId[]; + defaultLens: AgentLensId | null; + allowedLenses: readonly AgentLensId[]; + promptPackIds: readonly PromptPackId[]; + modelPreference?: ModelPreference; + thinkingLevel?: ThinkingLevel; } export interface ResolvedBrunchAgentState extends BrunchAgentState { - operationalModeDefinition: OperationalModeDefinition - agentRoleDefinition: AgentRoleDefinition + operationalModeDefinition: OperationalModeDefinition; + agentRoleDefinition: AgentRoleDefinition; } export interface BrunchAgentStateEntryData { - schemaVersion: 1 - reason: "init" | "switch" - state: BrunchAgentState - previous?: BrunchAgentState - source: "system" | "user" | "agent" | "extension" + schemaVersion: 1; + reason: 'init' | 'switch'; + state: BrunchAgentState; + previous?: BrunchAgentState; + source: 'system' | 'user' | 'agent' | 'extension'; } export const DEFAULT_BRUNCH_AGENT_STATE: BrunchAgentState = { schemaVersion: 1, - operationalMode: "elicit", - agentRole: "elicitor", - agentStrategy: "step-by-step", - agentLens: "step-by-step", -} - -export const OPERATIONAL_MODE_DEFINITIONS: Record = - { - elicit: { - id: "elicit", - defaultRole: "elicitor", - allowedRoles: ["elicitor"], - toolPolicyId: "elicit-read-only", - promptPackIds: ["brunch-base", "elicit"], - }, - } - -export const AGENT_ROLE_DEFINITIONS: Record = - { - elicitor: { - id: "elicitor", - operationalMode: "elicit", - defaultStrategy: "step-by-step", - allowedStrategies: ["step-by-step", "disambiguate-via-examples"], - defaultLens: "step-by-step", - allowedLenses: ["step-by-step", "disambiguate-via-examples"], - promptPackIds: ["elicitor"], - }, - } + operationalMode: 'elicit', + agentRole: 'elicitor', + agentStrategy: 'step-by-step', + agentLens: 'step-by-step', +}; + +export const OPERATIONAL_MODE_DEFINITIONS: Record = { + elicit: { + id: 'elicit', + defaultRole: 'elicitor', + allowedRoles: ['elicitor'], + toolPolicyId: 'elicit-read-only', + promptPackIds: ['brunch-base', 'elicit'], + }, +}; + +export const AGENT_ROLE_DEFINITIONS: Record = { + elicitor: { + id: 'elicitor', + operationalMode: 'elicit', + defaultStrategy: 'step-by-step', + allowedStrategies: ['step-by-step', 'disambiguate-via-examples'], + defaultLens: 'step-by-step', + allowedLenses: ['step-by-step', 'disambiguate-via-examples'], + promptPackIds: ['elicitor'], + }, +}; interface CustomEntryLike { - type?: unknown - customType?: unknown - data?: unknown + type?: unknown; + customType?: unknown; + data?: unknown; } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === 'object' && value !== null; } -function isOneOf( - value: unknown, - allowed: readonly T[], -): value is T { - return typeof value === "string" && allowed.includes(value as T) +function isOneOf(value: unknown, allowed: readonly T[]): value is T { + return typeof value === 'string' && allowed.includes(value as T); } function parseBrunchAgentState(value: unknown): BrunchAgentState | undefined { - if (!isRecord(value)) return undefined - const operationalModes = Object.keys( - OPERATIONAL_MODE_DEFINITIONS, - ) as OperationalModeId[] - const agentRoles = Object.keys(AGENT_ROLE_DEFINITIONS) as AgentRoleId[] - - if (value.schemaVersion !== 1) return undefined - if (!isOneOf(value.operationalMode, operationalModes)) return undefined - if (!isOneOf(value.agentRole, agentRoles)) return undefined - - const mode = OPERATIONAL_MODE_DEFINITIONS[value.operationalMode] - const role = AGENT_ROLE_DEFINITIONS[value.agentRole] - if (!mode.allowedRoles.includes(value.agentRole)) return undefined - if (role.operationalMode !== value.operationalMode) return undefined - if (!isOneOf(value.agentStrategy, role.allowedStrategies)) return undefined - if ( - value.agentLens !== null && - !isOneOf(value.agentLens, role.allowedLenses) - ) { - return undefined + if (!isRecord(value)) return undefined; + const operationalModes = Object.keys(OPERATIONAL_MODE_DEFINITIONS) as OperationalModeId[]; + const agentRoles = Object.keys(AGENT_ROLE_DEFINITIONS) as AgentRoleId[]; + + if (value.schemaVersion !== 1) return undefined; + if (!isOneOf(value.operationalMode, operationalModes)) return undefined; + if (!isOneOf(value.agentRole, agentRoles)) return undefined; + + const mode = OPERATIONAL_MODE_DEFINITIONS[value.operationalMode]; + const role = AGENT_ROLE_DEFINITIONS[value.agentRole]; + if (!mode.allowedRoles.includes(value.agentRole)) return undefined; + if (role.operationalMode !== value.operationalMode) return undefined; + if (!isOneOf(value.agentStrategy, role.allowedStrategies)) return undefined; + if (value.agentLens !== null && !isOneOf(value.agentLens, role.allowedLenses)) { + return undefined; } return { @@ -153,30 +142,25 @@ function parseBrunchAgentState(value: unknown): BrunchAgentState | undefined { agentRole: value.agentRole, agentStrategy: value.agentStrategy, agentLens: value.agentLens, - } + }; } -function parseBrunchAgentStateEntryData( - value: unknown, -): BrunchAgentStateEntryData | undefined { - if (!isRecord(value)) return undefined - if (value.schemaVersion !== 1) return undefined - if (value.reason !== "init" && value.reason !== "switch") return undefined +function parseBrunchAgentStateEntryData(value: unknown): BrunchAgentStateEntryData | undefined { + if (!isRecord(value)) return undefined; + if (value.schemaVersion !== 1) return undefined; + if (value.reason !== 'init' && value.reason !== 'switch') return undefined; if ( - value.source !== "system" && - value.source !== "user" && - value.source !== "agent" && - value.source !== "extension" + value.source !== 'system' && + value.source !== 'user' && + value.source !== 'agent' && + value.source !== 'extension' ) { - return undefined + return undefined; } - const state = parseBrunchAgentState(value.state) - if (!state) return undefined - const previous = - value.previous === undefined - ? undefined - : parseBrunchAgentState(value.previous) - if (value.previous !== undefined && !previous) return undefined + const state = parseBrunchAgentState(value.state); + if (!state) return undefined; + const previous = value.previous === undefined ? undefined : parseBrunchAgentState(value.previous); + if (value.previous !== undefined && !previous) return undefined; return { schemaVersion: 1, @@ -184,130 +168,113 @@ function parseBrunchAgentStateEntryData( state, ...(previous ? { previous } : {}), source: value.source, - } + }; } -function resolveBrunchAgentState( - state: BrunchAgentState, -): ResolvedBrunchAgentState { +function resolveBrunchAgentState(state: BrunchAgentState): ResolvedBrunchAgentState { return { ...state, - operationalModeDefinition: - OPERATIONAL_MODE_DEFINITIONS[state.operationalMode], + operationalModeDefinition: OPERATIONAL_MODE_DEFINITIONS[state.operationalMode], agentRoleDefinition: AGENT_ROLE_DEFINITIONS[state.agentRole], - } + }; } function latestValidBrunchAgentStateEntryData( entries: readonly CustomEntryLike[], ): BrunchAgentStateEntryData | undefined { - let latest: BrunchAgentStateEntryData | undefined + let latest: BrunchAgentStateEntryData | undefined; for (const entry of entries) { - if ( - entry.type !== "custom" || - entry.customType !== BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE - ) { - continue + if (entry.type !== 'custom' || entry.customType !== BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE) { + continue; } - const data = parseBrunchAgentStateEntryData(entry.data) - if (data) latest = data + const data = parseBrunchAgentStateEntryData(entry.data); + if (data) latest = data; } - return latest + return latest; } -export function projectBrunchAgentState( - entries: readonly CustomEntryLike[], -): ResolvedBrunchAgentState { +export function projectBrunchAgentState(entries: readonly CustomEntryLike[]): ResolvedBrunchAgentState { return resolveBrunchAgentState( - latestValidBrunchAgentStateEntryData(entries)?.state ?? - DEFAULT_BRUNCH_AGENT_STATE, - ) + latestValidBrunchAgentStateEntryData(entries)?.state ?? DEFAULT_BRUNCH_AGENT_STATE, + ); } export interface BrunchAgentStateEntrySessionManager { - getEntries(): readonly CustomEntryLike[] - appendCustomEntry(customType: string, data: BrunchAgentStateEntryData): string + getEntries(): readonly CustomEntryLike[]; + appendCustomEntry(customType: string, data: BrunchAgentStateEntryData): string; } -function requireValidBrunchAgentState( - state: BrunchAgentState, -): BrunchAgentState { - const valid = parseBrunchAgentState(state) +function requireValidBrunchAgentState(state: BrunchAgentState): BrunchAgentState { + const valid = parseBrunchAgentState(state); if (!valid) { - throw new Error("Invalid BrunchAgentState runtime selection.") + throw new Error('Invalid BrunchAgentState runtime selection.'); } - return valid + return valid; } export function appendBrunchAgentRuntimeInit( sessionManager: BrunchAgentStateEntrySessionManager, - source: BrunchAgentStateEntryData["source"] = "extension", + source: BrunchAgentStateEntryData['source'] = 'extension', ): string | undefined { if (latestValidBrunchAgentStateEntryData(sessionManager.getEntries())) { - return undefined + return undefined; } - return sessionManager.appendCustomEntry( - BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, - { - schemaVersion: 1, - reason: "init", - state: DEFAULT_BRUNCH_AGENT_STATE, - source, - }, - ) + return sessionManager.appendCustomEntry(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, { + schemaVersion: 1, + reason: 'init', + state: DEFAULT_BRUNCH_AGENT_STATE, + source, + }); } export function appendBrunchAgentRuntimeSwitch( sessionManager: BrunchAgentStateEntrySessionManager, state: BrunchAgentState, - source: BrunchAgentStateEntryData["source"] = "user", + source: BrunchAgentStateEntryData['source'] = 'user', ): string { - const validState = requireValidBrunchAgentState(state) - const previous = projectBrunchAgentState(sessionManager.getEntries()) - - return sessionManager.appendCustomEntry( - BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, - { - schemaVersion: 1, - reason: "switch", - state: validState, - previous: { - schemaVersion: previous.schemaVersion, - operationalMode: previous.operationalMode, - agentRole: previous.agentRole, - agentStrategy: previous.agentStrategy, - agentLens: previous.agentLens, - }, - source, + const validState = requireValidBrunchAgentState(state); + const previous = projectBrunchAgentState(sessionManager.getEntries()); + + return sessionManager.appendCustomEntry(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, { + schemaVersion: 1, + reason: 'switch', + state: validState, + previous: { + schemaVersion: previous.schemaVersion, + operationalMode: previous.operationalMode, + agentRole: previous.agentRole, + agentStrategy: previous.agentStrategy, + agentLens: previous.agentLens, }, - ) + source, + }); } function shortenPath(path: string): string { - const home = homedir() - if (path.startsWith(home)) return `~${path.slice(home.length)}` - return path + const home = homedir(); + if (path.startsWith(home)) return `~${path.slice(home.length)}`; + return path; } function elicitToolNames(pi: ExtensionAPI): string[] { - const blocked = new Set(ELICIT_BLOCKED_TOOLS) + const blocked = new Set(ELICIT_BLOCKED_TOOLS); return pi .getAllTools() .map((tool) => tool.name) - .filter((name) => !blocked.has(name)) + .filter((name) => !blocked.has(name)); } interface SessionManagerLike { - getEntries(): readonly CustomEntryLike[] + getEntries(): readonly CustomEntryLike[]; } function projectBrunchAgentStateFromSessionManager( sessionManager: SessionManagerLike | undefined, ): ResolvedBrunchAgentState { - return projectBrunchAgentState(sessionManager?.getEntries() ?? []) + return projectBrunchAgentState(sessionManager?.getEntries() ?? []); } function supportsBrunchAgentStateEntries( @@ -315,67 +282,60 @@ function supportsBrunchAgentStateEntries( ): sessionManager is BrunchAgentStateEntrySessionManager { return ( sessionManager !== undefined && - typeof (sessionManager as Partial) - .appendCustomEntry === "function" - ) + typeof (sessionManager as Partial).appendCustomEntry === 'function' + ); } export function activeToolNamesForBrunchAgentState( pi: ExtensionAPI, state: ResolvedBrunchAgentState, ): string[] { - if (state.operationalModeDefinition.toolPolicyId === "elicit-read-only") { - return elicitToolNames(pi) + if (state.operationalModeDefinition.toolPolicyId === 'elicit-read-only') { + return elicitToolNames(pi); } - return [] + return []; } -function isBlockedElicitTool( - toolName: string, -): toolName is ElicitBlockedToolName { - return ELICIT_BLOCKED_TOOLS.includes(toolName as ElicitBlockedToolName) +function isBlockedElicitTool(toolName: string): toolName is ElicitBlockedToolName { + return ELICIT_BLOCKED_TOOLS.includes(toolName as ElicitBlockedToolName); } -function applyBrunchToolPolicy( - pi: ExtensionAPI, - state: ResolvedBrunchAgentState, -): void { - pi.setActiveTools(activeToolNamesForBrunchAgentState(pi, state)) +function applyBrunchToolPolicy(pi: ExtensionAPI, state: ResolvedBrunchAgentState): void { + pi.setActiveTools(activeToolNamesForBrunchAgentState(pi, state)); } interface TextLikeContent { - type: string - text?: string + type: string; + text?: string; } interface TextToolResultLike { - content?: TextLikeContent[] + content?: TextLikeContent[]; } interface TextContent { - type: "text" - text: string + type: 'text'; + text: string; } function firstText(result: TextToolResultLike): TextContent | undefined { return result.content?.find( - (content): content is TextContent => - content.type === "text" && typeof content.text === "string", - ) + (content): content is TextContent => content.type === 'text' && typeof content.text === 'string', + ); } function nonEmptyLineCount(text: string): number { return text .trim() - .split("\n") - .filter((line) => line.trim().length > 0).length + .split('\n') + .filter((line) => line.trim().length > 0).length; } function emptyResult() { - return new Text("", 0, 0) + return new Text('', 0, 0); } -const toolCache = new Map>() +const toolCache = new Map>(); function createReadOnlyTools(cwd: string) { return { @@ -383,193 +343,161 @@ function createReadOnlyTools(cwd: string) { grep: createGrepTool(cwd), find: createFindTool(cwd), ls: createLsTool(cwd), - } + }; } function getReadOnlyTools(cwd: string) { - let tools = toolCache.get(cwd) + let tools = toolCache.get(cwd); if (!tools) { - tools = createReadOnlyTools(cwd) - toolCache.set(cwd, tools) + tools = createReadOnlyTools(cwd); + toolCache.set(cwd, tools); } - return tools + return tools; } function supportsOperationalModePolicy(pi: ExtensionAPI): boolean { - const candidate = pi as Partial + const candidate = pi as Partial; return ( - typeof candidate.registerTool === "function" && - typeof candidate.getAllTools === "function" && - typeof candidate.setActiveTools === "function" - ) + typeof candidate.registerTool === 'function' && + typeof candidate.getAllTools === 'function' && + typeof candidate.setActiveTools === 'function' + ); } export function registerBrunchOperationalModePolicy(pi: ExtensionAPI) { if (!supportsOperationalModePolicy(pi)) { - return + return; } pi.registerTool({ ...getReadOnlyTools(process.cwd()).read, - label: "read", + label: 'read', async execute(toolCallId, params, signal, onUpdate, ctx) { - return getReadOnlyTools(ctx.cwd).read.execute( - toolCallId, - params, - signal, - onUpdate, - ) + return getReadOnlyTools(ctx.cwd).read.execute(toolCallId, params, signal, onUpdate); }, renderCall(args, theme) { - const path = shortenPath(args.path || "") + const path = shortenPath(args.path || ''); const range = args.offset !== undefined || args.limit !== undefined ? theme.fg( - "muted", + 'muted', `:${args.offset ?? 1}${ - args.limit !== undefined - ? `-${(args.offset ?? 1) + args.limit - 1}` - : "" + args.limit !== undefined ? `-${(args.offset ?? 1) + args.limit - 1}` : '' }`, ) - : "" + : ''; return new Text( - `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", path || "…")}${range}`, + `${theme.fg('toolTitle', theme.bold('read'))} ${theme.fg('accent', path || '…')}${range}`, 0, 0, - ) + ); }, renderResult() { - return emptyResult() + return emptyResult(); }, - }) + }); pi.registerTool({ ...getReadOnlyTools(process.cwd()).grep, - label: "grep", + label: 'grep', async execute(toolCallId, params, signal, onUpdate, ctx) { - return getReadOnlyTools(ctx.cwd).grep.execute( - toolCallId, - params, - signal, - onUpdate, - ) + return getReadOnlyTools(ctx.cwd).grep.execute(toolCallId, params, signal, onUpdate); }, renderCall(args, theme) { - const path = shortenPath(args.path || ".") - const glob = args.glob ? theme.fg("muted", ` ${args.glob}`) : "" + const path = shortenPath(args.path || '.'); + const glob = args.glob ? theme.fg('muted', ` ${args.glob}`) : ''; return new Text( - `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${args.pattern || "…"}/`)} ${theme.fg("muted", path)}${glob}`, + `${theme.fg('toolTitle', theme.bold('grep'))} ${theme.fg('accent', `/${args.pattern || '…'}/`)} ${theme.fg('muted', path)}${glob}`, 0, 0, - ) + ); }, renderResult(result, { expanded }, theme) { - const text = firstText(result)?.text ?? "" + const text = firstText(result)?.text ?? ''; if (expanded && text.trim().length > 0) { - return new Text(`\n${theme.fg("toolOutput", text.trim())}`, 0, 0) + return new Text(`\n${theme.fg('toolOutput', text.trim())}`, 0, 0); } - const count = nonEmptyLineCount(text) - return count > 0 - ? new Text(theme.fg("muted", `→ ${count} matches`), 0, 0) - : emptyResult() + const count = nonEmptyLineCount(text); + return count > 0 ? new Text(theme.fg('muted', `→ ${count} matches`), 0, 0) : emptyResult(); }, - }) + }); pi.registerTool({ ...getReadOnlyTools(process.cwd()).find, - label: "find", + label: 'find', async execute(toolCallId, params, signal, onUpdate, ctx) { - return getReadOnlyTools(ctx.cwd).find.execute( - toolCallId, - params, - signal, - onUpdate, - ) + return getReadOnlyTools(ctx.cwd).find.execute(toolCallId, params, signal, onUpdate); }, renderCall(args, theme) { - const path = shortenPath(args.path || ".") + const path = shortenPath(args.path || '.'); return new Text( - `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", args.pattern || "…")} ${theme.fg("muted", path)}`, + `${theme.fg('toolTitle', theme.bold('find'))} ${theme.fg('accent', args.pattern || '…')} ${theme.fg('muted', path)}`, 0, 0, - ) + ); }, renderResult(result, { expanded }, theme) { - const text = firstText(result)?.text ?? "" + const text = firstText(result)?.text ?? ''; if (expanded && text.trim().length > 0) { - return new Text(`\n${theme.fg("toolOutput", text.trim())}`, 0, 0) + return new Text(`\n${theme.fg('toolOutput', text.trim())}`, 0, 0); } - const count = nonEmptyLineCount(text) - return count > 0 - ? new Text(theme.fg("muted", `→ ${count} files`), 0, 0) - : emptyResult() + const count = nonEmptyLineCount(text); + return count > 0 ? new Text(theme.fg('muted', `→ ${count} files`), 0, 0) : emptyResult(); }, - }) + }); pi.registerTool({ ...getReadOnlyTools(process.cwd()).ls, - label: "ls", + label: 'ls', async execute(toolCallId, params, signal, onUpdate, ctx) { - return getReadOnlyTools(ctx.cwd).ls.execute( - toolCallId, - params, - signal, - onUpdate, - ) + return getReadOnlyTools(ctx.cwd).ls.execute(toolCallId, params, signal, onUpdate); }, renderCall(args, theme) { - const path = shortenPath(args.path || ".") - return new Text( - `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", path)}`, - 0, - 0, - ) + const path = shortenPath(args.path || '.'); + return new Text(`${theme.fg('toolTitle', theme.bold('ls'))} ${theme.fg('accent', path)}`, 0, 0); }, renderResult(result, { expanded }, theme) { - const text = firstText(result)?.text ?? "" + const text = firstText(result)?.text ?? ''; if (expanded && text.trim().length > 0) { - return new Text(`\n${theme.fg("toolOutput", text.trim())}`, 0, 0) + return new Text(`\n${theme.fg('toolOutput', text.trim())}`, 0, 0); } - const count = nonEmptyLineCount(text) - return count > 0 - ? new Text(theme.fg("muted", `→ ${count} entries`), 0, 0) - : emptyResult() + const count = nonEmptyLineCount(text); + return count > 0 ? new Text(theme.fg('muted', `→ ${count} entries`), 0, 0) : emptyResult(); }, - }) + }); - pi.on("session_start", async (_event, ctx) => { + pi.on('session_start', async (_event, ctx) => { if (supportsBrunchAgentStateEntries(ctx?.sessionManager)) { - appendBrunchAgentRuntimeInit(ctx.sessionManager) + appendBrunchAgentRuntimeInit(ctx.sessionManager); } - const state = projectBrunchAgentStateFromSessionManager(ctx?.sessionManager) - applyBrunchToolPolicy(pi, state) - }) + const state = projectBrunchAgentStateFromSessionManager(ctx?.sessionManager); + applyBrunchToolPolicy(pi, state); + }); - pi.on("before_agent_start", async (_event, ctx) => { - const state = projectBrunchAgentStateFromSessionManager(ctx?.sessionManager) - applyBrunchToolPolicy(pi, state) - }) + pi.on('before_agent_start', async (_event, ctx) => { + const state = projectBrunchAgentStateFromSessionManager(ctx?.sessionManager); + applyBrunchToolPolicy(pi, state); + }); - pi.on("tool_call", async (event) => { - if (!isBlockedElicitTool(event.toolName)) return + pi.on('tool_call', async (event) => { + if (!isBlockedElicitTool(event.toolName)) return; return { block: true, reason: `Brunch tool policy blocks "${event.toolName}". ` + - `Blocked tools in elicit mode: ${ELICIT_BLOCKED_TOOLS.join(", ")}.`, - } - }) + `Blocked tools in elicit mode: ${ELICIT_BLOCKED_TOOLS.join(', ')}.`, + }; + }); - pi.on("user_bash", (event) => ({ + pi.on('user_bash', (event) => ({ result: { output: `Brunch tool policy blocks shell commands: ${event.command}`, exitCode: 1, cancelled: false, truncated: false, }, - })) + })); } -export default registerBrunchOperationalModePolicy +export default registerBrunchOperationalModePolicy; diff --git a/src/.pi/extensions/prompting.ts b/src/.pi/extensions/prompting.ts index da0bfc3ea..6a6a8cc63 100644 --- a/src/.pi/extensions/prompting.ts +++ b/src/.pi/extensions/prompting.ts @@ -1,57 +1,54 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; -import { composeBrunchPrompt } from "../context/compose-brunch-prompt.js" -import { - activeToolNamesForBrunchAgentState, - projectBrunchAgentState, -} from "./operational-mode.js" +import { composeBrunchPrompt } from '../context/compose-brunch-prompt.js'; +import { activeToolNamesForBrunchAgentState, projectBrunchAgentState } from './operational-mode.js'; -type BrunchAgentStateEntries = Parameters[0] +type BrunchAgentStateEntries = Parameters[0]; interface SessionManagerLike { - getEntries(): BrunchAgentStateEntries + getEntries(): BrunchAgentStateEntries; } interface BeforeAgentStartEventLike { - systemPrompt?: string + systemPrompt?: string; } interface BeforeAgentStartContextLike { - sessionManager?: SessionManagerLike + sessionManager?: SessionManagerLike; } function supportsPrompting(pi: ExtensionAPI): boolean { - return typeof (pi as Partial).on === "function" + return typeof (pi as Partial).on === 'function'; } function projectState(ctx: BeforeAgentStartContextLike | undefined) { - return projectBrunchAgentState(ctx?.sessionManager?.getEntries() ?? []) + return projectBrunchAgentState(ctx?.sessionManager?.getEntries() ?? []); } export function registerBrunchPrompting(pi: ExtensionAPI): void { - if (!supportsPrompting(pi)) return + if (!supportsPrompting(pi)) return; - pi.on("before_agent_start", async (event, ctx) => { - const state = projectState(ctx as BeforeAgentStartContextLike | undefined) + pi.on('before_agent_start', async (event, ctx) => { + const state = projectState(ctx as BeforeAgentStartContextLike | undefined); const activeTools = - typeof (pi as Partial).getAllTools === "function" + typeof (pi as Partial).getAllTools === 'function' ? activeToolNamesForBrunchAgentState(pi, state) - : [] + : []; const { prompt } = composeBrunchPrompt({ operationalMode: state.operationalMode, agentRole: state.agentRole, agentStrategy: state.agentStrategy, agentLens: state.agentLens, activeTools, - }) + }); - if (prompt.trim().length === 0) return undefined + if (prompt.trim().length === 0) return undefined; - const basePrompt = (event as BeforeAgentStartEventLike).systemPrompt ?? "" + const basePrompt = (event as BeforeAgentStartEventLike).systemPrompt ?? ''; return { systemPrompt: `${basePrompt}\n\n${prompt}`, - } - }) + }; + }); } -export default registerBrunchPrompting +export default registerBrunchPrompting; diff --git a/src/.pi/extensions/session-lifecycle.ts b/src/.pi/extensions/session-lifecycle.ts index a70b46b30..898ca8b67 100644 --- a/src/.pi/extensions/session-lifecycle.ts +++ b/src/.pi/extensions/session-lifecycle.ts @@ -1,50 +1,36 @@ -import { - SessionManager, - type ExtensionAPI, -} from "@earendil-works/pi-coding-agent" +import { SessionManager, type ExtensionAPI } from '@earendil-works/pi-coding-agent'; -export type BrunchSessionBoundaryHandler = ( - sessionManager: SessionManager, -) => Promise | void +export type BrunchSessionBoundaryHandler = (sessionManager: SessionManager) => Promise | void; export async function bindBrunchSessionBoundary( sessionManager: SessionManager, onSessionBoundary?: BrunchSessionBoundaryHandler, ): Promise { - await onSessionBoundary?.(sessionManager) + await onSessionBoundary?.(sessionManager); } export function registerBrunchSessionBoundaryRefreshHandlers( pi: ExtensionAPI, onSessionBoundary?: BrunchSessionBoundaryHandler, ): void { - pi.on("before_agent_start", async (_event, ctx) => { - await bindBrunchSessionBoundary( - ctx.sessionManager as SessionManager, - onSessionBoundary, - ) - }) - pi.on("message_start", async (event, ctx) => { - if (event.message.role === "assistant") { - await bindBrunchSessionBoundary( - ctx.sessionManager as SessionManager, - onSessionBoundary, - ) + pi.on('before_agent_start', async (_event, ctx) => { + await bindBrunchSessionBoundary(ctx.sessionManager as SessionManager, onSessionBoundary); + }); + pi.on('message_start', async (event, ctx) => { + if (event.message.role === 'assistant') { + await bindBrunchSessionBoundary(ctx.sessionManager as SessionManager, onSessionBoundary); } - }) + }); } export function registerBrunchSessionBoundary( pi: ExtensionAPI, onSessionBoundary?: BrunchSessionBoundaryHandler, ): void { - pi.on("session_start", async (_event, ctx) => { - await bindBrunchSessionBoundary( - ctx.sessionManager as SessionManager, - onSessionBoundary, - ) - }) - registerBrunchSessionBoundaryRefreshHandlers(pi, onSessionBoundary) + pi.on('session_start', async (_event, ctx) => { + await bindBrunchSessionBoundary(ctx.sessionManager as SessionManager, onSessionBoundary); + }); + registerBrunchSessionBoundaryRefreshHandlers(pi, onSessionBoundary); } -export default registerBrunchSessionBoundary +export default registerBrunchSessionBoundary; diff --git a/src/.pi/extensions/structured-exchange/index.ts b/src/.pi/extensions/structured-exchange/index.ts index cbe5fb034..96b639d53 100644 --- a/src/.pi/extensions/structured-exchange/index.ts +++ b/src/.pi/extensions/structured-exchange/index.ts @@ -1,36 +1,27 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; -import { - PRESENT_CANDIDATES_TOOL, - presentCandidatesTool, -} from "./present-candidates.js" -import { PRESENT_OPTIONS_TOOL, presentOptionsTool } from "./present-options.js" -import { - PRESENT_QUESTION_TOOL, - presentQuestionTool, -} from "./present-question.js" -import { - PRESENT_REVIEW_SET_TOOL, - presentReviewSetTool, -} from "./present-review-set.js" -import { REQUEST_ANSWER_TOOL, requestAnswerTool } from "./request-answer.js" -import { REQUEST_CHOICE_TOOL, requestChoiceTool } from "./request-choice.js" -import { REQUEST_CHOICES_TOOL, requestChoicesTool } from "./request-choices.js" -import { REQUEST_REVIEW_TOOL, requestReviewTool } from "./request-review.js" +import { PRESENT_CANDIDATES_TOOL, presentCandidatesTool } from './present-candidates.js'; +import { PRESENT_OPTIONS_TOOL, presentOptionsTool } from './present-options.js'; +import { PRESENT_QUESTION_TOOL, presentQuestionTool } from './present-question.js'; +import { PRESENT_REVIEW_SET_TOOL, presentReviewSetTool } from './present-review-set.js'; +import { REQUEST_ANSWER_TOOL, requestAnswerTool } from './request-answer.js'; +import { REQUEST_CHOICE_TOOL, requestChoiceTool } from './request-choice.js'; +import { REQUEST_CHOICES_TOOL, requestChoicesTool } from './request-choices.js'; +import { REQUEST_REVIEW_TOOL, requestReviewTool } from './request-review.js'; -export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from "../../../structured-exchange.js" +export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from '../../../structured-exchange.js'; export { buildStructuredExchangeEditorPrefill, parseStructuredExchangeEditorResponse, structuredExchangeResultFromEditor, type StructuredExchangeEditorPrefillParams, -} from "./shared/editor-fallback.js" +} from './shared/editor-fallback.js'; export { findIncompleteStructuredExchangePresents, isStructuredExchangePresentDetails, isStructuredExchangeRequestDetails, -} from "./shared/recovery.js" +} from './shared/recovery.js'; export { STRUCTURED_EXCHANGE_PRESENT_SCHEMA, STRUCTURED_EXCHANGE_REQUEST_SCHEMA, @@ -38,7 +29,7 @@ export { type RequestToolName, type StructuredExchangePresentDetails, type StructuredExchangeRequestDetails, -} from "./shared/model.js" +} from './shared/model.js'; export { PRESENT_CANDIDATES_TOOL, PRESENT_OPTIONS_TOOL, @@ -48,7 +39,7 @@ export { REQUEST_CHOICE_TOOL, REQUEST_CHOICES_TOOL, REQUEST_REVIEW_TOOL, -} +}; export const STRUCTURED_EXCHANGE_IMPLEMENTED_TOOLS = [ presentQuestionTool, @@ -56,22 +47,22 @@ export const STRUCTURED_EXCHANGE_IMPLEMENTED_TOOLS = [ requestAnswerTool, requestChoiceTool, requestChoicesTool, -] as const +] as const; export const STRUCTURED_EXCHANGE_STUB_TOOL_NAMES = [ PRESENT_REVIEW_SET_TOOL, PRESENT_CANDIDATES_TOOL, REQUEST_REVIEW_TOOL, -] as const +] as const; -void presentReviewSetTool -void presentCandidatesTool -void requestReviewTool +void presentReviewSetTool; +void presentCandidatesTool; +void requestReviewTool; export function registerStructuredExchange(pi: ExtensionAPI) { for (const tool of STRUCTURED_EXCHANGE_IMPLEMENTED_TOOLS) { - pi.registerTool(tool) + pi.registerTool(tool); } } -export default registerStructuredExchange +export default registerStructuredExchange; diff --git a/src/.pi/extensions/structured-exchange/present-candidates.ts b/src/.pi/extensions/structured-exchange/present-candidates.ts index 4e5d02599..8db005f40 100644 --- a/src/.pi/extensions/structured-exchange/present-candidates.ts +++ b/src/.pi/extensions/structured-exchange/present-candidates.ts @@ -1,5 +1,5 @@ -export const PRESENT_CANDIDATES_TOOL = "present_candidates" as const +export const PRESENT_CANDIDATES_TOOL = 'present_candidates' as const; // Stubbed intentionally: candidate presentation semantics are named now, but // not registered until candidate artefact rendering has a product owner. -export const presentCandidatesTool = undefined +export const presentCandidatesTool = undefined; diff --git a/src/.pi/extensions/structured-exchange/present-options.ts b/src/.pi/extensions/structured-exchange/present-options.ts index 24215e3b5..24526b07e 100644 --- a/src/.pi/extensions/structured-exchange/present-options.ts +++ b/src/.pi/extensions/structured-exchange/present-options.ts @@ -1,107 +1,98 @@ -import { defineTool } from "@earendil-works/pi-coding-agent" -import { Type } from "typebox" +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; -import { markdownEscape, renderMarkdownResult } from "./shared/markdown.js" -import { - STRUCTURED_EXCHANGE_PRESENT_SCHEMA, - type StructuredExchangePresentDetails, -} from "./shared/model.js" +import { markdownEscape, renderMarkdownResult } from './shared/markdown.js'; +import { STRUCTURED_EXCHANGE_PRESENT_SCHEMA, type StructuredExchangePresentDetails } from './shared/model.js'; -export const PRESENT_OPTIONS_TOOL = "present_options" as const +export const PRESENT_OPTIONS_TOOL = 'present_options' as const; const PresentedOptionSchema = Type.Object({ id: Type.String({ - description: "Stable option id for later request_* response correlation.", + description: 'Stable option id for later request_* response correlation.', }), - content: Type.String({ description: "Markdown-readable option content." }), + content: Type.String({ description: 'Markdown-readable option content.' }), rationale: Type.Optional( Type.String({ - description: "Why this option is plausible or recommended.", + description: 'Why this option is plausible or recommended.', }), ), -}) +}); export const PresentOptionsParams = Type.Object({ exchangeId: Type.String({ - description: - "Stable id tying this presented offer to the later request_* response.", + description: 'Stable id tying this presented offer to the later request_* response.', }), - heading: Type.String({ description: "Heading for the presented options." }), - body: Type.Optional( - Type.String({ description: "Markdown body shown before the options." }), - ), + heading: Type.String({ description: 'Heading for the presented options.' }), + body: Type.Optional(Type.String({ description: 'Markdown body shown before the options.' })), options: Type.Array(PresentedOptionSchema, { - description: "Options to display.", + description: 'Options to display.', }), expectedRequestTool: Type.Optional( - Type.Union( - [Type.Literal("request_choice"), Type.Literal("request_choices")], - { - description: "The request_* tool expected to collect the response.", - }, - ), + Type.Union([Type.Literal('request_choice'), Type.Literal('request_choices')], { + description: 'The request_* tool expected to collect the response.', + }), ), -}) +}); interface OptionsMarkdownParams { - heading: string - body?: string + heading: string; + body?: string; options: Array<{ - id: string - content: string - rationale?: string - }> + id: string; + content: string; + rationale?: string; + }>; } function optionsMarkdown(params: OptionsMarkdownParams): string { - const lines = [`## ${params.heading.trim()}`] - const body = params.body?.trim() - if (body) lines.push("", body) + const lines = [`## ${params.heading.trim()}`]; + const body = params.body?.trim(); + if (body) lines.push('', body); params.options.forEach((option, index) => { - lines.push("", `### ${index + 1}. ${option.content.trim()}`) - const rationale = option.rationale?.trim() - if (rationale) lines.push("", `**Rationale:** ${rationale}`) - lines.push("", ``) - }) - return lines.join("\n") + lines.push('', `### ${index + 1}. ${option.content.trim()}`); + const rationale = option.rationale?.trim(); + if (rationale) lines.push('', `**Rationale:** ${rationale}`); + lines.push('', ``); + }); + return lines.join('\n'); } export const presentOptionsTool = defineTool({ name: PRESENT_OPTIONS_TOOL, - label: "Present options", + label: 'Present options', description: - "Persist and display a set of structured options as the present half of a Brunch structured exchange. Call the matching request_choice/request_choices tool after this result is available.", - promptSnippet: "Present structured options before requesting a choice", + 'Persist and display a set of structured options as the present half of a Brunch structured exchange. Call the matching request_choice/request_choices tool after this result is available.', + promptSnippet: 'Present structured options before requesting a choice', promptGuidelines: [ - "Use present_options before request_choice or request_choices.", - "Do not rely on renderCall for semantic display; the durable offer is this tool result.", + 'Use present_options before request_choice or request_choices.', + 'Do not rely on renderCall for semantic display; the durable offer is this tool result.', ], parameters: PresentOptionsParams, - executionMode: "sequential", + executionMode: 'sequential', async execute(toolCallId, params) { - const markdown = optionsMarkdown(params) + const markdown = optionsMarkdown(params); const details: StructuredExchangePresentDetails = { schema: STRUCTURED_EXCHANGE_PRESENT_SCHEMA, schemaVersion: 1, exchangeId: params.exchangeId, presentTool: PRESENT_OPTIONS_TOOL, - kind: "options", - status: "presented", + kind: 'options', + status: 'presented', expectedRequest: { - tool: params.expectedRequestTool ?? "request_choice", + tool: params.expectedRequestTool ?? 'request_choice', required: true, }, createdAtToolCallId: toolCallId, - } - return { content: [{ type: "text" as const, text: markdown }], details } + }; + return { content: [{ type: 'text' as const, text: markdown }], details }; }, renderCall() { - return renderMarkdownResult({ content: [] }) + return renderMarkdownResult({ content: [] }); }, renderResult(result, _options, theme) { - return renderMarkdownResult(result, theme) + return renderMarkdownResult(result, theme); }, -}) +}); diff --git a/src/.pi/extensions/structured-exchange/present-question.ts b/src/.pi/extensions/structured-exchange/present-question.ts index eb1f530a1..fb992899a 100644 --- a/src/.pi/extensions/structured-exchange/present-question.ts +++ b/src/.pi/extensions/structured-exchange/present-question.ts @@ -1,70 +1,63 @@ -import { defineTool } from "@earendil-works/pi-coding-agent" -import { Type } from "typebox" +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; -import { renderMarkdownResult } from "./shared/markdown.js" -import { - STRUCTURED_EXCHANGE_PRESENT_SCHEMA, - type StructuredExchangePresentDetails, -} from "./shared/model.js" +import { renderMarkdownResult } from './shared/markdown.js'; +import { STRUCTURED_EXCHANGE_PRESENT_SCHEMA, type StructuredExchangePresentDetails } from './shared/model.js'; -export const PRESENT_QUESTION_TOOL = "present_question" as const +export const PRESENT_QUESTION_TOOL = 'present_question' as const; export const PresentQuestionParams = Type.Object({ exchangeId: Type.String({ - description: - "Stable id tying this question to the later request_answer response.", + description: 'Stable id tying this question to the later request_answer response.', }), - heading: Type.String({ description: "Question heading." }), + heading: Type.String({ description: 'Question heading.' }), body: Type.Optional( Type.String({ - description: "Markdown body for context before the answer request.", + description: 'Markdown body for context before the answer request.', }), ), - expectedRequestTool: Type.Optional(Type.Literal("request_answer")), -}) + expectedRequestTool: Type.Optional(Type.Literal('request_answer')), +}); export const presentQuestionTool = defineTool({ name: PRESENT_QUESTION_TOOL, - label: "Present question", + label: 'Present question', description: - "Persist and display a structured question as the present half of a Brunch structured exchange. Call request_answer after this result is available.", - promptSnippet: "Present a structured question before requesting an answer", + 'Persist and display a structured question as the present half of a Brunch structured exchange. Call request_answer after this result is available.', + promptSnippet: 'Present a structured question before requesting an answer', promptGuidelines: [ - "Use present_question before request_answer.", - "The durable user-visible question is this tool result, not renderCall.", + 'Use present_question before request_answer.', + 'The durable user-visible question is this tool result, not renderCall.', ], parameters: PresentQuestionParams, - executionMode: "sequential", + executionMode: 'sequential', async execute(toolCallId, params) { - const body = params.body?.trim() - const markdown = [ - `## ${params.heading.trim()}`, - body ? `\n${body}` : undefined, - ] + const body = params.body?.trim(); + const markdown = [`## ${params.heading.trim()}`, body ? `\n${body}` : undefined] .filter(Boolean) - .join("\n") + .join('\n'); const details: StructuredExchangePresentDetails = { schema: STRUCTURED_EXCHANGE_PRESENT_SCHEMA, schemaVersion: 1, exchangeId: params.exchangeId, presentTool: PRESENT_QUESTION_TOOL, - kind: "question", - status: "presented", + kind: 'question', + status: 'presented', expectedRequest: { - tool: params.expectedRequestTool ?? "request_answer", + tool: params.expectedRequestTool ?? 'request_answer', required: true, }, createdAtToolCallId: toolCallId, - } - return { content: [{ type: "text" as const, text: markdown }], details } + }; + return { content: [{ type: 'text' as const, text: markdown }], details }; }, renderCall() { - return renderMarkdownResult({ content: [] }) + return renderMarkdownResult({ content: [] }); }, renderResult(result, _options, theme) { - return renderMarkdownResult(result, theme) + return renderMarkdownResult(result, theme); }, -}) +}); diff --git a/src/.pi/extensions/structured-exchange/present-review-set.ts b/src/.pi/extensions/structured-exchange/present-review-set.ts index 6247f574f..b30977de7 100644 --- a/src/.pi/extensions/structured-exchange/present-review-set.ts +++ b/src/.pi/extensions/structured-exchange/present-review-set.ts @@ -1,5 +1,5 @@ -export const PRESENT_REVIEW_SET_TOOL = "present_review_set" as const +export const PRESENT_REVIEW_SET_TOOL = 'present_review_set' as const; // Stubbed intentionally: review-set presentation semantics are named now, but // not registered until review-set proposal/acceptance flow lands. -export const presentReviewSetTool = undefined +export const presentReviewSetTool = undefined; diff --git a/src/.pi/extensions/structured-exchange/request-answer.ts b/src/.pi/extensions/structured-exchange/request-answer.ts index 8654601a1..ba65dcb53 100644 --- a/src/.pi/extensions/structured-exchange/request-answer.ts +++ b/src/.pi/extensions/structured-exchange/request-answer.ts @@ -1,47 +1,41 @@ -import { defineTool } from "@earendil-works/pi-coding-agent" -import { Type } from "typebox" +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; -import { renderMarkdownResult } from "./shared/markdown.js" -import { - STRUCTURED_EXCHANGE_REQUEST_SCHEMA, - type StructuredExchangeRequestDetails, -} from "./shared/model.js" +import { renderMarkdownResult } from './shared/markdown.js'; +import { STRUCTURED_EXCHANGE_REQUEST_SCHEMA, type StructuredExchangeRequestDetails } from './shared/model.js'; -export const REQUEST_ANSWER_TOOL = "request_answer" as const +export const REQUEST_ANSWER_TOOL = 'request_answer' as const; export const RequestAnswerParams = Type.Object({ exchangeId: Type.String({ - description: - "The structured exchange id from the corresponding present_question entry.", + description: 'The structured exchange id from the corresponding present_question entry.', }), - respondsToPresentTool: Type.Optional(Type.Literal("present_question")), + respondsToPresentTool: Type.Optional(Type.Literal('present_question')), prompt: Type.String({ - description: - "Short live-input prompt. Do not repeat the presented question body.", + description: 'Short live-input prompt. Do not repeat the presented question body.', }), -}) +}); function responseMarkdown(details: StructuredExchangeRequestDetails): string { - if (details.status === "cancelled") - return "### Response\n\n_User cancelled the request._" - if (details.status === "unavailable") { - return `### Response\n\n_${details.message ?? "Response UI unavailable."}_` + if (details.status === 'cancelled') return '### Response\n\n_User cancelled the request._'; + if (details.status === 'unavailable') { + return `### Response\n\n_${details.message ?? 'Response UI unavailable.'}_`; } - return ["### Response", "", details.answer ?? ""].join("\n") + return ['### Response', '', details.answer ?? ''].join('\n'); } export const requestAnswerTool = defineTool({ name: REQUEST_ANSWER_TOOL, - label: "Request answer", + label: 'Request answer', description: - "Collect a freeform user answer as the request half of a Brunch structured exchange. Use only after present_question.", - promptSnippet: "Request a freeform answer after presenting a question", + 'Collect a freeform user answer as the request half of a Brunch structured exchange. Use only after present_question.', + promptSnippet: 'Request a freeform answer after presenting a question', promptGuidelines: [ - "Use request_answer only after the matching present_question tool.", - "Do not repeat the present_question markdown content in request_answer parameters; reference it by exchangeId.", + 'Use request_answer only after the matching present_question tool.', + 'Do not repeat the present_question markdown content in request_answer parameters; reference it by exchangeId.', ], parameters: RequestAnswerParams, - executionMode: "sequential", + executionMode: 'sequential', async execute(toolCallId, params, _signal, _onUpdate, ctx) { const base = { @@ -51,51 +45,51 @@ export const requestAnswerTool = defineTool({ requestTool: REQUEST_ANSWER_TOOL, respondsTo: { exchangeId: params.exchangeId, - presentTool: params.respondsToPresentTool ?? "present_question", + presentTool: params.respondsToPresentTool ?? 'present_question', }, createdAtToolCallId: toolCallId, - } + }; - if (!ctx.hasUI || typeof ctx.ui.editor !== "function") { + if (!ctx.hasUI || typeof ctx.ui.editor !== 'function') { const details: StructuredExchangeRequestDetails = { ...base, - status: "unavailable", - message: "request_answer requires interactive UI", - } + status: 'unavailable', + message: 'request_answer requires interactive UI', + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; } - const answer = await ctx.ui.editor(params.prompt) + const answer = await ctx.ui.editor(params.prompt); if (answer === undefined) { const details: StructuredExchangeRequestDetails = { ...base, - status: "cancelled", - } + status: 'cancelled', + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; } const details: StructuredExchangeRequestDetails = { ...base, - status: "answered", + status: 'answered', answer: answer.trim(), - } + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; }, renderCall() { - return renderMarkdownResult({ content: [] }) + return renderMarkdownResult({ content: [] }); }, renderResult(result, _options, theme) { - return renderMarkdownResult(result, theme) + return renderMarkdownResult(result, theme); }, -}) +}); diff --git a/src/.pi/extensions/structured-exchange/request-choice.ts b/src/.pi/extensions/structured-exchange/request-choice.ts index 758ab442b..1f140cede 100644 --- a/src/.pi/extensions/structured-exchange/request-choice.ts +++ b/src/.pi/extensions/structured-exchange/request-choice.ts @@ -1,172 +1,151 @@ -import { defineTool } from "@earendil-works/pi-coding-agent" -import { Type } from "typebox" +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; -import { - normalizeOptionalText, - renderMarkdownResult, -} from "./shared/markdown.js" +import { normalizeOptionalText, renderMarkdownResult } from './shared/markdown.js'; import { STRUCTURED_EXCHANGE_REQUEST_SCHEMA, type StructuredExchangeChoice, type StructuredExchangeRequestDetails, -} from "./shared/model.js" +} from './shared/model.js'; -export const REQUEST_CHOICE_TOOL = "request_choice" as const +export const REQUEST_CHOICE_TOOL = 'request_choice' as const; const ChoiceSchema = Type.Object({ id: Type.String({ - description: "Stable choice id from the corresponding present_* entry.", + description: 'Stable choice id from the corresponding present_* entry.', }), label: Type.String({ - description: "Short choice label shown in the live selection UI.", + description: 'Short choice label shown in the live selection UI.', }), -}) +}); export const RequestChoiceParams = Type.Object({ exchangeId: Type.String({ - description: - "The structured exchange id from the corresponding present_* entry.", + description: 'The structured exchange id from the corresponding present_* entry.', }), - respondsToPresentTool: Type.Union([ - Type.Literal("present_options"), - Type.Literal("present_candidates"), - ]), + respondsToPresentTool: Type.Union([Type.Literal('present_options'), Type.Literal('present_candidates')]), prompt: Type.String({ - description: - "Short live-input prompt. Do not repeat the presented content.", + description: 'Short live-input prompt. Do not repeat the presented content.', }), choices: Type.Array(ChoiceSchema, { - description: "Choices available for this response.", + description: 'Choices available for this response.', }), - allowOther: Type.Optional( - Type.Boolean({ description: "Whether the user may choose Other." }), - ), + allowOther: Type.Optional(Type.Boolean({ description: 'Whether the user may choose Other.' })), commentPrompt: Type.Optional( Type.String({ - description: "Prompt for optional comment after a listed choice.", + description: 'Prompt for optional comment after a listed choice.', }), ), -}) +}); function responseMarkdown(details: StructuredExchangeRequestDetails): string { - if (details.status === "cancelled") - return "### Response\n\n_User cancelled the request._" - if (details.status === "unavailable") { - return `### Response\n\n_${details.message ?? "Response UI unavailable."}_` + if (details.status === 'cancelled') return '### Response\n\n_User cancelled the request._'; + if (details.status === 'unavailable') { + return `### Response\n\n_${details.message ?? 'Response UI unavailable.'}_`; } - const lines = ["### Response"] - if (details.choice) lines.push("", `Selected: **${details.choice.label}**`) - if (details.comment) lines.push("", "Comment:", "", `> ${details.comment}`) - return lines.join("\n") + const lines = ['### Response']; + if (details.choice) lines.push('', `Selected: **${details.choice.label}**`); + if (details.comment) lines.push('', 'Comment:', '', `> ${details.comment}`); + return lines.join('\n'); } function choiceByLabel( choices: readonly StructuredExchangeChoice[], selected: string, ): StructuredExchangeChoice | undefined { - return choices.find( - (choice) => choice.label === selected || choice.id === selected, - ) + return choices.find((choice) => choice.label === selected || choice.id === selected); } export const requestChoiceTool = defineTool({ name: REQUEST_CHOICE_TOOL, - label: "Request choice", + label: 'Request choice', description: - "Collect one user choice as the request half of a Brunch structured exchange. Use only after the corresponding present_* tool result has displayed the offer content.", - promptSnippet: "Request one choice after presenting a structured offer", + 'Collect one user choice as the request half of a Brunch structured exchange. Use only after the corresponding present_* tool result has displayed the offer content.', + promptSnippet: 'Request one choice after presenting a structured offer', promptGuidelines: [ - "Use request_choice only after the matching present_options or present_candidates tool.", - "Do not repeat the present_* markdown content in request_choice parameters; reference it by exchangeId.", + 'Use request_choice only after the matching present_options or present_candidates tool.', + 'Do not repeat the present_* markdown content in request_choice parameters; reference it by exchangeId.', ], parameters: RequestChoiceParams, - executionMode: "sequential", + executionMode: 'sequential', async execute(toolCallId, params, _signal, _onUpdate, ctx) { - const choices: StructuredExchangeChoice[] = params.choices.map( - (choice) => ({ - id: choice.id, - label: choice.label, - }), - ) + const choices: StructuredExchangeChoice[] = params.choices.map((choice) => ({ + id: choice.id, + label: choice.label, + })); const unavailable = (message: string) => { const details: StructuredExchangeRequestDetails = { schema: STRUCTURED_EXCHANGE_REQUEST_SCHEMA, schemaVersion: 1, exchangeId: params.exchangeId, requestTool: REQUEST_CHOICE_TOOL, - status: "unavailable", + status: 'unavailable', respondsTo: { exchangeId: params.exchangeId, presentTool: params.respondsToPresentTool, }, message, createdAtToolCallId: toolCallId, - } + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } - } + }; + }; - if (!ctx.hasUI || typeof ctx.ui.select !== "function") { - return unavailable("request_choice requires interactive UI") + if (!ctx.hasUI || typeof ctx.ui.select !== 'function') { + return unavailable('request_choice requires interactive UI'); } - const labels = [ - ...choices.map((choice) => choice.label), - ...(params.allowOther ? ["Other"] : []), - ] - const selected = await ctx.ui.select(params.prompt, labels) + const labels = [...choices.map((choice) => choice.label), ...(params.allowOther ? ['Other'] : [])]; + const selected = await ctx.ui.select(params.prompt, labels); if (selected === undefined) { const details: StructuredExchangeRequestDetails = { schema: STRUCTURED_EXCHANGE_REQUEST_SCHEMA, schemaVersion: 1, exchangeId: params.exchangeId, requestTool: REQUEST_CHOICE_TOOL, - status: "cancelled", + status: 'cancelled', respondsTo: { exchangeId: params.exchangeId, presentTool: params.respondsToPresentTool, }, createdAtToolCallId: toolCallId, - } + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; } - const picked = choiceByLabel(choices, selected) - let choice = picked - let comment = "" + const picked = choiceByLabel(choices, selected); + let choice = picked; + let comment = ''; if (!choice) { const other = - typeof ctx.ui.input === "function" - ? await ctx.ui.input("Other", "Describe your answer") - : undefined + typeof ctx.ui.input === 'function' ? await ctx.ui.input('Other', 'Describe your answer') : undefined; if (other === undefined || other.trim().length === 0) { const details: StructuredExchangeRequestDetails = { schema: STRUCTURED_EXCHANGE_REQUEST_SCHEMA, schemaVersion: 1, exchangeId: params.exchangeId, requestTool: REQUEST_CHOICE_TOOL, - status: "cancelled", + status: 'cancelled', respondsTo: { exchangeId: params.exchangeId, presentTool: params.respondsToPresentTool, }, createdAtToolCallId: toolCallId, - } + }; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; } - choice = { id: "other", label: other.trim() } - } else if (typeof ctx.ui.input === "function") { - comment = - (await ctx.ui.input(params.commentPrompt ?? "Optional comment")) ?? "" + choice = { id: 'other', label: other.trim() }; + } else if (typeof ctx.ui.input === 'function') { + comment = (await ctx.ui.input(params.commentPrompt ?? 'Optional comment')) ?? ''; } const details: StructuredExchangeRequestDetails = { @@ -174,27 +153,27 @@ export const requestChoiceTool = defineTool({ schemaVersion: 1, exchangeId: params.exchangeId, requestTool: REQUEST_CHOICE_TOOL, - status: "answered", + status: 'answered', respondsTo: { exchangeId: params.exchangeId, presentTool: params.respondsToPresentTool, }, choice, createdAtToolCallId: toolCallId, - } - const normalizedComment = normalizeOptionalText(comment) - if (normalizedComment !== undefined) details.comment = normalizedComment + }; + const normalizedComment = normalizeOptionalText(comment); + if (normalizedComment !== undefined) details.comment = normalizedComment; return { - content: [{ type: "text" as const, text: responseMarkdown(details) }], + content: [{ type: 'text' as const, text: responseMarkdown(details) }], details, - } + }; }, renderCall() { - return renderMarkdownResult({ content: [] }) + return renderMarkdownResult({ content: [] }); }, renderResult(result, _options, theme) { - return renderMarkdownResult(result, theme) + return renderMarkdownResult(result, theme); }, -}) +}); diff --git a/src/.pi/extensions/structured-exchange/request-choices.ts b/src/.pi/extensions/structured-exchange/request-choices.ts index cec83b562..b6eb92426 100644 --- a/src/.pi/extensions/structured-exchange/request-choices.ts +++ b/src/.pi/extensions/structured-exchange/request-choices.ts @@ -1,214 +1,192 @@ -import { defineTool } from "@earendil-works/pi-coding-agent" -import { Type } from "typebox" +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; -import { - markdownEscape, - normalizeOptionalText, - renderMarkdownResult, -} from "./shared/markdown.js" +import { markdownEscape, normalizeOptionalText, renderMarkdownResult } from './shared/markdown.js'; import { isRecord, STRUCTURED_EXCHANGE_REQUEST_SCHEMA, type StructuredExchangeChoice, type StructuredExchangeRequestDetails, -} from "./shared/model.js" +} from './shared/model.js'; -export const REQUEST_CHOICES_TOOL = "request_choices" as const +export const REQUEST_CHOICES_TOOL = 'request_choices' as const; const ChoiceSchema = Type.Object({ id: Type.String({ - description: "Stable choice id from the corresponding present_* entry.", + description: 'Stable choice id from the corresponding present_* entry.', }), label: Type.String({ - description: "Short choice label shown in the live selection UI.", + description: 'Short choice label shown in the live selection UI.', }), -}) +}); export const RequestChoicesParams = Type.Object({ exchangeId: Type.String({ - description: - "The structured exchange id from the corresponding present_options entry.", + description: 'The structured exchange id from the corresponding present_options entry.', }), - respondsToPresentTool: Type.Literal("present_options"), + respondsToPresentTool: Type.Literal('present_options'), prompt: Type.String({ - description: - "Short live-input prompt. Do not repeat the presented content.", + description: 'Short live-input prompt. Do not repeat the presented content.', }), choices: Type.Array(ChoiceSchema, { - description: "Listed choices available for this multi-choice response.", + description: 'Listed choices available for this multi-choice response.', }), - allowOther: Type.Optional( - Type.Boolean({ description: "Whether the user may choose Other." }), - ), - allowNone: Type.Optional( - Type.Boolean({ description: "Whether the user may choose None." }), - ), + allowOther: Type.Optional(Type.Boolean({ description: 'Whether the user may choose Other.' })), + allowNone: Type.Optional(Type.Boolean({ description: 'Whether the user may choose None.' })), commentPrompt: Type.Optional( Type.String({ - description: - "Prompt for an optional comment. Required when Other or None is selected.", + description: 'Prompt for an optional comment. Required when Other or None is selected.', }), ), -}) +}); interface EditorChoice { - id: string - label?: string + id: string; + label?: string; } interface EditorResponse { - status: "answered" | "cancelled" - choices: EditorChoice[] - comment: string + status: 'answered' | 'cancelled'; + choices: EditorChoice[]; + comment: string; } function buildEditorPrefill(params: { - prompt: string - choices: readonly StructuredExchangeChoice[] - allowOther?: boolean - allowNone?: boolean - commentPrompt?: string + prompt: string; + choices: readonly StructuredExchangeChoice[]; + allowOther?: boolean; + allowNone?: boolean; + commentPrompt?: string; }): string { const choices = [ ...params.choices, - ...(params.allowOther ? [{ id: "other", label: "Other" }] : []), - ...(params.allowNone ? [{ id: "none", label: "None" }] : []), - ] + ...(params.allowOther ? [{ id: 'other', label: 'Other' }] : []), + ...(params.allowNone ? [{ id: 'none', label: 'None' }] : []), + ]; return JSON.stringify( { - schema: "brunch.structured_exchange.request_choices.editor", + schema: 'brunch.structured_exchange.request_choices.editor', schemaVersion: 1, prompt: params.prompt, - mode: "multi-choice", + mode: 'multi-choice', choices, instructions: [ - "Edit only response.", - "Set response.status to answered or cancelled.", - "For each selected choice, include its id in response.choices.", - "Set response.comment to a string. Other or None requires a nonblank comment.", + 'Edit only response.', + 'Set response.status to answered or cancelled.', + 'For each selected choice, include its id in response.choices.', + 'Set response.comment to a string. Other or None requires a nonblank comment.', ], - commentPrompt: params.commentPrompt ?? "Optional comment", - response: { status: "cancelled", choices: [], comment: "" }, + commentPrompt: params.commentPrompt ?? 'Optional comment', + response: { status: 'cancelled', choices: [], comment: '' }, }, null, 2, - ) + ); } function parseEditorResponse(value: string): EditorResponse | null { - let parsed: unknown + let parsed: unknown; try { - parsed = JSON.parse(value) + parsed = JSON.parse(value); } catch { - return null + return null; } - if (!isRecord(parsed)) return null - const response = parsed.response - if (!isRecord(response)) return null + if (!isRecord(parsed)) return null; + const response = parsed.response; + if (!isRecord(response)) return null; - if (response.status === "cancelled") { - return { status: "cancelled", choices: [], comment: "" } + if (response.status === 'cancelled') { + return { status: 'cancelled', choices: [], comment: '' }; } - if (response.status !== "answered") return null - if (!Array.isArray(response.choices)) return null - if (typeof response.comment !== "string") return null + if (response.status !== 'answered') return null; + if (!Array.isArray(response.choices)) return null; + if (typeof response.comment !== 'string') return null; const choices = response.choices.map((choice): EditorChoice | null => { - if (!isRecord(choice) || typeof choice.id !== "string") return null + if (!isRecord(choice) || typeof choice.id !== 'string') return null; return { id: choice.id, - ...(typeof choice.label === "string" ? { label: choice.label } : {}), - } - }) - if (choices.some((choice) => choice === null)) return null + ...(typeof choice.label === 'string' ? { label: choice.label } : {}), + }; + }); + if (choices.some((choice) => choice === null)) return null; return { - status: "answered", + status: 'answered', choices: choices as EditorChoice[], comment: response.comment, - } + }; } function requestMarkdown(details: StructuredExchangeRequestDetails): string { - if (details.status === "cancelled") - return "### Response\n\n_User cancelled the request._" - if (details.status === "unavailable") { - return `### Response\n\n_${details.message ?? "Response UI unavailable."}_` + if (details.status === 'cancelled') return '### Response\n\n_User cancelled the request._'; + if (details.status === 'unavailable') { + return `### Response\n\n_${details.message ?? 'Response UI unavailable.'}_`; } - const lines = ["### Response"] + const lines = ['### Response']; if (details.choices && details.choices.length > 0) { - lines.push( - "", - ...details.choices.map((choice) => `- ${markdownEscape(choice.label)}`), - ) + lines.push('', ...details.choices.map((choice) => `- ${markdownEscape(choice.label)}`)); } - if (details.comment) lines.push("", "Comment:", "", `> ${details.comment}`) - return lines.join("\n") + if (details.comment) lines.push('', 'Comment:', '', `> ${details.comment}`); + return lines.join('\n'); } -function unavailable( - base: Omit, - message: string, -) { +function unavailable(base: Omit, message: string) { const details: StructuredExchangeRequestDetails = { ...base, - status: "unavailable", + status: 'unavailable', message, - } + }; return { - content: [{ type: "text" as const, text: requestMarkdown(details) }], + content: [{ type: 'text' as const, text: requestMarkdown(details) }], details, - } + }; } function matchSelectedChoices( selected: readonly EditorChoice[], params: { - choices: readonly StructuredExchangeChoice[] - allowOther?: boolean - allowNone?: boolean + choices: readonly StructuredExchangeChoice[]; + allowOther?: boolean; + allowNone?: boolean; }, ): StructuredExchangeChoice[] | string { - const allowed = new Map(params.choices.map((choice) => [choice.id, choice])) - if (params.allowOther) allowed.set("other", { id: "other", label: "Other" }) - if (params.allowNone) allowed.set("none", { id: "none", label: "None" }) + const allowed = new Map(params.choices.map((choice) => [choice.id, choice])); + if (params.allowOther) allowed.set('other', { id: 'other', label: 'Other' }); + if (params.allowNone) allowed.set('none', { id: 'none', label: 'None' }); - const matched: StructuredExchangeChoice[] = [] - const seen = new Set() + const matched: StructuredExchangeChoice[] = []; + const seen = new Set(); for (const choice of selected) { - const known = allowed.get(choice.id) - if (!known) - return `request_choices received unknown choice id: ${choice.id}` - if (seen.has(choice.id)) continue - seen.add(choice.id) - matched.push({ id: known.id, label: choice.label ?? known.label }) + const known = allowed.get(choice.id); + if (!known) return `request_choices received unknown choice id: ${choice.id}`; + if (seen.has(choice.id)) continue; + seen.add(choice.id); + matched.push({ id: known.id, label: choice.label ?? known.label }); } - if (matched.length === 0) - return "request_choices requires at least one choice" - return matched + if (matched.length === 0) return 'request_choices requires at least one choice'; + return matched; } export const requestChoicesTool = defineTool({ name: REQUEST_CHOICES_TOOL, - label: "Request choices", + label: 'Request choices', description: - "Collect one-or-more user choices as the request half of a Brunch structured exchange. Use only after the corresponding present_options tool result has displayed the offer content.", - promptSnippet: "Request multiple choices after presenting structured options", + 'Collect one-or-more user choices as the request half of a Brunch structured exchange. Use only after the corresponding present_options tool result has displayed the offer content.', + promptSnippet: 'Request multiple choices after presenting structured options', promptGuidelines: [ - "Use request_choices only after the matching present_options tool.", - "Do not repeat the present_options markdown content in request_choices parameters; reference it by exchangeId.", - "Require a comment when the response selects Other or None.", + 'Use request_choices only after the matching present_options tool.', + 'Do not repeat the present_options markdown content in request_choices parameters; reference it by exchangeId.', + 'Require a comment when the response selects Other or None.', ], parameters: RequestChoicesParams, - executionMode: "sequential", + executionMode: 'sequential', async execute(toolCallId, params, _signal, _onUpdate, ctx) { - const choices: StructuredExchangeChoice[] = params.choices.map( - (choice) => ({ - id: choice.id, - label: choice.label, - }), - ) + const choices: StructuredExchangeChoice[] = params.choices.map((choice) => ({ + id: choice.id, + label: choice.label, + })); const base = { schema: STRUCTURED_EXCHANGE_REQUEST_SCHEMA, schemaVersion: 1 as const, @@ -219,89 +197,76 @@ export const requestChoicesTool = defineTool({ presentTool: params.respondsToPresentTool, }, createdAtToolCallId: toolCallId, - } + }; - if (!ctx.hasUI || typeof ctx.ui.editor !== "function") { - return unavailable(base, "request_choices requires interactive UI") + if (!ctx.hasUI || typeof ctx.ui.editor !== 'function') { + return unavailable(base, 'request_choices requires interactive UI'); } const editorPrefillParams: Parameters[0] = { prompt: params.prompt, choices, - } - if (params.allowOther !== undefined) - editorPrefillParams.allowOther = params.allowOther - if (params.allowNone !== undefined) - editorPrefillParams.allowNone = params.allowNone - if (params.commentPrompt !== undefined) - editorPrefillParams.commentPrompt = params.commentPrompt + }; + if (params.allowOther !== undefined) editorPrefillParams.allowOther = params.allowOther; + if (params.allowNone !== undefined) editorPrefillParams.allowNone = params.allowNone; + if (params.commentPrompt !== undefined) editorPrefillParams.commentPrompt = params.commentPrompt; - const edited = await ctx.ui.editor(buildEditorPrefill(editorPrefillParams)) + const edited = await ctx.ui.editor(buildEditorPrefill(editorPrefillParams)); if (edited === undefined) { const details: StructuredExchangeRequestDetails = { ...base, - status: "cancelled", - } + status: 'cancelled', + }; return { - content: [{ type: "text" as const, text: requestMarkdown(details) }], + content: [{ type: 'text' as const, text: requestMarkdown(details) }], details, - } + }; } - const response = parseEditorResponse(edited) + const response = parseEditorResponse(edited); if (!response) { - return unavailable( - base, - "request_choices editor fallback returned invalid JSON", - ) + return unavailable(base, 'request_choices editor fallback returned invalid JSON'); } - if (response.status === "cancelled") { + if (response.status === 'cancelled') { const details: StructuredExchangeRequestDetails = { ...base, - status: "cancelled", - } + status: 'cancelled', + }; return { - content: [{ type: "text" as const, text: requestMarkdown(details) }], + content: [{ type: 'text' as const, text: requestMarkdown(details) }], details, - } + }; } - const matchParams: Parameters[1] = { choices } - if (params.allowOther !== undefined) - matchParams.allowOther = params.allowOther - if (params.allowNone !== undefined) matchParams.allowNone = params.allowNone + const matchParams: Parameters[1] = { choices }; + if (params.allowOther !== undefined) matchParams.allowOther = params.allowOther; + if (params.allowNone !== undefined) matchParams.allowNone = params.allowNone; - const matched = matchSelectedChoices(response.choices, matchParams) - if (typeof matched === "string") return unavailable(base, matched) + const matched = matchSelectedChoices(response.choices, matchParams); + if (typeof matched === 'string') return unavailable(base, matched); - const comment = normalizeOptionalText(response.comment) - if ( - matched.some((choice) => choice.id === "other" || choice.id === "none") && - comment === undefined - ) { - return unavailable( - base, - "request_choices requires a comment for Other or None selections", - ) + const comment = normalizeOptionalText(response.comment); + if (matched.some((choice) => choice.id === 'other' || choice.id === 'none') && comment === undefined) { + return unavailable(base, 'request_choices requires a comment for Other or None selections'); } const details: StructuredExchangeRequestDetails = { ...base, - status: "answered", + status: 'answered', choices: matched, ...(comment !== undefined ? { comment } : {}), - } + }; return { - content: [{ type: "text" as const, text: requestMarkdown(details) }], + content: [{ type: 'text' as const, text: requestMarkdown(details) }], details, - } + }; }, renderCall() { - return renderMarkdownResult({ content: [] }) + return renderMarkdownResult({ content: [] }); }, renderResult(result, _options, theme) { - return renderMarkdownResult(result, theme) + return renderMarkdownResult(result, theme); }, -}) +}); diff --git a/src/.pi/extensions/structured-exchange/request-review.ts b/src/.pi/extensions/structured-exchange/request-review.ts index 67eabbf4e..24401b868 100644 --- a/src/.pi/extensions/structured-exchange/request-review.ts +++ b/src/.pi/extensions/structured-exchange/request-review.ts @@ -1,5 +1,5 @@ -export const REQUEST_REVIEW_TOOL = "request_review" as const +export const REQUEST_REVIEW_TOOL = 'request_review' as const; // Stubbed intentionally: review response semantics are named now, but not // registered until review-set proposal/acceptance flow lands. -export const requestReviewTool = undefined +export const requestReviewTool = undefined; diff --git a/src/.pi/extensions/structured-exchange/schemas/capture.ts b/src/.pi/extensions/structured-exchange/schemas/capture.ts index faeb35171..28ef07fd0 100644 --- a/src/.pi/extensions/structured-exchange/schemas/capture.ts +++ b/src/.pi/extensions/structured-exchange/schemas/capture.ts @@ -1,86 +1,75 @@ -import * as z from "zod" +import * as z from 'zod'; -import { zCaptureDetailsHeader } from "./shared.js" +import { zCaptureDetailsHeader } from './shared.js'; export const zCaptureAnswerDetails = zCaptureDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("request_answer"), - curr: z.literal("capture_answer"), + prev: z.literal('request_answer'), + curr: z.literal('capture_answer'), }) .strict(), }) - .strict() -export type CaptureAnswerDetails = z.infer -export const CaptureAnswerDetailsSchema = z.toJSONSchema( - zCaptureAnswerDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureAnswerDetails = z.infer; +export const CaptureAnswerDetailsSchema = z.toJSONSchema(zCaptureAnswerDetails, { unrepresentable: 'throw' }); export const zCaptureChoiceDetails = zCaptureDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("request_choice"), - curr: z.literal("capture_choice"), + prev: z.literal('request_choice'), + curr: z.literal('capture_choice'), }) .strict(), }) - .strict() -export type CaptureChoiceDetails = z.infer -export const CaptureChoiceDetailsSchema = z.toJSONSchema( - zCaptureChoiceDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureChoiceDetails = z.infer; +export const CaptureChoiceDetailsSchema = z.toJSONSchema(zCaptureChoiceDetails, { unrepresentable: 'throw' }); export const zCaptureChoicesDetails = zCaptureDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("request_choices"), - curr: z.literal("capture_choices"), + prev: z.literal('request_choices'), + curr: z.literal('capture_choices'), }) .strict(), }) - .strict() -export type CaptureChoicesDetails = z.infer -export const CaptureChoicesDetailsSchema = z.toJSONSchema( - zCaptureChoicesDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureChoicesDetails = z.infer; +export const CaptureChoicesDetailsSchema = z.toJSONSchema(zCaptureChoicesDetails, { + unrepresentable: 'throw', +}); export const zCaptureReviewDetails = zCaptureDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("request_review"), - curr: z.literal("capture_review"), + prev: z.literal('request_review'), + curr: z.literal('capture_review'), }) .strict(), }) - .strict() -export type CaptureReviewDetails = z.infer -export const CaptureReviewDetailsSchema = z.toJSONSchema( - zCaptureReviewDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureReviewDetails = z.infer; +export const CaptureReviewDetailsSchema = z.toJSONSchema(zCaptureReviewDetails, { unrepresentable: 'throw' }); export const zCaptureCandidateDetails = zCaptureDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("request_choice"), - curr: z.literal("capture_candidate"), + prev: z.literal('request_choice'), + curr: z.literal('capture_candidate'), }) .strict(), }) - .strict() -export type CaptureCandidateDetails = z.infer -export const CaptureCandidateDetailsSchema = z.toJSONSchema( - zCaptureCandidateDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureCandidateDetails = z.infer; +export const CaptureCandidateDetailsSchema = z.toJSONSchema(zCaptureCandidateDetails, { + unrepresentable: 'throw', +}); export const zCaptureDetails = z.union([ zCaptureAnswerDetails, @@ -88,8 +77,8 @@ export const zCaptureDetails = z.union([ zCaptureChoicesDetails, zCaptureReviewDetails, zCaptureCandidateDetails, -]) -export type CaptureDetails = z.infer +]); +export type CaptureDetails = z.infer; export const CaptureDetailsSchema = z.toJSONSchema(zCaptureDetails, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); diff --git a/src/.pi/extensions/structured-exchange/schemas/index.ts b/src/.pi/extensions/structured-exchange/schemas/index.ts index 5094d82f8..cb6d5e274 100644 --- a/src/.pi/extensions/structured-exchange/schemas/index.ts +++ b/src/.pi/extensions/structured-exchange/schemas/index.ts @@ -1,4 +1,4 @@ -export * from "./capture.js" -export * from "./present.js" -export * from "./request.js" -export * from "./shared.js" +export * from './capture.js'; +export * from './present.js'; +export * from './request.js'; +export * from './shared.js'; diff --git a/src/.pi/extensions/structured-exchange/schemas/present.ts b/src/.pi/extensions/structured-exchange/schemas/present.ts index b38fd01de..fe195fb8b 100644 --- a/src/.pi/extensions/structured-exchange/schemas/present.ts +++ b/src/.pi/extensions/structured-exchange/schemas/present.ts @@ -1,6 +1,6 @@ -import * as z from "zod" +import * as z from 'zod'; -import { zGraphNodeRef, zMarkdown, zPresentDetailsHeader } from "./shared.js" +import { zGraphNodeRef, zMarkdown, zPresentDetailsHeader } from './shared.js'; export const zPresentDisplay = z .object({ @@ -8,28 +8,27 @@ export const zPresentDisplay = z body: zMarkdown.optional(), preface: zMarkdown.optional(), }) - .strict() -export type PresentDisplay = z.infer + .strict(); +export type PresentDisplay = z.infer; export const PresentDisplaySchema = z.toJSONSchema(zPresentDisplay, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentQuestionDetails = zPresentDetailsHeader .extend({ tool_meta: z .object({ - curr: z.literal("present_question"), - next: z.literal("request_answer"), + curr: z.literal('present_question'), + next: z.literal('request_answer'), }) .strict(), display: zPresentDisplay, }) - .strict() -export type PresentQuestionDetails = z.infer -export const PresentQuestionDetailsSchema = z.toJSONSchema( - zPresentQuestionDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type PresentQuestionDetails = z.infer; +export const PresentQuestionDetailsSchema = z.toJSONSchema(zPresentQuestionDetails, { + unrepresentable: 'throw', +}); export const zPresentOption = z .object({ @@ -37,36 +36,35 @@ export const zPresentOption = z content: zMarkdown, rationale: zMarkdown.optional(), }) - .strict() -export type PresentOption = z.infer + .strict(); +export type PresentOption = z.infer; export const PresentOptionSchema = z.toJSONSchema(zPresentOption, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentOptionsDetails = zPresentDetailsHeader .extend({ tool_meta: z .object({ - curr: z.literal("present_options"), - next: z.enum(["request_choice", "request_choices"]), + curr: z.literal('present_options'), + next: z.enum(['request_choice', 'request_choices']), }) .strict(), display: zPresentDisplay, options: z.array(zPresentOption).min(1), }) - .strict() -export type PresentOptionsDetails = z.infer -export const PresentOptionsDetailsSchema = z.toJSONSchema( - zPresentOptionsDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type PresentOptionsDetails = z.infer; +export const PresentOptionsDetailsSchema = z.toJSONSchema(zPresentOptionsDetails, { + unrepresentable: 'throw', +}); export const zPresentReviewSetDetails = zPresentDetailsHeader .extend({ tool_meta: z .object({ - curr: z.literal("present_review_set"), - next: z.literal("request_review"), + curr: z.literal('present_review_set'), + next: z.literal('request_review'), }) .strict(), display: zPresentDisplay, @@ -76,12 +74,11 @@ export const zPresentReviewSetDetails = zPresentDetailsHeader }) .strict(), }) - .strict() -export type PresentReviewSetDetails = z.infer -export const PresentReviewSetDetailsSchema = z.toJSONSchema( - zPresentReviewSetDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type PresentReviewSetDetails = z.infer; +export const PresentReviewSetDetailsSchema = z.toJSONSchema(zPresentReviewSetDetails, { + unrepresentable: 'throw', +}); export const zCandidateUserRubric = z .object({ @@ -93,11 +90,11 @@ export const zCandidateUserRubric = z lock_in_constraints: zMarkdown, recommendation: zMarkdown.optional(), }) - .strict() -export type CandidateUserRubric = z.infer + .strict(); +export type CandidateUserRubric = z.infer; export const CandidateUserRubricSchema = z.toJSONSchema(zCandidateUserRubric, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zCandidateMetaRubric = z .object({ @@ -106,11 +103,11 @@ export const zCandidateMetaRubric = z coverage_range: zMarkdown.optional(), commitment: zMarkdown.optional(), }) - .strict() -export type CandidateMetaRubric = z.infer + .strict(); +export type CandidateMetaRubric = z.infer; export const CandidateMetaRubricSchema = z.toJSONSchema(zCandidateMetaRubric, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentedCandidate = z .object({ @@ -120,18 +117,18 @@ export const zPresentedCandidate = z meta_rubric: zCandidateMetaRubric, graph_refs: z.array(zGraphNodeRef), }) - .strict() -export type PresentedCandidate = z.infer + .strict(); +export type PresentedCandidate = z.infer; export const PresentedCandidateSchema = z.toJSONSchema(zPresentedCandidate, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentCandidatesDetails = zPresentDetailsHeader .extend({ tool_meta: z .object({ - curr: z.literal("present_candidates"), - next: z.literal("request_choice"), + curr: z.literal('present_candidates'), + next: z.literal('request_choice'), }) .strict(), display: z @@ -142,20 +139,19 @@ export const zPresentCandidatesDetails = zPresentDetailsHeader .strict(), candidates: z.array(zPresentedCandidate).min(1), }) - .strict() -export type PresentCandidatesDetails = z.infer -export const PresentCandidatesDetailsSchema = z.toJSONSchema( - zPresentCandidatesDetails, - { unrepresentable: "throw" }, -) + .strict(); +export type PresentCandidatesDetails = z.infer; +export const PresentCandidatesDetailsSchema = z.toJSONSchema(zPresentCandidatesDetails, { + unrepresentable: 'throw', +}); export const zPresentDetails = z.union([ zPresentQuestionDetails, zPresentOptionsDetails, zPresentReviewSetDetails, zPresentCandidatesDetails, -]) -export type PresentDetails = z.infer +]); +export type PresentDetails = z.infer; export const PresentDetailsSchema = z.toJSONSchema(zPresentDetails, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); diff --git a/src/.pi/extensions/structured-exchange/schemas/request.ts b/src/.pi/extensions/structured-exchange/schemas/request.ts index aa9d06f4c..5c93eb729 100644 --- a/src/.pi/extensions/structured-exchange/schemas/request.ts +++ b/src/.pi/extensions/structured-exchange/schemas/request.ts @@ -1,6 +1,6 @@ -import * as z from "zod" +import * as z from 'zod'; -import { zMarkdown, zRequestDetailsHeader } from "./shared.js" +import { zMarkdown, zRequestDetailsHeader } from './shared.js'; export const zCancelledOutcome = z .object({ @@ -10,11 +10,11 @@ export const zCancelledOutcome = z }) .strict(), }) - .strict() -export type CancelledOutcome = z.infer + .strict(); +export type CancelledOutcome = z.infer; export const CancelledOutcomeSchema = z.toJSONSchema(zCancelledOutcome, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zUnavailableOutcome = z .object({ @@ -24,17 +24,17 @@ export const zUnavailableOutcome = z }) .strict(), }) - .strict() -export type UnavailableOutcome = z.infer + .strict(); +export type UnavailableOutcome = z.infer; export const UnavailableOutcomeSchema = z.toJSONSchema(zUnavailableOutcome, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); -export const zChoiceKind = z.enum(["listed", "other", "none"]) -export type ChoiceKind = z.infer +export const zChoiceKind = z.enum(['listed', 'other', 'none']); +export type ChoiceKind = z.infer; export const ChoiceKindSchema = z.toJSONSchema(zChoiceKind, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zSelectedChoice = z .object({ @@ -42,11 +42,11 @@ export const zSelectedChoice = z label: z.string().min(1), kind: zChoiceKind, }) - .strict() -export type SelectedChoice = z.infer + .strict(); +export type SelectedChoice = z.infer; export const SelectedChoiceSchema = z.toJSONSchema(zSelectedChoice, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); const zChoiceAnsweredPayload = z .object({ @@ -56,18 +56,18 @@ const zChoiceAnsweredPayload = z .strict() .superRefine((payload, ctx) => { if ( - (payload.choice.kind === "other" || payload.choice.kind === "none") && + (payload.choice.kind === 'other' || payload.choice.kind === 'none') && (!payload.comment || payload.comment.trim().length === 0) ) { ctx.addIssue({ - code: "custom", - path: ["comment"], - message: "other and none choices require comment", - }) + code: 'custom', + path: ['comment'], + message: 'other and none choices require comment', + }); } - }) -export const zRequestChoiceAnswered = zChoiceAnsweredPayload -export type RequestChoiceAnswered = z.infer + }); +export const zRequestChoiceAnswered = zChoiceAnsweredPayload; +export type RequestChoiceAnswered = z.infer; const zChoicesAnsweredPayload = z .object({ @@ -77,29 +77,27 @@ const zChoicesAnsweredPayload = z .strict() .superRefine((payload, ctx) => { if ( - payload.choices.some( - (choice) => choice.kind === "other" || choice.kind === "none", - ) && + payload.choices.some((choice) => choice.kind === 'other' || choice.kind === 'none') && (!payload.comment || payload.comment.trim().length === 0) ) { ctx.addIssue({ - code: "custom", - path: ["comment"], - message: "other and none choices require comment", - }) + code: 'custom', + path: ['comment'], + message: 'other and none choices require comment', + }); } - }) -export const zRequestChoicesAnswered = zChoicesAnsweredPayload -export type RequestChoicesAnswered = z.infer + }); +export const zRequestChoicesAnswered = zChoicesAnsweredPayload; +export type RequestChoicesAnswered = z.infer; export const zRequestAnswerDetails = z.union([ zRequestDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("present_question"), - curr: z.literal("request_answer"), - next: z.literal("capture_answer").optional(), + prev: z.literal('present_question'), + curr: z.literal('request_answer'), + next: z.literal('capture_answer').optional(), }) .strict(), answered: z @@ -113,8 +111,8 @@ export const zRequestAnswerDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_question"), - curr: z.literal("request_answer"), + prev: z.literal('present_question'), + curr: z.literal('request_answer'), }) .strict(), cancelled: zCancelledOutcome.shape.cancelled, @@ -124,19 +122,16 @@ export const zRequestAnswerDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_question"), - curr: z.literal("request_answer"), + prev: z.literal('present_question'), + curr: z.literal('request_answer'), }) .strict(), unavailable: zUnavailableOutcome.shape.unavailable, }) .strict(), -]) -export type RequestAnswerDetails = z.infer -export const RequestAnswerDetailsSchema = z.toJSONSchema( - zRequestAnswerDetails, - { unrepresentable: "throw" }, -) +]); +export type RequestAnswerDetails = z.infer; +export const RequestAnswerDetailsSchema = z.toJSONSchema(zRequestAnswerDetails, { unrepresentable: 'throw' }); export const zRequestChoiceDetails = z.union([ zRequestDetailsHeader @@ -144,16 +139,16 @@ export const zRequestChoiceDetails = z.union([ tool_meta: z.union([ z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choice"), - next: z.literal("capture_choice").optional(), + prev: z.literal('present_options'), + curr: z.literal('request_choice'), + next: z.literal('capture_choice').optional(), }) .strict(), z .object({ - prev: z.literal("present_candidates"), - curr: z.literal("request_choice"), - next: z.literal("capture_candidate").optional(), + prev: z.literal('present_candidates'), + curr: z.literal('request_choice'), + next: z.literal('capture_candidate').optional(), }) .strict(), ]), @@ -165,14 +160,14 @@ export const zRequestChoiceDetails = z.union([ tool_meta: z.union([ z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choice"), + prev: z.literal('present_options'), + curr: z.literal('request_choice'), }) .strict(), z .object({ - prev: z.literal("present_candidates"), - curr: z.literal("request_choice"), + prev: z.literal('present_candidates'), + curr: z.literal('request_choice'), }) .strict(), ]), @@ -184,35 +179,32 @@ export const zRequestChoiceDetails = z.union([ tool_meta: z.union([ z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choice"), + prev: z.literal('present_options'), + curr: z.literal('request_choice'), }) .strict(), z .object({ - prev: z.literal("present_candidates"), - curr: z.literal("request_choice"), + prev: z.literal('present_candidates'), + curr: z.literal('request_choice'), }) .strict(), ]), unavailable: zUnavailableOutcome.shape.unavailable, }) .strict(), -]) -export type RequestChoiceDetails = z.infer -export const RequestChoiceDetailsSchema = z.toJSONSchema( - zRequestChoiceDetails, - { unrepresentable: "throw" }, -) +]); +export type RequestChoiceDetails = z.infer; +export const RequestChoiceDetailsSchema = z.toJSONSchema(zRequestChoiceDetails, { unrepresentable: 'throw' }); export const zRequestChoicesDetails = z.union([ zRequestDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choices"), - next: z.literal("capture_choices").optional(), + prev: z.literal('present_options'), + curr: z.literal('request_choices'), + next: z.literal('capture_choices').optional(), }) .strict(), answered: zRequestChoicesAnswered, @@ -222,8 +214,8 @@ export const zRequestChoicesDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choices"), + prev: z.literal('present_options'), + curr: z.literal('request_choices'), }) .strict(), cancelled: zCancelledOutcome.shape.cancelled, @@ -233,59 +225,58 @@ export const zRequestChoicesDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choices"), + prev: z.literal('present_options'), + curr: z.literal('request_choices'), }) .strict(), unavailable: zUnavailableOutcome.shape.unavailable, }) .strict(), -]) -export type RequestChoicesDetails = z.infer -export const RequestChoicesDetailsSchema = z.toJSONSchema( - zRequestChoicesDetails, - { unrepresentable: "throw" }, -) +]); +export type RequestChoicesDetails = z.infer; +export const RequestChoicesDetailsSchema = z.toJSONSchema(zRequestChoicesDetails, { + unrepresentable: 'throw', +}); -export const zReviewDecision = z.enum(["approve", "request_changes", "reject"]) -export type ReviewDecision = z.infer +export const zReviewDecision = z.enum(['approve', 'request_changes', 'reject']); +export type ReviewDecision = z.infer; export const ReviewDecisionSchema = z.toJSONSchema(zReviewDecision, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); const zReviewAnsweredPayload = z.union([ z .object({ - decision: z.literal("approve"), + decision: z.literal('approve'), comment: zMarkdown.optional(), }) .strict(), z .object({ - decision: z.literal("request_changes"), + decision: z.literal('request_changes'), comment: zMarkdown.refine((value) => value.trim().length > 0, { - message: "request_changes requires comment", + message: 'request_changes requires comment', }), }) .strict(), z .object({ - decision: z.literal("reject"), + decision: z.literal('reject'), comment: zMarkdown.optional(), }) .strict(), -]) -export const zRequestReviewAnswered = zReviewAnsweredPayload -export type RequestReviewAnswered = z.infer +]); +export const zRequestReviewAnswered = zReviewAnsweredPayload; +export type RequestReviewAnswered = z.infer; export const zRequestReviewDetails = z.union([ zRequestDetailsHeader .extend({ tool_meta: z .object({ - prev: z.literal("present_review_set"), - curr: z.literal("request_review"), - next: z.literal("capture_review").optional(), + prev: z.literal('present_review_set'), + curr: z.literal('request_review'), + next: z.literal('capture_review').optional(), }) .strict(), answered: zRequestReviewAnswered, @@ -295,8 +286,8 @@ export const zRequestReviewDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_review_set"), - curr: z.literal("request_review"), + prev: z.literal('present_review_set'), + curr: z.literal('request_review'), }) .strict(), cancelled: zCancelledOutcome.shape.cancelled, @@ -306,27 +297,24 @@ export const zRequestReviewDetails = z.union([ .extend({ tool_meta: z .object({ - prev: z.literal("present_review_set"), - curr: z.literal("request_review"), + prev: z.literal('present_review_set'), + curr: z.literal('request_review'), }) .strict(), unavailable: zUnavailableOutcome.shape.unavailable, }) .strict(), -]) -export type RequestReviewDetails = z.infer -export const RequestReviewDetailsSchema = z.toJSONSchema( - zRequestReviewDetails, - { unrepresentable: "throw" }, -) +]); +export type RequestReviewDetails = z.infer; +export const RequestReviewDetailsSchema = z.toJSONSchema(zRequestReviewDetails, { unrepresentable: 'throw' }); export const zRequestDetails = z.union([ zRequestAnswerDetails, zRequestChoiceDetails, zRequestChoicesDetails, zRequestReviewDetails, -]) -export type RequestDetails = z.infer +]); +export type RequestDetails = z.infer; export const RequestDetailsSchema = z.toJSONSchema(zRequestDetails, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); diff --git a/src/.pi/extensions/structured-exchange/schemas/shared.ts b/src/.pi/extensions/structured-exchange/schemas/shared.ts index cb5f4f1cf..ff8331118 100644 --- a/src/.pi/extensions/structured-exchange/schemas/shared.ts +++ b/src/.pi/extensions/structured-exchange/schemas/shared.ts @@ -1,58 +1,55 @@ -import * as z from "zod" +import * as z from 'zod'; -export const STRUCTURED_EXCHANGE_PRESENT_DETAILS_SCHEMA = - "brunch.structured_exchange.present" as const -export const STRUCTURED_EXCHANGE_REQUEST_DETAILS_SCHEMA = - "brunch.structured_exchange.request" as const -export const STRUCTURED_EXCHANGE_CAPTURE_DETAILS_SCHEMA = - "brunch.structured_exchange.capture" as const -export const STRUCTURED_EXCHANGE_DETAILS_VERSION = 1 as const +export const STRUCTURED_EXCHANGE_PRESENT_DETAILS_SCHEMA = 'brunch.structured_exchange.present' as const; +export const STRUCTURED_EXCHANGE_REQUEST_DETAILS_SCHEMA = 'brunch.structured_exchange.request' as const; +export const STRUCTURED_EXCHANGE_CAPTURE_DETAILS_SCHEMA = 'brunch.structured_exchange.capture' as const; +export const STRUCTURED_EXCHANGE_DETAILS_VERSION = 1 as const; -export const zMarkdown = z.string() -export type Markdown = z.infer +export const zMarkdown = z.string(); +export type Markdown = z.infer; export const MarkdownSchema = z.toJSONSchema(zMarkdown, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); -export const zGraphNodeRef = z.object({ node_id: z.string().min(1) }).strict() -export type GraphNodeRef = z.infer +export const zGraphNodeRef = z.object({ node_id: z.string().min(1) }).strict(); +export type GraphNodeRef = z.infer; export const GraphNodeRefSchema = z.toJSONSchema(zGraphNodeRef, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentToolName = z.enum([ - "present_question", - "present_options", - "present_review_set", - "present_candidates", -]) -export type PresentToolName = z.infer + 'present_question', + 'present_options', + 'present_review_set', + 'present_candidates', +]); +export type PresentToolName = z.infer; export const PresentToolNameSchema = z.toJSONSchema(zPresentToolName, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zRequestToolName = z.enum([ - "request_answer", - "request_choice", - "request_choices", - "request_review", -]) -export type RequestToolName = z.infer + 'request_answer', + 'request_choice', + 'request_choices', + 'request_review', +]); +export type RequestToolName = z.infer; export const RequestToolNameSchema = z.toJSONSchema(zRequestToolName, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zCaptureToolName = z.enum([ - "capture_answer", - "capture_choice", - "capture_choices", - "capture_review", - "capture_candidate", -]) -export type CaptureToolName = z.infer + 'capture_answer', + 'capture_choice', + 'capture_choices', + 'capture_review', + 'capture_candidate', +]); +export type CaptureToolName = z.infer; export const CaptureToolNameSchema = z.toJSONSchema(zCaptureToolName, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zPresentDetailsHeader = z .object({ @@ -60,12 +57,9 @@ export const zPresentDetailsHeader = z v: z.literal(STRUCTURED_EXCHANGE_DETAILS_VERSION), exchange_id: z.string().min(1), }) - .strict() -export type PresentDetailsHeader = z.infer -export const PresentDetailsHeaderSchema = z.toJSONSchema( - zPresentDetailsHeader, - { unrepresentable: "throw" }, -) + .strict(); +export type PresentDetailsHeader = z.infer; +export const PresentDetailsHeaderSchema = z.toJSONSchema(zPresentDetailsHeader, { unrepresentable: 'throw' }); export const zRequestDetailsHeader = z .object({ @@ -73,12 +67,9 @@ export const zRequestDetailsHeader = z v: z.literal(STRUCTURED_EXCHANGE_DETAILS_VERSION), exchange_id: z.string().min(1), }) - .strict() -export type RequestDetailsHeader = z.infer -export const RequestDetailsHeaderSchema = z.toJSONSchema( - zRequestDetailsHeader, - { unrepresentable: "throw" }, -) + .strict(); +export type RequestDetailsHeader = z.infer; +export const RequestDetailsHeaderSchema = z.toJSONSchema(zRequestDetailsHeader, { unrepresentable: 'throw' }); export const zCaptureDetailsHeader = z .object({ @@ -86,119 +77,116 @@ export const zCaptureDetailsHeader = z v: z.literal(STRUCTURED_EXCHANGE_DETAILS_VERSION), exchange_id: z.string().min(1), }) - .strict() -export type CaptureDetailsHeader = z.infer -export const CaptureDetailsHeaderSchema = z.toJSONSchema( - zCaptureDetailsHeader, - { unrepresentable: "throw" }, -) + .strict(); +export type CaptureDetailsHeader = z.infer; +export const CaptureDetailsHeaderSchema = z.toJSONSchema(zCaptureDetailsHeader, { unrepresentable: 'throw' }); -export const zPresentToolMeta = z.discriminatedUnion("curr", [ +export const zPresentToolMeta = z.discriminatedUnion('curr', [ z .object({ - curr: z.literal("present_question"), - next: z.literal("request_answer"), + curr: z.literal('present_question'), + next: z.literal('request_answer'), }) .strict(), z .object({ - curr: z.literal("present_options"), - next: z.enum(["request_choice", "request_choices"]), + curr: z.literal('present_options'), + next: z.enum(['request_choice', 'request_choices']), }) .strict(), z .object({ - curr: z.literal("present_review_set"), - next: z.literal("request_review"), + curr: z.literal('present_review_set'), + next: z.literal('request_review'), }) .strict(), z .object({ - curr: z.literal("present_candidates"), - next: z.literal("request_choice"), + curr: z.literal('present_candidates'), + next: z.literal('request_choice'), }) .strict(), -]) -export type PresentToolMeta = z.infer +]); +export type PresentToolMeta = z.infer; export const PresentToolMetaSchema = z.toJSONSchema(zPresentToolMeta, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zRequestToolMeta = z.union([ z .object({ - prev: z.literal("present_question"), - curr: z.literal("request_answer"), - next: z.literal("capture_answer").optional(), + prev: z.literal('present_question'), + curr: z.literal('request_answer'), + next: z.literal('capture_answer').optional(), }) .strict(), z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choice"), - next: z.literal("capture_choice").optional(), + prev: z.literal('present_options'), + curr: z.literal('request_choice'), + next: z.literal('capture_choice').optional(), }) .strict(), z .object({ - prev: z.literal("present_candidates"), - curr: z.literal("request_choice"), - next: z.literal("capture_candidate").optional(), + prev: z.literal('present_candidates'), + curr: z.literal('request_choice'), + next: z.literal('capture_candidate').optional(), }) .strict(), z .object({ - prev: z.literal("present_options"), - curr: z.literal("request_choices"), - next: z.literal("capture_choices").optional(), + prev: z.literal('present_options'), + curr: z.literal('request_choices'), + next: z.literal('capture_choices').optional(), }) .strict(), z .object({ - prev: z.literal("present_review_set"), - curr: z.literal("request_review"), - next: z.literal("capture_review").optional(), + prev: z.literal('present_review_set'), + curr: z.literal('request_review'), + next: z.literal('capture_review').optional(), }) .strict(), -]) -export type RequestToolMeta = z.infer +]); +export type RequestToolMeta = z.infer; export const RequestToolMetaSchema = z.toJSONSchema(zRequestToolMeta, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); export const zCaptureToolMeta = z.union([ z .object({ - prev: z.literal("request_answer"), - curr: z.literal("capture_answer"), + prev: z.literal('request_answer'), + curr: z.literal('capture_answer'), }) .strict(), z .object({ - prev: z.literal("request_choice"), - curr: z.literal("capture_choice"), + prev: z.literal('request_choice'), + curr: z.literal('capture_choice'), }) .strict(), z .object({ - prev: z.literal("request_choices"), - curr: z.literal("capture_choices"), + prev: z.literal('request_choices'), + curr: z.literal('capture_choices'), }) .strict(), z .object({ - prev: z.literal("request_review"), - curr: z.literal("capture_review"), + prev: z.literal('request_review'), + curr: z.literal('capture_review'), }) .strict(), z .object({ - prev: z.literal("request_choice"), - curr: z.literal("capture_candidate"), + prev: z.literal('request_choice'), + curr: z.literal('capture_candidate'), }) .strict(), -]) -export type CaptureToolMeta = z.infer +]); +export type CaptureToolMeta = z.infer; export const CaptureToolMetaSchema = z.toJSONSchema(zCaptureToolMeta, { - unrepresentable: "throw", -}) + unrepresentable: 'throw', +}); diff --git a/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts index 751cfa6f1..6a19dfcbd 100644 --- a/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts +++ b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts @@ -3,72 +3,70 @@ import { type StructuredExchangeAnswer, type StructuredExchangeMode, type StructuredExchangeOption, -} from "../../../../structured-exchange.js" -import { isRecord } from "./model.js" +} from '../../../../structured-exchange.js'; +import { isRecord } from './model.js'; export interface StructuredExchangeEditorPrefillParams { - question: string - context?: string - mode: Exclude - options: StructuredExchangeOption[] + question: string; + context?: string; + mode: Exclude; + options: StructuredExchangeOption[]; } interface StructuredExchangeEditorResponse { - status: "answered" | "cancelled" - answers: StructuredExchangeAnswer[] - note: string + status: 'answered' | 'cancelled'; + answers: StructuredExchangeAnswer[]; + note: string; } function answerSortRank(answer: StructuredExchangeAnswer): number { switch (answer.type) { - case "option": - return answer.index - case "other": - return Number.MAX_SAFE_INTEGER - 1 - case "text": - return Number.MAX_SAFE_INTEGER + case 'option': + return answer.index; + case 'other': + return Number.MAX_SAFE_INTEGER - 1; + case 'text': + return Number.MAX_SAFE_INTEGER; } } -function sortAnswers( - answers: StructuredExchangeAnswer[], -): StructuredExchangeAnswer[] { - return [...answers].sort((a, b) => answerSortRank(a) - answerSortRank(b)) +function sortAnswers(answers: StructuredExchangeAnswer[]): StructuredExchangeAnswer[] { + return [...answers].sort((a, b) => answerSortRank(a) - answerSortRank(b)); } function parseEditorAnswer(value: unknown): StructuredExchangeAnswer | null { - if (!isRecord(value)) return null + if (!isRecord(value)) return null; - if (value.type === "option") { + if (value.type === 'option') { if ( - typeof value.label !== "string" || - typeof value.value !== "string" || - typeof value.index !== "number" || + typeof value.label !== 'string' || + typeof value.value !== 'string' || + typeof value.index !== 'number' || !Number.isInteger(value.index) || value.index < 1 ) { - return null + return null; } return { - type: "option", + type: 'option', label: value.label, value: value.value, index: value.index, - } + }; } - if (value.type === "other") { - if (typeof value.label !== "string" || typeof value.value !== "string") { - return null + if (value.type === 'other') { + if (typeof value.label !== 'string' || typeof value.value !== 'string') { + return null; } - return { type: "other", label: value.label, value: value.value } + return { type: 'other', label: value.label, value: value.value }; } - return null + return null; } function buildLegacyResult( - status: "answered" | "cancelled" | "unavailable", + status: 'answered' | 'cancelled' | 'unavailable', params: StructuredExchangeEditorPrefillParams, answers: StructuredExchangeAnswer[], note: string, @@ -76,25 +74,22 @@ function buildLegacyResult( ) { const selected = answers .map((answer) => - answer.type === "option" + answer.type === 'option' ? `${answer.index}. ${answer.label}` - : answer.type === "other" + : answer.type === 'other' ? `Other: ${answer.label}` : answer.label, ) - .join("\n") + .join('\n'); const text = - status === "answered" - ? [ - `User selected:${selected ? `\n${selected}` : ""}`, - note ? `Note: ${note}` : undefined, - ] + status === 'answered' + ? [`User selected:${selected ? `\n${selected}` : ''}`, note ? `Note: ${note}` : undefined] .filter(Boolean) - .join("\n") - : (message ?? `User ${status} the question`) + .join('\n') + : (message ?? `User ${status} the question`); return { - content: [{ type: "text" as const, text }], + content: [{ type: 'text' as const, text }], details: { schema: STRUCTURED_EXCHANGE_RESULT_SCHEMA, schemaVersion: 1 as const, @@ -108,23 +103,19 @@ function buildLegacyResult( (option) => !answers.some( (answer) => - answer.type === "option" && - answer.label === option.label && - answer.value === option.value, + answer.type === 'option' && answer.label === option.label && answer.value === option.value, ), ), note, - transport: { surface: "rpc-editor" as const }, + transport: { surface: 'rpc-editor' as const }, ...(message !== undefined ? { message } : {}), }, - } + }; } -export function buildStructuredExchangeEditorPrefill( - params: StructuredExchangeEditorPrefillParams, -): string { +export function buildStructuredExchangeEditorPrefill(params: StructuredExchangeEditorPrefillParams): string { const payload: Record = { - schema: "brunch.structured_exchange.editor", + schema: 'brunch.structured_exchange.editor', schemaVersion: 1, question: params.question, mode: params.mode, @@ -135,69 +126,63 @@ export function buildStructuredExchangeEditorPrefill( ...(option.description ? { description: option.description } : {}), })), instructions: [ - "Edit only response.", + 'Edit only response.', 'For a selected listed option, add an answer like {"type":"option","label":"Alpha","value":"alpha","index":1}.', 'For Other, add an answer like {"type":"other","label":"Custom answer","value":"Custom answer"}.', 'Set response.note to a string. Use "" when there is no additional note.', ], - response: { status: "cancelled", answers: [], note: "" }, - } - if (params.context !== undefined) payload.context = params.context - return JSON.stringify(payload, null, 2) + response: { status: 'cancelled', answers: [], note: '' }, + }; + if (params.context !== undefined) payload.context = params.context; + return JSON.stringify(payload, null, 2); } export function parseStructuredExchangeEditorResponse( value: string, ): StructuredExchangeEditorResponse | null { - let parsed: unknown + let parsed: unknown; try { - parsed = JSON.parse(value) + parsed = JSON.parse(value); } catch { - return null + return null; } - if (!isRecord(parsed)) return null - const response = parsed.response - if (!isRecord(response)) return null + if (!isRecord(parsed)) return null; + const response = parsed.response; + if (!isRecord(response)) return null; - if (response.status === "cancelled") { - return { status: "cancelled", answers: [], note: "" } + if (response.status === 'cancelled') { + return { status: 'cancelled', answers: [], note: '' }; } - if (response.status !== "answered") return null - if (!Array.isArray(response.answers)) return null - if (typeof response.note !== "string") return null + if (response.status !== 'answered') return null; + if (!Array.isArray(response.answers)) return null; + if (typeof response.note !== 'string') return null; - const answers = response.answers.map(parseEditorAnswer) - if (answers.some((answer) => answer === null)) return null + const answers = response.answers.map(parseEditorAnswer); + if (answers.some((answer) => answer === null)) return null; return { - status: "answered", + status: 'answered', answers: sortAnswers(answers as StructuredExchangeAnswer[]), note: response.note.trim(), - } + }; } export function structuredExchangeResultFromEditor( params: StructuredExchangeEditorPrefillParams, edited: string | undefined, ) { - const response = parseStructuredExchangeEditorResponse(edited ?? "") - if (edited === undefined || response?.status === "cancelled") { - return buildLegacyResult( - "cancelled", - params, - [], - "", - "User cancelled the question", - ) + const response = parseStructuredExchangeEditorResponse(edited ?? ''); + if (edited === undefined || response?.status === 'cancelled') { + return buildLegacyResult('cancelled', params, [], '', 'User cancelled the question'); } if (!response) { return buildLegacyResult( - "unavailable", + 'unavailable', params, [], - "", - "structured_exchange editor fallback returned invalid JSON", - ) + '', + 'structured_exchange editor fallback returned invalid JSON', + ); } - return buildLegacyResult("answered", params, response.answers, response.note) + return buildLegacyResult('answered', params, response.answers, response.note); } diff --git a/src/.pi/extensions/structured-exchange/shared/markdown.ts b/src/.pi/extensions/structured-exchange/shared/markdown.ts index 99b508ff8..00d050030 100644 --- a/src/.pi/extensions/structured-exchange/shared/markdown.ts +++ b/src/.pi/extensions/structured-exchange/shared/markdown.ts @@ -1,76 +1,63 @@ -import { Markdown, Text, type MarkdownTheme } from "@earendil-works/pi-tui" +import { Markdown, Text, type MarkdownTheme } from '@earendil-works/pi-tui'; interface ThemeLike { - fg?: (color: never, text: string) => string - bold?: (text: string) => string - italic?: (text: string) => string - underline?: (text: string) => string - strikethrough?: (text: string) => string + fg?: (color: never, text: string) => string; + bold?: (text: string) => string; + italic?: (text: string) => string; + underline?: (text: string) => string; + strikethrough?: (text: string) => string; } interface ToolTextContentLike { - type?: string - text?: string + type?: string; + text?: string; } interface ToolResultLike { - content?: ToolTextContentLike[] + content?: ToolTextContentLike[]; } export function textFromToolContent(result: ToolResultLike): string { - const first = result.content?.[0] - return first?.type === "text" && typeof first.text === "string" - ? first.text - : "" + const first = result.content?.[0]; + return first?.type === 'text' && typeof first.text === 'string' ? first.text : ''; } -export function createStructuredExchangeMarkdownTheme( - theme?: ThemeLike, -): MarkdownTheme { - const color = (name: string) => (text: string) => - theme?.fg ? theme.fg(name as never, text) : text - const identity = (text: string) => text +export function createStructuredExchangeMarkdownTheme(theme?: ThemeLike): MarkdownTheme { + const color = (name: string) => (text: string) => (theme?.fg ? theme.fg(name as never, text) : text); + const identity = (text: string) => text; return { - heading: color("mdHeading"), - link: color("mdLink"), - linkUrl: color("mdLinkUrl"), - code: color("mdCode"), - codeBlock: color("mdCodeBlock"), - codeBlockBorder: color("mdCodeBlockBorder"), - quote: color("mdQuote"), - quoteBorder: color("mdQuoteBorder"), - hr: color("mdHr"), - listBullet: color("mdListBullet"), + heading: color('mdHeading'), + link: color('mdLink'), + linkUrl: color('mdLinkUrl'), + code: color('mdCode'), + codeBlock: color('mdCodeBlock'), + codeBlockBorder: color('mdCodeBlockBorder'), + quote: color('mdQuote'), + quoteBorder: color('mdQuoteBorder'), + hr: color('mdHr'), + listBullet: color('mdListBullet'), bold: theme?.bold ?? identity, italic: theme?.italic ?? identity, underline: theme?.underline ?? identity, strikethrough: theme?.strikethrough ?? identity, - highlightCode: (code: string) => code.split("\n").map(color("mdCodeBlock")), - } + highlightCode: (code: string) => code.split('\n').map(color('mdCodeBlock')), + }; } -export function renderMarkdownResult( - result: ToolResultLike, - theme?: ThemeLike, -) { - return new Markdown( - textFromToolContent(result), - 0, - 0, - createStructuredExchangeMarkdownTheme(theme), - ) +export function renderMarkdownResult(result: ToolResultLike, theme?: ThemeLike) { + return new Markdown(textFromToolContent(result), 0, 0, createStructuredExchangeMarkdownTheme(theme)); } export function renderPlainResult(result: ToolResultLike) { - return new Text(textFromToolContent(result), 0, 0) + return new Text(textFromToolContent(result), 0, 0); } export function markdownEscape(text: string): string { - return text.replace(/([\\`*_{}\[\]()#+\-.!|>])/g, "\\$1") + return text.replace(/([\\`*_{}\[\]()#+\-.!|>])/g, '\\$1'); } export function normalizeOptionalText(value: unknown): string | undefined { - if (typeof value !== "string") return undefined - const trimmed = value.trim() - return trimmed.length > 0 ? trimmed : undefined + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; } diff --git a/src/.pi/extensions/structured-exchange/shared/model.ts b/src/.pi/extensions/structured-exchange/shared/model.ts index 4bfcc8e05..bd9850f39 100644 --- a/src/.pi/extensions/structured-exchange/shared/model.ts +++ b/src/.pi/extensions/structured-exchange/shared/model.ts @@ -1,63 +1,65 @@ -export const STRUCTURED_EXCHANGE_PRESENT_SCHEMA = - "brunch.structured_exchange.present" as const -export const STRUCTURED_EXCHANGE_REQUEST_SCHEMA = - "brunch.structured_exchange.request" as const +export const STRUCTURED_EXCHANGE_PRESENT_SCHEMA = 'brunch.structured_exchange.present' as const; +export const STRUCTURED_EXCHANGE_REQUEST_SCHEMA = 'brunch.structured_exchange.request' as const; -export type PresentToolName = "present_question" | "present_options" | "present_review_set" | "present_candidates" -export type RequestToolName = "request_answer" | "request_choice" | "request_choices" | "request_review" +export type PresentToolName = + | 'present_question' + | 'present_options' + | 'present_review_set' + | 'present_candidates'; +export type RequestToolName = 'request_answer' | 'request_choice' | 'request_choices' | 'request_review'; -export type StructuredExchangePresentKind = "question" | "options" | "review_set" | "candidates" +export type StructuredExchangePresentKind = 'question' | 'options' | 'review_set' | 'candidates'; export interface StructuredExchangeExpectedRequest { - tool: RequestToolName - required: boolean + tool: RequestToolName; + required: boolean; } export interface StructuredExchangePresentDetails { - schema: typeof STRUCTURED_EXCHANGE_PRESENT_SCHEMA - schemaVersion: 1 - exchangeId: string - presentTool: PresentToolName - kind: StructuredExchangePresentKind - status: "presented" - expectedRequest?: StructuredExchangeExpectedRequest - createdAtToolCallId: string + schema: typeof STRUCTURED_EXCHANGE_PRESENT_SCHEMA; + schemaVersion: 1; + exchangeId: string; + presentTool: PresentToolName; + kind: StructuredExchangePresentKind; + status: 'presented'; + expectedRequest?: StructuredExchangeExpectedRequest; + createdAtToolCallId: string; } export interface StructuredExchangeChoice { - id: string - label: string + id: string; + label: string; } export interface StructuredExchangeRequestDetails { - schema: typeof STRUCTURED_EXCHANGE_REQUEST_SCHEMA - schemaVersion: 1 - exchangeId: string - requestTool: RequestToolName - status: "answered" | "cancelled" | "unavailable" + schema: typeof STRUCTURED_EXCHANGE_REQUEST_SCHEMA; + schemaVersion: 1; + exchangeId: string; + requestTool: RequestToolName; + status: 'answered' | 'cancelled' | 'unavailable'; respondsTo: { - exchangeId: string - presentTool: PresentToolName - } - choice?: StructuredExchangeChoice - choices?: StructuredExchangeChoice[] - answer?: string - review?: "approve" | "change-request" | "reject" - comment?: string - message?: string - createdAtToolCallId: string + exchangeId: string; + presentTool: PresentToolName; + }; + choice?: StructuredExchangeChoice; + choices?: StructuredExchangeChoice[]; + answer?: string; + review?: 'approve' | 'change-request' | 'reject'; + comment?: string; + message?: string; + createdAtToolCallId: string; } export interface ToolTextContent { - type: "text" - text: string + type: 'text'; + text: string; } export interface ToolTextResult { - content: ToolTextContent[] - details: TDetails + content: ToolTextContent[]; + details: TDetails; } export function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === 'object' && value !== null; } diff --git a/src/.pi/extensions/structured-exchange/shared/recovery.ts b/src/.pi/extensions/structured-exchange/shared/recovery.ts index 04c7992cd..b1c3a9606 100644 --- a/src/.pi/extensions/structured-exchange/shared/recovery.ts +++ b/src/.pi/extensions/structured-exchange/shared/recovery.ts @@ -6,124 +6,110 @@ import { type StructuredExchangePresentDetails, type StructuredExchangeRequestDetails, isRecord, -} from "./model.js" +} from './model.js'; const PRESENT_TOOLS: readonly PresentToolName[] = [ - "present_question", - "present_options", - "present_review_set", - "present_candidates", -] + 'present_question', + 'present_options', + 'present_review_set', + 'present_candidates', +]; const REQUEST_TOOLS: readonly RequestToolName[] = [ - "request_answer", - "request_choice", - "request_choices", - "request_review", -] + 'request_answer', + 'request_choice', + 'request_choices', + 'request_review', +]; function isPresentToolName(value: unknown): value is PresentToolName { - return ( - typeof value === "string" && - PRESENT_TOOLS.includes(value as PresentToolName) - ) + return typeof value === 'string' && PRESENT_TOOLS.includes(value as PresentToolName); } function isRequestToolName(value: unknown): value is RequestToolName { - return ( - typeof value === "string" && - REQUEST_TOOLS.includes(value as RequestToolName) - ) + return typeof value === 'string' && REQUEST_TOOLS.includes(value as RequestToolName); } export function isStructuredExchangePresentDetails( value: unknown, ): value is StructuredExchangePresentDetails { - if (!isRecord(value)) return false - if (value.schema !== STRUCTURED_EXCHANGE_PRESENT_SCHEMA) return false - if (value.schemaVersion !== 1) return false - if (typeof value.exchangeId !== "string" || value.exchangeId.length === 0) { - return false + if (!isRecord(value)) return false; + if (value.schema !== STRUCTURED_EXCHANGE_PRESENT_SCHEMA) return false; + if (value.schemaVersion !== 1) return false; + if (typeof value.exchangeId !== 'string' || value.exchangeId.length === 0) { + return false; } - if (!isPresentToolName(value.presentTool)) return false + if (!isPresentToolName(value.presentTool)) return false; if ( - value.kind !== "question" && - value.kind !== "options" && - value.kind !== "review_set" && - value.kind !== "candidates" + value.kind !== 'question' && + value.kind !== 'options' && + value.kind !== 'review_set' && + value.kind !== 'candidates' ) { - return false + return false; } - if (value.status !== "presented") return false - if (typeof value.createdAtToolCallId !== "string") return false + if (value.status !== 'presented') return false; + if (typeof value.createdAtToolCallId !== 'string') return false; if (value.expectedRequest !== undefined) { - if (!isRecord(value.expectedRequest)) return false - if (!isRequestToolName(value.expectedRequest.tool)) return false - if (typeof value.expectedRequest.required !== "boolean") return false + if (!isRecord(value.expectedRequest)) return false; + if (!isRequestToolName(value.expectedRequest.tool)) return false; + if (typeof value.expectedRequest.required !== 'boolean') return false; } - return true + return true; } export function isStructuredExchangeRequestDetails( value: unknown, ): value is StructuredExchangeRequestDetails { - if (!isRecord(value)) return false - if (value.schema !== STRUCTURED_EXCHANGE_REQUEST_SCHEMA) return false - if (value.schemaVersion !== 1) return false - if (typeof value.exchangeId !== "string" || value.exchangeId.length === 0) { - return false + if (!isRecord(value)) return false; + if (value.schema !== STRUCTURED_EXCHANGE_REQUEST_SCHEMA) return false; + if (value.schemaVersion !== 1) return false; + if (typeof value.exchangeId !== 'string' || value.exchangeId.length === 0) { + return false; } - if (!isRequestToolName(value.requestTool)) return false - if ( - value.status !== "answered" && - value.status !== "cancelled" && - value.status !== "unavailable" - ) { - return false + if (!isRequestToolName(value.requestTool)) return false; + if (value.status !== 'answered' && value.status !== 'cancelled' && value.status !== 'unavailable') { + return false; } - if (!isRecord(value.respondsTo)) return false - if (value.respondsTo.exchangeId !== value.exchangeId) return false - if (!isPresentToolName(value.respondsTo.presentTool)) return false - if (typeof value.createdAtToolCallId !== "string") return false - return true + if (!isRecord(value.respondsTo)) return false; + if (value.respondsTo.exchangeId !== value.exchangeId) return false; + if (!isPresentToolName(value.respondsTo.presentTool)) return false; + if (typeof value.createdAtToolCallId !== 'string') return false; + return true; } interface EntryLike { - type?: unknown + type?: unknown; message?: { - role?: unknown - details?: unknown - } + role?: unknown; + details?: unknown; + }; } function toolResultDetails(entry: EntryLike): unknown { - return entry.type === "message" && entry.message?.role === "toolResult" - ? entry.message.details - : undefined + return entry.type === 'message' && entry.message?.role === 'toolResult' ? entry.message.details : undefined; } export interface IncompleteStructuredExchangePresent { - entry: EntryLike - details: StructuredExchangePresentDetails + entry: EntryLike; + details: StructuredExchangePresentDetails; } export function findIncompleteStructuredExchangePresents( entries: readonly EntryLike[], ): IncompleteStructuredExchangePresent[] { - const presents = new Map() - const completed = new Set() + const presents = new Map(); + const completed = new Set(); for (const entry of entries) { - const details = toolResultDetails(entry) + const details = toolResultDetails(entry); if (isStructuredExchangePresentDetails(details)) { if (details.expectedRequest?.required !== false) { - presents.set(details.exchangeId, { entry, details }) + presents.set(details.exchangeId, { entry, details }); } } else if (isStructuredExchangeRequestDetails(details)) { - completed.add(details.exchangeId) + completed.add(details.exchangeId); } } - return [...presents.values()].filter( - (present) => !completed.has(present.details.exchangeId), - ) + return [...presents.values()].filter((present) => !completed.has(present.details.exchangeId)); } diff --git a/src/.pi/extensions/workspace-dialog.ts b/src/.pi/extensions/workspace-dialog.ts index dfda09c8a..c57eef9f9 100644 --- a/src/.pi/extensions/workspace-dialog.ts +++ b/src/.pi/extensions/workspace-dialog.ts @@ -1,24 +1,21 @@ -import type { - ExtensionAPI, - ExtensionCommandContext, -} from "@earendil-works/pi-coding-agent" +import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent'; import { type WorkspaceSessionReadyState, type SpecSessionActivationCoordinator, type SpecSessionActivationDecision, -} from "../../workspace-session-coordinator.js" +} from '../../workspace-session-coordinator.js'; import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, -} from "../components/workspace-dialog/index.js" -import { chromeStateForWorkspace, renderBrunchChrome } from "./chrome.js" +} from '../components/workspace-dialog/index.js'; +import { chromeStateForWorkspace, renderBrunchChrome } from './chrome.js'; -export const BRUNCH_WORKSPACE_COMMAND = "brunch" -export const BRUNCH_WORKSPACE_SHORTCUT = "ctrl+shift+b" +export const BRUNCH_WORKSPACE_COMMAND = 'brunch'; +export const BRUNCH_WORKSPACE_SHORTCUT = 'ctrl+shift+b'; export interface BrunchSpecSessionPickerOptions { - coordinator: SpecSessionActivationCoordinator + coordinator: SpecSessionActivationCoordinator; } export function registerBrunchWorkspaceDialog( @@ -26,39 +23,39 @@ export function registerBrunchWorkspaceDialog( { coordinator }: BrunchSpecSessionPickerOptions, ): void { pi.registerCommand(BRUNCH_WORKSPACE_COMMAND, { - description: "Open the Brunch spec/session picker", + description: 'Open the Brunch spec/session picker', handler: async (_args, ctx) => { - await runBrunchWorkspaceCommand(ctx, coordinator) + await runBrunchWorkspaceCommand(ctx, coordinator); }, - }) + }); pi.registerShortcut?.(BRUNCH_WORKSPACE_SHORTCUT, { - description: "Open the Brunch spec/session picker", + description: 'Open the Brunch spec/session picker', handler: async (ctx) => { ctx.ui.notify( - "Use /brunch to switch specs or sessions; Pi shortcut contexts cannot switch sessions yet.", - "warning", - ) + 'Use /brunch to switch specs or sessions; Pi shortcut contexts cannot switch sessions yet.', + 'warning', + ); }, - }) + }); } export default function brunchWorkspaceDialog(pi: ExtensionAPI): void { pi.registerCommand(BRUNCH_WORKSPACE_COMMAND, { - description: "Open the Brunch spec/session picker", + description: 'Open the Brunch spec/session picker', handler: async (_args, ctx) => { ctx.ui.notify( - "The Brunch workspace picker needs a product coordinator and is only available through the Brunch CLI.", - "warning", - ) + 'The Brunch workspace picker needs a product coordinator and is only available through the Brunch CLI.', + 'warning', + ); }, - }) + }); } export async function runBrunchWorkspaceCommand( ctx: ExtensionCommandContext, coordinator: SpecSessionActivationCoordinator, ): Promise { - await runBrunchWorkspaceAction(ctx, coordinator) + await runBrunchWorkspaceAction(ctx, coordinator); } export async function runBrunchWorkspaceAction( @@ -67,9 +64,9 @@ export async function runBrunchWorkspaceAction( options: { waitForIdle?: boolean } = {}, ): Promise { if (options.waitForIdle !== false && canWaitForIdle(ctx)) { - await ctx.waitForIdle() + await ctx.waitForIdle(); } - const inventory = await coordinator.inspectWorkspace() + const inventory = await coordinator.inspectWorkspace(); const decision = await ctx.ui.custom( (_tui, theme, _keybindings, done) => createWorkspaceDialogComponent({ @@ -81,66 +78,66 @@ export async function runBrunchWorkspaceAction( { overlay: true, overlayOptions: { - anchor: "center", + anchor: 'center', width: WORKSPACE_DIALOG_WIDTH, - maxHeight: "90%", + maxHeight: '90%', margin: 1, }, }, - ) - const activated = await coordinator.activateWorkspace(decision) + ); + const activated = await coordinator.activateWorkspace(decision); - if (activated.status === "cancelled") { - ctx.ui.notify("Spec/session switch cancelled.", "info") - return + if (activated.status === 'cancelled') { + ctx.ui.notify('Spec/session switch cancelled.', 'info'); + return; } - if (activated.status === "needs_human") { - ctx.ui.notify(activated.reason, "warning") - return + if (activated.status === 'needs_human') { + ctx.ui.notify(activated.reason, 'warning'); + return; } - await switchToActivatedWorkspace(ctx, activated) + await switchToActivatedWorkspace(ctx, activated); } function canWaitForIdle( ctx: ExtensionCommandContext, ): ctx is ExtensionCommandContext & { waitForIdle: () => Promise } { - return typeof ctx.waitForIdle === "function" + return typeof ctx.waitForIdle === 'function'; } async function switchToActivatedWorkspace( ctx: ExtensionCommandContext, activated: WorkspaceSessionReadyState, ): Promise { - if (typeof ctx.switchSession !== "function") { + if (typeof ctx.switchSession !== 'function') { ctx.ui.notify( - "Use /brunch to switch specs or sessions; this Pi context cannot switch sessions.", - "warning", - ) - return + 'Use /brunch to switch specs or sessions; this Pi context cannot switch sessions.', + 'warning', + ); + return; } - const targetFile = activated.session.file + const targetFile = activated.session.file; if (ctx.sessionManager.getSessionFile() === targetFile) { - renderBrunchChrome(ctx.ui, chromeStateForWorkspace(activated)) - ctx.ui.notify("Already using the selected Brunch spec/session.", "info") - return + renderBrunchChrome(ctx.ui, chromeStateForWorkspace(activated)); + ctx.ui.notify('Already using the selected Brunch spec/session.', 'info'); + return; } - const targetSessionId = activated.session.id - const targetSpecTitle = activated.spec.title - const targetChrome = chromeStateForWorkspace(activated) + const targetSessionId = activated.session.id; + const targetSpecTitle = activated.spec.title; + const targetChrome = chromeStateForWorkspace(activated); const result = await ctx.switchSession(targetFile, { withSession: async (replacementCtx) => { - renderBrunchChrome(replacementCtx.ui, targetChrome) + renderBrunchChrome(replacementCtx.ui, targetChrome); replacementCtx.ui.notify( `Switched Brunch spec/session to ${targetSpecTitle} (${targetSessionId}).`, - "info", - ) + 'info', + ); }, - }) + }); if (result.cancelled) { - ctx.ui.notify("Spec/session switch was cancelled by Pi.", "warning") + ctx.ui.notify('Spec/session switch was cancelled by Pi.', 'warning'); } } diff --git a/src/.pi/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts index cc091bfa2..4f410e0b8 100644 --- a/src/.pi/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -1,36 +1,30 @@ -import { - type ExtensionAPI, - type ExtensionFactory, -} from "@earendil-works/pi-coding-agent" +import { type ExtensionAPI, type ExtensionFactory } from '@earendil-works/pi-coding-agent'; -import { registerBrunchAlternatives } from "./extensions/alternatives.js" -import { registerBrunchChrome } from "./extensions/chrome.js" -import { registerBrunchBranchPolicyHandlers } from "./extensions/command-policy.js" -import { type GraphMentionSource } from "./extensions/mention-autocomplete.js" +import { registerBrunchAlternatives } from './extensions/alternatives.js'; +import { registerBrunchChrome } from './extensions/chrome.js'; +import { type BrunchChromeState } from './extensions/chrome.js'; +import { registerBrunchBranchPolicyHandlers } from './extensions/command-policy.js'; +import { registerBrunchGraph, type BrunchGraphDeps } from './extensions/graph/index.js'; +import { type GraphMentionSource } from './extensions/mention-autocomplete.js'; import { FIXTURE_GRAPH_MENTION_SOURCE, registerBrunchMentionAutocomplete, -} from "./extensions/mention-autocomplete.js" -import { registerBrunchOperationalModePolicy } from "./extensions/operational-mode.js" -import { registerBrunchPrompting } from "./extensions/prompting.js" -import { registerBrunchSessionBoundary } from "./extensions/session-lifecycle.js" -import { registerStructuredExchange } from "./extensions/structured-exchange/index.js" -import { type BrunchChromeState } from "./extensions/chrome.js" -import { type BrunchSessionBoundaryHandler } from "./extensions/session-lifecycle.js" -import { type BrunchSpecSessionPickerOptions } from "./extensions/workspace-dialog.js" -import { registerBrunchWorkspaceDialog } from "./extensions/workspace-dialog.js" -import { - registerBrunchGraph, - type BrunchGraphDeps, -} from "./extensions/graph/index.js" +} from './extensions/mention-autocomplete.js'; +import { registerBrunchOperationalModePolicy } from './extensions/operational-mode.js'; +import { registerBrunchPrompting } from './extensions/prompting.js'; +import { registerBrunchSessionBoundary } from './extensions/session-lifecycle.js'; +import { type BrunchSessionBoundaryHandler } from './extensions/session-lifecycle.js'; +import { registerStructuredExchange } from './extensions/structured-exchange/index.js'; +import { type BrunchSpecSessionPickerOptions } from './extensions/workspace-dialog.js'; +import { registerBrunchWorkspaceDialog } from './extensions/workspace-dialog.js'; -export { registerBrunchAlternatives } from "./extensions/alternatives.js" -export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from "./extensions/command-policy.js" +export { registerBrunchAlternatives } from './extensions/alternatives.js'; +export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE } from './extensions/command-policy.js'; export { registerBrunchMentionAutocomplete, type GraphMentionCandidate, type GraphMentionSource, -} from "./extensions/mention-autocomplete.js" +} from './extensions/mention-autocomplete.js'; export { BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, DEFAULT_BRUNCH_AGENT_STATE, @@ -49,8 +43,8 @@ export { type OperationalModeDefinition, type OperationalModeId, type ResolvedBrunchAgentState, -} from "./extensions/operational-mode.js" -export { registerBrunchPrompting } from "./extensions/prompting.js" +} from './extensions/operational-mode.js'; +export { registerBrunchPrompting } from './extensions/prompting.js'; export { chromeStateForWorkspace, projectBrunchChromeFooterLines, @@ -62,13 +56,13 @@ export { type BrunchChromeState, type BrunchChromeUi, type BrunchChromeWorkerStatus, -} from "./extensions/chrome.js" +} from './extensions/chrome.js'; export { bindBrunchSessionBoundary, registerBrunchSessionBoundary, registerBrunchSessionBoundaryRefreshHandlers, type BrunchSessionBoundaryHandler, -} from "./extensions/session-lifecycle.js" +} from './extensions/session-lifecycle.js'; export { BRUNCH_WORKSPACE_COMMAND, BRUNCH_WORKSPACE_SHORTCUT, @@ -76,23 +70,20 @@ export { runBrunchWorkspaceAction, runBrunchWorkspaceCommand, type BrunchSpecSessionPickerOptions, -} from "./extensions/workspace-dialog.js" +} from './extensions/workspace-dialog.js'; export { registerBrunchGraph, type BrunchGraphDeps, type GraphSnapshotReaders, -} from "./extensions/graph/index.js" +} from './extensions/graph/index.js'; -export interface BrunchPiExtensionShellOptions - extends BrunchSpecSessionPickerOptions { - graphMentionSource?: GraphMentionSource - graph?: BrunchGraphDeps +export interface BrunchPiExtensionShellOptions extends BrunchSpecSessionPickerOptions { + graphMentionSource?: GraphMentionSource; + graph?: BrunchGraphDeps; } -type BrunchProductExtensionRegistrar = ( - pi: ExtensionAPI, -) => void | Promise +type BrunchProductExtensionRegistrar = (pi: ExtensionAPI) => void | Promise; export function createBrunchPiExtensionShell( chrome: BrunchChromeState, @@ -100,8 +91,7 @@ export function createBrunchPiExtensionShell( options: BrunchPiExtensionShellOptions, ): ExtensionFactory { return async (pi) => { - const graphMentionSource = - options.graphMentionSource ?? FIXTURE_GRAPH_MENTION_SOURCE + const graphMentionSource = options.graphMentionSource ?? FIXTURE_GRAPH_MENTION_SOURCE; const extensions: readonly BrunchProductExtensionRegistrar[] = [ (api) => registerBrunchSessionBoundary(api, onSessionBoundary), (api) => registerBrunchChrome(api, chrome), @@ -112,13 +102,11 @@ export function createBrunchPiExtensionShell( registerBrunchAlternatives, registerStructuredExchange, (api) => registerBrunchWorkspaceDialog(api, options), - ...(options.graph - ? [(api: ExtensionAPI) => registerBrunchGraph(api, options.graph!)] - : []), - ] + ...(options.graph ? [(api: ExtensionAPI) => registerBrunchGraph(api, options.graph!)] : []), + ]; for (const registerExtension of extensions) { - await registerExtension(pi) + await registerExtension(pi); } - } + }; } diff --git a/src/.pi/settings.json b/src/.pi/settings.json index ce6cc161e..29bbb4911 100644 --- a/src/.pi/settings.json +++ b/src/.pi/settings.json @@ -1,6 +1,3 @@ { - "extensions": [ - "-extensions/operational-mode.ts", - "-extensions/command-policy.ts" - ] -} \ No newline at end of file + "extensions": ["-extensions/operational-mode.ts", "-extensions/command-policy.ts"] +} diff --git a/src/brunch-pi-profile.ts b/src/brunch-pi-profile.ts index 831f66951..ab5a01aa1 100644 --- a/src/brunch-pi-profile.ts +++ b/src/brunch-pi-profile.ts @@ -1,9 +1,6 @@ -import process from "node:process" +import process from 'node:process'; -import { - SettingsManager, - type ExtensionFactory, -} from "@earendil-works/pi-coding-agent" +import { SettingsManager, type ExtensionFactory } from '@earendil-works/pi-coding-agent'; export const BRUNCH_SETTINGS_POLICY = { quietStartup: true, @@ -13,7 +10,7 @@ export const BRUNCH_SETTINGS_POLICY = { prompts: [], themes: [], enableSkillCommands: false, - doubleEscapeAction: "none", + doubleEscapeAction: 'none', compaction: { enabled: true, reserveTokens: 16384, @@ -41,87 +38,87 @@ export const BRUNCH_SETTINGS_POLICY = { autoResize: true, blockImages: false, }, - transport: "auto", + transport: 'auto', collapseChangelog: false, enableInstallTelemetry: false, showHardwareCursor: false, editorPaddingX: 0, autocompleteMaxVisible: 5, markdown: { - codeBlockIndent: " ", + codeBlockIndent: ' ', }, warnings: {}, -} satisfies Parameters[0] +} satisfies Parameters[0]; export const BRUNCH_SETTINGS_AUDITED_GETTERS = [ - "getGlobalSettings", - "getProjectSettings", - "getLastChangelogVersion", - "getSessionDir", - "getDefaultProvider", - "getDefaultModel", - "getSteeringMode", - "getFollowUpMode", - "getTheme", - "getDefaultThinkingLevel", - "getTransport", - "getCompactionEnabled", - "getCompactionReserveTokens", - "getCompactionKeepRecentTokens", - "getCompactionSettings", - "getBranchSummarySettings", - "getBranchSummarySkipPrompt", - "getRetryEnabled", - "getRetrySettings", - "getProviderRetrySettings", - "getHideThinkingBlock", - "getShellPath", - "getQuietStartup", - "getShellCommandPrefix", - "getNpmCommand", - "getCollapseChangelog", - "getEnableInstallTelemetry", - "getPackages", - "getExtensionPaths", - "getSkillPaths", - "getPromptTemplatePaths", - "getThemePaths", - "getEnableSkillCommands", - "getThinkingBudgets", - "getShowImages", - "getImageWidthCells", - "getClearOnShrink", - "getShowTerminalProgress", - "getImageAutoResize", - "getBlockImages", - "getEnabledModels", - "getDoubleEscapeAction", - "getTreeFilterMode", - "getShowHardwareCursor", - "getEditorPaddingX", - "getAutocompleteMaxVisible", - "getCodeBlockIndent", - "getWarnings", -] as const + 'getGlobalSettings', + 'getProjectSettings', + 'getLastChangelogVersion', + 'getSessionDir', + 'getDefaultProvider', + 'getDefaultModel', + 'getSteeringMode', + 'getFollowUpMode', + 'getTheme', + 'getDefaultThinkingLevel', + 'getTransport', + 'getCompactionEnabled', + 'getCompactionReserveTokens', + 'getCompactionKeepRecentTokens', + 'getCompactionSettings', + 'getBranchSummarySettings', + 'getBranchSummarySkipPrompt', + 'getRetryEnabled', + 'getRetrySettings', + 'getProviderRetrySettings', + 'getHideThinkingBlock', + 'getShellPath', + 'getQuietStartup', + 'getShellCommandPrefix', + 'getNpmCommand', + 'getCollapseChangelog', + 'getEnableInstallTelemetry', + 'getPackages', + 'getExtensionPaths', + 'getSkillPaths', + 'getPromptTemplatePaths', + 'getThemePaths', + 'getEnableSkillCommands', + 'getThinkingBudgets', + 'getShowImages', + 'getImageWidthCells', + 'getClearOnShrink', + 'getShowTerminalProgress', + 'getImageAutoResize', + 'getBlockImages', + 'getEnabledModels', + 'getDoubleEscapeAction', + 'getTreeFilterMode', + 'getShowHardwareCursor', + 'getEditorPaddingX', + 'getAutocompleteMaxVisible', + 'getCodeBlockIndent', + 'getWarnings', +] as const; export interface BrunchPiProfileOptions { - cwd: string - agentDir: string - extensionFactories: ExtensionFactory[] + cwd: string; + agentDir: string; + extensionFactories: ExtensionFactory[]; } export interface BrunchPiProfile { - settingsManager: SettingsManager - resourceLoaderOptions: BrunchResourceLoaderOptions + settingsManager: SettingsManager; + resourceLoaderOptions: BrunchResourceLoaderOptions; } export interface BrunchResourceLoaderOptions { - noContextFiles: true - noExtensions: true - noPromptTemplates: true - noSkills: true - noThemes: true - extensionFactories: ExtensionFactory[] + noContextFiles: true; + noExtensions: true; + noPromptTemplates: true; + noSkills: true; + noThemes: true; + extensionFactories: ExtensionFactory[]; } export function createBrunchPiProfile({ @@ -132,7 +129,7 @@ export function createBrunchPiProfile({ return { settingsManager: createBrunchSettingsManager(cwd, agentDir), resourceLoaderOptions: brunchResourceLoaderOptions(extensionFactories), - } + }; } export function brunchResourceLoaderOptions( @@ -145,18 +142,13 @@ export function brunchResourceLoaderOptions( noSkills: true, noThemes: true, extensionFactories, - } + }; } -export function applyBrunchOfflineDefault( - env: { PI_OFFLINE?: string } = process.env, -): void { - env.PI_OFFLINE ??= "1" +export function applyBrunchOfflineDefault(env: { PI_OFFLINE?: string } = process.env): void { + env.PI_OFFLINE ??= '1'; } -export function createBrunchSettingsManager( - _cwd: string, - _agentDir: string, -): SettingsManager { - return SettingsManager.inMemory(BRUNCH_SETTINGS_POLICY) +export function createBrunchSettingsManager(_cwd: string, _agentDir: string): SettingsManager { + return SettingsManager.inMemory(BRUNCH_SETTINGS_POLICY); } diff --git a/src/brunch-session-envelope.ts b/src/brunch-session-envelope.ts index 0146c19ed..bc606c608 100644 --- a/src/brunch-session-envelope.ts +++ b/src/brunch-session-envelope.ts @@ -1,47 +1,39 @@ -import { readFile } from "node:fs/promises" +import { readFile } from 'node:fs/promises'; -import { - type FileEntry, - type SessionEntry, -} from "@earendil-works/pi-coding-agent" +import { type FileEntry, type SessionEntry } from '@earendil-works/pi-coding-agent'; -import { - isSessionBindingEntry, - type SessionBindingData, -} from "./session-binding.js" +import { isSessionBindingEntry, type SessionBindingData } from './session-binding.js'; export interface BrunchSessionEnvelope { - header: PiSessionHeader - binding: SessionBindingData - entries: FileEntry[] + header: PiSessionHeader; + binding: SessionBindingData; + entries: FileEntry[]; } -export type BrunchSessionEnvelopeReadResult = { - ok: true - envelope: BrunchSessionEnvelope -} | { - ok: false - observedSessionIds: string[] -} +export type BrunchSessionEnvelopeReadResult = + | { + ok: true; + envelope: BrunchSessionEnvelope; + } + | { + ok: false; + observedSessionIds: string[]; + }; export class NonLinearTranscriptError extends Error { - readonly code = "BRUNCH_NON_LINEAR_TRANSCRIPT" + readonly code = 'BRUNCH_NON_LINEAR_TRANSCRIPT'; constructor(message: string) { - super(message) - this.name = "NonLinearTranscriptError" + super(message); + this.name = 'NonLinearTranscriptError'; } } -export async function readBrunchSessionEnvelope( - file: string, -): Promise { - const entries = await readJsonlEntries(file) +export async function readBrunchSessionEnvelope(file: string): Promise { + const entries = await readJsonlEntries(file); - const headers = entries.filter(isPiSessionHeader) - const bindings = entries - .filter(isSessionBindingEntry) - .map((entry) => entry.data) + const headers = entries.filter(isPiSessionHeader); + const bindings = entries.filter(isSessionBindingEntry).map((entry) => entry.data); if (headers.length !== 1 || bindings.length !== 1) { return { @@ -50,138 +42,126 @@ export async function readBrunchSessionEnvelope( ...headers.map((header) => header.id), ...bindings.map((binding) => binding.sessionId), ]), - } + }; } - const header = headers[0]! - const binding = bindings[0]! + const header = headers[0]!; + const binding = bindings[0]!; if (binding.sessionId !== header.id) { return { ok: false, observedSessionIds: uniqueStrings([header.id, binding.sessionId]), - } + }; } - assertFileBackedTranscriptEntries(entries) - return { ok: true, envelope: { header, binding, entries } } + assertFileBackedTranscriptEntries(entries); + return { ok: true, envelope: { header, binding, entries } }; } -export function assertLinearBrunchSessionEnvelope( - envelope: BrunchSessionEnvelope, -): void { - assertLinearTranscriptEntries(envelope.entries) +export function assertLinearBrunchSessionEnvelope(envelope: BrunchSessionEnvelope): void { + assertLinearTranscriptEntries(envelope.entries); } -export async function loadJsonlTranscriptEntries( - file: string, -): Promise { - const entries = await readJsonlEntries(file) - assertFileBackedTranscriptEntries(entries) - assertLinearTranscriptEntries(entries) - return entries +export async function loadJsonlTranscriptEntries(file: string): Promise { + const entries = await readJsonlEntries(file); + assertFileBackedTranscriptEntries(entries); + assertLinearTranscriptEntries(entries); + return entries; } async function readJsonlEntries(file: string): Promise { - return (await readFile(file, "utf8")) - .split("\n") + return (await readFile(file, 'utf8')) + .split('\n') .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as unknown) + .map((line) => JSON.parse(line) as unknown); } -function assertFileBackedTranscriptEntries( - entries: readonly unknown[], -): asserts entries is FileEntry[] { - const headerCount = entries.filter(isPiSessionHeader).length +function assertFileBackedTranscriptEntries(entries: readonly unknown[]): asserts entries is FileEntry[] { + const headerCount = entries.filter(isPiSessionHeader).length; if (headerCount !== 1) { throw new Error( `Invalid Pi JSONL transcript: expected exactly one Pi session header, found ${headerCount}`, - ) + ); } for (const entry of entries) { if (isPiSessionHeader(entry)) { - continue + continue; } if (!hasRequiredSessionEntryShape(entry)) { throw new Error( - "Invalid Pi JSONL transcript: every non-header entry must have a string id, string-or-null parentId, and string type", - ) + 'Invalid Pi JSONL transcript: every non-header entry must have a string id, string-or-null parentId, and string type', + ); } } } function assertLinearTranscriptEntries(entries: readonly FileEntry[]): void { for (const entry of entries) { - if (isPiSessionHeader(entry) && typeof entry.parentSession === "string") { - throw new NonLinearTranscriptError( - "Brunch does not support branch-derived Pi sessions", - ) + if (isPiSessionHeader(entry) && typeof entry.parentSession === 'string') { + throw new NonLinearTranscriptError('Brunch does not support branch-derived Pi sessions'); } - if (isSessionEntry(entry) && entry.type === "branch_summary") { - throw new NonLinearTranscriptError( - "Brunch does not support Pi branch-summary transcript entries", - ) + if (isSessionEntry(entry) && entry.type === 'branch_summary') { + throw new NonLinearTranscriptError('Brunch does not support Pi branch-summary transcript entries'); } } - const childrenByParent = new Map() + const childrenByParent = new Map(); for (const entry of entries) { if (!isSessionEntry(entry)) { - continue + continue; } - const siblings = childrenByParent.get(entry.parentId) ?? [] - siblings.push(entry.id) - childrenByParent.set(entry.parentId, siblings) + const siblings = childrenByParent.get(entry.parentId) ?? []; + siblings.push(entry.id); + childrenByParent.set(entry.parentId, siblings); if (siblings.length > 1) { - throw new NonLinearTranscriptError( - "Brunch does not support non-linear Pi transcript branches", - ) + throw new NonLinearTranscriptError('Brunch does not support non-linear Pi transcript branches'); } } } -interface PiSessionHeader extends Extract { - id: string +interface PiSessionHeader extends Extract { + id: string; } function isPiSessionHeader(value: unknown): value is PiSessionHeader { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type === "session" && - typeof (value as { id?: unknown }).id === "string" - ) + (value as { type?: unknown }).type === 'session' && + typeof (value as { id?: unknown }).id === 'string' + ); } function isSessionEntry(value: unknown): value is SessionEntry { - return isTranscriptEntry(value) && hasStringOrNullParentId(value) + return isTranscriptEntry(value) && hasStringOrNullParentId(value); } function hasRequiredSessionEntryShape(value: unknown): value is SessionEntry { - return isTranscriptEntry(value) && hasStringOrNullParentId(value) + return isTranscriptEntry(value) && hasStringOrNullParentId(value); } function isTranscriptEntry(value: unknown): value is SessionEntry { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type !== "session" && - typeof (value as { id?: unknown }).id === "string" && - typeof (value as { type?: unknown }).type === "string" - ) + (value as { type?: unknown }).type !== 'session' && + typeof (value as { id?: unknown }).id === 'string' && + typeof (value as { type?: unknown }).type === 'string' + ); } function hasStringOrNullParentId(value: unknown): boolean { return ( (value as { parentId?: unknown }).parentId === null || - typeof (value as { parentId?: unknown }).parentId === "string" - ) + typeof (value as { parentId?: unknown }).parentId === 'string' + ); } function uniqueStrings(values: readonly string[]): string[] { - return [...new Set(values)] + return [...new Set(values)]; } diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index 0667acad2..e556f6a8c 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -1,9 +1,6 @@ -import { userMessage } from "./test-helpers.js" -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" - -import { describe, expect, it } from "vitest" +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { SessionManager, @@ -11,17 +8,9 @@ import { type ExtensionContext, type ExtensionUIContext, type RegisteredCommand, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { - BRUNCH_SETTINGS_AUDITED_GETTERS, - BRUNCH_SETTINGS_POLICY, - applyBrunchOfflineDefault, - brunchResourceLoaderOptions, - createBrunchSettingsManager, - runBrunchTui, -} from "./brunch-tui.js" -import { createBrunchPiProfile } from "./brunch-pi-profile.js" import { BRUNCH_WORKSPACE_COMMAND, BRUNCH_WORKSPACE_SHORTCUT, @@ -31,488 +20,457 @@ import { registerBrunchOperationalModePolicy, runBrunchWorkspaceCommand, runBrunchWorkspaceAction, -} from "./.pi/pi-extension-shell.js" +} from './.pi/pi-extension-shell.js'; +import { createBrunchPiProfile } from './brunch-pi-profile.js'; +import { + BRUNCH_SETTINGS_AUDITED_GETTERS, + BRUNCH_SETTINGS_POLICY, + applyBrunchOfflineDefault, + brunchResourceLoaderOptions, + createBrunchSettingsManager, + runBrunchTui, +} from './brunch-tui.js'; +import { userMessage } from './test-helpers.js'; import { createWorkspaceSessionCoordinator, verifyWorkspaceSessionStores, type WorkspaceLaunchInventory, type WorkspaceSessionReadyState, -} from "./workspace-session-coordinator.js" +} from './workspace-session-coordinator.js'; -describe("Brunch TUI boot", () => { - it("gates spec selection through the coordinator before launching interactive mode", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const events: string[] = [] +describe('Brunch TUI boot', () => { + it('gates spec selection through the coordinator before launching interactive mode', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const events: string[] = []; await runBrunchTui({ cwd, selectSpecTitle: async () => { - events.push("select-spec") - return "Gated spec" + events.push('select-spec'); + return 'Gated spec'; }, launchInteractive: async ({ workspace }) => { - events.push(`launch:${workspace.spec.title}`) + events.push(`launch:${workspace.spec.title}`); }, - }) + }); - expect(events).toEqual(["select-spec", "launch:Gated spec"]) + expect(events).toEqual(['select-spec', 'launch:Gated spec']); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 1, - }) - expect(oracle.ok).toBe(true) + }); + expect(oracle.ok).toBe(true); if (!oracle.ok) { - expect(oracle.errors).toEqual([]) + expect(oracle.errors).toEqual([]); } - }) + }); - it("runs inspect, preflight, and activation before launching interactive mode", async () => { - const events: string[] = [] - const workspace = readyWorkspace("/tmp/project", "session-ready") + it('runs inspect, preflight, and activation before launching interactive mode', async () => { + const events: string[] = []; + const workspace = readyWorkspace('/tmp/project', 'session-ready'); await runBrunchTui({ - cwd: "/tmp/project", + cwd: '/tmp/project', coordinator: { inspectWorkspace: async () => { - events.push("inspect") + events.push('inspect'); return { - cwd: "/tmp/project", + cwd: '/tmp/project', currentSpec: workspace.spec, currentSessionFile: workspace.session.file, needsNewSpec: false, specs: [], unavailableSessions: [], - } + }; }, activateWorkspace: async (decision) => { - events.push(`activate:${decision.action}`) - return workspace + events.push(`activate:${decision.action}`); + return workspace; }, bindCurrentSpecToReplacementSession: async () => workspace, }, runWorkspaceDialogPreflight: async () => { - events.push("preflight") + events.push('preflight'); return { - action: "continue", + action: 'continue', specId: workspace.spec.id, sessionFile: workspace.session.file, - } + }; }, launchInteractive: async ({ workspace: launched }) => { - events.push(`launch:${launched.session.id}`) + events.push(`launch:${launched.session.id}`); }, - }) + }); - expect(events).toEqual([ - "inspect", - "preflight", - "activate:continue", - "launch:session-ready", - ]) - }) + expect(events).toEqual(['inspect', 'preflight', 'activate:continue', 'launch:session-ready']); + }); - it("does not launch interactive mode when startup preflight is cancelled", async () => { - const events: string[] = [] - const workspace = readyWorkspace("/tmp/project", "session-ready") + it('does not launch interactive mode when startup preflight is cancelled', async () => { + const events: string[] = []; + const workspace = readyWorkspace('/tmp/project', 'session-ready'); await runBrunchTui({ - cwd: "/tmp/project", + cwd: '/tmp/project', coordinator: { inspectWorkspace: async () => { - events.push("inspect") + events.push('inspect'); return { - cwd: "/tmp/project", + cwd: '/tmp/project', currentSpec: workspace.spec, currentSessionFile: workspace.session.file, needsNewSpec: false, specs: [], unavailableSessions: [], - } + }; }, activateWorkspace: async () => { - events.push("activate") + events.push('activate'); return { - status: "cancelled", - cwd: "/tmp/project", + status: 'cancelled', + cwd: '/tmp/project', chrome: workspace.chrome, - } + }; }, bindCurrentSpecToReplacementSession: async () => workspace, }, runWorkspaceDialogPreflight: async () => { - events.push("preflight") - return { action: "cancel" } + events.push('preflight'); + return { action: 'cancel' }; }, launchInteractive: async () => { - events.push("launch") + events.push('launch'); }, - }) + }); - expect(events).toEqual(["inspect", "preflight", "activate"]) - }) + expect(events).toEqual(['inspect', 'preflight', 'activate']); + }); - it("chooses a new binding-only session instead of implicitly resuming stale transcript", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('chooses a new binding-only session instead of implicitly resuming stale transcript', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinator.createSetupSession({ - specTitle: "Spec One", - }) - first.session.manager.appendMessage(userMessage("stale transcript")) - const firstContent = await readFile(first.session.file, "utf8") - let launchedSessionFile: string | undefined + specTitle: 'Spec One', + }); + first.session.manager.appendMessage(userMessage('stale transcript')); + const firstContent = await readFile(first.session.file, 'utf8'); + let launchedSessionFile: string | undefined; await runBrunchTui({ cwd, coordinator, runWorkspaceDialogPreflight: async () => ({ - action: "newSession", + action: 'newSession', specId: first.spec.id, }), launchInteractive: async ({ workspace }) => { - launchedSessionFile = workspace.session.file + launchedSessionFile = workspace.session.file; }, - }) - - expect(launchedSessionFile).toBeDefined() - expect(launchedSessionFile).not.toBe(first.session.file) - await expect(readFile(first.session.file, "utf8")).resolves.toBe( - firstContent, - ) - expect(await readFile(launchedSessionFile!, "utf8")).not.toContain( - "stale transcript", - ) - }) - - it("binds replacement sessions through internal session boundary events", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch", "sessions")) - const boundSessionIds: string[] = [] - const widgets = new Map() - const titles: string[] = [] + }); + + expect(launchedSessionFile).toBeDefined(); + expect(launchedSessionFile).not.toBe(first.session.file); + await expect(readFile(first.session.file, 'utf8')).resolves.toBe(firstContent); + expect(await readFile(launchedSessionFile!, 'utf8')).not.toContain('stale transcript'); + }); + + it('binds replacement sessions through internal session boundary events', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch', 'sessions')); + const boundSessionIds: string[] = []; + const widgets = new Map(); + const titles: string[] = []; const ui: FakeExtensionUi = { setHeader: (_factory) => {}, setFooter: (_factory) => {}, setStatus: (_key, _text) => {}, setWidget: (key: string, content: unknown) => { if (isStringArray(content)) { - widgets.set(key, content) + widgets.set(key, content); } }, setWorkingIndicator: (_options) => {}, setTitle: (title: string) => titles.push(title), - notify: (_message: string, _type?: "info" | "warning" | "error") => {}, - } - const ctx: FakeExtensionContext = { sessionManager: manager, ui } - const sessionStart: Array<( - event: unknown, - ctx: FakeExtensionContext, - ) => Promise> = [] - const beforeAgentStart: Array<( - event: unknown, - ctx: FakeExtensionContext, - ) => Promise> = [] - const messageStart: Array<( - event: unknown, - ctx: FakeExtensionContext, - ) => Promise> = [] + notify: (_message: string, _type?: 'info' | 'warning' | 'error') => {}, + }; + const ctx: FakeExtensionContext = { sessionManager: manager, ui }; + const sessionStart: Array<(event: unknown, ctx: FakeExtensionContext) => Promise> = []; + const beforeAgentStart: Array<(event: unknown, ctx: FakeExtensionContext) => Promise> = []; + const messageStart: Array<(event: unknown, ctx: FakeExtensionContext) => Promise> = []; await createBrunchPiExtensionShell( chromeStateForWorkspace(readyWorkspace(cwd, manager.getSessionId())), (sessionManager) => { - boundSessionIds.push(sessionManager.getSessionId()) + boundSessionIds.push(sessionManager.getSessionId()); }, { coordinator: noOpWorkspaceCoordinator(cwd) }, )({ on: (event: string, handler: never) => { - if (event === "session_start") { - sessionStart.push(handler) + if (event === 'session_start') { + sessionStart.push(handler); } - if (event === "before_agent_start") { - beforeAgentStart.push(handler) + if (event === 'before_agent_start') { + beforeAgentStart.push(handler); } - if (event === "message_start") { - messageStart.push(handler) + if (event === 'message_start') { + messageStart.push(handler); } }, registerCommand: (_name: string, _options: unknown) => {}, registerTool: (_tool: unknown) => {}, - } as never) + } as never); - for (const handler of sessionStart) await handler({}, ctx) - for (const handler of beforeAgentStart) await handler({}, ctx) + for (const handler of sessionStart) await handler({}, ctx); + for (const handler of beforeAgentStart) await handler({}, ctx); for (const handler of messageStart) { - await handler({ type: "message_start", message: { role: "user" } }, ctx) + await handler({ type: 'message_start', message: { role: 'user' } }, ctx); } for (const handler of messageStart) { - await handler( - { type: "message_start", message: { role: "assistant" } }, - ctx, - ) + await handler({ type: 'message_start', message: { role: 'assistant' } }, ctx); } - expect(boundSessionIds).toEqual([ - manager.getSessionId(), - manager.getSessionId(), - manager.getSessionId(), - ]) - expect(widgets.get("brunch.chrome")?.join("\n")).toContain( - "chat mode: responding-to-elicitation", - ) - expect(titles).toEqual(["brunch — Spec One"]) - }) - - it("registers the Brunch spec/session picker command and shortcut", async () => { - const commands = - new Map>() - const shortcuts = - new Map>() - const registeredTools: string[] = [] + expect(boundSessionIds).toEqual([manager.getSessionId(), manager.getSessionId(), manager.getSessionId()]); + expect(widgets.get('brunch.chrome')?.join('\n')).toContain('chat mode: responding-to-elicitation'); + expect(titles).toEqual(['brunch — Spec One']); + }); + + it('registers the Brunch spec/session picker command and shortcut', async () => { + const commands = new Map>(); + const shortcuts = new Map>(); + const registeredTools: string[] = []; await createBrunchPiExtensionShell( - chromeStateForWorkspace(readyWorkspace("/tmp/project", "session-1")), + chromeStateForWorkspace(readyWorkspace('/tmp/project', 'session-1')), undefined, { coordinator: { - inspectWorkspace: async () => emptyInventory("/tmp/project"), - activateWorkspace: async () => - readyWorkspace("/tmp/project", "session-1"), + inspectWorkspace: async () => emptyInventory('/tmp/project'), + activateWorkspace: async () => readyWorkspace('/tmp/project', 'session-1'), }, }, )({ on: (_event: string, _handler: unknown) => {}, - registerCommand: (name: string, opts: unknown) => - commands.set(name, opts as never), - registerShortcut: (name: string, opts: unknown) => - shortcuts.set(name, opts as never), + registerCommand: (name: string, opts: unknown) => commands.set(name, opts as never), + registerShortcut: (name: string, opts: unknown) => shortcuts.set(name, opts as never), registerTool: (tool: { name: string }) => registeredTools.push(tool.name), registerMessageRenderer: (_type: string) => {}, sendMessage: (_message: unknown) => {}, - getAllTools: () => - ["read", "grep", "find", "ls", "bash"].map((name) => ({ name })), + getAllTools: () => ['read', 'grep', 'find', 'ls', 'bash'].map((name) => ({ name })), setActiveTools: (_tools: string[]) => {}, - } as never) + } as never); expect(registeredTools).toEqual([ - "read", - "grep", - "find", - "ls", - "present_alternatives", - "present_question", - "present_options", - "request_answer", - "request_choice", - "request_choices", - ]) - expect(commands.get(BRUNCH_WORKSPACE_COMMAND)?.description).toBe( - "Open the Brunch spec/session picker", - ) - const retiredWorkspaceCommand = ["brunch", "workspace"].join("-") - expect(commands.has(retiredWorkspaceCommand)).toBe(false) - expect(shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT)?.description).toBe( - "Open the Brunch spec/session picker", - ) - expect(shortcuts.has("ctrl+b")).toBe(false) - - const shortcutEvents: string[] = [] - const shortcut = shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT) - expect(shortcut).toBeDefined() - const shortcutHandler = shortcut!.handler as ( - ctx: unknown, - ) => Promise | void + 'read', + 'grep', + 'find', + 'ls', + 'present_alternatives', + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + ]); + expect(commands.get(BRUNCH_WORKSPACE_COMMAND)?.description).toBe('Open the Brunch spec/session picker'); + const retiredWorkspaceCommand = ['brunch', 'workspace'].join('-'); + expect(commands.has(retiredWorkspaceCommand)).toBe(false); + expect(shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT)?.description).toBe('Open the Brunch spec/session picker'); + expect(shortcuts.has('ctrl+b')).toBe(false); + + const shortcutEvents: string[] = []; + const shortcut = shortcuts.get(BRUNCH_WORKSPACE_SHORTCUT); + expect(shortcut).toBeDefined(); + const shortcutHandler = shortcut!.handler as (ctx: unknown) => Promise | void; await shortcutHandler({ ui: fakeUi((method, type) => shortcutEvents.push(`${method}:${type}`)), - }) - expect(shortcutEvents).toEqual(["notify:warning"]) - }) + }); + expect(shortcutEvents).toEqual(['notify:warning']); + }); - it("opens the spec/session picker from the Brunch command", async () => { - const events: string[] = [] - const target = readyWorkspace("/tmp/project", "session-target") + it('opens the spec/session picker from the Brunch command', async () => { + const events: string[] = []; + const target = readyWorkspace('/tmp/project', 'session-target'); const ctx = fakeCommandContext({ - currentSessionFile: "/sessions/session-old.jsonl", + currentSessionFile: '/sessions/session-old.jsonl', decisions: [ { - action: "openSession", + action: 'openSession', specId: target.spec.id, sessionFile: target.session.file, }, ], onEvent: (event) => events.push(event), - }) + }); await runBrunchWorkspaceCommand(ctx, { inspectWorkspace: async () => { - events.push("inspect") - return inventoryWithWorkspace(target) + events.push('inspect'); + return inventoryWithWorkspace(target); }, activateWorkspace: async (decision) => { - events.push(`activate:${decision.action}`) - return target + events.push(`activate:${decision.action}`); + return target; }, - }) + }); expect(events).toEqual([ - "waitForIdle", - "inspect", - "custom", - "activate:openSession", + 'waitForIdle', + 'inspect', + 'custom', + 'activate:openSession', `switch:${target.session.file}`, - "notify:info", - ]) - }) - - it("runs the in-session workspace switch through coordinator activation and replacement context", async () => { - const events: string[] = [] - const customOptions: unknown[] = [] - const target = readyWorkspace("/tmp/project", "session-target") - const replacementUi = fakeUi((method) => - events.push(`replacement:${method}`), - ) + 'notify:info', + ]); + }); + + it('runs the in-session workspace switch through coordinator activation and replacement context', async () => { + const events: string[] = []; + const customOptions: unknown[] = []; + const target = readyWorkspace('/tmp/project', 'session-target'); + const replacementUi = fakeUi((method) => events.push(`replacement:${method}`)); const ctx = fakeCommandContext({ - currentSessionFile: "/sessions/session-old.jsonl", + currentSessionFile: '/sessions/session-old.jsonl', decision: { - action: "openSession", + action: 'openSession', specId: target.spec.id, sessionFile: target.session.file, }, onCustomOptions: (options) => customOptions.push(options), onEvent: (event) => events.push(event), replacementUi, - }) + }); await runBrunchWorkspaceAction(ctx, { inspectWorkspace: async () => { - events.push("inspect") - return inventoryWithWorkspace(target) + events.push('inspect'); + return inventoryWithWorkspace(target); }, activateWorkspace: async (decision) => { - events.push(`activate:${decision.action}`) - return target + events.push(`activate:${decision.action}`); + return target; }, - }) + }); expect(events).toEqual([ - "waitForIdle", - "inspect", - "custom", - "activate:openSession", + 'waitForIdle', + 'inspect', + 'custom', + 'activate:openSession', `switch:${target.session.file}`, - "replacement:setHeader", - "replacement:setFooter", - "replacement:setWidget", - "replacement:setTitle", - "replacement:notify", - ]) + 'replacement:setHeader', + 'replacement:setFooter', + 'replacement:setWidget', + 'replacement:setTitle', + 'replacement:notify', + ]); expect(customOptions).toEqual([ { overlay: true, overlayOptions: { - anchor: "center", + anchor: 'center', width: 80, - maxHeight: "90%", + maxHeight: '90%', margin: 1, }, }, - ]) - }) + ]); + }); - it("opens the spec/session picker from shortcut contexts without waitForIdle", async () => { - const events: string[] = [] - const target = readyWorkspace("/tmp/project", "session-target") + it('opens the spec/session picker from shortcut contexts without waitForIdle', async () => { + const events: string[] = []; + const target = readyWorkspace('/tmp/project', 'session-target'); const ctx = fakeCommandContext({ - currentSessionFile: "/sessions/session-old.jsonl", + currentSessionFile: '/sessions/session-old.jsonl', decision: { - action: "openSession", + action: 'openSession', specId: target.spec.id, sessionFile: target.session.file, }, onEvent: (event) => events.push(event), - }) - delete (ctx as Partial).waitForIdle + }); + delete (ctx as Partial).waitForIdle; await runBrunchWorkspaceAction(ctx, { inspectWorkspace: async () => { - events.push("inspect") - return inventoryWithWorkspace(target) + events.push('inspect'); + return inventoryWithWorkspace(target); }, activateWorkspace: async (decision) => { - events.push(`activate:${decision.action}`) - return target + events.push(`activate:${decision.action}`); + return target; }, - }) + }); expect(events).toEqual([ - "inspect", - "custom", - "activate:openSession", + 'inspect', + 'custom', + 'activate:openSession', `switch:${target.session.file}`, - "notify:info", - ]) - }) + 'notify:info', + ]); + }); - it("leaves the current session untouched when workspace switch is cancelled", async () => { - const events: string[] = [] + it('leaves the current session untouched when workspace switch is cancelled', async () => { + const events: string[] = []; const ctx = fakeCommandContext({ - currentSessionFile: "/sessions/session-old.jsonl", - decision: { action: "cancel" }, + currentSessionFile: '/sessions/session-old.jsonl', + decision: { action: 'cancel' }, onEvent: (event) => events.push(event), - }) + }); await runBrunchWorkspaceAction(ctx, { - inspectWorkspace: async () => emptyInventory("/tmp/project"), + inspectWorkspace: async () => emptyInventory('/tmp/project'), activateWorkspace: async () => ({ - status: "cancelled", - cwd: "/tmp/project", + status: 'cancelled', + cwd: '/tmp/project', chrome: { - cwd: "/tmp/project", + cwd: '/tmp/project', spec: null, - phase: "select_spec", - chatMode: "select-spec", + phase: 'select_spec', + chatMode: 'select-spec', }, }), - }) + }); - expect(events).toEqual(["waitForIdle", "custom", "notify:info"]) - }) + expect(events).toEqual(['waitForIdle', 'custom', 'notify:info']); + }); - it("reports needs-human workspace switch decisions without switching sessions", async () => { - const events: string[] = [] + it('reports needs-human workspace switch decisions without switching sessions', async () => { + const events: string[] = []; const ctx = fakeCommandContext({ - currentSessionFile: "/sessions/session-old.jsonl", + currentSessionFile: '/sessions/session-old.jsonl', decision: { - action: "openSession", - specId: "missing", - sessionFile: "/sessions/missing.jsonl", + action: 'openSession', + specId: 'missing', + sessionFile: '/sessions/missing.jsonl', }, onEvent: (event) => events.push(event), - }) + }); await runBrunchWorkspaceAction(ctx, { - inspectWorkspace: async () => emptyInventory("/tmp/project"), + inspectWorkspace: async () => emptyInventory('/tmp/project'), activateWorkspace: async () => ({ - status: "needs_human", - cwd: "/tmp/project", - reason: "Selected session is not available.", + status: 'needs_human', + cwd: '/tmp/project', + reason: 'Selected session is not available.', chrome: { - cwd: "/tmp/project", + cwd: '/tmp/project', spec: null, - phase: "select_spec", - chatMode: "select-spec", + phase: 'select_spec', + chatMode: 'select-spec', }, }), - }) + }); - expect(events).toEqual(["waitForIdle", "custom", "notify:warning"]) - }) + expect(events).toEqual(['waitForIdle', 'custom', 'notify:warning']); + }); - it("cancels Pi branch-flow hooks with a stable user-facing reason", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch", "sessions")) + it('cancels Pi branch-flow hooks with a stable user-facing reason', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch', 'sessions')); const notifications: Array<{ - message: string - type: "info" | "warning" | "error" | undefined - }> = [] + message: string; + type: 'info' | 'warning' | 'error' | undefined; + }> = []; const ctx: FakeExtensionContext = { sessionManager: manager, ui: { @@ -524,117 +482,99 @@ describe("Brunch TUI boot", () => { setTitle: (_title: string) => {}, notify: (message, type) => notifications.push({ message, type }), }, - } - const handlers = new Map unknown>() + }; + const handlers = new Map unknown>(); await createBrunchPiExtensionShell( chromeStateForWorkspace(readyWorkspace(cwd, manager.getSessionId())), undefined, { coordinator: noOpWorkspaceCoordinator(cwd) }, )({ - on: ( - event: string, - handler: (event: unknown, ctx: FakeExtensionContext) => unknown, - ) => { - handlers.set(event, handler) + on: (event: string, handler: (event: unknown, ctx: FakeExtensionContext) => unknown) => { + handlers.set(event, handler); }, registerCommand: (_name: string, _options: unknown) => {}, registerTool: (_tool: unknown) => {}, - } as never) + } as never); await expect( - Promise.resolve( - handlers.get("session_before_tree")?.( - { type: "session_before_tree" }, - ctx, - ), - ), - ).resolves.toEqual({ cancel: true }) + Promise.resolve(handlers.get('session_before_tree')?.({ type: 'session_before_tree' }, ctx)), + ).resolves.toEqual({ cancel: true }); await expect( - Promise.resolve( - handlers.get("session_before_fork")?.( - { type: "session_before_fork" }, - ctx, - ), - ), - ).resolves.toEqual({ cancel: true }) + Promise.resolve(handlers.get('session_before_fork')?.({ type: 'session_before_fork' }, ctx)), + ).resolves.toEqual({ cancel: true }); expect(notifications).toEqual([ { message: - "Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec.", - type: "warning", + 'Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec.', + type: 'warning', }, { message: - "Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec.", - type: "warning", + 'Brunch does not support Pi session branches in this POC. Use /new to continue within the selected spec.', + type: 'warning', }, - ]) - }) - - it("registers alternatives cards as a transcript primitive without demo commands", async () => { - const commands: string[] = [] - const renderers: string[] = [] - const tools = new Map unknown - }>() - const messages: unknown[] = [] + ]); + }); + + it('registers alternatives cards as a transcript primitive without demo commands', async () => { + const commands: string[] = []; + const renderers: string[] = []; + const tools = new Map< + string, + { + execute: (id: string, params: never) => unknown; + } + >(); + const messages: unknown[] = []; registerBrunchAlternatives({ registerMessageRenderer: (type: string) => renderers.push(type), - registerTool: (tool: { - name: string - execute: (id: string, params: never) => unknown - }) => tools.set(tool.name, tool), + registerTool: (tool: { name: string; execute: (id: string, params: never) => unknown }) => + tools.set(tool.name, tool), registerCommand: (name: string) => commands.push(name), sendMessage: (message: unknown) => messages.push(message), - } as never) + } as never); await expect( - Promise.resolve(tools.get("present_alternatives")?.execute("tool-1", { - headline: "Choose", - alternatives: [{ title: "A", body: "Alpha", flavor: "accent" }], - } as never)), + Promise.resolve( + tools.get('present_alternatives')?.execute('tool-1', { + headline: 'Choose', + alternatives: [{ title: 'A', body: 'Alpha', flavor: 'accent' }], + } as never), + ), ).resolves.toMatchObject({ - content: [{ type: "text", text: "Presented 1 alternative." }], + content: [{ type: 'text', text: 'Presented 1 alternative.' }], details: { count: 1 }, terminate: true, - }) + }); - expect(renderers).toEqual(["alternatives-card-set"]) + expect(renderers).toEqual(['alternatives-card-set']); expect(messages).toEqual([ { - customType: "alternatives-card-set", - content: "## Choose\n\n---\n\n### A\n\nAlpha", + customType: 'alternatives-card-set', + content: '## Choose\n\n---\n\n### A\n\nAlpha', display: true, details: { - headline: "Choose", - alternatives: [{ title: "A", body: "Alpha", flavor: "accent" }], + headline: 'Choose', + alternatives: [{ title: 'A', body: 'Alpha', flavor: 'accent' }], }, }, - ]) - expect(commands).toEqual([]) - }) - - it("wires the fixture graph-code mention source through the Brunch shell", async () => { - let providerFactory: (( - current: FakeAutocompleteProvider, - ) => FakeAutocompleteProvider) | undefined - const sessionStart: Array<( - event: unknown, - ctx: FakeExtensionContext, - ) => Promise | void> = [] + ]); + expect(commands).toEqual([]); + }); + + it('wires the fixture graph-code mention source through the Brunch shell', async () => { + let providerFactory: ((current: FakeAutocompleteProvider) => FakeAutocompleteProvider) | undefined; + const sessionStart: Array<(event: unknown, ctx: FakeExtensionContext) => Promise | void> = []; await createBrunchPiExtensionShell( - chromeStateForWorkspace(readyWorkspace("/tmp/project", "session-1")), + chromeStateForWorkspace(readyWorkspace('/tmp/project', 'session-1')), undefined, - { coordinator: noOpWorkspaceCoordinator("/tmp/project") }, + { coordinator: noOpWorkspaceCoordinator('/tmp/project') }, )({ on: (event: string, handler: never) => { - if (event === "session_start") sessionStart.push(handler) + if (event === 'session_start') sessionStart.push(handler); }, registerCommand: (_name: string, _options: unknown) => {}, registerShortcut: (_name: string, _options: unknown) => {}, @@ -643,12 +583,12 @@ describe("Brunch TUI boot", () => { sendMessage: (_message: unknown) => {}, getAllTools: () => [], setActiveTools: (_tools: string[]) => {}, - } as never) + } as never); const ctx: FakeExtensionContext = { sessionManager: { getEntries: () => [], - } as unknown as FakeExtensionContext["sessionManager"], + } as unknown as FakeExtensionContext['sessionManager'], ui: { setHeader: (_factory) => {}, setFooter: (_factory) => {}, @@ -656,105 +596,99 @@ describe("Brunch TUI boot", () => { setWidget: (_key: string, _content: unknown) => {}, setWorkingIndicator: (_options) => {}, setTitle: (_title: string) => {}, - notify: (_message: string, _type?: "info" | "warning" | "error") => {}, + notify: (_message: string, _type?: 'info' | 'warning' | 'error') => {}, addAutocompleteProvider: (factory: typeof providerFactory) => { - providerFactory = factory + providerFactory = factory; }, } as FakeExtensionUi & { - addAutocompleteProvider: (factory: typeof providerFactory) => void + addAutocompleteProvider: (factory: typeof providerFactory) => void; }, - } + }; - for (const handler of sessionStart) await handler({}, ctx) + for (const handler of sessionStart) await handler({}, ctx); const fallback: FakeAutocompleteProvider = { - getSuggestions: async () => ({ items: [], prefix: "" }), + getSuggestions: async () => ({ items: [], prefix: '' }), applyCompletion: (lines) => ({ lines, cursorLine: 0, cursorCol: 0 }), shouldTriggerFileCompletion: () => true, - } - const provider = providerFactory?.(fallback) + }; + const provider = providerFactory?.(fallback); - await expect( - provider?.getSuggestions(["Discuss #"], 0, 9, {} as never), - ).resolves.toMatchObject({ - prefix: "#", - items: expect.arrayContaining([ - expect.objectContaining({ value: "#D12" }), - ]), - }) - }) - - it("loads the elicit operational-mode tool policy from product code", async () => { - const events: Record unknown> = {} - const activeTools: string[][] = [] - const registeredTools: string[] = [] + await expect(provider?.getSuggestions(['Discuss #'], 0, 9, {} as never)).resolves.toMatchObject({ + prefix: '#', + items: expect.arrayContaining([expect.objectContaining({ value: '#D12' })]), + }); + }); + + it('loads the elicit operational-mode tool policy from product code', async () => { + const events: Record unknown> = {}; + const activeTools: string[][] = []; + const registeredTools: string[] = []; registerBrunchOperationalModePolicy({ registerTool: (tool: { name: string }) => registeredTools.push(tool.name), getAllTools: () => [ - "read", - "grep", - "find", - "ls", - "present_question", - "present_options", - "request_answer", - "request_choice", - "request_choices", - "bash", - "edit", - "write", + 'read', + 'grep', + 'find', + 'ls', + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', + 'bash', + 'edit', + 'write', ].map((name) => ({ name, })), setActiveTools: (tools: string[]) => activeTools.push(tools), on: (event: string, handler: (event: never) => unknown) => { - events[event] = handler + events[event] = handler; }, - } as never) + } as never); - expect(registeredTools).toEqual(["read", "grep", "find", "ls"]) - await events.session_start?.({} as never) + expect(registeredTools).toEqual(['read', 'grep', 'find', 'ls']); + await events.session_start?.({} as never); expect(activeTools).toEqual([ [ - "read", - "grep", - "find", - "ls", - "present_question", - "present_options", - "request_answer", - "request_choice", - "request_choices", + 'read', + 'grep', + 'find', + 'ls', + 'present_question', + 'present_options', + 'request_answer', + 'request_choice', + 'request_choices', ], - ]) + ]); await expect( - Promise.resolve( - events.before_agent_start?.({ systemPrompt: "base" } as never), - ), - ).resolves.toBeUndefined() - await expect( - Promise.resolve(events.tool_call?.({ toolName: "write" } as never)), - ).resolves.toMatchObject({ block: true }) - expect(events.user_bash?.({ command: "rm -rf ." } as never)).toMatchObject({ + Promise.resolve(events.before_agent_start?.({ systemPrompt: 'base' } as never)), + ).resolves.toBeUndefined(); + await expect(Promise.resolve(events.tool_call?.({ toolName: 'write' } as never))).resolves.toMatchObject({ + block: true, + }); + expect(events.user_bash?.({ command: 'rm -rf .' } as never)).toMatchObject({ result: { exitCode: 1, - output: "Brunch tool policy blocks shell commands: rm -rf .", + output: 'Brunch tool policy blocks shell commands: rm -rf .', }, - }) - }) + }); + }); - it("suppresses generic Pi startup resources for the Brunch shell", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const settingsManager = createBrunchSettingsManager(cwd, cwd) - const extension = () => {} - const resourceOptions = brunchResourceLoaderOptions([extension]) - const env: { PI_OFFLINE?: string } = {} + it('suppresses generic Pi startup resources for the Brunch shell', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const settingsManager = createBrunchSettingsManager(cwd, cwd); + const extension = () => {}; + const resourceOptions = brunchResourceLoaderOptions([extension]); + const env: { PI_OFFLINE?: string } = {}; - applyBrunchOfflineDefault(env) + applyBrunchOfflineDefault(env); - expect(settingsManager.getQuietStartup()).toBe(true) + expect(settingsManager.getQuietStartup()).toBe(true); expect(resourceOptions).toEqual({ noContextFiles: true, noExtensions: true, @@ -762,88 +696,88 @@ describe("Brunch TUI boot", () => { noSkills: true, noThemes: true, extensionFactories: [extension], - }) - expect(env.PI_OFFLINE).toBe("1") - }) - - it("ignores hostile ambient Pi settings for behavior-shaping profile policy", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const agentDir = join(cwd, "home-pi") - await writeHostilePiSettings(cwd, agentDir) - - const settingsManager = createBrunchSettingsManager(cwd, agentDir) - - expect(settingsManager.getShellPath()).toBeUndefined() - expect(settingsManager.getShellCommandPrefix()).toBeUndefined() - expect(settingsManager.getNpmCommand()).toBeUndefined() - expect(settingsManager.getPackages()).toEqual([]) - expect(settingsManager.getExtensionPaths()).toEqual([]) - expect(settingsManager.getSkillPaths()).toEqual([]) - expect(settingsManager.getPromptTemplatePaths()).toEqual([]) - expect(settingsManager.getThemePaths()).toEqual([]) - expect(settingsManager.getEnableSkillCommands()).toBe(false) - expect(settingsManager.getDoubleEscapeAction()).toBe("none") + }); + expect(env.PI_OFFLINE).toBe('1'); + }); + + it('ignores hostile ambient Pi settings for behavior-shaping profile policy', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const agentDir = join(cwd, 'home-pi'); + await writeHostilePiSettings(cwd, agentDir); + + const settingsManager = createBrunchSettingsManager(cwd, agentDir); + + expect(settingsManager.getShellPath()).toBeUndefined(); + expect(settingsManager.getShellCommandPrefix()).toBeUndefined(); + expect(settingsManager.getNpmCommand()).toBeUndefined(); + expect(settingsManager.getPackages()).toEqual([]); + expect(settingsManager.getExtensionPaths()).toEqual([]); + expect(settingsManager.getSkillPaths()).toEqual([]); + expect(settingsManager.getPromptTemplatePaths()).toEqual([]); + expect(settingsManager.getThemePaths()).toEqual([]); + expect(settingsManager.getEnableSkillCommands()).toBe(false); + expect(settingsManager.getDoubleEscapeAction()).toBe('none'); expect(settingsManager.getCompactionSettings()).toEqual({ enabled: true, reserveTokens: 16384, keepRecentTokens: 20000, - }) + }); expect(settingsManager.getRetrySettings()).toEqual({ enabled: true, maxRetries: 3, baseDelayMs: 2000, - }) + }); expect(settingsManager.getProviderRetrySettings()).toEqual({ timeoutMs: undefined, maxRetries: undefined, maxRetryDelayMs: 60000, - }) - expect(settingsManager.getShowImages()).toBe(true) - expect(settingsManager.getImageWidthCells()).toBe(60) - expect(settingsManager.getClearOnShrink()).toBe(false) - expect(settingsManager.getShowTerminalProgress()).toBe(false) - expect(settingsManager.getImageAutoResize()).toBe(true) - expect(settingsManager.getBlockImages()).toBe(false) - expect(settingsManager.getTransport()).toBe("auto") - expect(settingsManager.getTheme()).toBeUndefined() - expect(settingsManager.getLastChangelogVersion()).toBeUndefined() - expect(settingsManager.getCollapseChangelog()).toBe(false) - expect(settingsManager.getEnableInstallTelemetry()).toBe(false) - expect(settingsManager.getShowHardwareCursor()).toBe(false) - expect(settingsManager.getEditorPaddingX()).toBe(0) - expect(settingsManager.getAutocompleteMaxVisible()).toBe(5) - }) - - it("keeps sealed Brunch settings after Pi settings reload", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const agentDir = join(cwd, "home-pi") - await writeHostilePiSettings(cwd, agentDir) - const settingsManager = createBrunchSettingsManager(cwd, agentDir) - - await settingsManager.reload() - - expect(settingsManager.getQuietStartup()).toBe(true) - expect(settingsManager.getPackages()).toEqual([]) - expect(settingsManager.getExtensionPaths()).toEqual([]) - expect(settingsManager.getSkillPaths()).toEqual([]) - expect(settingsManager.getPromptTemplatePaths()).toEqual([]) - expect(settingsManager.getThemePaths()).toEqual([]) - expect(settingsManager.getEnableSkillCommands()).toBe(false) - expect(settingsManager.getDoubleEscapeAction()).toBe("none") - expect(settingsManager.getShellPath()).toBeUndefined() - expect(settingsManager.getNpmCommand()).toBeUndefined() - }) - - it("keeps ambient resource suppression and explicit product extensions behind one profile boundary", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-tui-")) - const extension = () => {} + }); + expect(settingsManager.getShowImages()).toBe(true); + expect(settingsManager.getImageWidthCells()).toBe(60); + expect(settingsManager.getClearOnShrink()).toBe(false); + expect(settingsManager.getShowTerminalProgress()).toBe(false); + expect(settingsManager.getImageAutoResize()).toBe(true); + expect(settingsManager.getBlockImages()).toBe(false); + expect(settingsManager.getTransport()).toBe('auto'); + expect(settingsManager.getTheme()).toBeUndefined(); + expect(settingsManager.getLastChangelogVersion()).toBeUndefined(); + expect(settingsManager.getCollapseChangelog()).toBe(false); + expect(settingsManager.getEnableInstallTelemetry()).toBe(false); + expect(settingsManager.getShowHardwareCursor()).toBe(false); + expect(settingsManager.getEditorPaddingX()).toBe(0); + expect(settingsManager.getAutocompleteMaxVisible()).toBe(5); + }); + + it('keeps sealed Brunch settings after Pi settings reload', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const agentDir = join(cwd, 'home-pi'); + await writeHostilePiSettings(cwd, agentDir); + const settingsManager = createBrunchSettingsManager(cwd, agentDir); + + await settingsManager.reload(); + + expect(settingsManager.getQuietStartup()).toBe(true); + expect(settingsManager.getPackages()).toEqual([]); + expect(settingsManager.getExtensionPaths()).toEqual([]); + expect(settingsManager.getSkillPaths()).toEqual([]); + expect(settingsManager.getPromptTemplatePaths()).toEqual([]); + expect(settingsManager.getThemePaths()).toEqual([]); + expect(settingsManager.getEnableSkillCommands()).toBe(false); + expect(settingsManager.getDoubleEscapeAction()).toBe('none'); + expect(settingsManager.getShellPath()).toBeUndefined(); + expect(settingsManager.getNpmCommand()).toBeUndefined(); + }); + + it('keeps ambient resource suppression and explicit product extensions behind one profile boundary', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-')); + const extension = () => {}; const profile = createBrunchPiProfile({ cwd, agentDir: cwd, extensionFactories: [extension], - }) + }); - expect(profile.settingsManager.getQuietStartup()).toBe(true) + expect(profile.settingsManager.getQuietStartup()).toBe(true); expect(profile.resourceLoaderOptions).toEqual({ noContextFiles: true, noExtensions: true, @@ -851,52 +785,40 @@ describe("Brunch TUI boot", () => { noSkills: true, noThemes: true, extensionFactories: [extension], - }) - }) - - it("keeps Pi settings/resource policy out of the TUI launcher", async () => { - const launcherSource = await readFile( - join(import.meta.dirname, "brunch-tui.ts"), - "utf8", - ) - const profileSource = await readFile( - join(import.meta.dirname, "brunch-pi-profile.ts"), - "utf8", - ) - - expect(launcherSource).toContain("createBrunchPiProfile") - expect(launcherSource).not.toContain("SettingsManager.create") - expect(launcherSource).not.toContain("noContextFiles") - expect(profileSource).toContain("SettingsManager.inMemory") - expect(profileSource).toContain("noContextFiles: true") - }) - - it("keeps the Brunch settings override and audit list in the profile boundary", async () => { - const launcherSource = await readFile( - join(import.meta.dirname, "brunch-tui.ts"), - "utf8", - ) - const profileSource = await readFile( - join(import.meta.dirname, "brunch-pi-profile.ts"), - "utf8", - ) + }); + }); + + it('keeps Pi settings/resource policy out of the TUI launcher', async () => { + const launcherSource = await readFile(join(import.meta.dirname, 'brunch-tui.ts'), 'utf8'); + const profileSource = await readFile(join(import.meta.dirname, 'brunch-pi-profile.ts'), 'utf8'); + + expect(launcherSource).toContain('createBrunchPiProfile'); + expect(launcherSource).not.toContain('SettingsManager.create'); + expect(launcherSource).not.toContain('noContextFiles'); + expect(profileSource).toContain('SettingsManager.inMemory'); + expect(profileSource).toContain('noContextFiles: true'); + }); + + it('keeps the Brunch settings override and audit list in the profile boundary', async () => { + const launcherSource = await readFile(join(import.meta.dirname, 'brunch-tui.ts'), 'utf8'); + const profileSource = await readFile(join(import.meta.dirname, 'brunch-pi-profile.ts'), 'utf8'); const settingsManagerTypes = await readFile( join( import.meta.dirname, - "..", - "node_modules", - "@earendil-works", - "pi-coding-agent", - "dist", - "core", - "settings-manager.d.ts", + '..', + 'node_modules', + '@earendil-works', + 'pi-coding-agent', + 'dist', + 'core', + 'settings-manager.d.ts', ), - "utf8", - ) + 'utf8', + ); const getterNames = Array.from( settingsManagerTypes.matchAll(/\n (get[A-Z][A-Za-z0-9]+)\(/g), (match) => match[1]!, - ) + ); expect(BRUNCH_SETTINGS_POLICY).toMatchObject({ quietStartup: true, @@ -906,27 +828,22 @@ describe("Brunch TUI boot", () => { prompts: [], themes: [], enableSkillCommands: false, - doubleEscapeAction: "none", - }) - expect(getterNames.sort()).toEqual( - [...BRUNCH_SETTINGS_AUDITED_GETTERS].sort(), - ) - expect(launcherSource).not.toContain("SettingsManager.inMemory") - expect(profileSource).toContain("BRUNCH_SETTINGS_POLICY") - expect(profileSource).toContain("SettingsManager.inMemory") - }) -}) - -async function writeHostilePiSettings( - cwd: string, - agentDir: string, -): Promise { + doubleEscapeAction: 'none', + }); + expect(getterNames.sort()).toEqual([...BRUNCH_SETTINGS_AUDITED_GETTERS].sort()); + expect(launcherSource).not.toContain('SettingsManager.inMemory'); + expect(profileSource).toContain('BRUNCH_SETTINGS_POLICY'); + expect(profileSource).toContain('SettingsManager.inMemory'); + }); +}); + +async function writeHostilePiSettings(cwd: string, agentDir: string): Promise { const hostileSettings = { - lastChangelogVersion: "999.0.0-hostile", - defaultProvider: "hostile-provider", - defaultModel: "hostile-model", - transport: "websocket", - theme: "hostile-theme", + lastChangelogVersion: '999.0.0-hostile', + defaultProvider: 'hostile-provider', + defaultModel: 'hostile-model', + transport: 'websocket', + theme: 'hostile-theme', compaction: { enabled: false, reserveTokens: 1, @@ -946,17 +863,17 @@ async function writeHostilePiSettings( maxRetryDelayMs: 2, }, }, - shellPath: "/tmp/hostile-shell", + shellPath: '/tmp/hostile-shell', quietStartup: false, - shellCommandPrefix: "hostile-prefix", - npmCommand: ["hostile-npm"], + shellCommandPrefix: 'hostile-prefix', + npmCommand: ['hostile-npm'], collapseChangelog: true, enableInstallTelemetry: true, - packages: ["hostile-package"], - extensions: ["hostile-extension"], - skills: ["hostile-skill"], - prompts: ["hostile-prompt"], - themes: ["hostile-theme-path"], + packages: ['hostile-package'], + extensions: ['hostile-extension'], + skills: ['hostile-skill'], + prompts: ['hostile-prompt'], + themes: ['hostile-theme-path'], enableSkillCommands: true, terminal: { showImages: false, @@ -968,45 +885,36 @@ async function writeHostilePiSettings( autoResize: false, blockImages: true, }, - doubleEscapeAction: "tree", + doubleEscapeAction: 'tree', showHardwareCursor: true, editorPaddingX: 3, autocompleteMaxVisible: 20, - } - - await mkdir(agentDir, { recursive: true }) - await mkdir(join(cwd, ".pi"), { recursive: true }) - await writeFile( - join(agentDir, "settings.json"), - JSON.stringify(hostileSettings, null, 2), - ) - await writeFile( - join(cwd, ".pi", "settings.json"), - JSON.stringify(hostileSettings, null, 2), - ) + }; + + await mkdir(agentDir, { recursive: true }); + await mkdir(join(cwd, '.pi'), { recursive: true }); + await writeFile(join(agentDir, 'settings.json'), JSON.stringify(hostileSettings, null, 2)); + await writeFile(join(cwd, '.pi', 'settings.json'), JSON.stringify(hostileSettings, null, 2)); } -function readyWorkspace( - cwd: string, - sessionId: string, -): WorkspaceSessionReadyState { - const spec = { id: "spec-1", title: "Spec One" } +function readyWorkspace(cwd: string, sessionId: string): WorkspaceSessionReadyState { + const spec = { id: 'spec-1', title: 'Spec One' }; return { - status: "ready", + status: 'ready', cwd, spec, session: { id: sessionId, file: `/sessions/${sessionId}.jsonl`, - manager: {} as WorkspaceSessionReadyState["session"]["manager"], + manager: {} as WorkspaceSessionReadyState['session']['manager'], }, chrome: { cwd, spec, - phase: "elicitation", - chatMode: "responding-to-elicitation", + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - } + }; } function emptyInventory(cwd: string): WorkspaceLaunchInventory { @@ -1017,12 +925,10 @@ function emptyInventory(cwd: string): WorkspaceLaunchInventory { needsNewSpec: true, specs: [], unavailableSessions: [], - } + }; } -function inventoryWithWorkspace( - workspace: WorkspaceSessionReadyState, -): WorkspaceLaunchInventory { +function inventoryWithWorkspace(workspace: WorkspaceSessionReadyState): WorkspaceLaunchInventory { return { cwd: workspace.cwd, currentSpec: workspace.spec, @@ -1043,108 +949,102 @@ function inventoryWithWorkspace( }, ], unavailableSessions: [], - } + }; } function noOpWorkspaceCoordinator(cwd: string) { return { inspectWorkspace: async () => emptyInventory(cwd), - activateWorkspace: async () => readyWorkspace(cwd, "session-1"), - } + activateWorkspace: async () => readyWorkspace(cwd, 'session-1'), + }; } function fakeCommandContext(options: { - currentSessionFile: string - decision?: Awaited> - decisions?: Array>> - onCustomOptions?: (customOptions: unknown) => void - onEvent: (event: string) => void - replacementUi?: FakeExtensionUi + currentSessionFile: string; + decision?: Awaited>; + decisions?: Array>>; + onCustomOptions?: (customOptions: unknown) => void; + onEvent: (event: string) => void; + replacementUi?: FakeExtensionUi; }): ExtensionCommandContext { const ui = fakeUi((method, type) => { - if (method === "notify") { - options.onEvent(`notify:${type}`) + if (method === 'notify') { + options.onEvent(`notify:${type}`); } - }) - const decisions = [...(options.decisions ?? [options.decision])] + }); + const decisions = [...(options.decisions ?? [options.decision])]; const ctx = { - cwd: "/tmp/project", + cwd: '/tmp/project', sessionManager: { getSessionFile: () => options.currentSessionFile, }, ui: { ...ui, custom: async (_component: unknown, customOptions?: unknown) => { - options.onEvent("custom") + options.onEvent('custom'); if (customOptions !== undefined) { - options.onCustomOptions?.(customOptions) + options.onCustomOptions?.(customOptions); } - return decisions.shift() + return decisions.shift(); }, }, - waitForIdle: async () => options.onEvent("waitForIdle"), + waitForIdle: async () => options.onEvent('waitForIdle'), switchSession: async ( sessionPath: string, - switchOptions?: Parameters[1], + switchOptions?: Parameters[1], ) => { - options.onEvent(`switch:${sessionPath}`) + options.onEvent(`switch:${sessionPath}`); await switchOptions?.withSession?.({ ...ctx, ui: options.replacementUi ?? ui, sessionManager: { getSessionFile: () => sessionPath }, - } as never) - return { cancelled: false } + } as never); + return { cancelled: false }; }, - } - return ctx as unknown as ExtensionCommandContext + }; + return ctx as unknown as ExtensionCommandContext; } function fakeUi( - onCall: (method: string, notifyType?: "info" | "warning" | "error") => void, + onCall: (method: string, notifyType?: 'info' | 'warning' | 'error') => void, ): FakeExtensionUi { return { - setHeader: (_factory) => onCall("setHeader"), - setFooter: (_factory) => onCall("setFooter"), - setStatus: (_key, _text) => onCall("setStatus"), - setWidget: (_key, _content, _options) => onCall("setWidget"), - setWorkingIndicator: (_options) => onCall("setWorkingIndicator"), - setTitle: (_title) => onCall("setTitle"), - notify: (_message, type) => onCall("notify", type), - } + setHeader: (_factory) => onCall('setHeader'), + setFooter: (_factory) => onCall('setFooter'), + setStatus: (_key, _text) => onCall('setStatus'), + setWidget: (_key, _content, _options) => onCall('setWidget'), + setWorkingIndicator: (_options) => onCall('setWorkingIndicator'), + setTitle: (_title) => onCall('setTitle'), + notify: (_message, type) => onCall('notify', type), + }; } -type FakeExtensionContext = Pick & { - ui: FakeExtensionUi -} +type FakeExtensionContext = Pick & { + ui: FakeExtensionUi; +}; interface FakeAutocompleteItem { - value: string - label: string + value: string; + label: string; } interface FakeAutocompleteProvider { - getSuggestions( - lines: string[], - cursorLine: number, - cursorCol: number, - options: never, - ): Promise + getSuggestions(lines: string[], cursorLine: number, cursorCol: number, options: never): Promise; applyCompletion( lines: string[], cursorLine: number, cursorCol: number, item: FakeAutocompleteItem, prefix: string, - ): unknown - shouldTriggerFileCompletion( - lines: string[], - cursorLine: number, - cursorCol: number, - ): boolean + ): unknown; + shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean; } -type FakeExtensionUi = Pick +type FakeExtensionUi = Pick< + ExtensionUIContext, + 'setFooter' | 'setHeader' | 'setStatus' | 'setWidget' | 'setWorkingIndicator' | 'setTitle' | 'notify' +>; function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === "string") + return Array.isArray(value) && value.every((item) => typeof item === 'string'); } diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index 9bc53351e..749c7ebe7 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -1,4 +1,4 @@ -import process from "node:process" +import process from 'node:process'; import { createAgentSessionFromServices, @@ -7,8 +7,16 @@ import { getAgentDir, InteractiveMode, type CreateAgentSessionRuntimeFactory, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-coding-agent'; +import { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; +import { chromeStateForWorkspace, createBrunchPiExtensionShell } from './.pi/pi-extension-shell.js'; +import { + applyBrunchOfflineDefault, + brunchResourceLoaderOptions, + createBrunchPiProfile, + createBrunchSettingsManager, +} from './brunch-pi-profile.js'; import { createWorkspaceSessionCoordinator, type WorkspaceLaunchInventory, @@ -16,18 +24,7 @@ import { type WorkspaceSessionReadyState, type SpecSessionActivationCoordinator, type SpecSessionActivationDecision, -} from "./workspace-session-coordinator.js" -import { - chromeStateForWorkspace, - createBrunchPiExtensionShell, -} from "./.pi/pi-extension-shell.js" -import { runWorkspaceDialogPreflight } from "./.pi/components/workspace-dialog.js" -import { - applyBrunchOfflineDefault, - brunchResourceLoaderOptions, - createBrunchPiProfile, - createBrunchSettingsManager, -} from "./brunch-pi-profile.js" +} from './workspace-session-coordinator.js'; export { BRUNCH_SETTINGS_AUDITED_GETTERS, BRUNCH_SETTINGS_POLICY, @@ -35,7 +32,7 @@ export { brunchResourceLoaderOptions, createBrunchPiProfile, createBrunchSettingsManager, -} from "./brunch-pi-profile.js" +} from './brunch-pi-profile.js'; export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, chromeStateForWorkspace, @@ -47,48 +44,45 @@ export { type BrunchChromeStage, type BrunchChromeState, type BrunchChromeWorkerStatus, -} from "./.pi/pi-extension-shell.js" -export { runWorkspaceDialogPreflight } from "./.pi/components/workspace-dialog.js" +} from './.pi/pi-extension-shell.js'; +export { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; -export type BrunchTuiCoordinator = SpecSessionActivationCoordinator & WorkspaceSessionBoundaryCoordinator +export type BrunchTuiCoordinator = SpecSessionActivationCoordinator & WorkspaceSessionBoundaryCoordinator; export interface BrunchTuiLaunchContext { - workspace: WorkspaceSessionReadyState - coordinator: BrunchTuiCoordinator + workspace: WorkspaceSessionReadyState; + coordinator: BrunchTuiCoordinator; } export interface BrunchTuiOptions { - cwd?: string - coordinator?: BrunchTuiCoordinator - selectSpecTitle?: () => Promise + cwd?: string; + coordinator?: BrunchTuiCoordinator; + selectSpecTitle?: () => Promise; runWorkspaceDialogPreflight?: ( inventory: WorkspaceLaunchInventory, - ) => Promise - launchInteractive?: (context: BrunchTuiLaunchContext) => Promise + ) => Promise; + launchInteractive?: (context: BrunchTuiLaunchContext) => Promise; } -export async function runBrunchTui( - options: BrunchTuiOptions = {}, -): Promise { - const cwd = options.cwd ?? process.cwd() - const coordinator = - options.coordinator ?? createWorkspaceSessionCoordinator({ cwd }) +export async function runBrunchTui(options: BrunchTuiOptions = {}): Promise { + const cwd = options.cwd ?? process.cwd(); + const coordinator = options.coordinator ?? createWorkspaceSessionCoordinator({ cwd }); - const inventory = await coordinator.inspectWorkspace() - const decision = await chooseSpecSessionActivationDecision(inventory, options) - const workspaceState = await coordinator.activateWorkspace(decision) + const inventory = await coordinator.inspectWorkspace(); + const decision = await chooseSpecSessionActivationDecision(inventory, options); + const workspaceState = await coordinator.activateWorkspace(decision); - if (workspaceState.status === "cancelled") { - return + if (workspaceState.status === 'cancelled') { + return; } - if (workspaceState.status === "needs_human") { - throw new Error(workspaceState.reason) + if (workspaceState.status === 'needs_human') { + throw new Error(workspaceState.reason); } await (options.launchInteractive ?? launchPiInteractive)({ workspace: workspaceState, coordinator, - }) + }); } async function chooseSpecSessionActivationDecision( @@ -96,20 +90,17 @@ async function chooseSpecSessionActivationDecision( options: BrunchTuiOptions, ): Promise { if (options.runWorkspaceDialogPreflight) { - return options.runWorkspaceDialogPreflight(inventory) + return options.runWorkspaceDialogPreflight(inventory); } if (options.selectSpecTitle && inventory.needsNewSpec) { - const title = await options.selectSpecTitle() - return title ? { action: "newSpec", title } : { action: "cancel" } + const title = await options.selectSpecTitle(); + return title ? { action: 'newSpec', title } : { action: 'cancel' }; } - return runWorkspaceDialogPreflight(inventory) + return runWorkspaceDialogPreflight(inventory); } -async function launchPiInteractive({ - workspace, - coordinator, -}: BrunchTuiLaunchContext): Promise { - const agentDir = getAgentDir() +async function launchPiInteractive({ workspace, coordinator }: BrunchTuiLaunchContext): Promise { + const agentDir = getAgentDir(); const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, agentDir: runtimeAgentDir, @@ -122,37 +113,35 @@ async function launchPiInteractive({ createBrunchPiExtensionShell( chromeStateForWorkspace(workspace), async (sessionManager) => { - await coordinator.bindCurrentSpecToReplacementSession( - sessionManager, - ) + await coordinator.bindCurrentSpecToReplacementSession(sessionManager); }, { coordinator }, ), ], - }) + }); const services = await createAgentSessionServices({ cwd, agentDir: runtimeAgentDir, settingsManager: profile.settingsManager, resourceLoaderOptions: profile.resourceLoaderOptions, - }) + }); const created = await createAgentSessionFromServices({ services, sessionManager, - }) + }); return { ...created, services, diagnostics: services.diagnostics, - } - } + }; + }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: workspace.cwd, agentDir, sessionManager: workspace.session.manager, - }) + }); - applyBrunchOfflineDefault() - await new InteractiveMode(runtime).run() + applyBrunchOfflineDefault(); + await new InteractiveMode(runtime).run(); } diff --git a/src/brunch.smoke.test.ts b/src/brunch.smoke.test.ts index be8fde4a6..0d632d958 100644 --- a/src/brunch.smoke.test.ts +++ b/src/brunch.smoke.test.ts @@ -1,15 +1,11 @@ -import { describe, expect, it } from "vitest" +import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { - createAgentSession, - SessionManager, -} from "@earendil-works/pi-coding-agent" - -describe("pi-coding-agent import surface", () => { - it("exposes createAgentSession and SessionManager", () => { - expect(typeof createAgentSession).toBe("function") - expect(typeof SessionManager).toBe("function") - expect(typeof SessionManager.inMemory).toBe("function") - expect(typeof SessionManager.create).toBe("function") - }) -}) +describe('pi-coding-agent import surface', () => { + it('exposes createAgentSession and SessionManager', () => { + expect(typeof createAgentSession).toBe('function'); + expect(typeof SessionManager).toBe('function'); + expect(typeof SessionManager.inMemory).toBe('function'); + expect(typeof SessionManager.create).toBe('function'); + }); +}); diff --git a/src/brunch.test.ts b/src/brunch.test.ts index e5f1861f5..d8ac601e4 100644 --- a/src/brunch.test.ts +++ b/src/brunch.test.ts @@ -1,19 +1,18 @@ -import { mkdtemp } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" -import { PassThrough } from "node:stream" +import { mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PassThrough } from 'node:stream'; -import { describe, expect, it } from "vitest" +import { SessionManager } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { SessionManager } from "@earendil-works/pi-coding-agent" - -import { runBrunchCli, type WebHostRunnerOptions } from "./brunch.js" -import { createSessionBindingData } from "./session-binding.js" -import { assistantMessage, userMessage } from "./test-helpers.js" +import { runBrunchCli, type WebHostRunnerOptions } from './brunch.js'; +import { createSessionBindingData } from './session-binding.js'; +import { assistantMessage, userMessage } from './test-helpers.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from "./workspace-session-coordinator.js" +} from './workspace-session-coordinator.js'; function coordinator(sessionFile?: string): WorkspaceSessionCoordinator { return { @@ -21,185 +20,185 @@ function coordinator(sessionFile?: string): WorkspaceSessionCoordinator { return { ...(sessionFile ? { - status: "ready" as const, - spec: { id: "spec-1", title: "Alpha spec" }, + status: 'ready' as const, + spec: { id: 'spec-1', title: 'Alpha spec' }, session: { - id: "session-1", + id: 'session-1', file: sessionFile, manager: {} as never, }, chrome: { - cwd: "/tmp/brunch-project", - spec: { id: "spec-1", title: "Alpha spec" }, - phase: "elicitation" as const, - chatMode: "responding-to-elicitation" as const, + cwd: '/tmp/brunch-project', + spec: { id: 'spec-1', title: 'Alpha spec' }, + phase: 'elicitation' as const, + chatMode: 'responding-to-elicitation' as const, }, } : { - status: "select_spec" as const, + status: 'select_spec' as const, chrome: { - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', spec: null, - phase: "select_spec" as const, - chatMode: "select-spec" as const, + phase: 'select_spec' as const, + chatMode: 'select-spec' as const, }, }), - cwd: "/tmp/brunch-project", - } + cwd: '/tmp/brunch-project', + }; }, async createSetupSession() { - throw new Error("print must not create a session") + throw new Error('print must not create a session'); }, async createSetupSessionForCurrentSpec() { - throw new Error("not used") + throw new Error('not used'); }, async bindCurrentSpecToReplacementSession() { - throw new Error("not used") + throw new Error('not used'); }, async deriveDefaultChromeState() { - throw new Error("not used") + throw new Error('not used'); }, - } as unknown as WorkspaceSessionCoordinator + } as unknown as WorkspaceSessionCoordinator; } function rpcRequest(method: string, id = 1): PassThrough { - const stdin = new PassThrough() - stdin.end(`${JSON.stringify({ jsonrpc: "2.0", id, method })}\n`) - return stdin + const stdin = new PassThrough(); + stdin.end(`${JSON.stringify({ jsonrpc: '2.0', id, method })}\n`); + return stdin; } function collectStream(stream: PassThrough): string[] { - const chunks: string[] = [] - stream.on("data", (chunk) => chunks.push(String(chunk))) - return chunks + const chunks: string[] = []; + stream.on('data', (chunk) => chunks.push(String(chunk))); + return chunks; } -describe("Brunch CLI dispatch", () => { - it("routes --mode web through an injectable web host runner", async () => { - let launchedWith: WebHostRunnerOptions | null = null +describe('Brunch CLI dispatch', () => { + it('routes --mode web through an injectable web host runner', async () => { + let launchedWith: WebHostRunnerOptions | null = null; const code = await runBrunchCli({ - argv: ["--mode=web"], - cwd: "/tmp/brunch-project", + argv: ['--mode=web'], + cwd: '/tmp/brunch-project', coordinator: coordinator(), webHostRunner: async (options) => { - launchedWith = options + launchedWith = options; }, - }) + }); - expect(code).toBe(0) - expect(launchedWith).toMatchObject({ cwd: "/tmp/brunch-project" }) - }) + expect(code).toBe(0); + expect(launchedWith).toMatchObject({ cwd: '/tmp/brunch-project' }); + }); - it("routes --mode print through the coordinator snapshot and exits", async () => { - let output = "" + it('routes --mode print through the coordinator snapshot and exits', async () => { + let output = ''; const code = await runBrunchCli({ - argv: ["--mode", "print"], - cwd: "/tmp/brunch-project", + argv: ['--mode', 'print'], + cwd: '/tmp/brunch-project', coordinator: coordinator(), stdout: (chunk) => { - output += chunk + output += chunk; }, - }) + }); - expect(code).toBe(0) - expect(output).toContain("status: select_spec") - expect(output).toContain("spec: ") - }) + expect(code).toBe(0); + expect(output).toContain('status: select_spec'); + expect(output).toContain('spec: '); + }); - it("routes --mode rpc session projection through the coordinator-selected session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-cli-rpc-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) + it('routes --mode rpc session projection through the coordinator-selected session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-cli-rpc-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); manager.appendCustomEntry( - "brunch.session_binding", + 'brunch.session_binding', createSessionBindingData({ sessionId: manager.getSessionId(), - specId: "spec-1", - specTitle: "Spec", + specId: 'spec-1', + specTitle: 'Spec', }), - ) - manager.appendMessage(assistantMessage("Question")) - manager.appendMessage(userMessage("Answer")) - const stdout = new PassThrough() - const chunks = collectStream(stdout) + ); + manager.appendMessage(assistantMessage('Question')); + manager.appendMessage(userMessage('Answer')); + const stdout = new PassThrough(); + const chunks = collectStream(stdout); const code = await runBrunchCli({ - argv: ["--mode=rpc"], - cwd: "/tmp/brunch-project", + argv: ['--mode=rpc'], + cwd: '/tmp/brunch-project', coordinator: coordinator(manager.getSessionFile()!), - stdin: rpcRequest("session.elicitationExchanges", 2), + stdin: rpcRequest('session.elicitationExchanges', 2), stdout, - }) + }); - expect(code).toBe(0) - expect(JSON.parse(chunks.join(""))).toMatchObject({ - jsonrpc: "2.0", + expect(code).toBe(0); + expect(JSON.parse(chunks.join(''))).toMatchObject({ + jsonrpc: '2.0', id: 2, result: { - status: "ready", + status: 'ready', exchanges: [{ promptEntryIds: [expect.any(String)] }], }, - }) - }) + }); + }); - it("routes --mode rpc through the named JSON-RPC stdio adapter", async () => { - const stdout = new PassThrough() - const chunks = collectStream(stdout) + it('routes --mode rpc through the named JSON-RPC stdio adapter', async () => { + const stdout = new PassThrough(); + const chunks = collectStream(stdout); const code = await runBrunchCli({ - argv: ["--mode=rpc"], - cwd: "/tmp/brunch-project", + argv: ['--mode=rpc'], + cwd: '/tmp/brunch-project', coordinator: coordinator(), - stdin: rpcRequest("workspace.snapshot"), + stdin: rpcRequest('workspace.snapshot'), stdout, - }) + }); - expect(code).toBe(0) - expect(JSON.parse(chunks.join(""))).toMatchObject({ - jsonrpc: "2.0", + expect(code).toBe(0); + expect(JSON.parse(chunks.join(''))).toMatchObject({ + jsonrpc: '2.0', id: 1, - result: { status: "select_spec" }, - }) - }) + result: { status: 'select_spec' }, + }); + }); - it("exposes matching print and RPC workspace snapshots from a real coordinator store", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-parity-")) + it('exposes matching print and RPC workspace snapshots from a real coordinator store', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-parity-')); await createWorkspaceSessionCoordinator({ cwd }).createSetupSession({ - specTitle: "Parity spec", - }) - let printOutput = "" - const rpcOutput = new PassThrough() - const rpcChunks = collectStream(rpcOutput) + specTitle: 'Parity spec', + }); + let printOutput = ''; + const rpcOutput = new PassThrough(); + const rpcChunks = collectStream(rpcOutput); await runBrunchCli({ - argv: ["--mode=print"], + argv: ['--mode=print'], cwd, stdout: (chunk) => { - printOutput += chunk + printOutput += chunk; }, - }) + }); await runBrunchCli({ - argv: ["--mode=rpc"], + argv: ['--mode=rpc'], cwd, - stdin: rpcRequest("workspace.snapshot"), + stdin: rpcRequest('workspace.snapshot'), stdout: rpcOutput, - }) - - const rpcSnapshot = JSON.parse(rpcChunks.join("")).result - expect(printOutput).toContain("status: ready") - expect(printOutput).toContain(`cwd: ${rpcSnapshot.cwd}`) - expect(printOutput).toContain("spec: Parity spec") - expect(printOutput).toContain(`phase: ${rpcSnapshot.chrome.phase}`) - expect(printOutput).toContain(`chatMode: ${rpcSnapshot.chrome.chatMode}`) + }); + + const rpcSnapshot = JSON.parse(rpcChunks.join('')).result; + expect(printOutput).toContain('status: ready'); + expect(printOutput).toContain(`cwd: ${rpcSnapshot.cwd}`); + expect(printOutput).toContain('spec: Parity spec'); + expect(printOutput).toContain(`phase: ${rpcSnapshot.chrome.phase}`); + expect(printOutput).toContain(`chatMode: ${rpcSnapshot.chrome.chatMode}`); expect(rpcSnapshot).toMatchObject({ - status: "ready", + status: 'ready', cwd, - spec: { title: "Parity spec" }, + spec: { title: 'Parity spec' }, chrome: { - phase: "elicitation", - chatMode: "responding-to-elicitation", + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - }) - }) -}) + }); + }); +}); diff --git a/src/brunch.ts b/src/brunch.ts index 072ef0b83..8a4a40de0 100644 --- a/src/brunch.ts +++ b/src/brunch.ts @@ -1,132 +1,121 @@ -import process from "node:process" -import type { Readable, Writable } from "node:stream" -import { fileURLToPath } from "node:url" - -import { runBrunchTui } from "./brunch-tui.js" -import { - renderWorkspaceSnapshot, - workspaceSnapshotFromState, -} from "./print-snapshot.js" -import { createRpcHandlers, runJsonRpcLineServer } from "./rpc/handlers.js" -import { startWebHost } from "./web-host.js" +import process from 'node:process'; +import type { Readable, Writable } from 'node:stream'; +import { fileURLToPath } from 'node:url'; + +import { runBrunchTui } from './brunch-tui.js'; +import { renderWorkspaceSnapshot, workspaceSnapshotFromState } from './print-snapshot.js'; +import { createRpcHandlers, runJsonRpcLineServer } from './rpc/handlers.js'; +import { startWebHost } from './web-host.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from "./workspace-session-coordinator.js" +} from './workspace-session-coordinator.js'; export interface WebHostRunnerOptions { - cwd: string - coordinator: WorkspaceSessionCoordinator + cwd: string; + coordinator: WorkspaceSessionCoordinator; } export interface BrunchCliOptions { - argv?: string[] - cwd?: string - coordinator?: WorkspaceSessionCoordinator - stdin?: Readable - stdout?: Writable | ((chunk: string) => void) - webHostRunner?: (options: WebHostRunnerOptions) => Promise + argv?: string[]; + cwd?: string; + coordinator?: WorkspaceSessionCoordinator; + stdin?: Readable; + stdout?: Writable | ((chunk: string) => void); + webHostRunner?: (options: WebHostRunnerOptions) => Promise; } -export async function runBrunchCli( - options: BrunchCliOptions = {}, -): Promise { - const argv = options.argv ?? process.argv.slice(2) - const cwd = options.cwd ?? process.cwd() - const mode = parseMode(argv) - const coordinator = - options.coordinator ?? createWorkspaceSessionCoordinator({ cwd }) - - if (mode === "print") { - const state = await coordinator.openDefaultWorkspace() - const snapshot = workspaceSnapshotFromState(state) - writeStdout(options.stdout, renderWorkspaceSnapshot(snapshot)) - return 0 +export async function runBrunchCli(options: BrunchCliOptions = {}): Promise { + const argv = options.argv ?? process.argv.slice(2); + const cwd = options.cwd ?? process.cwd(); + const mode = parseMode(argv); + const coordinator = options.coordinator ?? createWorkspaceSessionCoordinator({ cwd }); + + if (mode === 'print') { + const state = await coordinator.openDefaultWorkspace(); + const snapshot = workspaceSnapshotFromState(state); + writeStdout(options.stdout, renderWorkspaceSnapshot(snapshot)); + return 0; } - if (mode === "rpc") { + if (mode === 'rpc') { await runJsonRpcLineServer({ input: options.stdin ?? process.stdin, output: stdoutStream(options.stdout), handlers: createRpcHandlers({ coordinator, cwd }), - }) - return 0 + }); + return 0; } - if (mode === "web") { - await (options.webHostRunner ?? runDefaultWebHost)({ cwd, coordinator }) - return 0 + if (mode === 'web') { + await (options.webHostRunner ?? runDefaultWebHost)({ cwd, coordinator }); + return 0; } - if (mode === "tui") { - await runBrunchTui({ cwd, coordinator }) - return 0 + if (mode === 'tui') { + await runBrunchTui({ cwd, coordinator }); + return 0; } - throw new Error(`Unsupported Brunch mode: ${mode}`) + throw new Error(`Unsupported Brunch mode: ${mode}`); } async function runDefaultWebHost(options: WebHostRunnerOptions): Promise { const host = await startWebHost({ cwd: options.cwd, coordinator: options.coordinator, - }) - process.stdout.write(`Brunch web listening on ${host.url}\n`) - await new Promise(() => {}) + }); + process.stdout.write(`Brunch web listening on ${host.url}\n`); + await new Promise(() => {}); } -function writeStdout( - stdout: Writable | ((chunk: string) => void) | undefined, - chunk: string, -): void { +function writeStdout(stdout: Writable | ((chunk: string) => void) | undefined, chunk: string): void { if (!stdout) { - process.stdout.write(chunk) - } else if (typeof stdout === "function") { - stdout(chunk) + process.stdout.write(chunk); + } else if (typeof stdout === 'function') { + stdout(chunk); } else { - stdout.write(chunk) + stdout.write(chunk); } } -function stdoutStream( - stdout: Writable | ((chunk: string) => void) | undefined, -): Writable { +function stdoutStream(stdout: Writable | ((chunk: string) => void) | undefined): Writable { if (!stdout) { - return process.stdout + return process.stdout; } - if (typeof stdout !== "function") { - return stdout + if (typeof stdout !== 'function') { + return stdout; } return { write(chunk: string | Uint8Array) { - stdout(String(chunk)) - return true + stdout(String(chunk)); + return true; }, - } as Writable + } as Writable; } function parseMode(argv: string[]): string { - const modeFlagIndex = argv.indexOf("--mode") + const modeFlagIndex = argv.indexOf('--mode'); if (modeFlagIndex >= 0) { - return argv[modeFlagIndex + 1] ?? "tui" + return argv[modeFlagIndex + 1] ?? 'tui'; } - const modeEquals = argv.find((arg) => arg.startsWith("--mode=")) + const modeEquals = argv.find((arg) => arg.startsWith('--mode=')); if (modeEquals) { - return modeEquals.slice("--mode=".length) + return modeEquals.slice('--mode='.length); } - return "tui" + return 'tui'; } async function main(): Promise { - process.exitCode = await runBrunchCli() + process.exitCode = await runBrunchCli(); } if (process.argv[1] === fileURLToPath(import.meta.url)) { main().catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error) - process.stderr.write(`${message}\n`) - process.exitCode = 1 - }) + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; + }); } diff --git a/src/db/connection.ts b/src/db/connection.ts index 2e9022879..6d0e83264 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -5,12 +5,12 @@ * Stack: drizzle-orm@0.45.2 + better-sqlite3@12.8.0 */ -import Database from "better-sqlite3" -import { drizzle } from "drizzle-orm/better-sqlite3" +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; -import * as schema from "./schema.js" +import * as schema from './schema.js'; -export type BrunchDb = ReturnType> +export type BrunchDb = ReturnType>; /** * Create a Brunch database connection with schema initialized. @@ -22,11 +22,11 @@ export type BrunchDb = ReturnType> * replace `initSchema` with `drizzle-kit`-managed migrations. */ export function createDb(path: string): BrunchDb { - const sqlite = new Database(path) - sqlite.pragma("journal_mode = WAL") - sqlite.pragma("foreign_keys = ON") - initSchema(sqlite) - return drizzle(sqlite, { schema }) + const sqlite = new Database(path); + sqlite.pragma('journal_mode = WAL'); + sqlite.pragma('foreign_keys = ON'); + initSchema(sqlite); + return drizzle(sqlite, { schema }); } /** @@ -90,5 +90,5 @@ function initSchema(sqlite: Database.Database): void { ); INSERT OR IGNORE INTO graph_clock (id, lsn) VALUES (1, 0); - `) + `); } diff --git a/src/db/row-schemas.ts b/src/db/row-schemas.ts index ae5bd15a0..b7f7b4dbf 100644 --- a/src/db/row-schemas.ts +++ b/src/db/row-schemas.ts @@ -8,33 +8,25 @@ * derived source for insert/select validation inside db/ and graph/. */ -import { createInsertSchema, createSelectSchema } from "drizzle-typebox" +import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'; -import { - changeLog, - edges, - graphClock, - nodes, - reconciliationNeed, -} from "./schema.js" +import { changeLog, edges, graphClock, nodes, reconciliationNeed } from './schema.js'; // --- Node schemas --- -export const insertNodeSchema = createInsertSchema(nodes) -export const selectNodeSchema = createSelectSchema(nodes) +export const insertNodeSchema = createInsertSchema(nodes); +export const selectNodeSchema = createSelectSchema(nodes); // --- Edge schemas --- -export const insertEdgeSchema = createInsertSchema(edges) -export const selectEdgeSchema = createSelectSchema(edges) +export const insertEdgeSchema = createInsertSchema(edges); +export const selectEdgeSchema = createSelectSchema(edges); // --- Change log schemas --- -export const insertChangeLogSchema = createInsertSchema(changeLog) -export const selectChangeLogSchema = createSelectSchema(changeLog) +export const insertChangeLogSchema = createInsertSchema(changeLog); +export const selectChangeLogSchema = createSelectSchema(changeLog); // --- Graph clock schemas --- -export const insertGraphClockSchema = createInsertSchema(graphClock) +export const insertGraphClockSchema = createInsertSchema(graphClock); // --- Reconciliation need schemas --- -export const insertReconciliationNeedSchema = - createInsertSchema(reconciliationNeed) -export const selectReconciliationNeedSchema = - createSelectSchema(reconciliationNeed) +export const insertReconciliationNeedSchema = createInsertSchema(reconciliationNeed); +export const selectReconciliationNeedSchema = createSelectSchema(reconciliationNeed); diff --git a/src/db/schema.ts b/src/db/schema.ts index 16622d4e7..fe580e702 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -8,8 +8,8 @@ * and by Pi tool parameter schemas (via typebox v1.x). */ -import { sql } from "drizzle-orm" -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { sql } from 'drizzle-orm'; +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; // --------------------------------------------------------------------------- // Shared enum arrays — the single source for text enum columns, @@ -17,63 +17,58 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" // --------------------------------------------------------------------------- export const INTENT_KINDS = [ - "goal", - "thesis", - "term", - "context", - "requirement", - "assumption", - "constraint", - "invariant", - "decision", - "criterion", - "example", -] as const + 'goal', + 'thesis', + 'term', + 'context', + 'requirement', + 'assumption', + 'constraint', + 'invariant', + 'decision', + 'criterion', + 'example', +] as const; -export const ORACLE_KINDS = [ - "check", - "validation_method", - "evidence", - "obligation", -] as const +export const ORACLE_KINDS = ['check', 'validation_method', 'evidence', 'obligation'] as const; -export const DESIGN_KINDS = ["module", "interface"] as const +export const DESIGN_KINDS = ['module', 'interface'] as const; -export const PLAN_KINDS = ["milestone", "frontier", "slice"] as const +export const PLAN_KINDS = ['milestone', 'frontier', 'slice'] as const; -export const NODE_BASES = ["explicit", "accepted_review_set"] as const +export const NODE_BASES = ['explicit', 'accepted_review_set'] as const; export const EDGE_CATEGORIES = [ - "dependency", - "proof", - "support", - "realization", - "boundary", - "composition", - "association", - "supersession", -] as const + 'dependency', + 'proof', + 'support', + 'realization', + 'boundary', + 'composition', + 'association', + 'supersession', +] as const; -export const EDGE_STANCES = ["for", "against"] as const +export const EDGE_STANCES = ['for', 'against'] as const; // --------------------------------------------------------------------------- // Tables // --------------------------------------------------------------------------- -export const nodes = sqliteTable("nodes", { +export const nodes = sqliteTable('nodes', { id: integer().primaryKey({ autoIncrement: true }), - plane: text({ enum: ["intent", "oracle", "design", "plan"] }).notNull(), + plane: text({ enum: ['intent', 'oracle', 'design', 'plan'] }).notNull(), kind: text().notNull(), // validated at domain layer against plane-specific enum title: text().notNull(), body: text(), - basis: text({ enum: NODE_BASES }).notNull().default("explicit"), + basis: text({ enum: NODE_BASES }).notNull().default('explicit'), source: text(), detail: text(), // JSON column: decision → {chosen_option, rejected, rationale}, term → {definition, aliases?} created_at_lsn: integer().notNull(), updated_at_lsn: integer().notNull(), -}) +}); -export const edges = sqliteTable("edges", { +export const edges = sqliteTable('edges', { id: integer().primaryKey({ autoIncrement: true }), category: text({ enum: EDGE_CATEGORIES }).notNull(), source_id: integer() @@ -83,36 +78,38 @@ export const edges = sqliteTable("edges", { .notNull() .references(() => nodes.id), stance: text({ enum: EDGE_STANCES }), - basis: text({ enum: NODE_BASES }).notNull().default("explicit"), + basis: text({ enum: NODE_BASES }).notNull().default('explicit'), rationale: text(), created_at_lsn: integer().notNull(), updated_at_lsn: integer().notNull(), -}) +}); -export const graphClock = sqliteTable("graph_clock", { +export const graphClock = sqliteTable('graph_clock', { id: integer().primaryKey(), // always row 1 lsn: integer().notNull().default(0), -}) +}); -export const changeLog = sqliteTable("change_log", { +export const changeLog = sqliteTable('change_log', { lsn: integer().primaryKey(), operation: text().notNull(), payload: text().notNull(), // JSON summary of the mutation - created_at: text().notNull().default(sql`(datetime('now'))`), -}) + created_at: text() + .notNull() + .default(sql`(datetime('now'))`), +}); -export const reconciliationNeed = sqliteTable("reconciliation_need", { +export const reconciliationNeed = sqliteTable('reconciliation_need', { id: integer().primaryKey({ autoIncrement: true }), // target is {kind:'edge', edgeId} or {kind:'node_pair', aId, bId} - target_kind: text({ enum: ["edge", "node_pair"] }).notNull(), + target_kind: text({ enum: ['edge', 'node_pair'] }).notNull(), target_edge_id: integer().references(() => edges.id), target_a_id: integer().references(() => nodes.id), target_b_id: integer().references(() => nodes.id), kind: text().notNull(), // substantive taxonomy deferred per A8-L - status: text({ enum: ["open", "resolved"] }) + status: text({ enum: ['open', 'resolved'] }) .notNull() - .default("open"), + .default('open'), reason: text(), created_at_lsn: integer().notNull(), resolved_at_lsn: integer(), -}) +}); diff --git a/src/elicitation-exchange.test.ts b/src/elicitation-exchange.test.ts index 636907562..0482bca9e 100644 --- a/src/elicitation-exchange.test.ts +++ b/src/elicitation-exchange.test.ts @@ -1,13 +1,10 @@ -import { mkdtemp, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" -import { describe, expect, it } from "vitest" +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { SessionManager } from "@earendil-works/pi-coding-agent" +import { SessionManager } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { createSessionBindingData } from "./session-binding.js" -import { STRUCTURED_EXCHANGE_RESULT_SCHEMA } from "./structured-exchange.js" -import { assistantMessage, userMessage } from "./test-helpers.js" import { loadJsonlTranscriptEntries, loadLinearElicitationExchangeProjection, @@ -15,362 +12,348 @@ import { NonLinearTranscriptError, projectElicitationExchanges, projectTranscriptDisplay, -} from "./elicitation-exchange.js" +} from './elicitation-exchange.js'; +import { createSessionBindingData } from './session-binding.js'; +import { STRUCTURED_EXCHANGE_RESULT_SCHEMA } from './structured-exchange.js'; +import { assistantMessage, userMessage } from './test-helpers.js'; const assistant = { - id: "a1", - type: "message", - message: assistantMessage("Pick one"), -} + id: 'a1', + type: 'message', + message: assistantMessage('Pick one'), +}; const structuredPrompt = { - id: "p1", - type: "custom", - customType: "brunch.elicitation_prompt", - data: { choices: ["A", "B"] }, -} + id: 'p1', + type: 'custom', + customType: 'brunch.elicitation_prompt', + data: { choices: ['A', 'B'] }, +}; const toolResult = { - id: "t1", - type: "message", + id: 't1', + type: 'message', message: { - role: "toolResult", - toolCallId: "call-1", - toolName: "read", - content: [{ type: "text", text: "tool output" }], + role: 'toolResult', + toolCallId: 'call-1', + toolName: 'read', + content: [{ type: 'text', text: 'tool output' }], isError: false, }, -} +}; const presentQuestionToolResult = { - id: "present-question-1", - type: "message", + id: 'present-question-1', + type: 'message', parentId: null, message: { - role: "toolResult", - toolCallId: "present-call-1", - toolName: "present_question", - content: [{ type: "text", text: "## Domain?\n\nWhat are we specifying?" }], + role: 'toolResult', + toolCallId: 'present-call-1', + toolName: 'present_question', + content: [{ type: 'text', text: '## Domain?\n\nWhat are we specifying?' }], details: { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', schemaVersion: 1, - exchangeId: "domain", - presentTool: "present_question", - kind: "question", - status: "presented", - expectedRequest: { tool: "request_answer", required: true }, - createdAtToolCallId: "present-call-1", + exchangeId: 'domain', + presentTool: 'present_question', + kind: 'question', + status: 'presented', + expectedRequest: { tool: 'request_answer', required: true }, + createdAtToolCallId: 'present-call-1', }, isError: false, }, -} +}; const requestAnswerToolResult = { - id: "request-answer-1", - type: "message", - parentId: "present-question-1", + id: 'request-answer-1', + type: 'message', + parentId: 'present-question-1', message: { - role: "toolResult", - toolCallId: "request-call-1", - toolName: "request_answer", - content: [{ type: "text", text: "### Response\n\nDeveloper tooling" }], + role: 'toolResult', + toolCallId: 'request-call-1', + toolName: 'request_answer', + content: [{ type: 'text', text: '### Response\n\nDeveloper tooling' }], details: { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, - exchangeId: "domain", - requestTool: "request_answer", - status: "answered", - respondsTo: { exchangeId: "domain", presentTool: "present_question" }, - answer: "Developer tooling", - createdAtToolCallId: "request-call-1", + exchangeId: 'domain', + requestTool: 'request_answer', + status: 'answered', + respondsTo: { exchangeId: 'domain', presentTool: 'present_question' }, + answer: 'Developer tooling', + createdAtToolCallId: 'request-call-1', }, isError: false, }, -} +}; const mismatchedRequestAnswerToolResult = { ...requestAnswerToolResult, - id: "request-answer-mismatch", + id: 'request-answer-mismatch', message: { ...requestAnswerToolResult.message, details: { ...requestAnswerToolResult.message.details, - exchangeId: "other-domain", + exchangeId: 'other-domain', respondsTo: { - exchangeId: "other-domain", - presentTool: "present_question", + exchangeId: 'other-domain', + presentTool: 'present_question', }, }, }, -} +}; const requestChoicesToolResult = { - id: "request-choices-1", - type: "message", - parentId: "present-options-1", + id: 'request-choices-1', + type: 'message', + parentId: 'present-options-1', message: { - role: "toolResult", - toolCallId: "request-call-choices-1", - toolName: "request_choices", + role: 'toolResult', + toolCallId: 'request-call-choices-1', + toolName: 'request_choices', content: [ { - type: "text", - text: "### Response\n\n- Move quickly\n- Other\n\nComment:\n\n> Keep it deterministic.", + type: 'text', + text: '### Response\n\n- Move quickly\n- Other\n\nComment:\n\n> Keep it deterministic.', }, ], details: { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, - exchangeId: "domain", - requestTool: "request_choices", - status: "answered", - respondsTo: { exchangeId: "domain", presentTool: "present_options" }, + exchangeId: 'domain', + requestTool: 'request_choices', + status: 'answered', + respondsTo: { exchangeId: 'domain', presentTool: 'present_options' }, choices: [ - { id: "speed", label: "Move quickly" }, - { id: "other", label: "Other" }, + { id: 'speed', label: 'Move quickly' }, + { id: 'other', label: 'Other' }, ], - comment: "Keep it deterministic.", - createdAtToolCallId: "request-call-choices-1", + comment: 'Keep it deterministic.', + createdAtToolCallId: 'request-call-choices-1', }, isError: false, }, -} +}; const structuredExchangeToolResult = { - id: "sq1", - type: "message", + id: 'sq1', + type: 'message', message: { - role: "toolResult", - toolCallId: "call-exchange-1", - toolName: "structured_exchange", - content: [{ type: "text", text: "User answered: Developer tooling" }], + role: 'toolResult', + toolCallId: 'call-exchange-1', + toolName: 'structured_exchange', + content: [{ type: 'text', text: 'User answered: Developer tooling' }], details: { schema: STRUCTURED_EXCHANGE_RESULT_SCHEMA, schemaVersion: 1, - status: "answered", - question: "Domain?", - mode: "text", + status: 'answered', + question: 'Domain?', + mode: 'text', answers: [ { - type: "text", - label: "Developer tooling", - value: "Developer tooling", + type: 'text', + label: 'Developer tooling', + value: 'Developer tooling', }, ], - transport: { surface: "rpc-editor" }, + transport: { surface: 'rpc-editor' }, }, isError: false, }, -} +}; const unavailableStructuredExchangeToolResult = { - id: "sq-unavailable", - type: "message", + id: 'sq-unavailable', + type: 'message', message: { - role: "toolResult", - toolCallId: "call-exchange-2", - toolName: "structured_exchange", - content: [{ type: "text", text: "Structured exchange unavailable." }], + role: 'toolResult', + toolCallId: 'call-exchange-2', + toolName: 'structured_exchange', + content: [{ type: 'text', text: 'Structured exchange unavailable.' }], details: { schema: STRUCTURED_EXCHANGE_RESULT_SCHEMA, schemaVersion: 1, - status: "unavailable", - question: "Domain?", - mode: "text", + status: 'unavailable', + question: 'Domain?', + mode: 'text', answers: [], - transport: { surface: "headless" }, - message: "Structured exchange UI is unavailable.", + transport: { surface: 'headless' }, + message: 'Structured exchange UI is unavailable.', }, isError: false, }, -} +}; const user = { - id: "u1", - type: "message", - message: userMessage("A"), -} + id: 'u1', + type: 'message', + message: userMessage('A'), +}; const structuredResponse = { - id: "r1", - type: "custom", - customType: "brunch.elicitation_response", - data: { choice: "A" }, -} + id: 'r1', + type: 'custom', + customType: 'brunch.elicitation_response', + data: { choice: 'A' }, +}; function appendBinding(manager: SessionManager): void { manager.appendCustomEntry( - "brunch.session_binding", + 'brunch.session_binding', createSessionBindingData({ sessionId: manager.getSessionId(), - specId: "spec-1", - specTitle: "Spec", + specId: 'spec-1', + specTitle: 'Spec', }), - ) + ); } -describe("elicitation exchange projection", () => { - it("projects assistant prompt spans and user response spans with stable ranges", () => { +describe('elicitation exchange projection', () => { + it('projects assistant prompt spans and user response spans with stable ranges', () => { const exchanges = projectElicitationExchanges([ - { id: "s1", type: "session" }, + { id: 's1', type: 'session' }, assistant, structuredPrompt, user, { - id: "a2", - type: "message", - message: assistantMessage("Why?"), + id: 'a2', + type: 'message', + message: assistantMessage('Why?'), }, { - id: "u2", - type: "message", - message: userMessage("Because"), + id: 'u2', + type: 'message', + message: userMessage('Because'), }, - ]) + ]); expect(exchanges).toEqual({ - status: "ready", + status: 'ready', exchanges: [ { - promptRange: { start: "a1", end: "p1" }, - responseRange: { start: "u1", end: "u1" }, - promptEntryIds: ["a1", "p1"], - responseEntryIds: ["u1"], + promptRange: { start: 'a1', end: 'p1' }, + responseRange: { start: 'u1', end: 'u1' }, + promptEntryIds: ['a1', 'p1'], + responseEntryIds: ['u1'], }, { - promptRange: { start: "a2", end: "a2" }, - responseRange: { start: "u2", end: "u2" }, - promptEntryIds: ["a2"], - responseEntryIds: ["u2"], + promptRange: { start: 'a2', end: 'a2' }, + responseRange: { start: 'u2', end: 'u2' }, + promptEntryIds: ['a2'], + responseEntryIds: ['u2'], }, ], openPrompt: null, - }) - }) + }); + }); - it("includes known elicitor custom entries on the prompt side", () => { + it('includes known elicitor custom entries on the prompt side', () => { const projection = projectElicitationExchanges([ assistant, { - id: "offer-1", - type: "custom", - customType: "brunch.establishment_offer", - data: { lens: "step-by-step" }, + id: 'offer-1', + type: 'custom', + customType: 'brunch.establishment_offer', + data: { lens: 'step-by-step' }, }, { - id: "proposal-1", - type: "custom", - customType: "brunch.review_set_proposal", - data: { lens: "propose-scenarios-with-tradeoffs" }, + id: 'proposal-1', + type: 'custom', + customType: 'brunch.review_set_proposal', + data: { lens: 'propose-scenarios-with-tradeoffs' }, }, user, - ]) + ]); - expect(projection.exchanges[0]?.promptEntryIds).toEqual([ - "a1", - "offer-1", - "proposal-1", - ]) - }) + expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1', 'offer-1', 'proposal-1']); + }); - it("ignores unknown custom entries even when their type contains prompt", () => { + it('ignores unknown custom entries even when their type contains prompt', () => { const projection = projectElicitationExchanges([ assistant, { - id: "operational-1", - type: "custom", - customType: "brunch.operational_prompt_cache", + id: 'operational-1', + type: 'custom', + customType: 'brunch.operational_prompt_cache', data: {}, }, user, - ]) + ]); - expect(projection.exchanges[0]?.promptEntryIds).toEqual(["a1"]) - }) + expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1']); + }); - it("includes structured response entries on the response side", () => { - const projection = projectElicitationExchanges([ - assistant, - user, - structuredResponse, - ]) + it('includes structured response entries on the response side', () => { + const projection = projectElicitationExchanges([assistant, user, structuredResponse]); - expect(projection.exchanges[0]?.responseEntryIds).toEqual(["u1", "r1"]) + expect(projection.exchanges[0]?.responseEntryIds).toEqual(['u1', 'r1']); expect(projection.exchanges[0]?.responseRange).toEqual({ - start: "u1", - end: "r1", - }) - }) + start: 'u1', + end: 'r1', + }); + }); - it("includes Pi toolResult messages on the prompt side", () => { - const projection = projectElicitationExchanges([ - assistant, - toolResult, - user, - ]) + it('includes Pi toolResult messages on the prompt side', () => { + const projection = projectElicitationExchanges([assistant, toolResult, user]); - expect(projection.exchanges[0]?.promptEntryIds).toEqual(["a1", "t1"]) + expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1', 't1']); expect(projection.exchanges[0]?.promptRange).toEqual({ - start: "a1", - end: "t1", - }) - }) + start: 'a1', + end: 't1', + }); + }); - it("projects an unmatched present tool result as an open prompt", () => { - const projection = projectElicitationExchanges([presentQuestionToolResult]) + it('projects an unmatched present tool result as an open prompt', () => { + const projection = projectElicitationExchanges([presentQuestionToolResult]); expect(projection).toEqual({ - status: "open_prompt", + status: 'open_prompt', exchanges: [], openPrompt: { - promptRange: { start: "present-question-1", end: "present-question-1" }, - promptEntryIds: ["present-question-1"], + promptRange: { start: 'present-question-1', end: 'present-question-1' }, + promptEntryIds: ['present-question-1'], }, - }) - }) + }); + }); - it("closes a present/request structured-exchange tuple only when request details match", () => { - const projection = projectElicitationExchanges([ - presentQuestionToolResult, - requestAnswerToolResult, - ]) + it('closes a present/request structured-exchange tuple only when request details match', () => { + const projection = projectElicitationExchanges([presentQuestionToolResult, requestAnswerToolResult]); expect(projection).toEqual({ - status: "ready", + status: 'ready', exchanges: [ { promptRange: { - start: "present-question-1", - end: "present-question-1", + start: 'present-question-1', + end: 'present-question-1', }, - responseRange: { start: "request-answer-1", end: "request-answer-1" }, - promptEntryIds: ["present-question-1"], - responseEntryIds: ["request-answer-1"], + responseRange: { start: 'request-answer-1', end: 'request-answer-1' }, + promptEntryIds: ['present-question-1'], + responseEntryIds: ['request-answer-1'], }, ], openPrompt: null, - }) - }) + }); + }); - it("does not close an open present with a mismatched request tuple", () => { + it('does not close an open present with a mismatched request tuple', () => { const projection = projectElicitationExchanges([ presentQuestionToolResult, mismatchedRequestAnswerToolResult, - ]) + ]); - expect(projection.exchanges).toEqual([]) - expect(projection.openPrompt?.promptEntryIds).toEqual([ - "present-question-1", - ]) - }) + expect(projection.exchanges).toEqual([]); + expect(projection.openPrompt?.promptEntryIds).toEqual(['present-question-1']); + }); - it.each(["answered", "cancelled", "unavailable"] as const)( - "closes present_options with a terminal %s request_choices result", + it.each(['answered', 'cancelled', 'unavailable'] as const)( + 'closes present_options with a terminal %s request_choices result', (status) => { const presentOptions = { ...presentQuestionToolResult, - id: "present-options-1", + id: 'present-options-1', message: { ...presentQuestionToolResult.message, - toolName: "present_options", + toolName: 'present_options', details: { ...presentQuestionToolResult.message.details, - presentTool: "present_options", - kind: "options", - expectedRequest: { tool: "request_choices", required: true }, + presentTool: 'present_options', + kind: 'options', + expectedRequest: { tool: 'request_choices', required: true }, }, }, - } + }; const requestChoices = { ...requestChoicesToolResult, id: `request-choices-${status}`, @@ -381,427 +364,370 @@ describe("elicitation exchange projection", () => { status, }, }, - } + }; - const projection = projectElicitationExchanges([ - presentOptions, - requestChoices, - ]) + const projection = projectElicitationExchanges([presentOptions, requestChoices]); - expect(projection.exchanges[0]?.responseEntryIds).toEqual([ - `request-choices-${status}`, - ]) - expect(projection.openPrompt).toBeNull() + expect(projection.exchanges[0]?.responseEntryIds).toEqual([`request-choices-${status}`]); + expect(projection.openPrompt).toBeNull(); }, - ) + ); - it("does not close a present when request tuple identity or tool expectations mismatch", () => { + it('does not close a present when request tuple identity or tool expectations mismatch', () => { const wrongPresentToolRequest = { ...requestAnswerToolResult, - id: "request-answer-wrong-present-tool", + id: 'request-answer-wrong-present-tool', message: { ...requestAnswerToolResult.message, details: { ...requestAnswerToolResult.message.details, - respondsTo: { exchangeId: "domain", presentTool: "present_options" }, + respondsTo: { exchangeId: 'domain', presentTool: 'present_options' }, }, }, - } + }; const unexpectedRequestTool = { ...requestChoicesToolResult, - id: "request-choices-unexpected-tool", + id: 'request-choices-unexpected-tool', message: { ...requestChoicesToolResult.message, details: { ...requestChoicesToolResult.message.details, - exchangeId: "domain", + exchangeId: 'domain', respondsTo: { - exchangeId: "domain", - presentTool: "present_question", + exchangeId: 'domain', + presentTool: 'present_question', }, }, }, - } + }; for (const request of [wrongPresentToolRequest, unexpectedRequestTool]) { - const projection = projectElicitationExchanges([ - presentQuestionToolResult, - request, - ]) - - expect(projection.exchanges).toEqual([]) - expect(projection.openPrompt?.promptEntryIds).toEqual([ - "present-question-1", - ]) + const projection = projectElicitationExchanges([presentQuestionToolResult, request]); + + expect(projection.exchanges).toEqual([]); + expect(projection.openPrompt?.promptEntryIds).toEqual(['present-question-1']); } - }) + }); - it("renders structured-exchange present/request tool markdown as transcript rows", () => { - const projection = projectTranscriptDisplay([ - presentQuestionToolResult, - requestAnswerToolResult, - ]) + it('renders structured-exchange present/request tool markdown as transcript rows', () => { + const projection = projectTranscriptDisplay([presentQuestionToolResult, requestAnswerToolResult]); expect(projection.rows).toEqual([ { - id: "present-question-1", - role: "prompt", - text: "## Domain?\n\nWhat are we specifying?", + id: 'present-question-1', + role: 'prompt', + text: '## Domain?\n\nWhat are we specifying?', }, { - id: "request-answer-1", - role: "user", - text: "### Response\n\nDeveloper tooling", + id: 'request-answer-1', + role: 'user', + text: '### Response\n\nDeveloper tooling', }, - ]) - }) + ]); + }); - it("classifies terminal structured-exchange tool results as response-side entries", () => { - const projection = projectElicitationExchanges([ - assistant, - structuredExchangeToolResult, - ]) + it('classifies terminal structured-exchange tool results as response-side entries', () => { + const projection = projectElicitationExchanges([assistant, structuredExchangeToolResult]); - expect(projection.exchanges[0]?.promptEntryIds).toEqual(["a1"]) - expect(projection.exchanges[0]?.responseEntryIds).toEqual(["sq1"]) + expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1']); + expect(projection.exchanges[0]?.responseEntryIds).toEqual(['sq1']); expect(projection.exchanges[0]?.responseRange).toEqual({ - start: "sq1", - end: "sq1", - }) - expect(projection.openPrompt).toBeNull() - }) + start: 'sq1', + end: 'sq1', + }); + expect(projection.openPrompt).toBeNull(); + }); - it("keeps non-terminal structured-exchange tool results on the prompt side", () => { - const projection = projectElicitationExchanges([ - assistant, - unavailableStructuredExchangeToolResult, - ]) + it('keeps non-terminal structured-exchange tool results on the prompt side', () => { + const projection = projectElicitationExchanges([assistant, unavailableStructuredExchangeToolResult]); - expect(projection.exchanges).toEqual([]) - expect(projection.openPrompt?.promptEntryIds).toEqual([ - "a1", - "sq-unavailable", - ]) - }) + expect(projection.exchanges).toEqual([]); + expect(projection.openPrompt?.promptEntryIds).toEqual(['a1', 'sq-unavailable']); + }); - it("returns an explicit empty/open shape for incomplete transcripts", () => { + it('returns an explicit empty/open shape for incomplete transcripts', () => { expect(projectElicitationExchanges([])).toEqual({ - status: "empty", + status: 'empty', exchanges: [], openPrompt: null, - }) + }); expect(projectElicitationExchanges([assistant])).toEqual({ - status: "open_prompt", + status: 'open_prompt', exchanges: [], openPrompt: { - promptRange: { start: "a1", end: "a1" }, - promptEntryIds: ["a1"], + promptRange: { start: 'a1', end: 'a1' }, + promptEntryIds: ['a1'], }, - }) - }) + }); + }); - it("ignores orphan user responses before a prompt", () => { + it('ignores orphan user responses before a prompt', () => { const projection = projectElicitationExchanges([ user, { - id: "a2", - type: "message", - message: assistantMessage("Later prompt"), + id: 'a2', + type: 'message', + message: assistantMessage('Later prompt'), }, - ]) + ]); expect(projection).toEqual({ - status: "open_prompt", + status: 'open_prompt', exchanges: [], openPrompt: { - promptRange: { start: "a2", end: "a2" }, - promptEntryIds: ["a2"], + promptRange: { start: 'a2', end: 'a2' }, + promptEntryIds: ['a2'], }, - }) - }) - - it("loads and projects a real SessionManager JSONL assistant/user transcript through the product helper", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-jsonl-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage(assistantMessage("Question")) - manager.appendMessage(userMessage("Answer")) - - const projection = await loadLinearElicitationExchangeProjection( - manager.getSessionFile()!, - ) - - expect(projection.status).toBe("ready") - expect(projection.exchanges).toHaveLength(1) - expect(projection.exchanges[0]?.promptEntryIds[0]).toEqual( - expect.any(String), - ) - expect(projection.exchanges[0]?.responseEntryIds[0]).toEqual( - expect.any(String), - ) - }) - - it("loads and projects terminal structured-exchange tool results as JSONL responses", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-structured-exchange-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage( - assistantMessage("Please answer the structured exchange."), - ) + }); + }); + + it('loads and projects a real SessionManager JSONL assistant/user transcript through the product helper', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-jsonl-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Question')); + manager.appendMessage(userMessage('Answer')); + + const projection = await loadLinearElicitationExchangeProjection(manager.getSessionFile()!); + + expect(projection.status).toBe('ready'); + expect(projection.exchanges).toHaveLength(1); + expect(projection.exchanges[0]?.promptEntryIds[0]).toEqual(expect.any(String)); + expect(projection.exchanges[0]?.responseEntryIds[0]).toEqual(expect.any(String)); + }); + + it('loads and projects terminal structured-exchange tool results as JSONL responses', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-structured-exchange-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Please answer the structured exchange.')); manager.appendMessage({ - role: "toolResult", - toolCallId: "call-exchange-jsonl", - toolName: "structured_exchange", - content: [{ type: "text", text: "User answered: Developer tooling" }], + role: 'toolResult', + toolCallId: 'call-exchange-jsonl', + toolName: 'structured_exchange', + content: [{ type: 'text', text: 'User answered: Developer tooling' }], details: { schema: STRUCTURED_EXCHANGE_RESULT_SCHEMA, schemaVersion: 1, - status: "answered", - question: "Domain?", - mode: "text", + status: 'answered', + question: 'Domain?', + mode: 'text', answers: [ { - type: "text", - label: "Developer tooling", - value: "Developer tooling", + type: 'text', + label: 'Developer tooling', + value: 'Developer tooling', }, ], - transport: { surface: "rpc-editor" }, + transport: { surface: 'rpc-editor' }, }, isError: false, timestamp: 0, - }) + }); - const projection = await loadLinearElicitationExchangeProjection( - manager.getSessionFile()!, - ) + const projection = await loadLinearElicitationExchangeProjection(manager.getSessionFile()!); - expect(projection.status).toBe("ready") - expect(projection.exchanges).toHaveLength(1) - expect(projection.exchanges[0]?.promptEntryIds).toHaveLength(1) - expect(projection.exchanges[0]?.responseEntryIds).toHaveLength(1) - }) + expect(projection.status).toBe('ready'); + expect(projection.exchanges).toHaveLength(1); + expect(projection.exchanges[0]?.promptEntryIds).toHaveLength(1); + expect(projection.exchanges[0]?.responseEntryIds).toHaveLength(1); + }); - it("loads displayable assistant and user transcript rows", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-display-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage(assistantMessage("Question")) - manager.appendMessage(userMessage("Answer")) + it('loads displayable assistant and user transcript rows', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-display-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Question')); + manager.appendMessage(userMessage('Answer')); - const projection = await loadLinearTranscriptDisplayProjection( - manager.getSessionFile()!, - ) + const projection = await loadLinearTranscriptDisplayProjection(manager.getSessionFile()!); expect(projection.rows).toEqual([ - { id: expect.any(String), role: "assistant", text: "Question" }, - { id: expect.any(String), role: "user", text: "Answer" }, - ]) - }) - - it("loads displayable elicitation prompt custom-message rows without operational custom entries", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-display-prompt-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendCustomMessageEntry( - "brunch.elicitation_prompt", - "Choose the better framing.", - true, - ) - manager.appendMessage(assistantMessage("Persistence sentinel")) - manager.appendMessage(userMessage("Option A")) - - const projection = await loadLinearTranscriptDisplayProjection( - manager.getSessionFile()!, - ) + { id: expect.any(String), role: 'assistant', text: 'Question' }, + { id: expect.any(String), role: 'user', text: 'Answer' }, + ]); + }); + + it('loads displayable elicitation prompt custom-message rows without operational custom entries', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-display-prompt-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendCustomMessageEntry('brunch.elicitation_prompt', 'Choose the better framing.', true); + manager.appendMessage(assistantMessage('Persistence sentinel')); + manager.appendMessage(userMessage('Option A')); + + const projection = await loadLinearTranscriptDisplayProjection(manager.getSessionFile()!); expect(projection.rows).toEqual([ { id: expect.any(String), - role: "prompt", - text: "Choose the better framing.", + role: 'prompt', + text: 'Choose the better framing.', }, { id: expect.any(String), - role: "assistant", - text: "Persistence sentinel", + role: 'assistant', + text: 'Persistence sentinel', }, - { id: expect.any(String), role: "user", text: "Option A" }, - ]) - }) + { id: expect.any(String), role: 'user', text: 'Option A' }, + ]); + }); - it("projects only text-bearing elicitation prompt custom messages as prompt display rows", () => { + it('projects only text-bearing elicitation prompt custom messages as prompt display rows', () => { const projection = projectTranscriptDisplay([ { - id: "binding-1", - type: "custom", + id: 'binding-1', + type: 'custom', parentId: null, - customType: "brunch.session_binding", - data: { sessionId: "session-1" }, + customType: 'brunch.session_binding', + data: { sessionId: 'session-1' }, }, { - id: "prompt-1", - type: "custom_message", - parentId: "binding-1", - customType: "brunch.elicitation_prompt", - content: [{ type: "text", text: "Describe the user." }], + id: 'prompt-1', + type: 'custom_message', + parentId: 'binding-1', + customType: 'brunch.elicitation_prompt', + content: [{ type: 'text', text: 'Describe the user.' }], display: true, }, { - id: "side-task-1", - type: "custom_message", - parentId: "prompt-1", - customType: "brunch.side_task_result", - content: "Operational note", + id: 'side-task-1', + type: 'custom_message', + parentId: 'prompt-1', + customType: 'brunch.side_task_result', + content: 'Operational note', display: true, }, - ]) - - expect(projection.rows).toEqual([ - { id: "prompt-1", role: "prompt", text: "Describe the user." }, - ]) - }) - - it("preserves the non-linear error discriminant through the product helper", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-helper-branch-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage(assistantMessage("Abandoned prompt")) - manager.appendMessage(userMessage("Abandoned answer")) - manager.resetLeaf() - manager.appendMessage(assistantMessage("Active prompt")) - - await expect( - loadLinearElicitationExchangeProjection(manager.getSessionFile()!), - ).rejects.toThrow(NonLinearTranscriptError) - }) - - it("rejects a Pi JSONL file with multiple children from one parent", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-branch-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - manager.appendMessage(assistantMessage("Abandoned prompt")) - manager.appendMessage(userMessage("Abandoned answer")) - manager.resetLeaf() - manager.appendMessage(assistantMessage("Active prompt")) - manager.appendMessage(userMessage("Active answer")) - - await expect( - loadJsonlTranscriptEntries(manager.getSessionFile()!), - ).rejects.toThrow(NonLinearTranscriptError) - }) - - it("rejects a Pi JSONL file with branched sibling responses", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-pi-branch-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - const sharedPromptId = manager.appendMessage( - assistantMessage("Choose a path"), - ) - manager.appendMessage(userMessage("Old path")) - manager.branch(sharedPromptId) - manager.appendMessage(userMessage("Selected path")) - - await expect( - loadJsonlTranscriptEntries(manager.getSessionFile()!), - ).rejects.toThrow("non-linear Pi transcript branches") - }) - - it("rejects branch-derived sessions and branch summaries before projection", async () => { - const dir = await mkdtemp(join(tmpdir(), "brunch-jsonl-branch-derived-")) - const branchDerivedFile = join(dir, "branch-derived.jsonl") - const branchSummaryFile = join(dir, "branch-summary.jsonl") + ]); + + expect(projection.rows).toEqual([{ id: 'prompt-1', role: 'prompt', text: 'Describe the user.' }]); + }); + + it('preserves the non-linear error discriminant through the product helper', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-helper-branch-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Abandoned prompt')); + manager.appendMessage(userMessage('Abandoned answer')); + manager.resetLeaf(); + manager.appendMessage(assistantMessage('Active prompt')); + + await expect(loadLinearElicitationExchangeProjection(manager.getSessionFile()!)).rejects.toThrow( + NonLinearTranscriptError, + ); + }); + + it('rejects a Pi JSONL file with multiple children from one parent', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-branch-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + manager.appendMessage(assistantMessage('Abandoned prompt')); + manager.appendMessage(userMessage('Abandoned answer')); + manager.resetLeaf(); + manager.appendMessage(assistantMessage('Active prompt')); + manager.appendMessage(userMessage('Active answer')); + + await expect(loadJsonlTranscriptEntries(manager.getSessionFile()!)).rejects.toThrow( + NonLinearTranscriptError, + ); + }); + + it('rejects a Pi JSONL file with branched sibling responses', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-pi-branch-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + const sharedPromptId = manager.appendMessage(assistantMessage('Choose a path')); + manager.appendMessage(userMessage('Old path')); + manager.branch(sharedPromptId); + manager.appendMessage(userMessage('Selected path')); + + await expect(loadJsonlTranscriptEntries(manager.getSessionFile()!)).rejects.toThrow( + 'non-linear Pi transcript branches', + ); + }); + + it('rejects branch-derived sessions and branch summaries before projection', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-jsonl-branch-derived-')); + const branchDerivedFile = join(dir, 'branch-derived.jsonl'); + const branchSummaryFile = join(dir, 'branch-summary.jsonl'); await writeFile( branchDerivedFile, `${JSON.stringify({ - type: "session", + type: 'session', version: 3, - id: "session-1", - timestamp: "2026-05-21T00:00:00.000Z", + id: 'session-1', + timestamp: '2026-05-21T00:00:00.000Z', cwd: dir, - parentSession: "/tmp/parent.jsonl", + parentSession: '/tmp/parent.jsonl', })}\n`, - ) + ); await writeFile( branchSummaryFile, - `${JSON.stringify({ type: "session", id: "session-1", cwd: dir })}\n${JSON.stringify( - { - id: "b1", - type: "branch_summary", - parentId: null, - timestamp: "2026-05-21T00:00:00.000Z", - fromId: "a1", - summary: "Branch summary", - }, - )}\n`, - ) + `${JSON.stringify({ type: 'session', id: 'session-1', cwd: dir })}\n${JSON.stringify({ + id: 'b1', + type: 'branch_summary', + parentId: null, + timestamp: '2026-05-21T00:00:00.000Z', + fromId: 'a1', + summary: 'Branch summary', + })}\n`, + ); - await expect(loadJsonlTranscriptEntries(branchDerivedFile)).rejects.toThrow( - "branch-derived Pi sessions", - ) + await expect(loadJsonlTranscriptEntries(branchDerivedFile)).rejects.toThrow('branch-derived Pi sessions'); await expect(loadJsonlTranscriptEntries(branchSummaryFile)).rejects.toThrow( - "branch-summary transcript entries", - ) - }) - - it("rejects file-backed transcripts without exactly one Pi session header", async () => { - const dir = await mkdtemp(join(tmpdir(), "brunch-jsonl-header-")) - const headerlessFile = join(dir, "headerless.jsonl") - const duplicateHeaderFile = join(dir, "duplicate-header.jsonl") - const header = { type: "session", id: "session-1", cwd: dir } - await writeFile( - headerlessFile, - `${JSON.stringify(assistant)}\n${JSON.stringify(user)}\n`, - ) + 'branch-summary transcript entries', + ); + }); + + it('rejects file-backed transcripts without exactly one Pi session header', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-jsonl-header-')); + const headerlessFile = join(dir, 'headerless.jsonl'); + const duplicateHeaderFile = join(dir, 'duplicate-header.jsonl'); + const header = { type: 'session', id: 'session-1', cwd: dir }; + await writeFile(headerlessFile, `${JSON.stringify(assistant)}\n${JSON.stringify(user)}\n`); await writeFile( duplicateHeaderFile, `${JSON.stringify(header)}\n${JSON.stringify(header)}\n${JSON.stringify({ ...assistant, parentId: null, })}\n`, - ) - - await expect(loadJsonlTranscriptEntries(headerlessFile)).rejects.toThrow( - "exactly one Pi session header", - ) - await expect( - loadJsonlTranscriptEntries(duplicateHeaderFile), - ).rejects.toThrow("exactly one Pi session header") - }) - - it("rejects malformed non-header Pi JSONL entries before projection", async () => { - const dir = await mkdtemp(join(tmpdir(), "brunch-jsonl-shape-")) - const file = join(dir, "malformed.jsonl") - const header = { type: "session", id: "session-1", cwd: dir } + ); + + await expect(loadJsonlTranscriptEntries(headerlessFile)).rejects.toThrow('exactly one Pi session header'); + await expect(loadJsonlTranscriptEntries(duplicateHeaderFile)).rejects.toThrow( + 'exactly one Pi session header', + ); + }); + + it('rejects malformed non-header Pi JSONL entries before projection', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-jsonl-shape-')); + const file = join(dir, 'malformed.jsonl'); + const header = { type: 'session', id: 'session-1', cwd: dir }; await writeFile( file, - `${JSON.stringify(header)}\n${JSON.stringify({ ...assistant, parentId: null })}\n${JSON.stringify( - { - id: "u1", - type: "message", - message: userMessage("A"), - }, - )}\n`, - ) - - await expect(loadJsonlTranscriptEntries(file)).rejects.toThrow( - "string-or-null parentId", - ) - }) - - it("loads newline-delimited Pi transcript entries from disk", async () => { - const dir = await mkdtemp(join(tmpdir(), "brunch-jsonl-")) - const file = join(dir, "session.jsonl") - const header = { type: "session", id: "session-1", cwd: dir } + `${JSON.stringify(header)}\n${JSON.stringify({ ...assistant, parentId: null })}\n${JSON.stringify({ + id: 'u1', + type: 'message', + message: userMessage('A'), + })}\n`, + ); + + await expect(loadJsonlTranscriptEntries(file)).rejects.toThrow('string-or-null parentId'); + }); + + it('loads newline-delimited Pi transcript entries from disk', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-jsonl-')); + const file = join(dir, 'session.jsonl'); + const header = { type: 'session', id: 'session-1', cwd: dir }; await writeFile( file, `${JSON.stringify(header)}\n${JSON.stringify({ ...assistant, parentId: null, - })}\n${JSON.stringify({ ...user, parentId: "a1" })}\n`, - ) + })}\n${JSON.stringify({ ...user, parentId: 'a1' })}\n`, + ); - const entries = await loadJsonlTranscriptEntries(file) + const entries = await loadJsonlTranscriptEntries(file); - expect(projectElicitationExchanges(entries).exchanges).toHaveLength(1) - }) -}) + expect(projectElicitationExchanges(entries).exchanges).toHaveLength(1); + }); +}); diff --git a/src/elicitation-exchange.ts b/src/elicitation-exchange.ts index 7c6443b30..967a87a67 100644 --- a/src/elicitation-exchange.ts +++ b/src/elicitation-exchange.ts @@ -3,225 +3,215 @@ import { type CustomMessageEntry, type SessionEntry, type SessionMessageEntry, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-coding-agent'; +import type { + StructuredExchangePresentDetails, + StructuredExchangeRequestDetails, +} from './.pi/extensions/structured-exchange/shared/model.js'; +import { + isStructuredExchangePresentDetails, + isStructuredExchangeRequestDetails, +} from './.pi/extensions/structured-exchange/shared/recovery.js'; import { assertLinearBrunchSessionEnvelope, loadJsonlTranscriptEntries, NonLinearTranscriptError, readBrunchSessionEnvelope, type BrunchSessionEnvelope, -} from "./brunch-session-envelope.js" -import { isTerminalStructuredExchangeResultDetails } from "./structured-exchange.js" -import { - isStructuredExchangePresentDetails, - isStructuredExchangeRequestDetails, -} from "./.pi/extensions/structured-exchange/shared/recovery.js" -import type { - StructuredExchangePresentDetails, - StructuredExchangeRequestDetails, -} from "./.pi/extensions/structured-exchange/shared/model.js" +} from './brunch-session-envelope.js'; +import { isTerminalStructuredExchangeResultDetails } from './structured-exchange.js'; const PROMPT_SIDE_CUSTOM_TYPES = new Set([ - "brunch.elicitation_prompt", - "brunch.elicitor_intent_hint", - "brunch.establishment_offer", - "brunch.review_set_proposal", -]) + 'brunch.elicitation_prompt', + 'brunch.elicitor_intent_hint', + 'brunch.establishment_offer', + 'brunch.review_set_proposal', +]); const STRUCTURED_RESPONSE_TYPES = new Set([ - "brunch.elicitation_response", - "brunch.action_response", - "brunch.choice_response", -]) + 'brunch.elicitation_response', + 'brunch.action_response', + 'brunch.choice_response', +]); export interface EntryRange { - start: string - end: string + start: string; + end: string; } export interface ElicitationExchange { - promptRange: EntryRange - responseRange: EntryRange - promptEntryIds: string[] - responseEntryIds: string[] + promptRange: EntryRange; + responseRange: EntryRange; + promptEntryIds: string[]; + responseEntryIds: string[]; } export interface OpenPromptProjection { - promptRange: EntryRange - promptEntryIds: string[] + promptRange: EntryRange; + promptEntryIds: string[]; } export interface ElicitationExchangeProjection { - status: "empty" | "open_prompt" | "ready" - exchanges: ElicitationExchange[] - openPrompt: OpenPromptProjection | null + status: 'empty' | 'open_prompt' | 'ready'; + exchanges: ElicitationExchange[]; + openPrompt: OpenPromptProjection | null; } export interface TranscriptDisplayRow { - id: string - role: "prompt" | "assistant" | "user" - text: string + id: string; + role: 'prompt' | 'assistant' | 'user'; + text: string; } export interface TranscriptDisplayProjection { - rows: TranscriptDisplayRow[] + rows: TranscriptDisplayRow[]; } -export { loadJsonlTranscriptEntries, NonLinearTranscriptError } +export { loadJsonlTranscriptEntries, NonLinearTranscriptError }; export async function loadLinearElicitationExchangeProjection( file: string, ): Promise { - return projectLinearElicitationExchangeProjection( - await loadBrunchSessionEnvelope(file), - ) + return projectLinearElicitationExchangeProjection(await loadBrunchSessionEnvelope(file)); } export async function loadLinearTranscriptDisplayProjection( file: string, ): Promise { - return projectLinearTranscriptDisplayProjection( - await loadBrunchSessionEnvelope(file), - ) + return projectLinearTranscriptDisplayProjection(await loadBrunchSessionEnvelope(file)); } export function projectLinearElicitationExchangeProjection( envelope: BrunchSessionEnvelope, ): ElicitationExchangeProjection { - assertLinearBrunchSessionEnvelope(envelope) - return projectElicitationExchanges(envelope.entries) + assertLinearBrunchSessionEnvelope(envelope); + return projectElicitationExchanges(envelope.entries); } export function projectLinearTranscriptDisplayProjection( envelope: BrunchSessionEnvelope, ): TranscriptDisplayProjection { - assertLinearBrunchSessionEnvelope(envelope) - return projectTranscriptDisplay(envelope.entries) + assertLinearBrunchSessionEnvelope(envelope); + return projectTranscriptDisplay(envelope.entries); } -async function loadBrunchSessionEnvelope( - file: string, -): Promise { - const readResult = await readBrunchSessionEnvelope(file) +async function loadBrunchSessionEnvelope(file: string): Promise { + const readResult = await readBrunchSessionEnvelope(file); if (!readResult.ok) { - throw new Error("Brunch session self-description is invalid") + throw new Error('Brunch session self-description is invalid'); } - return readResult.envelope + return readResult.envelope; } -export function projectTranscriptDisplay( - entries: readonly unknown[], -): TranscriptDisplayProjection { - const rows: TranscriptDisplayRow[] = [] +export function projectTranscriptDisplay(entries: readonly unknown[]): TranscriptDisplayProjection { + const rows: TranscriptDisplayRow[] = []; for (const entry of entries) { if (!isSessionEntry(entry)) { - continue + continue; } if (isDisplayableElicitationPrompt(entry)) { - const text = textContent(entry.content) + const text = textContent(entry.content); if (text.length > 0) { - rows.push({ id: entry.id, role: "prompt", text }) + rows.push({ id: entry.id, role: 'prompt', text }); } - continue + continue; } if (!isMessageEntry(entry)) { - continue + continue; } - const text = textContent((entry.message as { content?: unknown }).content) + const text = textContent((entry.message as { content?: unknown }).content); if (text.length === 0) { - continue + continue; } if (isStructuredExchangePresentToolResult(entry)) { - rows.push({ id: entry.id, role: "prompt", text }) - continue + rows.push({ id: entry.id, role: 'prompt', text }); + continue; } if (isStructuredExchangeRequestToolResult(entry)) { - rows.push({ id: entry.id, role: "user", text }) - continue + rows.push({ id: entry.id, role: 'user', text }); + continue; } - const role = entry.message.role - if (role !== "assistant" && role !== "user") { - continue + const role = entry.message.role; + if (role !== 'assistant' && role !== 'user') { + continue; } - rows.push({ id: entry.id, role, text }) + rows.push({ id: entry.id, role, text }); } - return { rows } + return { rows }; } -export function projectElicitationExchanges( - entries: readonly unknown[], -): ElicitationExchangeProjection { - const exchanges: ElicitationExchange[] = [] - let promptIds: string[] = [] - let responseIds: string[] = [] - let openStructuredExchange: StructuredExchangePresentDetails | undefined +export function projectElicitationExchanges(entries: readonly unknown[]): ElicitationExchangeProjection { + const exchanges: ElicitationExchange[] = []; + let promptIds: string[] = []; + let responseIds: string[] = []; + let openStructuredExchange: StructuredExchangePresentDetails | undefined; for (const entry of entries) { if (!isTranscriptEntry(entry)) { - continue + continue; } - const presentDetails = structuredExchangePresentDetails(entry) + const presentDetails = structuredExchangePresentDetails(entry); if (presentDetails) { - flushResponse() - promptIds.push(entry.id) - openStructuredExchange = presentDetails - continue + flushResponse(); + promptIds.push(entry.id); + openStructuredExchange = presentDetails; + continue; } - const requestDetails = structuredExchangeRequestDetails(entry) + const requestDetails = structuredExchangeRequestDetails(entry); if (requestDetails) { if ( promptIds.length > 0 && openStructuredExchange !== undefined && requestClosesPresent(requestDetails, openStructuredExchange) ) { - responseIds.push(entry.id) + responseIds.push(entry.id); } - continue + continue; } if (isPromptSideEntry(entry)) { - flushResponse() - promptIds.push(entry.id) - continue + flushResponse(); + promptIds.push(entry.id); + continue; } if (isResponseSideEntry(entry) && promptIds.length > 0) { - responseIds.push(entry.id) + responseIds.push(entry.id); } } - flushResponse() + flushResponse(); if (promptIds.length > 0) { return { - status: "open_prompt", + status: 'open_prompt', exchanges, openPrompt: { promptRange: rangeFor(promptIds), promptEntryIds: promptIds, }, - } + }; } return { - status: exchanges.length === 0 ? "empty" : "ready", + status: exchanges.length === 0 ? 'empty' : 'ready', exchanges, openPrompt: null, - } + }; function flushResponse(): void { if (promptIds.length === 0 || responseIds.length === 0) { - return + return; } exchanges.push({ @@ -229,36 +219,36 @@ export function projectElicitationExchanges( responseRange: rangeFor(responseIds), promptEntryIds: promptIds, responseEntryIds: responseIds, - }) - promptIds = [] - responseIds = [] - openStructuredExchange = undefined + }); + promptIds = []; + responseIds = []; + openStructuredExchange = undefined; } } function rangeFor(ids: string[]): EntryRange { - return { start: ids[0]!, end: ids[ids.length - 1]! } + return { start: ids[0]!, end: ids[ids.length - 1]! }; } function isTranscriptEntry(value: unknown): value is SessionEntry { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type !== "session" && - typeof (value as { id?: unknown }).id === "string" && - typeof (value as { type?: unknown }).type === "string" - ) + (value as { type?: unknown }).type !== 'session' && + typeof (value as { id?: unknown }).id === 'string' && + typeof (value as { type?: unknown }).type === 'string' + ); } function isSessionEntry(value: unknown): value is SessionEntry { - return isTranscriptEntry(value) && hasStringOrNullParentId(value) + return isTranscriptEntry(value) && hasStringOrNullParentId(value); } function hasStringOrNullParentId(value: unknown): boolean { return ( (value as { parentId?: unknown }).parentId === null || - typeof (value as { parentId?: unknown }).parentId === "string" - ) + typeof (value as { parentId?: unknown }).parentId === 'string' + ); } function requestClosesPresent( @@ -266,142 +256,112 @@ function requestClosesPresent( present: StructuredExchangePresentDetails, ): boolean { return ( - (request.status === "answered" || - request.status === "cancelled" || - request.status === "unavailable") && + (request.status === 'answered' || request.status === 'cancelled' || request.status === 'unavailable') && request.exchangeId === present.exchangeId && request.respondsTo.exchangeId === present.exchangeId && request.respondsTo.presentTool === present.presentTool && - (present.expectedRequest === undefined || - present.expectedRequest.tool === request.requestTool) - ) + (present.expectedRequest === undefined || present.expectedRequest.tool === request.requestTool) + ); } -function structuredExchangePresentDetails( - entry: SessionEntry, -): StructuredExchangePresentDetails | undefined { - if (!isStructuredExchangePresentToolResult(entry)) return undefined - return (entry.message as { details?: unknown }) - .details as StructuredExchangePresentDetails +function structuredExchangePresentDetails(entry: SessionEntry): StructuredExchangePresentDetails | undefined { + if (!isStructuredExchangePresentToolResult(entry)) return undefined; + return (entry.message as { details?: unknown }).details as StructuredExchangePresentDetails; } -function structuredExchangeRequestDetails( - entry: SessionEntry, -): StructuredExchangeRequestDetails | undefined { - if (!isStructuredExchangeRequestToolResult(entry)) return undefined - return (entry.message as { details?: unknown }) - .details as StructuredExchangeRequestDetails +function structuredExchangeRequestDetails(entry: SessionEntry): StructuredExchangeRequestDetails | undefined { + if (!isStructuredExchangeRequestToolResult(entry)) return undefined; + return (entry.message as { details?: unknown }).details as StructuredExchangeRequestDetails; } -function isStructuredExchangePresentToolResult( - entry: SessionEntry, -): entry is SessionMessageEntry & { - message: SessionMessageEntry["message"] & { details?: unknown } +function isStructuredExchangePresentToolResult(entry: SessionEntry): entry is SessionMessageEntry & { + message: SessionMessageEntry['message'] & { details?: unknown }; } { return ( isMessageEntry(entry) && - entry.message.role === "toolResult" && - isStructuredExchangePresentDetails( - (entry.message as { details?: unknown }).details, - ) - ) + entry.message.role === 'toolResult' && + isStructuredExchangePresentDetails((entry.message as { details?: unknown }).details) + ); } -function isStructuredExchangeRequestToolResult( - entry: SessionEntry, -): entry is SessionMessageEntry & { - message: SessionMessageEntry["message"] & { details?: unknown } +function isStructuredExchangeRequestToolResult(entry: SessionEntry): entry is SessionMessageEntry & { + message: SessionMessageEntry['message'] & { details?: unknown }; } { return ( isMessageEntry(entry) && - entry.message.role === "toolResult" && - isStructuredExchangeRequestDetails( - (entry.message as { details?: unknown }).details, - ) - ) + entry.message.role === 'toolResult' && + isStructuredExchangeRequestDetails((entry.message as { details?: unknown }).details) + ); } function isPromptSideEntry(entry: SessionEntry): boolean { if (isCustomTranscriptEntry(entry)) { - return PROMPT_SIDE_CUSTOM_TYPES.has(entry.customType) + return PROMPT_SIDE_CUSTOM_TYPES.has(entry.customType); } - const role = roleOf(entry) - if (role === "toolResult" && isTerminalStructuredExchangeToolResult(entry)) { - return false + const role = roleOf(entry); + if (role === 'toolResult' && isTerminalStructuredExchangeToolResult(entry)) { + return false; } - return role === "assistant" || role === "toolResult" + return role === 'assistant' || role === 'toolResult'; } function isResponseSideEntry(entry: SessionEntry): boolean { - if (roleOf(entry) === "user") { - return true + if (roleOf(entry) === 'user') { + return true; } if (isTerminalStructuredExchangeToolResult(entry)) { - return true + return true; } - return ( - isCustomTranscriptEntry(entry) && - STRUCTURED_RESPONSE_TYPES.has(entry.customType) - ) + return isCustomTranscriptEntry(entry) && STRUCTURED_RESPONSE_TYPES.has(entry.customType); } function isTerminalStructuredExchangeToolResult(entry: SessionEntry): boolean { return ( isMessageEntry(entry) && - entry.message.role === "toolResult" && - isTerminalStructuredExchangeResultDetails( - (entry.message as { details?: unknown }).details, - ) - ) + entry.message.role === 'toolResult' && + isTerminalStructuredExchangeResultDetails((entry.message as { details?: unknown }).details) + ); } -function isCustomTranscriptEntry( - entry: SessionEntry, -): entry is CustomEntry | CustomMessageEntry { - return entry.type === "custom" || entry.type === "custom_message" +function isCustomTranscriptEntry(entry: SessionEntry): entry is CustomEntry | CustomMessageEntry { + return entry.type === 'custom' || entry.type === 'custom_message'; } -function isDisplayableElicitationPrompt( - entry: SessionEntry, -): entry is CustomMessageEntry { +function isDisplayableElicitationPrompt(entry: SessionEntry): entry is CustomMessageEntry { return ( - entry.type === "custom_message" && - entry.customType === "brunch.elicitation_prompt" && + entry.type === 'custom_message' && + entry.customType === 'brunch.elicitation_prompt' && entry.display === true - ) + ); } -function roleOf( - entry: SessionEntry, -): SessionMessageEntry["message"]["role"] | undefined { +function roleOf(entry: SessionEntry): SessionMessageEntry['message']['role'] | undefined { if (isMessageEntry(entry)) { - return entry.message.role + return entry.message.role; } - return undefined + return undefined; } function isMessageEntry(entry: SessionEntry): entry is SessionMessageEntry { - return entry.type === "message" + return entry.type === 'message'; } function textContent(content: unknown): string { - if (typeof content === "string") { - return content + if (typeof content === 'string') { + return content; } if (Array.isArray(content)) { return content .map((part) => - typeof part === "object" && - part !== null && - typeof (part as { text?: unknown }).text === "string" + typeof part === 'object' && part !== null && typeof (part as { text?: unknown }).text === 'string' ? (part as { text: string }).text - : "", + : '', ) .filter((text) => text.length > 0) - .join("\n") + .join('\n'); } - return "" + return ''; } diff --git a/src/graph/architecture.test.ts b/src/graph/architecture.test.ts index 5aab75c37..5db3c6458 100644 --- a/src/graph/architecture.test.ts +++ b/src/graph/architecture.test.ts @@ -7,24 +7,25 @@ * SPEC: D52-L, I26-L */ -import { execSync } from "node:child_process" -import { describe, expect, it } from "vitest" +import { execSync } from 'node:child_process'; -describe("I26-L architectural boundary", () => { - it("no src/ module outside graph/ imports from db/", () => { +import { describe, expect, it } from 'vitest'; + +describe('I26-L architectural boundary', () => { + it('no src/ module outside graph/ imports from db/', () => { // Find all .ts files importing from db/ (excluding graph/, db/ itself, // and test files within graph/) const result = execSync( `rg --files-with-matches "from ['\\"]\\.\\./db/|from ['\\"]\\.\\./\\.\\./db/|from ['\\"]\\./db/" src/ --glob '*.ts' --glob '!*.test.*' || true`, - { cwd: process.cwd(), encoding: "utf-8" }, - ) + { cwd: process.cwd(), encoding: 'utf-8' }, + ); const importingFiles = result .trim() - .split("\n") + .split('\n') .filter(Boolean) - .filter((f) => !f.startsWith("src/graph/") && !f.startsWith("src/db/")) + .filter((f) => !f.startsWith('src/graph/') && !f.startsWith('src/db/')); - expect(importingFiles).toEqual([]) - }) -}) + expect(importingFiles).toEqual([]); + }); +}); diff --git a/src/graph/atoms.ts b/src/graph/atoms.ts index a933d8c12..a5a5a28ba 100644 --- a/src/graph/atoms.ts +++ b/src/graph/atoms.ts @@ -9,10 +9,10 @@ */ /** Stable id for a graph node (SQLite auto-increment integer). */ -export type NodeId = number +export type NodeId = number; /** Stable id for a graph edge (SQLite auto-increment integer). */ -export type EdgeId = number +export type EdgeId = number; /** Monotonic logical sequence number; one per CommandExecutor commit. */ -export type Lsn = number +export type Lsn = number; diff --git a/src/graph/command-executor.test.ts b/src/graph/command-executor.test.ts index 29b9a7985..2ee3a4e43 100644 --- a/src/graph/command-executor.test.ts +++ b/src/graph/command-executor.test.ts @@ -5,918 +5,882 @@ * Scope card: CommandExecutor skeleton with single-node proof-of-life */ -import { describe, expect, it, beforeEach } from "vitest" +import { describe, expect, it, beforeEach } from 'vitest'; -import { createDb, type BrunchDb } from "../db/connection.js" -import { graphClock, changeLog, edges, nodes } from "../db/schema.js" -import { CommandExecutor } from "./command-executor.js" -import type { CommitGraphInput } from "./command-executor.js" +import { createDb, type BrunchDb } from '../db/connection.js'; +import { graphClock, changeLog, edges, nodes } from '../db/schema.js'; +import { CommandExecutor } from './command-executor.js'; +import type { CommitGraphInput } from './command-executor.js'; function createTestDb(): BrunchDb { - return createDb(":memory:") + return createDb(':memory:'); } -describe("CommandExecutor", () => { - let db: BrunchDb - let executor: CommandExecutor +describe('CommandExecutor', () => { + let db: BrunchDb; + let executor: CommandExecutor; beforeEach(() => { - db = createTestDb() - executor = new CommandExecutor(db) - }) + db = createTestDb(); + executor = new CommandExecutor(db); + }); // --- graph_clock initialization --- - it("initializes graph_clock with lsn=0", () => { - const rows = db.select().from(graphClock).all() - expect(rows).toHaveLength(1) - expect(rows[0]!.lsn).toBe(0) - }) + it('initializes graph_clock with lsn=0', () => { + const rows = db.select().from(graphClock).all(); + expect(rows).toHaveLength(1); + expect(rows[0]!.lsn).toBe(0); + }); // --- createNode: success path --- - it("creates a valid intent node and returns success with nodeId and lsn", () => { + it('creates a valid intent node and returns success with nodeId and lsn', () => { const result = executor.createNode({ - plane: "intent", - kind: "requirement", - title: "System must be offline-capable", - body: "Works without network connectivity", - }) - - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - expect(result.nodeId).toBeTypeOf("number") - expect(result.lsn).toBe(1) - }) + plane: 'intent', + kind: 'requirement', + title: 'System must be offline-capable', + body: 'Works without network connectivity', + }); + + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.nodeId).toBeTypeOf('number'); + expect(result.lsn).toBe(1); + }); it("defaults basis to 'explicit' when omitted", () => { executor.createNode({ - plane: "intent", - kind: "goal", - title: "Some goal", - }) + plane: 'intent', + kind: 'goal', + title: 'Some goal', + }); - const row = db.select().from(nodes).all()[0] - expect(row!.basis).toBe("explicit") - }) + const row = db.select().from(nodes).all()[0]; + expect(row!.basis).toBe('explicit'); + }); - it("stores optional body and source fields", () => { + it('stores optional body and source fields', () => { executor.createNode({ - plane: "intent", - kind: "context", - title: "Target market", - body: "Enterprise B2B SaaS", - source: "stakeholder", - }) - - const row = db.select().from(nodes).all()[0] - expect(row!.body).toBe("Enterprise B2B SaaS") - expect(row!.source).toBe("stakeholder") - }) - - it("creates a decision node with required detail", () => { + plane: 'intent', + kind: 'context', + title: 'Target market', + body: 'Enterprise B2B SaaS', + source: 'stakeholder', + }); + + const row = db.select().from(nodes).all()[0]; + expect(row!.body).toBe('Enterprise B2B SaaS'); + expect(row!.source).toBe('stakeholder'); + }); + + it('creates a decision node with required detail', () => { const result = executor.createNode({ - plane: "intent", - kind: "decision", - title: "Use SQLite for persistence", + plane: 'intent', + kind: 'decision', + title: 'Use SQLite for persistence', detail: { - chosen_option: "SQLite via better-sqlite3", - rejected: ["PostgreSQL", "In-memory only"], - rationale: "Local-first single-process, no server needed", + chosen_option: 'SQLite via better-sqlite3', + rejected: ['PostgreSQL', 'In-memory only'], + rationale: 'Local-first single-process, no server needed', }, - }) + }); - expect(result.status).toBe("success") - const row = db.select().from(nodes).all()[0] - expect(row!.detail).not.toBeNull() - const detail = JSON.parse(row!.detail!) - expect(detail.chosen_option).toBe("SQLite via better-sqlite3") - expect(detail.rejected).toEqual(["PostgreSQL", "In-memory only"]) - }) + expect(result.status).toBe('success'); + const row = db.select().from(nodes).all()[0]; + expect(row!.detail).not.toBeNull(); + const detail = JSON.parse(row!.detail!); + expect(detail.chosen_option).toBe('SQLite via better-sqlite3'); + expect(detail.rejected).toEqual(['PostgreSQL', 'In-memory only']); + }); - it("creates a term node with required detail", () => { + it('creates a term node with required detail', () => { const result = executor.createNode({ - plane: "intent", - kind: "term", - title: "Reconciliation Need", + plane: 'intent', + kind: 'term', + title: 'Reconciliation Need', detail: { - definition: "A record of an open impasse over graph state", - aliases: ["recon need", "impasse"], + definition: 'A record of an open impasse over graph state', + aliases: ['recon need', 'impasse'], }, - }) + }); - expect(result.status).toBe("success") - const row = db.select().from(nodes).all()[0] - const detail = JSON.parse(row!.detail!) - expect(detail.definition).toBe( - "A record of an open impasse over graph state", - ) - expect(detail.aliases).toEqual(["recon need", "impasse"]) - }) + expect(result.status).toBe('success'); + const row = db.select().from(nodes).all()[0]; + const detail = JSON.parse(row!.detail!); + expect(detail.definition).toBe('A record of an open impasse over graph state'); + expect(detail.aliases).toEqual(['recon need', 'impasse']); + }); // --- createNode: structural_illegal rejections --- - it("rejects invalid kind for plane", () => { + it('rejects invalid kind for plane', () => { const result = executor.createNode({ - plane: "intent", - kind: "check", // oracle-plane kind, not intent - title: "Wrong plane", - }) + plane: 'intent', + kind: 'check', // oracle-plane kind, not intent + title: 'Wrong plane', + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field === "kind")).toBe(true) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'kind')).toBe(true); + }); - it("rejects decision without detail", () => { + it('rejects decision without detail', () => { const result = executor.createNode({ - plane: "intent", - kind: "decision", - title: "Some decision", - }) + plane: 'intent', + kind: 'decision', + title: 'Some decision', + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'detail')).toBe(true); + }); - it("rejects term without detail", () => { + it('rejects term without detail', () => { const result = executor.createNode({ - plane: "intent", - kind: "term", - title: "Some term", - }) + plane: 'intent', + kind: 'term', + title: 'Some term', + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'detail')).toBe(true); + }); - it("rejects non-decision/term node with detail present", () => { + it('rejects non-decision/term node with detail present', () => { const result = executor.createNode({ - plane: "intent", - kind: "requirement", - title: "Some requirement", - detail: { definition: "should not be here" }, - }) - - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field === "detail")).toBe(true) - }) - - it("rejects decision with empty rejected array", () => { + plane: 'intent', + kind: 'requirement', + title: 'Some requirement', + detail: { definition: 'should not be here' }, + }); + + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'detail')).toBe(true); + }); + + it('rejects decision with empty rejected array', () => { const result = executor.createNode({ - plane: "intent", - kind: "decision", - title: "Bad decision", + plane: 'intent', + kind: 'decision', + title: 'Bad decision', detail: { - chosen_option: "A", + chosen_option: 'A', rejected: [], - rationale: "because", + rationale: 'because', }, - }) + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field === "detail.rejected")).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'detail.rejected')).toBe(true); + }); - it("rejects decision detail with unknown fields", () => { + it('rejects decision detail with unknown fields', () => { const result = executor.createNode({ - plane: "intent", - kind: "decision", - title: "Leaky decision", + plane: 'intent', + kind: 'decision', + title: 'Leaky decision', detail: { - chosen_option: "A", - rejected: ["B"], - rationale: "because", - extra_field: "should not be here", + chosen_option: 'A', + rejected: ['B'], + rationale: 'because', + extra_field: 'should not be here', }, - }) + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect( - result.diagnostics.some((d) => d.field === "detail.extra_field"), - ).toBe(true) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field === 'detail.extra_field')).toBe(true); + }); // --- LSN / graph_clock --- - it("increments graph_clock atomically per command", () => { + it('increments graph_clock atomically per command', () => { executor.createNode({ - plane: "intent", - kind: "goal", - title: "First", - }) + plane: 'intent', + kind: 'goal', + title: 'First', + }); executor.createNode({ - plane: "intent", - kind: "goal", - title: "Second", - }) + plane: 'intent', + kind: 'goal', + title: 'Second', + }); - const [clock] = db.select().from(graphClock).all() - expect(clock!.lsn).toBe(2) - }) + const [clock] = db.select().from(graphClock).all(); + expect(clock!.lsn).toBe(2); + }); - it("assigns matching created_at_lsn and updated_at_lsn on new nodes", () => { + it('assigns matching created_at_lsn and updated_at_lsn on new nodes', () => { const result = executor.createNode({ - plane: "intent", - kind: "assumption", - title: "Pi exposes enough seams", - }) - - if (result.status !== "success") throw new Error("unreachable") - const row = db.select().from(nodes).all()[0] - expect(row!.created_at_lsn).toBe(result.lsn) - expect(row!.updated_at_lsn).toBe(result.lsn) - }) - - it("LSN is strictly monotonic across multiple creates", () => { - const lsns: number[] = [] + plane: 'intent', + kind: 'assumption', + title: 'Pi exposes enough seams', + }); + + if (result.status !== 'success') throw new Error('unreachable'); + const row = db.select().from(nodes).all()[0]; + expect(row!.created_at_lsn).toBe(result.lsn); + expect(row!.updated_at_lsn).toBe(result.lsn); + }); + + it('LSN is strictly monotonic across multiple creates', () => { + const lsns: number[] = []; for (let i = 0; i < 10; i++) { const result = executor.createNode({ - plane: "intent", - kind: "context", + plane: 'intent', + kind: 'context', title: `Context ${i}`, - }) - if (result.status !== "success") throw new Error("unreachable") - lsns.push(result.lsn) + }); + if (result.status !== 'success') throw new Error('unreachable'); + lsns.push(result.lsn); } for (let i = 1; i < lsns.length; i++) { - expect(lsns[i]).toBe(lsns[i - 1]! + 1) + expect(lsns[i]).toBe(lsns[i - 1]! + 1); } - }) + }); // --- change_log --- - it("appends exactly one change_log entry per successful command", () => { + it('appends exactly one change_log entry per successful command', () => { executor.createNode({ - plane: "intent", - kind: "requirement", - title: "Must persist", - }) + plane: 'intent', + kind: 'requirement', + title: 'Must persist', + }); - const logs = db.select().from(changeLog).all() - expect(logs).toHaveLength(1) - expect(logs[0]!.operation).toBe("create_node") - }) + const logs = db.select().from(changeLog).all(); + expect(logs).toHaveLength(1); + expect(logs[0]!.operation).toBe('create_node'); + }); - it("change_log payload contains nodeId, plane, and kind", () => { + it('change_log payload contains nodeId, plane, and kind', () => { const result = executor.createNode({ - plane: "intent", - kind: "invariant", - title: "LSN monotonicity", - }) - - if (result.status !== "success") throw new Error("unreachable") - const [log] = db.select().from(changeLog).all() - const payload = JSON.parse(log!.payload) - expect(payload.nodeId).toBe(result.nodeId) - expect(payload.plane).toBe("intent") - expect(payload.kind).toBe("invariant") - }) + plane: 'intent', + kind: 'invariant', + title: 'LSN monotonicity', + }); + + if (result.status !== 'success') throw new Error('unreachable'); + const [log] = db.select().from(changeLog).all(); + const payload = JSON.parse(log!.payload); + expect(payload.nodeId).toBe(result.nodeId); + expect(payload.plane).toBe('intent'); + expect(payload.kind).toBe('invariant'); + }); it("change_log.lsn matches the command's allocated LSN", () => { const result = executor.createNode({ - plane: "intent", - kind: "goal", - title: "Test", - }) + plane: 'intent', + kind: 'goal', + title: 'Test', + }); - if (result.status !== "success") throw new Error("unreachable") - const [log] = db.select().from(changeLog).all() - expect(log!.lsn).toBe(result.lsn) - }) + if (result.status !== 'success') throw new Error('unreachable'); + const [log] = db.select().from(changeLog).all(); + expect(log!.lsn).toBe(result.lsn); + }); // --- Transaction integrity --- - it("writes nothing on validation failure (no LSN bump, no change_log)", () => { + it('writes nothing on validation failure (no LSN bump, no change_log)', () => { executor.createNode({ - plane: "intent", - kind: "check", // invalid kind for intent plane - title: "Should fail", - }) + plane: 'intent', + kind: 'check', // invalid kind for intent plane + title: 'Should fail', + }); - const [clock] = db.select().from(graphClock).all() - expect(clock!.lsn).toBe(0) - expect(db.select().from(nodes).all()).toHaveLength(0) - expect(db.select().from(changeLog).all()).toHaveLength(0) - }) + const [clock] = db.select().from(graphClock).all(); + expect(clock!.lsn).toBe(0); + expect(db.select().from(nodes).all()).toHaveLength(0); + expect(db.select().from(changeLog).all()).toHaveLength(0); + }); // --- Oracle/design/plan plane nodes --- - it("creates oracle-plane nodes", () => { + it('creates oracle-plane nodes', () => { const result = executor.createNode({ - plane: "oracle", - kind: "check", - title: "Verify LSN monotonicity", - }) + plane: 'oracle', + kind: 'check', + title: 'Verify LSN monotonicity', + }); - expect(result.status).toBe("success") - }) + expect(result.status).toBe('success'); + }); - it("creates design-plane nodes", () => { + it('creates design-plane nodes', () => { const result = executor.createNode({ - plane: "design", - kind: "module", - title: "CommandExecutor", - }) + plane: 'design', + kind: 'module', + title: 'CommandExecutor', + }); - expect(result.status).toBe("success") - }) + expect(result.status).toBe('success'); + }); - it("creates plan-plane nodes", () => { + it('creates plan-plane nodes', () => { const result = executor.createNode({ - plane: "plan", - kind: "slice", - title: "M4 skeleton", - }) + plane: 'plan', + kind: 'slice', + title: 'M4 skeleton', + }); - expect(result.status).toBe("success") - }) + expect(result.status).toBe('success'); + }); // ========================================================================== // commitGraph // ========================================================================== - describe("commitGraph", () => { + describe('commitGraph', () => { // --- success path --- - it("creates multiple nodes + edges in one transaction with one LSN", () => { + it('creates multiple nodes + edges in one transaction with one LSN', () => { const input: CommitGraphInput = { nodes: [ - { ref: "n1", plane: "intent", kind: "requirement", title: "Req A" }, - { ref: "n2", plane: "intent", kind: "constraint", title: "Con B" }, + { ref: 'n1', plane: 'intent', kind: 'requirement', title: 'Req A' }, + { ref: 'n2', plane: 'intent', kind: 'constraint', title: 'Con B' }, ], - edges: [{ category: "boundary", source: "n2", target: "n1" }], - } + edges: [{ category: 'boundary', source: 'n2', target: 'n1' }], + }; - const result = executor.commitGraph(input) - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") + const result = executor.commitGraph(input); + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); - expect(result.lsn).toBe(1) - expect(Object.keys(result.nodes)).toHaveLength(2) - expect(result.edges).toHaveLength(1) + expect(result.lsn).toBe(1); + expect(Object.keys(result.nodes)).toHaveLength(2); + expect(result.edges).toHaveLength(1); // Verify DB state - expect(db.select().from(nodes).all()).toHaveLength(2) - expect(db.select().from(edges).all()).toHaveLength(1) - }) + expect(db.select().from(nodes).all()).toHaveLength(2); + expect(db.select().from(edges).all()).toHaveLength(1); + }); - it("resolves intra-batch refs to real NodeIds", () => { + it('resolves intra-batch refs to real NodeIds', () => { const result = executor.commitGraph({ nodes: [ - { ref: "a", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'a', plane: 'intent', kind: 'assumption', title: 'A1' }, { - ref: "b", - plane: "intent", - kind: "decision", - title: "D1", + ref: 'b', + plane: 'intent', + kind: 'decision', + title: 'D1', detail: { - chosen_option: "X", - rejected: ["Y"], - rationale: "because", + chosen_option: 'X', + rejected: ['Y'], + rationale: 'because', }, }, ], - edges: [{ category: "dependency", source: "a", target: "b" }], - }) + edges: [{ category: 'dependency', source: 'a', target: 'b' }], + }); - if (result.status !== "success") throw new Error("unreachable") - const edgeRow = db.select().from(edges).all()[0]! - expect(edgeRow.source_id).toBe(result.nodes["a"]) - expect(edgeRow.target_id).toBe(result.nodes["b"]) - }) + if (result.status !== 'success') throw new Error('unreachable'); + const edgeRow = db.select().from(edges).all()[0]!; + expect(edgeRow.source_id).toBe(result.nodes['a']); + expect(edgeRow.target_id).toBe(result.nodes['b']); + }); - it("resolves existing-node refs to verified NodeIds", () => { + it('resolves existing-node refs to verified NodeIds', () => { // Pre-create a node const pre = executor.createNode({ - plane: "intent", - kind: "goal", - title: "Existing goal", - }) - if (pre.status !== "success") throw new Error("unreachable") + plane: 'intent', + kind: 'goal', + title: 'Existing goal', + }); + if (pre.status !== 'success') throw new Error('unreachable'); const result = executor.commitGraph({ - nodes: [ - { ref: "n1", plane: "intent", kind: "requirement", title: "New req" }, - ], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'requirement', title: 'New req' }], edges: [ { - category: "support", + category: 'support', source: { existing: pre.nodeId }, - target: "n1", - stance: "for", + target: 'n1', + stance: 'for', }, ], - }) + }); - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - const edgeRow = db.select().from(edges).all()[0]! - expect(edgeRow.source_id).toBe(pre.nodeId) - expect(edgeRow.target_id).toBe(result.nodes["n1"]) - }) + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + const edgeRow = db.select().from(edges).all()[0]!; + expect(edgeRow.source_id).toBe(pre.nodeId); + expect(edgeRow.target_id).toBe(result.nodes['n1']); + }); - it("returns nodes mapping and edges array in success result", () => { + it('returns nodes mapping and edges array in success result', () => { const result = executor.commitGraph({ nodes: [ - { ref: "x", plane: "intent", kind: "context", title: "Ctx" }, - { ref: "y", plane: "intent", kind: "thesis", title: "Thesis" }, + { ref: 'x', plane: 'intent', kind: 'context', title: 'Ctx' }, + { ref: 'y', plane: 'intent', kind: 'thesis', title: 'Thesis' }, ], edges: [], - }) + }); - if (result.status !== "success") throw new Error("unreachable") - expect(result.nodes["x"]).toBeTypeOf("number") - expect(result.nodes["y"]).toBeTypeOf("number") - expect(result.nodes["x"]).not.toBe(result.nodes["y"]) - expect(result.edges).toEqual([]) - }) + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.nodes['x']).toBeTypeOf('number'); + expect(result.nodes['y']).toBeTypeOf('number'); + expect(result.nodes['x']).not.toBe(result.nodes['y']); + expect(result.edges).toEqual([]); + }); - it("appends one change_log entry for the entire batch", () => { + it('appends one change_log entry for the entire batch', () => { executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "G1" }, - { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'G1' }, + { ref: 'n2', plane: 'intent', kind: 'goal', title: 'G2' }, ], - edges: [{ category: "association", source: "n1", target: "n2" }], - }) + edges: [{ category: 'association', source: 'n1', target: 'n2' }], + }); - const logs = db.select().from(changeLog).all() - expect(logs).toHaveLength(1) - expect(logs[0]!.operation).toBe("commit_graph") - const payload = JSON.parse(logs[0]!.payload) - expect(Object.keys(payload.nodes)).toHaveLength(2) - expect(payload.edges).toHaveLength(1) - }) + const logs = db.select().from(changeLog).all(); + expect(logs).toHaveLength(1); + expect(logs[0]!.operation).toBe('commit_graph'); + const payload = JSON.parse(logs[0]!.payload); + expect(Object.keys(payload.nodes)).toHaveLength(2); + expect(payload.edges).toHaveLength(1); + }); // --- edge structural validation --- - it("rejects edge with invalid category", () => { + it('rejects edge with invalid category', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "G" }, - { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'G' }, + { ref: 'n2', plane: 'intent', kind: 'goal', title: 'G2' }, ], - edges: [{ category: "invented_relation", source: "n1", target: "n2" }], - }) + edges: [{ category: 'invented_relation', source: 'n1', target: 'n2' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("category"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('category'))).toBe(true); + }); - it("rejects proof edge without stance", () => { + it('rejects proof edge without stance', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "criterion", title: "Cr" }, - { ref: "n2", plane: "intent", kind: "invariant", title: "Inv" }, + { ref: 'n1', plane: 'intent', kind: 'criterion', title: 'Cr' }, + { ref: 'n2', plane: 'intent', kind: 'invariant', title: 'Inv' }, ], - edges: [{ category: "proof", source: "n1", target: "n2" }], - }) + edges: [{ category: 'proof', source: 'n1', target: 'n2' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("stance"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('stance'))).toBe(true); + }); - it("rejects support edge without stance", () => { + it('rejects support edge without stance', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "context", title: "Ctx" }, - { ref: "n2", plane: "intent", kind: "requirement", title: "Req" }, + { ref: 'n1', plane: 'intent', kind: 'context', title: 'Ctx' }, + { ref: 'n2', plane: 'intent', kind: 'requirement', title: 'Req' }, ], - edges: [{ category: "support", source: "n1", target: "n2" }], - }) + edges: [{ category: 'support', source: 'n1', target: 'n2' }], + }); - expect(result.status).toBe("structural_illegal") - }) + expect(result.status).toBe('structural_illegal'); + }); - it("rejects non-proof/non-support edge with stance", () => { + it('rejects non-proof/non-support edge with stance', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "assumption", title: "A" }, - { ref: "n2", plane: "intent", kind: "requirement", title: "R" }, + { ref: 'n1', plane: 'intent', kind: 'assumption', title: 'A' }, + { ref: 'n2', plane: 'intent', kind: 'requirement', title: 'R' }, ], - edges: [ - { category: "dependency", source: "n1", target: "n2", stance: "for" }, - ], - }) + edges: [{ category: 'dependency', source: 'n1', target: 'n2', stance: 'for' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("stance"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('stance'))).toBe(true); + }); - it("rejects edge referencing non-existent existing node", () => { + it('rejects edge referencing non-existent existing node', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], - edges: [ - { category: "dependency", source: { existing: 9999 }, target: "n1" }, - ], - }) + nodes: [{ ref: 'n1', plane: 'intent', kind: 'goal', title: 'G' }], + edges: [{ category: 'dependency', source: { existing: 9999 }, target: 'n1' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("source"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('source'))).toBe(true); + }); - it("rejects edge with unresolvable intra-batch ref", () => { + it('rejects edge with unresolvable intra-batch ref', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], - edges: [ - { category: "dependency", source: "n1", target: "missing_ref" }, - ], - }) + nodes: [{ ref: 'n1', plane: 'intent', kind: 'goal', title: 'G' }], + edges: [{ category: 'dependency', source: 'n1', target: 'missing_ref' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("target"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('target'))).toBe(true); + }); - it("rejects self-loop edge", () => { + it('rejects self-loop edge', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "G" }], - edges: [{ category: "association", source: "n1", target: "n1" }], - }) + nodes: [{ ref: 'n1', plane: 'intent', kind: 'goal', title: 'G' }], + edges: [{ category: 'association', source: 'n1', target: 'n1' }], + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect( - result.diagnostics.some((d) => d.message.includes("self-loop")), - ).toBe(true) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.message.includes('self-loop'))).toBe(true); + }); // --- node validation reuse --- - it("rejects batch node with invalid kind-for-plane", () => { + it('rejects batch node with invalid kind-for-plane', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "check", title: "Wrong" }], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'check', title: 'Wrong' }], edges: [], - }) + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics.some((d) => d.field.includes("nodes[0]"))).toBe( - true, - ) - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.includes('nodes[0]'))).toBe(true); + }); - it("rejects batch decision without detail", () => { + it('rejects batch decision without detail', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "decision", title: "D" }], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'decision', title: 'D' }], edges: [], - }) + }); - expect(result.status).toBe("structural_illegal") - }) + expect(result.status).toBe('structural_illegal'); + }); // --- all-or-nothing (I34-L) --- - it("if any node fails validation, entire batch rejected — nothing written", () => { + it('if any node fails validation, entire batch rejected — nothing written', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "Valid" }, - { ref: "n2", plane: "intent", kind: "check", title: "Invalid kind" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'Valid' }, + { ref: 'n2', plane: 'intent', kind: 'check', title: 'Invalid kind' }, ], edges: [], - }) + }); - expect(result.status).toBe("structural_illegal") - expect(db.select().from(nodes).all()).toHaveLength(0) - const [clock] = db.select().from(graphClock).all() - expect(clock!.lsn).toBe(0) - }) + expect(result.status).toBe('structural_illegal'); + expect(db.select().from(nodes).all()).toHaveLength(0); + const [clock] = db.select().from(graphClock).all(); + expect(clock!.lsn).toBe(0); + }); - it("if any edge fails validation, no nodes written", () => { + it('if any edge fails validation, no nodes written', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "Valid goal" }, - { ref: "n2", plane: "intent", kind: "context", title: "Valid ctx" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'Valid goal' }, + { ref: 'n2', plane: 'intent', kind: 'context', title: 'Valid ctx' }, ], edges: [ - { category: "proof", source: "n1", target: "n2" }, // missing stance + { category: 'proof', source: 'n1', target: 'n2' }, // missing stance ], - }) + }); - expect(result.status).toBe("structural_illegal") + expect(result.status).toBe('structural_illegal'); // Transaction rolled back — no nodes either - expect(db.select().from(nodes).all()).toHaveLength(0) - const [clock] = db.select().from(graphClock).all() - expect(clock!.lsn).toBe(0) - }) + expect(db.select().from(nodes).all()).toHaveLength(0); + const [clock] = db.select().from(graphClock).all(); + expect(clock!.lsn).toBe(0); + }); - it("diagnostics include which entry failed", () => { + it('diagnostics include which entry failed', () => { const result = executor.commitGraph({ - nodes: [{ ref: "n1", plane: "intent", kind: "goal", title: "OK" }], - edges: [ - { category: "dependency", source: "n1", target: { existing: 9999 } }, - ], - }) + nodes: [{ ref: 'n1', plane: 'intent', kind: 'goal', title: 'OK' }], + edges: [{ category: 'dependency', source: 'n1', target: { existing: 9999 } }], + }); - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect( - result.diagnostics.some((d) => d.field.startsWith("edges[0]")), - ).toBe(true) - }) + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics.some((d) => d.field.startsWith('edges[0]'))).toBe(true); + }); // --- edge cases --- - it("edge-only batch between existing nodes", () => { + it('edge-only batch between existing nodes', () => { const a = executor.createNode({ - plane: "intent", - kind: "requirement", - title: "R1", - }) + plane: 'intent', + kind: 'requirement', + title: 'R1', + }); const b = executor.createNode({ - plane: "intent", - kind: "assumption", - title: "A1", - }) - if (a.status !== "success" || b.status !== "success") - throw new Error("unreachable") + plane: 'intent', + kind: 'assumption', + title: 'A1', + }); + if (a.status !== 'success' || b.status !== 'success') throw new Error('unreachable'); const result = executor.commitGraph({ nodes: [], edges: [ { - category: "dependency", + category: 'dependency', source: { existing: b.nodeId }, target: { existing: a.nodeId }, }, ], - }) + }); - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - expect(Object.keys(result.nodes)).toHaveLength(0) - expect(result.edges).toHaveLength(1) - }) + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(Object.keys(result.nodes)).toHaveLength(0); + expect(result.edges).toHaveLength(1); + }); - it("node-only batch (no edges)", () => { + it('node-only batch (no edges)', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "context", title: "C1" }, - { ref: "n2", plane: "intent", kind: "context", title: "C2" }, + { ref: 'n1', plane: 'intent', kind: 'context', title: 'C1' }, + { ref: 'n2', plane: 'intent', kind: 'context', title: 'C2' }, ], edges: [], - }) + }); - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - expect(Object.keys(result.nodes)).toHaveLength(2) - expect(result.edges).toEqual([]) - }) + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(Object.keys(result.nodes)).toHaveLength(2); + expect(result.edges).toEqual([]); + }); - it("empty batch → structural_illegal", () => { - const result = executor.commitGraph({ nodes: [], edges: [] }) - expect(result.status).toBe("structural_illegal") - }) + it('empty batch → structural_illegal', () => { + const result = executor.commitGraph({ nodes: [], edges: [] }); + expect(result.status).toBe('structural_illegal'); + }); // --- mixed refs --- - it("edges can mix intra-batch source with existing target", () => { + it('edges can mix intra-batch source with existing target', () => { const pre = executor.createNode({ - plane: "intent", - kind: "goal", - title: "Existing", - }) - if (pre.status !== "success") throw new Error("unreachable") + plane: 'intent', + kind: 'goal', + title: 'Existing', + }); + if (pre.status !== 'success') throw new Error('unreachable'); const result = executor.commitGraph({ - nodes: [ - { ref: "new", plane: "intent", kind: "requirement", title: "New" }, - ], + nodes: [{ ref: 'new', plane: 'intent', kind: 'requirement', title: 'New' }], edges: [ { - category: "realization", + category: 'realization', source: { existing: pre.nodeId }, - target: "new", + target: 'new', }, ], - }) + }); - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - const edgeRow = db.select().from(edges).all()[0]! - expect(edgeRow.source_id).toBe(pre.nodeId) - expect(edgeRow.target_id).toBe(result.nodes["new"]) - }) + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + const edgeRow = db.select().from(edges).all()[0]!; + expect(edgeRow.source_id).toBe(pre.nodeId); + expect(edgeRow.target_id).toBe(result.nodes['new']); + }); // --- LSN behavior --- - it("uses one LSN for the entire batch (not per-entity)", () => { + it('uses one LSN for the entire batch (not per-entity)', () => { const result = executor.commitGraph({ nodes: [ - { ref: "n1", plane: "intent", kind: "goal", title: "G1" }, - { ref: "n2", plane: "intent", kind: "goal", title: "G2" }, + { ref: 'n1', plane: 'intent', kind: 'goal', title: 'G1' }, + { ref: 'n2', plane: 'intent', kind: 'goal', title: 'G2' }, ], - edges: [{ category: "association", source: "n1", target: "n2" }], - }) + edges: [{ category: 'association', source: 'n1', target: 'n2' }], + }); - if (result.status !== "success") throw new Error("unreachable") - const allNodes = db.select().from(nodes).all() - const allEdges = db.select().from(edges).all() + if (result.status !== 'success') throw new Error('unreachable'); + const allNodes = db.select().from(nodes).all(); + const allEdges = db.select().from(edges).all(); // All entities share the same LSN for (const n of allNodes) { - expect(n.created_at_lsn).toBe(result.lsn) + expect(n.created_at_lsn).toBe(result.lsn); } for (const e of allEdges) { - expect(e.created_at_lsn).toBe(result.lsn) + expect(e.created_at_lsn).toBe(result.lsn); } - }) - }) + }); + }); // --- createReconciliationNeed --- - describe("createReconciliationNeed", () => { - it("creates a recon need targeting an edge and returns success with id and lsn", () => { + describe('createReconciliationNeed', () => { + it('creates a recon need targeting an edge and returns success with id and lsn', () => { // Seed a node and edge first const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") - const edgeId = batch.edges[0]! + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); + const edgeId = batch.edges[0]!; const result = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId }, - needKind: "edge_revalidation", - reason: "upstream assumption changed", - }) - - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - expect(result.id).toBeTypeOf("number") - expect(result.lsn).toBeTypeOf("number") - }) - - it("creates a recon need targeting a node pair", () => { + target: { kind: 'edge', edgeId }, + needKind: 'edge_revalidation', + reason: 'upstream assumption changed', + }); + + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.id).toBeTypeOf('number'); + expect(result.lsn).toBeTypeOf('number'); + }); + + it('creates a recon need targeting a node pair', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "r2", plane: "intent", kind: "requirement", title: "R2" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'r2', plane: 'intent', kind: 'requirement', title: 'R2' }, ], edges: [], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") - const aId = batch.nodes["r1"]! - const bId = batch.nodes["r2"]! + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); + const aId = batch.nodes['r1']!; + const bId = batch.nodes['r2']!; const result = executor.createReconciliationNeed({ - target: { kind: "node_pair", aId, bId }, - needKind: "possible_duplicate", - }) + target: { kind: 'node_pair', aId, bId }, + needKind: 'possible_duplicate', + }); - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") - expect(result.id).toBeTypeOf("number") - }) + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.id).toBeTypeOf('number'); + }); - it("rejects edge target with non-existent edgeId", () => { + it('rejects edge target with non-existent edgeId', () => { const result = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId: 999 }, - needKind: "edge_revalidation", - }) + target: { kind: 'edge', edgeId: 999 }, + needKind: 'edge_revalidation', + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics[0]!.field).toBe("target.edgeId") - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics[0]!.field).toBe('target.edgeId'); + }); - it("rejects node_pair target with non-existent nodeId", () => { + it('rejects node_pair target with non-existent nodeId', () => { const n = executor.createNode({ - plane: "intent", - kind: "goal", - title: "G1", - }) - expect(n.status).toBe("success") - if (n.status !== "success") throw new Error("unreachable") + plane: 'intent', + kind: 'goal', + title: 'G1', + }); + expect(n.status).toBe('success'); + if (n.status !== 'success') throw new Error('unreachable'); const result = executor.createReconciliationNeed({ - target: { kind: "node_pair", aId: n.nodeId, bId: 999 }, - needKind: "possible_relation", - }) + target: { kind: 'node_pair', aId: n.nodeId, bId: 999 }, + needKind: 'possible_relation', + }); - expect(result.status).toBe("structural_illegal") - if (result.status !== "structural_illegal") throw new Error("unreachable") - expect(result.diagnostics[0]!.field).toBe("target.bId") - }) + expect(result.status).toBe('structural_illegal'); + if (result.status !== 'structural_illegal') throw new Error('unreachable'); + expect(result.diagnostics[0]!.field).toBe('target.bId'); + }); - it("allocates a new LSN for each recon need", () => { + it('allocates a new LSN for each recon need', () => { const n = executor.createNode({ - plane: "intent", - kind: "goal", - title: "G1", - }) - expect(n.status).toBe("success") - if (n.status !== "success") throw new Error("unreachable") + plane: 'intent', + kind: 'goal', + title: 'G1', + }); + expect(n.status).toBe('success'); + if (n.status !== 'success') throw new Error('unreachable'); const n2 = executor.createNode({ - plane: "intent", - kind: "goal", - title: "G2", - }) - expect(n2.status).toBe("success") - if (n2.status !== "success") throw new Error("unreachable") + plane: 'intent', + kind: 'goal', + title: 'G2', + }); + expect(n2.status).toBe('success'); + if (n2.status !== 'success') throw new Error('unreachable'); const r1 = executor.createReconciliationNeed({ - target: { kind: "node_pair", aId: n.nodeId, bId: n2.nodeId }, - needKind: "possible_relation", - }) - expect(r1.status).toBe("success") - if (r1.status !== "success") throw new Error("unreachable") + target: { kind: 'node_pair', aId: n.nodeId, bId: n2.nodeId }, + needKind: 'possible_relation', + }); + expect(r1.status).toBe('success'); + if (r1.status !== 'success') throw new Error('unreachable'); const r2 = executor.createReconciliationNeed({ - target: { kind: "node_pair", aId: n.nodeId, bId: n2.nodeId }, - needKind: "semantic_conflict", - }) - expect(r2.status).toBe("success") - if (r2.status !== "success") throw new Error("unreachable") + target: { kind: 'node_pair', aId: n.nodeId, bId: n2.nodeId }, + needKind: 'semantic_conflict', + }); + expect(r2.status).toBe('success'); + if (r2.status !== 'success') throw new Error('unreachable'); - expect(r2.lsn).toBeGreaterThan(r1.lsn) - }) - }) + expect(r2.lsn).toBeGreaterThan(r1.lsn); + }); + }); // --- resolveReconciliationNeed --- - describe("resolveReconciliationNeed", () => { - it("resolves an open need and records resolvedAtLsn", () => { + describe('resolveReconciliationNeed', () => { + it('resolves an open need and records resolvedAtLsn', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); const create = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId: batch.edges[0]! }, - needKind: "edge_revalidation", - }) - expect(create.status).toBe("success") - if (create.status !== "success") throw new Error("unreachable") - - const resolve = executor.resolveReconciliationNeed(create.id) - expect(resolve.status).toBe("success") - if (resolve.status !== "success") throw new Error("unreachable") - expect(resolve.lsn).toBeGreaterThan(create.lsn) - }) - - it("rejects non-existent need id", () => { - const result = executor.resolveReconciliationNeed(999) - expect(result.status).toBe("structural_illegal") - }) - - it("rejects already-resolved need", () => { + target: { kind: 'edge', edgeId: batch.edges[0]! }, + needKind: 'edge_revalidation', + }); + expect(create.status).toBe('success'); + if (create.status !== 'success') throw new Error('unreachable'); + + const resolve = executor.resolveReconciliationNeed(create.id); + expect(resolve.status).toBe('success'); + if (resolve.status !== 'success') throw new Error('unreachable'); + expect(resolve.lsn).toBeGreaterThan(create.lsn); + }); + + it('rejects non-existent need id', () => { + const result = executor.resolveReconciliationNeed(999); + expect(result.status).toBe('structural_illegal'); + }); + + it('rejects already-resolved need', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); const create = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId: batch.edges[0]! }, - needKind: "edge_revalidation", - }) - expect(create.status).toBe("success") - if (create.status !== "success") throw new Error("unreachable") - - const resolve1 = executor.resolveReconciliationNeed(create.id) - expect(resolve1.status).toBe("success") - - const resolve2 = executor.resolveReconciliationNeed(create.id) - expect(resolve2.status).toBe("structural_illegal") - if (resolve2.status !== "structural_illegal") - throw new Error("unreachable") - expect(resolve2.diagnostics[0]!.message).toContain("already resolved") - }) - }) -}) + target: { kind: 'edge', edgeId: batch.edges[0]! }, + needKind: 'edge_revalidation', + }); + expect(create.status).toBe('success'); + if (create.status !== 'success') throw new Error('unreachable'); + + const resolve1 = executor.resolveReconciliationNeed(create.id); + expect(resolve1.status).toBe('success'); + + const resolve2 = executor.resolveReconciliationNeed(create.id); + expect(resolve2.status).toBe('structural_illegal'); + if (resolve2.status !== 'structural_illegal') throw new Error('unreachable'); + expect(resolve2.diagnostics[0]!.message).toContain('already resolved'); + }); + }); +}); diff --git a/src/graph/command-executor.ts b/src/graph/command-executor.ts index 036b6764f..6f407a8c0 100644 --- a/src/graph/command-executor.ts +++ b/src/graph/command-executor.ts @@ -17,12 +17,12 @@ * even though pre-M6 policy classification is minimal. */ -import { eq, inArray, sql } from "drizzle-orm" +import { eq, inArray, sql } from 'drizzle-orm'; -import type { BrunchDb } from "../db/connection.js" -import * as schema from "../db/schema.js" -import type { EdgeCategory, EdgeStance } from "./schema/edges.js" -import type { NodeBasis, NodePlane } from "./schema/nodes.js" +import type { BrunchDb } from '../db/connection.js'; +import * as schema from '../db/schema.js'; +import type { EdgeCategory, EdgeStance } from './schema/edges.js'; +import type { NodeBasis, NodePlane } from './schema/nodes.js'; // --------------------------------------------------------------------------- // Result types @@ -30,73 +30,81 @@ import type { NodeBasis, NodePlane } from "./schema/nodes.js" /** A single validation problem discovered during structural checks. */ export interface Diagnostic { - readonly field: string - readonly message: string + readonly field: string; + readonly message: string; } /** Successful command execution. */ export interface CommandSuccess { - readonly status: "success" - readonly nodeId: number - readonly lsn: number + readonly status: 'success'; + readonly nodeId: number; + readonly lsn: number; } /** Structurally invalid input — validation failed before any write. */ export interface StructuralIllegal { - readonly status: "structural_illegal" - readonly diagnostics: readonly Diagnostic[] + readonly status: 'structural_illegal'; + readonly diagnostics: readonly Diagnostic[]; } /** Action requires human confirmation (M6 placeholder). */ export interface NeedsHuman { - readonly status: "needs_human" + readonly status: 'needs_human'; } /** Action blocked by authority policy (M6 placeholder). */ export interface PolicyBlocked { - readonly status: "policy_blocked" + readonly status: 'policy_blocked'; } /** Optimistic concurrency conflict (M6 placeholder). */ export interface VersionConflict { - readonly status: "version_conflict" + readonly status: 'version_conflict'; } /** Successful commitGraph batch execution. */ export interface CommitGraphSuccess { - readonly status: "success" - readonly lsn: number - readonly nodes: Readonly> - readonly edges: readonly number[] + readonly status: 'success'; + readonly lsn: number; + readonly nodes: Readonly>; + readonly edges: readonly number[]; } /** Successful reconciliation-need creation. */ export interface ReconNeedSuccess { - readonly status: "success" - readonly id: number - readonly lsn: number + readonly status: 'success'; + readonly id: number; + readonly lsn: number; } /** Successful reconciliation-need resolution. */ export interface ReconNeedResolveSuccess { - readonly status: "success" - readonly lsn: number + readonly status: 'success'; + readonly lsn: number; } /** Union of all possible command results. */ -export type CommandResult = CommandSuccess | CommitGraphSuccess | ReconNeedSuccess | ReconNeedResolveSuccess | StructuralIllegal | NeedsHuman | PolicyBlocked | VersionConflict +export type CommandResult = + | CommandSuccess + | CommitGraphSuccess + | ReconNeedSuccess + | ReconNeedResolveSuccess + | StructuralIllegal + | NeedsHuman + | PolicyBlocked + | VersionConflict; /** Result of a createNode command. */ -export type CreateNodeResult = CommandSuccess | StructuralIllegal +export type CreateNodeResult = CommandSuccess | StructuralIllegal; /** Result of a commitGraph command. */ -export type CommitGraphResult = CommitGraphSuccess | StructuralIllegal +export type CommitGraphResult = CommitGraphSuccess | StructuralIllegal; /** Result of a createReconciliationNeed command. */ -export type CreateReconNeedResult = ReconNeedSuccess | StructuralIllegal +export type CreateReconNeedResult = ReconNeedSuccess | StructuralIllegal; /** Result of a resolveReconciliationNeed command. */ -export type ResolveReconNeedResult = ReconNeedResolveSuccess | StructuralIllegal +export type ResolveReconNeedResult = ReconNeedResolveSuccess | StructuralIllegal; // --------------------------------------------------------------------------- // Input types @@ -104,13 +112,13 @@ export type ResolveReconNeedResult = ReconNeedResolveSuccess | StructuralIllegal /** Input for creating a single graph node. */ export interface CreateNodeInput { - readonly plane: NodePlane - readonly kind: string - readonly title: string - readonly body?: string | undefined - readonly basis?: NodeBasis | undefined - readonly source?: string | undefined - readonly detail?: unknown + readonly plane: NodePlane; + readonly kind: string; + readonly title: string; + readonly body?: string | undefined; + readonly basis?: NodeBasis | undefined; + readonly source?: string | undefined; + readonly detail?: unknown; } // --------------------------------------------------------------------------- @@ -119,25 +127,25 @@ export interface CreateNodeInput { /** Target for a reconciliation need — edge or node pair. */ export type ReconNeedTargetEdge = { - readonly kind: "edge" - readonly edgeId: number -} + readonly kind: 'edge'; + readonly edgeId: number; +}; /** Target for a reconciliation need — node pair. */ export type ReconNeedTargetNodePair = { - readonly kind: "node_pair" - readonly aId: number - readonly bId: number -} + readonly kind: 'node_pair'; + readonly aId: number; + readonly bId: number; +}; /** Target for a reconciliation need. */ -export type ReconNeedTarget = ReconNeedTargetEdge | ReconNeedTargetNodePair +export type ReconNeedTarget = ReconNeedTargetEdge | ReconNeedTargetNodePair; /** Input for creating a reconciliation need. */ export interface CreateReconNeedInput { - readonly target: ReconNeedTarget - readonly needKind: string - readonly reason?: string | undefined + readonly target: ReconNeedTarget; + readonly needKind: string; + readonly reason?: string | undefined; } // --------------------------------------------------------------------------- @@ -145,34 +153,34 @@ export interface CreateReconNeedInput { // --------------------------------------------------------------------------- /** Reference to a node endpoint in a batch edge. */ -export type BatchEdgeRef = string | { readonly existing: number } +export type BatchEdgeRef = string | { readonly existing: number }; /** A node to create inside a commitGraph batch. */ export interface BatchNodeInput { - readonly ref: string - readonly plane: NodePlane - readonly kind: string - readonly title: string - readonly body?: string | undefined - readonly basis?: NodeBasis | undefined - readonly source?: string | undefined - readonly detail?: unknown + readonly ref: string; + readonly plane: NodePlane; + readonly kind: string; + readonly title: string; + readonly body?: string | undefined; + readonly basis?: NodeBasis | undefined; + readonly source?: string | undefined; + readonly detail?: unknown; } /** An edge to create inside a commitGraph batch. */ export interface BatchEdgeInput { - readonly category: string - readonly source: BatchEdgeRef - readonly target: BatchEdgeRef - readonly stance?: string | undefined - readonly basis?: NodeBasis | undefined - readonly rationale?: string | undefined + readonly category: string; + readonly source: BatchEdgeRef; + readonly target: BatchEdgeRef; + readonly stance?: string | undefined; + readonly basis?: NodeBasis | undefined; + readonly rationale?: string | undefined; } /** Input for the commitGraph atomic batch mutation. */ export interface CommitGraphInput { - readonly nodes: readonly BatchNodeInput[] - readonly edges: readonly BatchEdgeInput[] + readonly nodes: readonly BatchNodeInput[]; + readonly edges: readonly BatchEdgeInput[]; } // --------------------------------------------------------------------------- @@ -184,130 +192,126 @@ const VALID_KINDS_BY_PLANE: Record = { oracle: schema.ORACLE_KINDS as unknown as string[], design: schema.DESIGN_KINDS as unknown as string[], plan: schema.PLAN_KINDS as unknown as string[], -} +}; -const KINDS_REQUIRING_DETAIL = new Set(["decision", "term"]) +const KINDS_REQUIRING_DETAIL = new Set(['decision', 'term']); function validateCreateNode(input: CreateNodeInput): Diagnostic[] { - const diagnostics: Diagnostic[] = [] + const diagnostics: Diagnostic[] = []; // Title must be non-empty if (!input.title.trim()) { - diagnostics.push({ field: "title", message: "title must be non-empty" }) + diagnostics.push({ field: 'title', message: 'title must be non-empty' }); } // Kind must be valid for the given plane - const validKinds = VALID_KINDS_BY_PLANE[input.plane] + const validKinds = VALID_KINDS_BY_PLANE[input.plane]; if (!validKinds?.includes(input.kind)) { diagnostics.push({ - field: "kind", + field: 'kind', message: `"${input.kind}" is not a valid kind for plane "${input.plane}"`, - }) - return diagnostics // can't validate detail if kind is wrong + }); + return diagnostics; // can't validate detail if kind is wrong } // Detail requirement: decision and term REQUIRE detail if (KINDS_REQUIRING_DETAIL.has(input.kind) && input.detail == null) { diagnostics.push({ - field: "detail", + field: 'detail', message: `"${input.kind}" nodes require a detail object`, - }) - return diagnostics + }); + return diagnostics; } // Detail prohibition: all other kinds must NOT have detail if (!KINDS_REQUIRING_DETAIL.has(input.kind) && input.detail != null) { diagnostics.push({ - field: "detail", + field: 'detail', message: `"${input.kind}" nodes must not have a detail object`, - }) - return diagnostics + }); + return diagnostics; } // Validate detail shape per kind - if (input.kind === "decision" && input.detail != null) { - validateDecisionDetail(input.detail, diagnostics) + if (input.kind === 'decision' && input.detail != null) { + validateDecisionDetail(input.detail, diagnostics); } - if (input.kind === "term" && input.detail != null) { - validateTermDetail(input.detail, diagnostics) + if (input.kind === 'term' && input.detail != null) { + validateTermDetail(input.detail, diagnostics); } - return diagnostics + return diagnostics; } -function validateDecisionDetail( - detail: unknown, - diagnostics: Diagnostic[], -): void { - if (typeof detail !== "object" || detail === null) { - diagnostics.push({ field: "detail", message: "must be an object" }) - return +function validateDecisionDetail(detail: unknown, diagnostics: Diagnostic[]): void { + if (typeof detail !== 'object' || detail === null) { + diagnostics.push({ field: 'detail', message: 'must be an object' }); + return; } - const d = detail as Record - const knownFields = new Set(["chosen_option", "rejected", "rationale"]) + const d = detail as Record; + const knownFields = new Set(['chosen_option', 'rejected', 'rationale']); - if (typeof d["chosen_option"] !== "string") { + if (typeof d['chosen_option'] !== 'string') { diagnostics.push({ - field: "detail.chosen_option", - message: "required string", - }) + field: 'detail.chosen_option', + message: 'required string', + }); } if ( - !Array.isArray(d["rejected"]) || - d["rejected"].length < 1 || - !d["rejected"].every((r) => typeof r === "string") + !Array.isArray(d['rejected']) || + d['rejected'].length < 1 || + !d['rejected'].every((r) => typeof r === 'string') ) { diagnostics.push({ - field: "detail.rejected", - message: "required non-empty string array", - }) + field: 'detail.rejected', + message: 'required non-empty string array', + }); } - if (typeof d["rationale"] !== "string") { - diagnostics.push({ field: "detail.rationale", message: "required string" }) + if (typeof d['rationale'] !== 'string') { + diagnostics.push({ field: 'detail.rationale', message: 'required string' }); } // Closed validation: reject unknown fields for (const key of Object.keys(d)) { if (!knownFields.has(key)) { - diagnostics.push({ field: `detail.${key}`, message: "unknown field" }) + diagnostics.push({ field: `detail.${key}`, message: 'unknown field' }); } } } function validateTermDetail(detail: unknown, diagnostics: Diagnostic[]): void { - if (typeof detail !== "object" || detail === null) { - diagnostics.push({ field: "detail", message: "must be an object" }) - return + if (typeof detail !== 'object' || detail === null) { + diagnostics.push({ field: 'detail', message: 'must be an object' }); + return; } - const d = detail as Record - const knownFields = new Set(["definition", "aliases"]) + const d = detail as Record; + const knownFields = new Set(['definition', 'aliases']); - if (typeof d["definition"] !== "string") { + if (typeof d['definition'] !== 'string') { diagnostics.push({ - field: "detail.definition", - message: "required string", - }) + field: 'detail.definition', + message: 'required string', + }); } if ( - d["aliases"] != null && - (!Array.isArray(d["aliases"]) || - !d["aliases"].every((a) => typeof a === "string")) + d['aliases'] != null && + (!Array.isArray(d['aliases']) || !d['aliases'].every((a) => typeof a === 'string')) ) { diagnostics.push({ - field: "detail.aliases", - message: "must be a string array if present", - }) + field: 'detail.aliases', + message: 'must be a string array if present', + }); } // Closed validation: reject unknown fields for (const key of Object.keys(d)) { if (!knownFields.has(key)) { - diagnostics.push({ field: `detail.${key}`, message: "unknown field" }) + diagnostics.push({ field: `detail.${key}`, message: 'unknown field' }); } } } @@ -316,22 +320,22 @@ function validateTermDetail(detail: unknown, diagnostics: Diagnostic[]): void { // Edge validation // --------------------------------------------------------------------------- -const VALID_CATEGORIES = schema.EDGE_CATEGORIES as unknown as string[] -const STANCE_REQUIRED_CATEGORIES = new Set(["proof", "support"]) -const VALID_STANCES = schema.EDGE_STANCES as unknown as string[] +const VALID_CATEGORIES = schema.EDGE_CATEGORIES as unknown as string[]; +const STANCE_REQUIRED_CATEGORIES = new Set(['proof', 'support']); +const VALID_STANCES = schema.EDGE_STANCES as unknown as string[]; interface ResolvedEdge { - sourceId: number - targetId: number - category: EdgeCategory - stance: EdgeStance | null - basis: NodeBasis - rationale: string | null + sourceId: number; + targetId: number; + category: EdgeCategory; + stance: EdgeStance | null; + basis: NodeBasis; + rationale: string | null; } interface EdgeValidationResult { - diagnostics: Diagnostic[] - resolved?: ResolvedEdge + diagnostics: Diagnostic[]; + resolved?: ResolvedEdge; } function validateAndResolveBatchEdge( @@ -340,76 +344,76 @@ function validateAndResolveBatchEdge( refMap: ReadonlyMap, existingNodeIds: ReadonlySet, ): EdgeValidationResult { - const diagnostics: Diagnostic[] = [] - const p = `edges[${index}]` + const diagnostics: Diagnostic[] = []; + const p = `edges[${index}]`; // Category must be in the closed set if (!VALID_CATEGORIES.includes(input.category)) { diagnostics.push({ field: `${p}.category`, message: `"${input.category}" is not a valid edge category`, - }) - return { diagnostics } + }); + return { diagnostics }; } // Stance: required iff proof/support, invalid otherwise - const stanceRequired = STANCE_REQUIRED_CATEGORIES.has(input.category) + const stanceRequired = STANCE_REQUIRED_CATEGORIES.has(input.category); if (stanceRequired && input.stance == null) { diagnostics.push({ field: `${p}.stance`, message: `stance is required for "${input.category}" edges`, - }) + }); } if (!stanceRequired && input.stance != null) { diagnostics.push({ field: `${p}.stance`, message: `stance is not allowed for "${input.category}" edges`, - }) + }); } if (input.stance != null && !VALID_STANCES.includes(input.stance)) { diagnostics.push({ field: `${p}.stance`, message: `"${input.stance}" is not a valid stance`, - }) + }); } // Resolve source ref - let resolvedSourceId: number | undefined - if (typeof input.source === "string") { - resolvedSourceId = refMap.get(input.source) + let resolvedSourceId: number | undefined; + if (typeof input.source === 'string') { + resolvedSourceId = refMap.get(input.source); if (resolvedSourceId === undefined) { diagnostics.push({ field: `${p}.source`, message: `unresolvable intra-batch ref "${input.source}"`, - }) + }); } } else { - resolvedSourceId = input.source.existing + resolvedSourceId = input.source.existing; if (!existingNodeIds.has(resolvedSourceId)) { diagnostics.push({ field: `${p}.source`, message: `existing node ${resolvedSourceId} not found`, - }) + }); } } // Resolve target ref - let resolvedTargetId: number | undefined - if (typeof input.target === "string") { - resolvedTargetId = refMap.get(input.target) + let resolvedTargetId: number | undefined; + if (typeof input.target === 'string') { + resolvedTargetId = refMap.get(input.target); if (resolvedTargetId === undefined) { diagnostics.push({ field: `${p}.target`, message: `unresolvable intra-batch ref "${input.target}"`, - }) + }); } } else { - resolvedTargetId = input.target.existing + resolvedTargetId = input.target.existing; if (!existingNodeIds.has(resolvedTargetId)) { diagnostics.push({ field: `${p}.target`, message: `existing node ${resolvedTargetId} not found`, - }) + }); } } @@ -421,11 +425,11 @@ function validateAndResolveBatchEdge( ) { diagnostics.push({ field: p, - message: "self-loop: source and target resolve to the same node", - }) + message: 'self-loop: source and target resolve to the same node', + }); } - if (diagnostics.length > 0) return { diagnostics } + if (diagnostics.length > 0) return { diagnostics }; return { diagnostics, @@ -433,17 +437,17 @@ function validateAndResolveBatchEdge( sourceId: resolvedSourceId!, targetId: resolvedTargetId!, category: input.category as EdgeCategory, - stance: input.stance as EdgeStance ?? null, - basis: input.basis as NodeBasis ?? "explicit", + stance: (input.stance as EdgeStance) ?? null, + basis: (input.basis as NodeBasis) ?? 'explicit', rationale: input.rationale ?? null, }, - } + }; } /** Thrown inside a transaction to trigger rollback on edge validation failure. */ class BatchValidationError extends Error { constructor(readonly diagnostics: readonly Diagnostic[]) { - super("batch validation failed") + super('batch validation failed'); } } @@ -463,9 +467,9 @@ export class CommandExecutor { * On validation failure, nothing is written. */ createNode(input: CreateNodeInput): CreateNodeResult { - const diagnostics = validateCreateNode(input) + const diagnostics = validateCreateNode(input); if (diagnostics.length > 0) { - return { status: "structural_illegal", diagnostics } + return { status: 'structural_illegal', diagnostics }; } return this.db.transaction((tx) => { @@ -475,8 +479,8 @@ export class CommandExecutor { .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) .where(eq(schema.graphClock.id, 1)) .returning() - .get() - const lsn = clock!.lsn + .get(); + const lsn = clock!.lsn; // 2. Insert node const node = tx @@ -486,31 +490,31 @@ export class CommandExecutor { kind: input.kind, title: input.title, body: input.body ?? null, - basis: input.basis ?? "explicit", + basis: input.basis ?? 'explicit', source: input.source ?? null, detail: input.detail != null ? JSON.stringify(input.detail) : null, created_at_lsn: lsn, updated_at_lsn: lsn, }) .returning() - .get() - const nodeId = node!.id + .get(); + const nodeId = node!.id; // 3. Append change_log tx.insert(schema.changeLog) .values({ lsn, - operation: "create_node", + operation: 'create_node', payload: JSON.stringify({ nodeId, plane: input.plane, kind: input.kind, }), }) - .run() + .run(); - return { status: "success" as const, nodeId, lsn } - }) + return { status: 'success' as const, nodeId, lsn }; + }); } /** @@ -525,40 +529,38 @@ export class CommandExecutor { // Empty batch is structural_illegal if (input.nodes.length === 0 && input.edges.length === 0) { return { - status: "structural_illegal", - diagnostics: [ - { field: "batch", message: "empty batch — nothing to commit" }, - ], - } + status: 'structural_illegal', + diagnostics: [{ field: 'batch', message: 'empty batch — nothing to commit' }], + }; } // --- Pre-transaction: validate all batch nodes (pure checks) --- - const preDiagnostics: Diagnostic[] = [] - const seenRefs = new Set() + const preDiagnostics: Diagnostic[] = []; + const seenRefs = new Set(); for (let i = 0; i < input.nodes.length; i++) { - const bn = input.nodes[i]! + const bn = input.nodes[i]!; // Duplicate ref check if (seenRefs.has(bn.ref)) { preDiagnostics.push({ field: `nodes[${i}].ref`, message: `duplicate batch ref "${bn.ref}"`, - }) + }); } - seenRefs.add(bn.ref) + seenRefs.add(bn.ref); // Structural node validation (reuse) for (const d of validateCreateNode(bn)) { preDiagnostics.push({ field: `nodes[${i}].${d.field}`, message: d.message, - }) + }); } } if (preDiagnostics.length > 0) { - return { status: "structural_illegal", diagnostics: preDiagnostics } + return { status: 'structural_illegal', diagnostics: preDiagnostics }; } // --- Transaction: insert nodes, resolve refs, validate + insert edges --- @@ -570,11 +572,11 @@ export class CommandExecutor { .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) .where(eq(schema.graphClock.id, 1)) .returning() - .get() - const lsn = clock!.lsn + .get(); + const lsn = clock!.lsn; // 2. Insert all nodes, build ref → id map - const refMap = new Map() + const refMap = new Map(); for (const bn of input.nodes) { const row = tx .insert(schema.nodes) @@ -583,57 +585,50 @@ export class CommandExecutor { kind: bn.kind, title: bn.title, body: bn.body ?? null, - basis: bn.basis ?? "explicit", + basis: bn.basis ?? 'explicit', source: bn.source ?? null, detail: bn.detail != null ? JSON.stringify(bn.detail) : null, created_at_lsn: lsn, updated_at_lsn: lsn, }) .returning() - .get() - refMap.set(bn.ref, row!.id) + .get(); + refMap.set(bn.ref, row!.id); } // 3. Collect and verify existing-node references - const existingRefs = new Set() + const existingRefs = new Set(); for (const edge of input.edges) { - if (typeof edge.source !== "string") - existingRefs.add(edge.source.existing) - if (typeof edge.target !== "string") - existingRefs.add(edge.target.existing) + if (typeof edge.source !== 'string') existingRefs.add(edge.source.existing); + if (typeof edge.target !== 'string') existingRefs.add(edge.target.existing); } - const verifiedExisting = new Set() + const verifiedExisting = new Set(); if (existingRefs.size > 0) { const rows = tx .select({ id: schema.nodes.id }) .from(schema.nodes) .where(inArray(schema.nodes.id, [...existingRefs])) - .all() - for (const row of rows) verifiedExisting.add(row.id) + .all(); + for (const row of rows) verifiedExisting.add(row.id); } // 4. Validate and resolve all edges - const edgeDiagnostics: Diagnostic[] = [] - const resolvedEdges: ResolvedEdge[] = [] + const edgeDiagnostics: Diagnostic[] = []; + const resolvedEdges: ResolvedEdge[] = []; for (let i = 0; i < input.edges.length; i++) { - const result = validateAndResolveBatchEdge( - input.edges[i]!, - i, - refMap, - verifiedExisting, - ) - edgeDiagnostics.push(...result.diagnostics) - if (result.resolved) resolvedEdges.push(result.resolved) + const result = validateAndResolveBatchEdge(input.edges[i]!, i, refMap, verifiedExisting); + edgeDiagnostics.push(...result.diagnostics); + if (result.resolved) resolvedEdges.push(result.resolved); } if (edgeDiagnostics.length > 0) { - throw new BatchValidationError(edgeDiagnostics) + throw new BatchValidationError(edgeDiagnostics); } // 5. Insert all edges - const edgeIds: number[] = [] + const edgeIds: number[] = []; for (const re of resolvedEdges) { const row = tx .insert(schema.edges) @@ -648,34 +643,34 @@ export class CommandExecutor { updated_at_lsn: lsn, }) .returning() - .get() - edgeIds.push(row!.id) + .get(); + edgeIds.push(row!.id); } // 6. Append one change_log entry for the entire batch tx.insert(schema.changeLog) .values({ lsn, - operation: "commit_graph", + operation: 'commit_graph', payload: JSON.stringify({ nodes: Object.fromEntries(refMap), edges: edgeIds, }), }) - .run() + .run(); return { - status: "success" as const, + status: 'success' as const, lsn, nodes: Object.fromEntries(refMap), edges: edgeIds, - } - }) + }; + }); } catch (e) { if (e instanceof BatchValidationError) { - return { status: "structural_illegal", diagnostics: e.diagnostics } + return { status: 'structural_illegal', diagnostics: e.diagnostics }; } - throw e + throw e; } } @@ -688,47 +683,47 @@ export class CommandExecutor { createReconciliationNeed(input: CreateReconNeedInput): CreateReconNeedResult { // Validate target references exist return this.db.transaction((tx) => { - const diagnostics: Diagnostic[] = [] + const diagnostics: Diagnostic[] = []; - if (input.target.kind === "edge") { + if (input.target.kind === 'edge') { const row = tx .select({ id: schema.edges.id }) .from(schema.edges) .where(eq(schema.edges.id, input.target.edgeId)) - .get() + .get(); if (!row) { diagnostics.push({ - field: "target.edgeId", + field: 'target.edgeId', message: `edge ${input.target.edgeId} does not exist`, - }) + }); } } else { const aRow = tx .select({ id: schema.nodes.id }) .from(schema.nodes) .where(eq(schema.nodes.id, input.target.aId)) - .get() + .get(); if (!aRow) { diagnostics.push({ - field: "target.aId", + field: 'target.aId', message: `node ${input.target.aId} does not exist`, - }) + }); } const bRow = tx .select({ id: schema.nodes.id }) .from(schema.nodes) .where(eq(schema.nodes.id, input.target.bId)) - .get() + .get(); if (!bRow) { diagnostics.push({ - field: "target.bId", + field: 'target.bId', message: `node ${input.target.bId} does not exist`, - }) + }); } } if (diagnostics.length > 0) { - return { status: "structural_illegal" as const, diagnostics } + return { status: 'structural_illegal' as const, diagnostics }; } // Allocate LSN @@ -737,42 +732,39 @@ export class CommandExecutor { .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) .where(eq(schema.graphClock.id, 1)) .returning() - .get() - const lsn = clock!.lsn + .get(); + const lsn = clock!.lsn; // Insert reconciliation need const row = tx .insert(schema.reconciliationNeed) .values({ target_kind: input.target.kind, - target_edge_id: - input.target.kind === "edge" ? input.target.edgeId : null, - target_a_id: - input.target.kind === "node_pair" ? input.target.aId : null, - target_b_id: - input.target.kind === "node_pair" ? input.target.bId : null, + target_edge_id: input.target.kind === 'edge' ? input.target.edgeId : null, + target_a_id: input.target.kind === 'node_pair' ? input.target.aId : null, + target_b_id: input.target.kind === 'node_pair' ? input.target.bId : null, kind: input.needKind, reason: input.reason ?? null, created_at_lsn: lsn, }) .returning() - .get() + .get(); // Append change_log tx.insert(schema.changeLog) .values({ lsn, - operation: "create_reconciliation_need", + operation: 'create_reconciliation_need', payload: JSON.stringify({ id: row!.id, target: input.target, kind: input.needKind, }), }) - .run() + .run(); - return { status: "success" as const, id: row!.id, lsn } - }) + return { status: 'success' as const, id: row!.id, lsn }; + }); } /** @@ -787,30 +779,30 @@ export class CommandExecutor { .select() .from(schema.reconciliationNeed) .where(eq(schema.reconciliationNeed.id, id)) - .get() + .get(); if (!existing) { return { - status: "structural_illegal" as const, + status: 'structural_illegal' as const, diagnostics: [ { - field: "id", + field: 'id', message: `reconciliation need ${id} does not exist`, }, ], - } + }; } - if (existing.status === "resolved") { + if (existing.status === 'resolved') { return { - status: "structural_illegal" as const, + status: 'structural_illegal' as const, diagnostics: [ { - field: "id", + field: 'id', message: `reconciliation need ${id} is already resolved`, }, ], - } + }; } // Allocate LSN @@ -819,25 +811,25 @@ export class CommandExecutor { .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) .where(eq(schema.graphClock.id, 1)) .returning() - .get() - const lsn = clock!.lsn + .get(); + const lsn = clock!.lsn; // Update status tx.update(schema.reconciliationNeed) - .set({ status: "resolved", resolved_at_lsn: lsn }) + .set({ status: 'resolved', resolved_at_lsn: lsn }) .where(eq(schema.reconciliationNeed.id, id)) - .run() + .run(); // Append change_log tx.insert(schema.changeLog) .values({ lsn, - operation: "resolve_reconciliation_need", + operation: 'resolve_reconciliation_need', payload: JSON.stringify({ id }), }) - .run() + .run(); - return { status: "success" as const, lsn } - }) + return { status: 'success' as const, lsn }; + }); } } diff --git a/src/graph/index.ts b/src/graph/index.ts index 47a999718..2d2488874 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -8,7 +8,7 @@ * M4 skeleton: CommandExecutor + result types. */ -export type { EdgeId, Lsn, NodeId } from "./atoms.js" +export type { EdgeId, Lsn, NodeId } from './atoms.js'; // Re-export shared enum const arrays so extensions can build // tool parameter schemas without importing db/ directly (I26-L). @@ -20,14 +20,9 @@ export { DESIGN_KINDS, PLAN_KINDS, NODE_BASES, -} from "../db/schema.js" +} from '../db/schema.js'; -export type { - EdgeBasis, - EdgeCategory, - EdgeStance, - GraphEdge, -} from "./schema/edges.js" +export type { EdgeBasis, EdgeCategory, EdgeStance, GraphEdge } from './schema/edges.js'; export type { DecisionDetail, @@ -42,37 +37,33 @@ export type { OracleKind, PlanKind, TermDetail, -} from "./schema/nodes.js" +} from './schema/nodes.js'; -export { intentKindCategory } from "./schema/nodes.js" +export { intentKindCategory } from './schema/nodes.js'; export type { ReconciliationNeed, ReconciliationNeedKind, ReconciliationNeedTarget, -} from "./schema/reconciliation-need.js" +} from './schema/reconciliation-need.js'; export { CATEGORY_POLICY, type CategoryPolicy, type ProjectionEffect, type ReconNeedTrigger, -} from "./policy/category-policy.js" +} from './policy/category-policy.js'; -export { - getGraphOverview, - getNodeNeighborhood, - getOpenReconciliationNeeds, -} from "./snapshot.js" +export { getGraphOverview, getNodeNeighborhood, getOpenReconciliationNeeds } from './snapshot.js'; export type { GraphOverview, NeighborhoodOptions, NeighborhoodNotFound, NeighborhoodResult, NeighborhoodSuccess, -} from "./snapshot.js" +} from './snapshot.js'; -export { CommandExecutor } from "./command-executor.js" +export { CommandExecutor } from './command-executor.js'; export type { BatchEdgeInput, BatchEdgeRef, @@ -95,4 +86,4 @@ export type { ResolveReconNeedResult, StructuralIllegal, VersionConflict, -} from "./command-executor.js" +} from './command-executor.js'; diff --git a/src/graph/policy/category-policy.ts b/src/graph/policy/category-policy.ts index 76fbd8227..f983f8e58 100644 --- a/src/graph/policy/category-policy.ts +++ b/src/graph/policy/category-policy.ts @@ -27,17 +27,17 @@ * consume this table in subsequent M4/M5 slices. */ -import type { EdgeCategory } from "../schema/edges.js" +import type { EdgeCategory } from '../schema/edges.js'; -export type ReconNeedTrigger = false | "advisory" | true +export type ReconNeedTrigger = false | 'advisory' | true; -export type ProjectionEffect = "none" | "hide_predecessor_from_active_context" +export type ProjectionEffect = 'none' | 'hide_predecessor_from_active_context'; export interface CategoryPolicy { - readonly cascadeOnSourceChange: boolean - readonly reconNeedOnSourceChange: ReconNeedTrigger - readonly criteriaHelpSignal: boolean - readonly projectionEffect: ProjectionEffect + readonly cascadeOnSourceChange: boolean; + readonly reconNeedOnSourceChange: ReconNeedTrigger; + readonly criteriaHelpSignal: boolean; + readonly projectionEffect: ProjectionEffect; } export const CATEGORY_POLICY: Readonly> = { @@ -45,48 +45,48 @@ export const CATEGORY_POLICY: Readonly> = { cascadeOnSourceChange: true, reconNeedOnSourceChange: true, criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, proof: { cascadeOnSourceChange: false, - reconNeedOnSourceChange: "advisory", + reconNeedOnSourceChange: 'advisory', criteriaHelpSignal: true, - projectionEffect: "none", + projectionEffect: 'none', }, support: { cascadeOnSourceChange: false, - reconNeedOnSourceChange: "advisory", + reconNeedOnSourceChange: 'advisory', criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, realization: { cascadeOnSourceChange: false, - reconNeedOnSourceChange: "advisory", + reconNeedOnSourceChange: 'advisory', criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, boundary: { cascadeOnSourceChange: false, reconNeedOnSourceChange: true, criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, composition: { cascadeOnSourceChange: false, reconNeedOnSourceChange: false, criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, association: { cascadeOnSourceChange: false, reconNeedOnSourceChange: false, criteriaHelpSignal: false, - projectionEffect: "none", + projectionEffect: 'none', }, supersession: { cascadeOnSourceChange: false, reconNeedOnSourceChange: false, criteriaHelpSignal: false, - projectionEffect: "hide_predecessor_from_active_context", + projectionEffect: 'hide_predecessor_from_active_context', }, -} +}; diff --git a/src/graph/schema/edges.ts b/src/graph/schema/edges.ts index 35df26258..b88d8f14c 100644 --- a/src/graph/schema/edges.ts +++ b/src/graph/schema/edges.ts @@ -10,8 +10,8 @@ * agent-facing link* command surface land with subsequent M4/M5 slices. */ -import { EDGE_CATEGORIES, EDGE_STANCES, NODE_BASES } from "../../db/schema.js" -import type { EdgeId, Lsn, NodeId } from "../atoms.js" +import { EDGE_CATEGORIES, EDGE_STANCES, NODE_BASES } from '../../db/schema.js'; +import type { EdgeId, Lsn, NodeId } from '../atoms.js'; /** * Closed set of structural edge categories. @@ -27,7 +27,7 @@ import type { EdgeId, Lsn, NodeId } from "../atoms.js" * - `supersession` successor → predecessor replacement lineage (acyclic) * - `association` peer ↔ peer weak relatedness (symmetric) */ -export type EdgeCategory = typeof EDGE_CATEGORIES[number] +export type EdgeCategory = (typeof EDGE_CATEGORIES)[number]; /** * Polarity for stance-bearing edges. @@ -35,7 +35,7 @@ export type EdgeCategory = typeof EDGE_CATEGORIES[number] * Required for `proof` and `support`. * Invalid (must be omitted) for every other category. */ -export type EdgeStance = typeof EDGE_STANCES[number] +export type EdgeStance = (typeof EDGE_STANCES)[number]; /** * How an edge entered graph truth. @@ -46,7 +46,7 @@ export type EdgeStance = typeof EDGE_STANCES[number] * preface or `capture_*` analysis until promoted through a review set * (D47-L, D50-L). */ -export type EdgeBasis = typeof NODE_BASES[number] +export type EdgeBasis = (typeof NODE_BASES)[number]; // EdgeProvenance retired — change_log owns the full audit trail. @@ -68,13 +68,13 @@ export type EdgeBasis = typeof NODE_BASES[number] * the edge (see `src/graph/schema/reconciliation-need.ts`). */ export interface GraphEdge { - readonly id: EdgeId - readonly category: EdgeCategory - readonly sourceId: NodeId - readonly targetId: NodeId - readonly stance?: EdgeStance - readonly basis: EdgeBasis - readonly rationale?: string - readonly createdAtLsn: Lsn - readonly updatedAtLsn: Lsn + readonly id: EdgeId; + readonly category: EdgeCategory; + readonly sourceId: NodeId; + readonly targetId: NodeId; + readonly stance?: EdgeStance; + readonly basis: EdgeBasis; + readonly rationale?: string; + readonly createdAtLsn: Lsn; + readonly updatedAtLsn: Lsn; } diff --git a/src/graph/schema/nodes.ts b/src/graph/schema/nodes.ts index 9cce08be1..12de3197d 100644 --- a/src/graph/schema/nodes.ts +++ b/src/graph/schema/nodes.ts @@ -8,14 +8,8 @@ * agent-facing node command surface land with subsequent slices. */ -import { - DESIGN_KINDS, - INTENT_KINDS, - NODE_BASES, - ORACLE_KINDS, - PLAN_KINDS, -} from "../../db/schema.js" -import type { Lsn, NodeId } from "../atoms.js" +import { DESIGN_KINDS, INTENT_KINDS, NODE_BASES, ORACLE_KINDS, PLAN_KINDS } from '../../db/schema.js'; +import type { Lsn, NodeId } from '../atoms.js'; // --------------------------------------------------------------------------- // Planes & basis @@ -30,14 +24,14 @@ import type { Lsn, NodeId } from "../atoms.js" * - `design` how it's shaped * - `plan` how it's sequenced */ -export type NodePlane = "intent" | "oracle" | "design" | "plan" +export type NodePlane = 'intent' | 'oracle' | 'design' | 'plan'; /** * How a node entered graph truth. * * Derived from `db/schema.ts` — same semantics as EdgeBasis. */ -export type NodeBasis = typeof NODE_BASES[number] +export type NodeBasis = (typeof NODE_BASES)[number]; // --------------------------------------------------------------------------- // Kind taxonomy — derived from db/schema.ts const arrays @@ -49,19 +43,19 @@ export type NodeBasis = typeof NODE_BASES[number] * - structural: `requirement`, `assumption`, `constraint`, `invariant` * - reasoning: `decision`, `criterion`, `example` */ -export type IntentKind = typeof INTENT_KINDS[number] +export type IntentKind = (typeof INTENT_KINDS)[number]; /** Oracle-plane kinds. */ -export type OracleKind = typeof ORACLE_KINDS[number] +export type OracleKind = (typeof ORACLE_KINDS)[number]; /** Design-plane kinds. */ -export type DesignKind = typeof DESIGN_KINDS[number] +export type DesignKind = (typeof DESIGN_KINDS)[number]; /** Plan-plane kinds. */ -export type PlanKind = typeof PLAN_KINDS[number] +export type PlanKind = (typeof PLAN_KINDS)[number]; /** Union of every node kind across all planes. */ -export type NodeKind = IntentKind | OracleKind | DesignKind | PlanKind +export type NodeKind = IntentKind | OracleKind | DesignKind | PlanKind; // --------------------------------------------------------------------------- // Intent kind categories (derived, not stored) @@ -72,25 +66,25 @@ export type NodeKind = IntentKind | OracleKind | DesignKind | PlanKind * * Never persisted — computed via {@link intentKindCategory}. */ -export type IntentKindCategory = "basic" | "structural" | "reasoning" +export type IntentKindCategory = 'basic' | 'structural' | 'reasoning'; /** Pure derivation: intent kind → category. */ export function intentKindCategory(kind: IntentKind): IntentKindCategory { switch (kind) { - case "goal": - case "thesis": - case "term": - case "context": - return "basic" - case "requirement": - case "assumption": - case "constraint": - case "invariant": - return "structural" - case "decision": - case "criterion": - case "example": - return "reasoning" + case 'goal': + case 'thesis': + case 'term': + case 'context': + return 'basic'; + case 'requirement': + case 'assumption': + case 'constraint': + case 'invariant': + return 'structural'; + case 'decision': + case 'criterion': + case 'example': + return 'reasoning'; } } @@ -100,19 +94,19 @@ export function intentKindCategory(kind: IntentKind): IntentKindCategory { /** Detail payload for `decision` nodes. */ export interface DecisionDetail { - readonly chosen_option: string - readonly rejected: readonly string[] - readonly rationale: string + readonly chosen_option: string; + readonly rejected: readonly string[]; + readonly rationale: string; } /** Detail payload for `term` nodes. */ export interface TermDetail { - readonly definition: string - readonly aliases?: readonly string[] + readonly definition: string; + readonly aliases?: readonly string[]; } /** Discriminated union of all per-kind detail payloads. */ -export type NodeDetail = DecisionDetail | TermDetail +export type NodeDetail = DecisionDetail | TermDetail; // --------------------------------------------------------------------------- // Main node interface @@ -130,14 +124,14 @@ export type NodeDetail = DecisionDetail | TermDetail * Stale nodes surface as `ReconciliationNeed` records. */ export interface GraphNode { - readonly id: NodeId - readonly plane: NodePlane - readonly kind: NodeKind - readonly title: string - readonly body?: string - readonly basis: NodeBasis - readonly source?: string - readonly detail?: NodeDetail - readonly createdAtLsn: Lsn - readonly updatedAtLsn: Lsn + readonly id: NodeId; + readonly plane: NodePlane; + readonly kind: NodeKind; + readonly title: string; + readonly body?: string; + readonly basis: NodeBasis; + readonly source?: string; + readonly detail?: NodeDetail; + readonly createdAtLsn: Lsn; + readonly updatedAtLsn: Lsn; } diff --git a/src/graph/schema/reconciliation-need.ts b/src/graph/schema/reconciliation-need.ts index a629bf045..495a03194 100644 --- a/src/graph/schema/reconciliation-need.ts +++ b/src/graph/schema/reconciliation-need.ts @@ -17,7 +17,7 @@ * with subsequent M4 slices. */ -import type { EdgeId, Lsn, NodeId } from "../atoms.js" +import type { EdgeId, Lsn, NodeId } from '../atoms.js'; /** * What sort of impasse this need records. @@ -25,7 +25,11 @@ import type { EdgeId, Lsn, NodeId } from "../atoms.js" * Open extension — new kinds may be added as concrete needs surface. * Most needs are `edge_revalidation`. */ -export type ReconciliationNeedKind = "edge_revalidation" | "possible_relation" | "possible_duplicate" | "semantic_conflict" +export type ReconciliationNeedKind = + | 'edge_revalidation' + | 'possible_relation' + | 'possible_duplicate' + | 'semantic_conflict'; /** * What this need is about. @@ -37,20 +41,22 @@ export type ReconciliationNeedKind = "edge_revalidation" | "possible_relation" | * duplicate, possible relation). When such a need resolves to * "yes, edge exists," create the edge and close the need. */ -export type ReconciliationNeedTarget = { - readonly kind: "edge" - readonly edgeId: EdgeId -} | { - readonly kind: "node_pair" - readonly aId: NodeId - readonly bId: NodeId -} +export type ReconciliationNeedTarget = + | { + readonly kind: 'edge'; + readonly edgeId: EdgeId; + } + | { + readonly kind: 'node_pair'; + readonly aId: NodeId; + readonly bId: NodeId; + }; export interface ReconciliationNeed { - readonly id: string - readonly kind: ReconciliationNeedKind - readonly target: ReconciliationNeedTarget - readonly rationale?: string - readonly createdAtLsn: Lsn - readonly resolvedAtLsn?: Lsn + readonly id: string; + readonly kind: ReconciliationNeedKind; + readonly target: ReconciliationNeedTarget; + readonly rationale?: string; + readonly createdAtLsn: Lsn; + readonly resolvedAtLsn?: Lsn; } diff --git a/src/graph/snapshot.test.ts b/src/graph/snapshot.test.ts index d460bca80..83e7fec10 100644 --- a/src/graph/snapshot.test.ts +++ b/src/graph/snapshot.test.ts @@ -7,348 +7,344 @@ * All graph state is seeded via CommandExecutor (no direct db writes). */ -import { beforeEach, describe, expect, it } from "vitest" +import { beforeEach, describe, expect, it } from 'vitest'; -import { createDb, type BrunchDb } from "../db/connection.js" -import { CommandExecutor } from "./command-executor.js" -import { - getGraphOverview, - getNodeNeighborhood, - getOpenReconciliationNeeds, -} from "./snapshot.js" +import { createDb, type BrunchDb } from '../db/connection.js'; +import { CommandExecutor } from './command-executor.js'; +import { getGraphOverview, getNodeNeighborhood, getOpenReconciliationNeeds } from './snapshot.js'; function createTestDb(): BrunchDb { - return createDb(":memory:") + return createDb(':memory:'); } -describe("getGraphOverview", () => { - let db: BrunchDb - let executor: CommandExecutor +describe('getGraphOverview', () => { + let db: BrunchDb; + let executor: CommandExecutor; beforeEach(() => { - db = createTestDb() - executor = new CommandExecutor(db) - }) - - it("returns empty arrays and zero counts on an empty graph", () => { - const overview = getGraphOverview(db) - expect(overview.nodes).toEqual([]) - expect(overview.edges).toEqual([]) - expect(overview.nodeCount).toBe(0) - expect(overview.edgeCount).toBe(0) - expect(overview.lsn).toBe(0) - }) - - it("returns current LSN from graph_clock", () => { - executor.createNode({ plane: "intent", kind: "goal", title: "G1" }) - executor.createNode({ plane: "intent", kind: "thesis", title: "T1" }) - const overview = getGraphOverview(db) - expect(overview.lsn).toBe(2) - }) - - it("returns typed domain objects with parsed detail JSON", () => { + db = createTestDb(); + executor = new CommandExecutor(db); + }); + + it('returns empty arrays and zero counts on an empty graph', () => { + const overview = getGraphOverview(db); + expect(overview.nodes).toEqual([]); + expect(overview.edges).toEqual([]); + expect(overview.nodeCount).toBe(0); + expect(overview.edgeCount).toBe(0); + expect(overview.lsn).toBe(0); + }); + + it('returns current LSN from graph_clock', () => { + executor.createNode({ plane: 'intent', kind: 'goal', title: 'G1' }); + executor.createNode({ plane: 'intent', kind: 'thesis', title: 'T1' }); + const overview = getGraphOverview(db); + expect(overview.lsn).toBe(2); + }); + + it('returns typed domain objects with parsed detail JSON', () => { executor.createNode({ - plane: "intent", - kind: "decision", - title: "Use SQLite", - body: "Settled on SQLite", + plane: 'intent', + kind: 'decision', + title: 'Use SQLite', + body: 'Settled on SQLite', detail: { - chosen_option: "SQLite", - rejected: ["PostgreSQL"], - rationale: "Simpler local deployment", + chosen_option: 'SQLite', + rejected: ['PostgreSQL'], + rationale: 'Simpler local deployment', }, - }) - - const overview = getGraphOverview(db) - expect(overview.nodes).toHaveLength(1) - const node = overview.nodes[0]! - expect(node.id).toBeTypeOf("number") - expect(node.plane).toBe("intent") - expect(node.kind).toBe("decision") - expect(node.title).toBe("Use SQLite") - expect(node.body).toBe("Settled on SQLite") - expect(node.basis).toBe("explicit") + }); + + const overview = getGraphOverview(db); + expect(overview.nodes).toHaveLength(1); + const node = overview.nodes[0]!; + expect(node.id).toBeTypeOf('number'); + expect(node.plane).toBe('intent'); + expect(node.kind).toBe('decision'); + expect(node.title).toBe('Use SQLite'); + expect(node.body).toBe('Settled on SQLite'); + expect(node.basis).toBe('explicit'); expect(node.detail).toEqual({ - chosen_option: "SQLite", - rejected: ["PostgreSQL"], - rationale: "Simpler local deployment", - }) - expect(node.createdAtLsn).toBe(1) - expect(node.updatedAtLsn).toBe(1) - }) - - it("returns nodes and edges with correct counts", () => { + chosen_option: 'SQLite', + rejected: ['PostgreSQL'], + rationale: 'Simpler local deployment', + }); + expect(node.createdAtLsn).toBe(1); + expect(node.updatedAtLsn).toBe(1); + }); + + it('returns nodes and edges with correct counts', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - - const overview = getGraphOverview(db) - expect(overview.nodeCount).toBe(2) - expect(overview.edgeCount).toBe(1) - expect(overview.nodes).toHaveLength(2) - expect(overview.edges).toHaveLength(1) - expect(overview.edges[0]!.category).toBe("dependency") - }) - - it("excludes superseded predecessors from overview", () => { + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + + const overview = getGraphOverview(db); + expect(overview.nodeCount).toBe(2); + expect(overview.edgeCount).toBe(1); + expect(overview.nodes).toHaveLength(2); + expect(overview.edges).toHaveLength(1); + expect(overview.edges[0]!.category).toBe('dependency'); + }); + + it('excludes superseded predecessors from overview', () => { // Create R_v0, then R_v1 that supersedes R_v0 const r0 = executor.createNode({ - plane: "intent", - kind: "requirement", - title: "R_offline_v0", - }) - expect(r0.status).toBe("success") - if (r0.status !== "success") throw new Error("unreachable") - const r0Id = r0.nodeId + plane: 'intent', + kind: 'requirement', + title: 'R_offline_v0', + }); + expect(r0.status).toBe('success'); + if (r0.status !== 'success') throw new Error('unreachable'); + const r0Id = r0.nodeId; const batch = executor.commitGraph({ nodes: [ { - ref: "r1", - plane: "intent", - kind: "requirement", - title: "R_offline_v1", + ref: 'r1', + plane: 'intent', + kind: 'requirement', + title: 'R_offline_v1', }, ], edges: [ { - category: "supersession", - source: "r1", + category: 'supersession', + source: 'r1', target: { existing: r0Id }, }, ], - }) - expect(batch.status).toBe("success") + }); + expect(batch.status).toBe('success'); - const overview = getGraphOverview(db) + const overview = getGraphOverview(db); // R_offline_v0 should be excluded (it is a superseded predecessor) - const titles = overview.nodes.map((n) => n.title) - expect(titles).toContain("R_offline_v1") - expect(titles).not.toContain("R_offline_v0") + const titles = overview.nodes.map((n) => n.title); + expect(titles).toContain('R_offline_v1'); + expect(titles).not.toContain('R_offline_v0'); // The supersession edge should still be present - expect(overview.edges).toHaveLength(1) - }) -}) + expect(overview.edges).toHaveLength(1); + }); +}); -describe("getNodeNeighborhood", () => { - let db: BrunchDb - let executor: CommandExecutor +describe('getNodeNeighborhood', () => { + let db: BrunchDb; + let executor: CommandExecutor; beforeEach(() => { - db = createTestDb() - executor = new CommandExecutor(db) - }) + db = createTestDb(); + executor = new CommandExecutor(db); + }); - it("returns error for non-existent nodeId", () => { - const result = getNodeNeighborhood(db, 999) - expect(result.status).toBe("not_found") - }) + it('returns error for non-existent nodeId', () => { + const result = getNodeNeighborhood(db, 999); + expect(result.status).toBe('not_found'); + }); - it("returns anchor node and directly connected nodes/edges at 1 hop (default)", () => { + it('returns anchor node and directly connected nodes/edges at 1 hop (default)', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, - { ref: "g1", plane: "intent", kind: "goal", title: "G1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, + { ref: 'g1', plane: 'intent', kind: 'goal', title: 'G1' }, ], edges: [ - { category: "dependency", source: "r1", target: "a1" }, - { category: "support", source: "g1", target: "r1", stance: "for" }, + { category: 'dependency', source: 'r1', target: 'a1' }, + { category: 'support', source: 'g1', target: 'r1', stance: 'for' }, ], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); - const r1Id = batch.nodes["r1"]! - const result = getNodeNeighborhood(db, r1Id) - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") + const r1Id = batch.nodes['r1']!; + const result = getNodeNeighborhood(db, r1Id); + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); - expect(result.anchor.title).toBe("R1") + expect(result.anchor.title).toBe('R1'); // Should include A1 (dependency target) and G1 (support source) - expect(result.neighbors).toHaveLength(2) - const neighborTitles = result.neighbors.map((n) => n.title).sort() - expect(neighborTitles).toEqual(["A1", "G1"]) - expect(result.edges).toHaveLength(2) - }) + expect(result.neighbors).toHaveLength(2); + const neighborTitles = result.neighbors.map((n) => n.title).sort(); + expect(neighborTitles).toEqual(['A1', 'G1']); + expect(result.edges).toHaveLength(2); + }); - it("reaches 2-hop neighbors", () => { + it('reaches 2-hop neighbors', () => { // G1 -> R1 -> A1 (chain of depth 2 from G1) const batch = executor.commitGraph({ nodes: [ - { ref: "g1", plane: "intent", kind: "goal", title: "G1" }, - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'g1', plane: 'intent', kind: 'goal', title: 'G1' }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], edges: [ - { category: "support", source: "g1", target: "r1", stance: "for" }, - { category: "dependency", source: "r1", target: "a1" }, + { category: 'support', source: 'g1', target: 'r1', stance: 'for' }, + { category: 'dependency', source: 'r1', target: 'a1' }, ], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); - const g1Id = batch.nodes["g1"]! + const g1Id = batch.nodes['g1']!; // 1 hop: only R1 - const hop1 = getNodeNeighborhood(db, g1Id, { hops: 1 }) - expect(hop1.status).toBe("success") - if (hop1.status !== "success") throw new Error("unreachable") - expect(hop1.neighbors.map((n) => n.title)).toEqual(["R1"]) + const hop1 = getNodeNeighborhood(db, g1Id, { hops: 1 }); + expect(hop1.status).toBe('success'); + if (hop1.status !== 'success') throw new Error('unreachable'); + expect(hop1.neighbors.map((n) => n.title)).toEqual(['R1']); // 2 hops: R1 and A1 - const hop2 = getNodeNeighborhood(db, g1Id, { hops: 2 }) - expect(hop2.status).toBe("success") - if (hop2.status !== "success") throw new Error("unreachable") - const titles = hop2.neighbors.map((n) => n.title).sort() - expect(titles).toEqual(["A1", "R1"]) - }) - - it("excludes superseded predecessors from neighborhood (unless anchor)", () => { + const hop2 = getNodeNeighborhood(db, g1Id, { hops: 2 }); + expect(hop2.status).toBe('success'); + if (hop2.status !== 'success') throw new Error('unreachable'); + const titles = hop2.neighbors.map((n) => n.title).sort(); + expect(titles).toEqual(['A1', 'R1']); + }); + + it('excludes superseded predecessors from neighborhood (unless anchor)', () => { // R_v0 superseded by R_v1, with A1 depending on R_v1 const r0 = executor.createNode({ - plane: "intent", - kind: "requirement", - title: "R_v0", - }) - expect(r0.status).toBe("success") - if (r0.status !== "success") throw new Error("unreachable") - const r0Id = r0.nodeId + plane: 'intent', + kind: 'requirement', + title: 'R_v0', + }); + expect(r0.status).toBe('success'); + if (r0.status !== 'success') throw new Error('unreachable'); + const r0Id = r0.nodeId; const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R_v1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R_v1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], edges: [ - { category: "supersession", source: "r1", target: { existing: r0Id } }, - { category: "dependency", source: "r1", target: "a1" }, + { category: 'supersession', source: 'r1', target: { existing: r0Id } }, + { category: 'dependency', source: 'r1', target: 'a1' }, ], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); - const r1Id = batch.nodes["r1"]! + const r1Id = batch.nodes['r1']!; // Neighborhood of R_v1: should include A1 but exclude R_v0 - const result = getNodeNeighborhood(db, r1Id) - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") + const result = getNodeNeighborhood(db, r1Id); + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); - const neighborTitles = result.neighbors.map((n) => n.title) - expect(neighborTitles).toContain("A1") - expect(neighborTitles).not.toContain("R_v0") + const neighborTitles = result.neighbors.map((n) => n.title); + expect(neighborTitles).toContain('A1'); + expect(neighborTitles).not.toContain('R_v0'); // But if R_v0 is the anchor, it should still be returned - const r0Result = getNodeNeighborhood(db, r0Id) - expect(r0Result.status).toBe("success") - if (r0Result.status !== "success") throw new Error("unreachable") - expect(r0Result.anchor.title).toBe("R_v0") - }) + const r0Result = getNodeNeighborhood(db, r0Id); + expect(r0Result.status).toBe('success'); + if (r0Result.status !== 'success') throw new Error('unreachable'); + expect(r0Result.anchor.title).toBe('R_v0'); + }); - it("returns typed GraphNode and GraphEdge domain objects", () => { + it('returns typed GraphNode and GraphEdge domain objects', () => { const batch = executor.commitGraph({ nodes: [ { - ref: "t1", - plane: "intent", - kind: "term", - title: "Widget", - detail: { definition: "A reusable component", aliases: ["gadget"] }, + ref: 't1', + plane: 'intent', + kind: 'term', + title: 'Widget', + detail: { definition: 'A reusable component', aliases: ['gadget'] }, }, - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, ], - edges: [{ category: "boundary", source: "t1", target: "r1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + edges: [{ category: 'boundary', source: 't1', target: 'r1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); - const t1Id = batch.nodes["t1"]! - const result = getNodeNeighborhood(db, t1Id) - expect(result.status).toBe("success") - if (result.status !== "success") throw new Error("unreachable") + const t1Id = batch.nodes['t1']!; + const result = getNodeNeighborhood(db, t1Id); + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); // Anchor has parsed detail expect(result.anchor.detail).toEqual({ - definition: "A reusable component", - aliases: ["gadget"], - }) + definition: 'A reusable component', + aliases: ['gadget'], + }); // Edge has typed fields - const edge = result.edges[0]! - expect(edge.category).toBe("boundary") - expect(edge.sourceId).toBe(t1Id) - expect(edge.createdAtLsn).toBeTypeOf("number") - }) -}) + const edge = result.edges[0]!; + expect(edge.category).toBe('boundary'); + expect(edge.sourceId).toBe(t1Id); + expect(edge.createdAtLsn).toBeTypeOf('number'); + }); +}); -describe("getOpenReconciliationNeeds", () => { - let db: BrunchDb - let executor: CommandExecutor +describe('getOpenReconciliationNeeds', () => { + let db: BrunchDb; + let executor: CommandExecutor; beforeEach(() => { - db = createTestDb() - executor = new CommandExecutor(db) - }) + db = createTestDb(); + executor = new CommandExecutor(db); + }); - it("returns empty array when no needs exist", () => { - const needs = getOpenReconciliationNeeds(db) - expect(needs).toEqual([]) - }) + it('returns empty array when no needs exist', () => { + const needs = getOpenReconciliationNeeds(db); + expect(needs).toEqual([]); + }); - it("returns open needs as typed domain objects", () => { + it('returns open needs as typed domain objects', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); const create = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId: batch.edges[0]! }, - needKind: "edge_revalidation", - reason: "upstream changed", - }) - expect(create.status).toBe("success") - if (create.status !== "success") throw new Error("unreachable") - - const needs = getOpenReconciliationNeeds(db) - expect(needs).toHaveLength(1) - expect(needs[0]!.kind).toBe("edge_revalidation") - expect(needs[0]!.target).toEqual({ kind: "edge", edgeId: batch.edges[0]! }) - expect(needs[0]!.rationale).toBe("upstream changed") - expect(needs[0]!.createdAtLsn).toBeTypeOf("number") - }) - - it("excludes resolved needs", () => { + target: { kind: 'edge', edgeId: batch.edges[0]! }, + needKind: 'edge_revalidation', + reason: 'upstream changed', + }); + expect(create.status).toBe('success'); + if (create.status !== 'success') throw new Error('unreachable'); + + const needs = getOpenReconciliationNeeds(db); + expect(needs).toHaveLength(1); + expect(needs[0]!.kind).toBe('edge_revalidation'); + expect(needs[0]!.target).toEqual({ kind: 'edge', edgeId: batch.edges[0]! }); + expect(needs[0]!.rationale).toBe('upstream changed'); + expect(needs[0]!.createdAtLsn).toBeTypeOf('number'); + }); + + it('excludes resolved needs', () => { const batch = executor.commitGraph({ nodes: [ - { ref: "r1", plane: "intent", kind: "requirement", title: "R1" }, - { ref: "a1", plane: "intent", kind: "assumption", title: "A1" }, + { ref: 'r1', plane: 'intent', kind: 'requirement', title: 'R1' }, + { ref: 'a1', plane: 'intent', kind: 'assumption', title: 'A1' }, ], - edges: [{ category: "dependency", source: "r1", target: "a1" }], - }) - expect(batch.status).toBe("success") - if (batch.status !== "success") throw new Error("unreachable") + edges: [{ category: 'dependency', source: 'r1', target: 'a1' }], + }); + expect(batch.status).toBe('success'); + if (batch.status !== 'success') throw new Error('unreachable'); const create = executor.createReconciliationNeed({ - target: { kind: "edge", edgeId: batch.edges[0]! }, - needKind: "edge_revalidation", - }) - expect(create.status).toBe("success") - if (create.status !== "success") throw new Error("unreachable") - - executor.resolveReconciliationNeed(create.id) - - const needs = getOpenReconciliationNeeds(db) - expect(needs).toEqual([]) - }) -}) + target: { kind: 'edge', edgeId: batch.edges[0]! }, + needKind: 'edge_revalidation', + }); + expect(create.status).toBe('success'); + if (create.status !== 'success') throw new Error('unreachable'); + + executor.resolveReconciliationNeed(create.id); + + const needs = getOpenReconciliationNeeds(db); + expect(needs).toEqual([]); + }); +}); diff --git a/src/graph/snapshot.ts b/src/graph/snapshot.ts index 0b41ad11a..fa641352c 100644 --- a/src/graph/snapshot.ts +++ b/src/graph/snapshot.ts @@ -9,16 +9,13 @@ * edge) are excluded per CATEGORY_POLICY projectionEffect. */ -import { eq, or, inArray } from "drizzle-orm" +import { eq, or, inArray } from 'drizzle-orm'; -import type { BrunchDb } from "../db/connection.js" -import * as schema from "../db/schema.js" -import type { GraphEdge } from "./schema/edges.js" -import type { GraphNode, NodeDetail } from "./schema/nodes.js" -import type { - ReconciliationNeed, - ReconciliationNeedTarget, -} from "./schema/reconciliation-need.js" +import type { BrunchDb } from '../db/connection.js'; +import * as schema from '../db/schema.js'; +import type { GraphEdge } from './schema/edges.js'; +import type { GraphNode, NodeDetail } from './schema/nodes.js'; +import type { ReconciliationNeed, ReconciliationNeedTarget } from './schema/reconciliation-need.js'; // --------------------------------------------------------------------------- // Return types @@ -26,32 +23,32 @@ import type { /** Full-graph cursory overview. */ export interface GraphOverview { - readonly nodes: readonly GraphNode[] - readonly edges: readonly GraphEdge[] - readonly nodeCount: number - readonly edgeCount: number + readonly nodes: readonly GraphNode[]; + readonly edges: readonly GraphEdge[]; + readonly nodeCount: number; + readonly edgeCount: number; /** Current LSN from graph_clock. */ - readonly lsn: number + readonly lsn: number; } /** Successful neighborhood result. */ export interface NeighborhoodSuccess { - readonly status: "success" - readonly anchor: GraphNode - readonly neighbors: readonly GraphNode[] - readonly edges: readonly GraphEdge[] + readonly status: 'success'; + readonly anchor: GraphNode; + readonly neighbors: readonly GraphNode[]; + readonly edges: readonly GraphEdge[]; } /** Node not found. */ export interface NeighborhoodNotFound { - readonly status: "not_found" + readonly status: 'not_found'; } -export type NeighborhoodResult = NeighborhoodSuccess | NeighborhoodNotFound +export type NeighborhoodResult = NeighborhoodSuccess | NeighborhoodNotFound; export interface NeighborhoodOptions { /** Number of hops from the anchor node. Defaults to 1. */ - readonly hops?: number + readonly hops?: number; } // --------------------------------------------------------------------------- @@ -61,41 +58,39 @@ export interface NeighborhoodOptions { function rowToNode(row: typeof schema.nodes.$inferSelect): GraphNode { return { id: row.id, - plane: row.plane as GraphNode["plane"], - kind: row.kind as GraphNode["kind"], + plane: row.plane as GraphNode['plane'], + kind: row.kind as GraphNode['kind'], title: row.title, ...(row.body != null ? { body: row.body } : {}), - basis: row.basis as GraphNode["basis"], + basis: row.basis as GraphNode['basis'], ...(row.source != null ? { source: row.source } : {}), - ...(row.detail != null - ? { detail: JSON.parse(row.detail) as NodeDetail } - : {}), + ...(row.detail != null ? { detail: JSON.parse(row.detail) as NodeDetail } : {}), createdAtLsn: row.created_at_lsn, updatedAtLsn: row.updated_at_lsn, - } + }; } function rowToEdge(row: typeof schema.edges.$inferSelect): GraphEdge { const base = { id: row.id, - category: row.category as GraphEdge["category"], + category: row.category as GraphEdge['category'], sourceId: row.source_id, targetId: row.target_id, - basis: row.basis as GraphEdge["basis"], + basis: row.basis as GraphEdge['basis'], createdAtLsn: row.created_at_lsn, updatedAtLsn: row.updated_at_lsn, - } + }; return row.stance != null ? row.rationale != null ? { ...base, - stance: row.stance as NonNullable, + stance: row.stance as NonNullable, rationale: row.rationale, } - : { ...base, stance: row.stance as NonNullable } + : { ...base, stance: row.stance as NonNullable } : row.rationale != null ? { ...base, rationale: row.rationale } - : base + : base; } // --------------------------------------------------------------------------- @@ -107,9 +102,9 @@ function getSupersededIds(db: BrunchDb): Set { const rows = db .select({ targetId: schema.edges.target_id }) .from(schema.edges) - .where(eq(schema.edges.category, "supersession")) - .all() - return new Set(rows.map((r) => r.targetId)) + .where(eq(schema.edges.category, 'supersession')) + .all(); + return new Set(rows.map((r) => r.targetId)); } // --------------------------------------------------------------------------- @@ -124,19 +119,17 @@ function getSupersededIds(db: BrunchDb): Set { * per CATEGORY_POLICY.supersession.projectionEffect. */ export function getGraphOverview(db: BrunchDb): GraphOverview { - const supersededIds = getSupersededIds(db) + const supersededIds = getSupersededIds(db); - const allNodeRows = db.select().from(schema.nodes).all() - const allEdgeRows = db.select().from(schema.edges).all() + const allNodeRows = db.select().from(schema.nodes).all(); + const allEdgeRows = db.select().from(schema.edges).all(); - const nodes = allNodeRows - .filter((r) => !supersededIds.has(r.id)) - .map(rowToNode) + const nodes = allNodeRows.filter((r) => !supersededIds.has(r.id)).map(rowToNode); - const edges = allEdgeRows.map(rowToEdge) + const edges = allEdgeRows.map(rowToEdge); - const clockRow = db.select().from(schema.graphClock).get() - const lsn = clockRow?.lsn ?? 0 + const clockRow = db.select().from(schema.graphClock).get(); + const lsn = clockRow?.lsn ?? 0; return { nodes, @@ -144,7 +137,7 @@ export function getGraphOverview(db: BrunchDb): GraphOverview { nodeCount: nodes.length, edgeCount: edges.length, lsn, - } + }; } // --------------------------------------------------------------------------- @@ -164,112 +157,91 @@ export function getNodeNeighborhood( nodeId: number, options?: NeighborhoodOptions, ): NeighborhoodResult { - const hops = options?.hops ?? 1 + const hops = options?.hops ?? 1; // Verify anchor exists - const anchorRow = db - .select() - .from(schema.nodes) - .where(eq(schema.nodes.id, nodeId)) - .get() + const anchorRow = db.select().from(schema.nodes).where(eq(schema.nodes.id, nodeId)).get(); if (!anchorRow) { - return { status: "not_found" } + return { status: 'not_found' }; } - const supersededIds = getSupersededIds(db) - const anchor = rowToNode(anchorRow) + const supersededIds = getSupersededIds(db); + const anchor = rowToNode(anchorRow); // BFS traversal: collect reachable node ids within hop distance - const visited = new Set([nodeId]) - let frontier = new Set([nodeId]) - const collectedEdgeIds = new Set() + const visited = new Set([nodeId]); + let frontier = new Set([nodeId]); + const collectedEdgeIds = new Set(); for (let hop = 0; hop < hops; hop++) { - if (frontier.size === 0) break + if (frontier.size === 0) break; // Find all edges touching frontier nodes - const frontierArr = [...frontier] + const frontierArr = [...frontier]; const edgeRows = db .select() .from(schema.edges) - .where( - or( - inArray(schema.edges.source_id, frontierArr), - inArray(schema.edges.target_id, frontierArr), - ), - ) - .all() - - const nextFrontier = new Set() + .where(or(inArray(schema.edges.source_id, frontierArr), inArray(schema.edges.target_id, frontierArr))) + .all(); + + const nextFrontier = new Set(); for (const edge of edgeRows) { - collectedEdgeIds.add(edge.id) + collectedEdgeIds.add(edge.id); for (const peerId of [edge.source_id, edge.target_id]) { if (!visited.has(peerId)) { // Exclude superseded predecessors (unless it's the anchor) - if (supersededIds.has(peerId) && peerId !== nodeId) continue - visited.add(peerId) - nextFrontier.add(peerId) + if (supersededIds.has(peerId) && peerId !== nodeId) continue; + visited.add(peerId); + nextFrontier.add(peerId); } } } - frontier = nextFrontier + frontier = nextFrontier; } // Fetch neighbor nodes (exclude anchor) - const neighborIds = [...visited].filter((id) => id !== nodeId) - const neighborNodes: GraphNode[] = [] + const neighborIds = [...visited].filter((id) => id !== nodeId); + const neighborNodes: GraphNode[] = []; if (neighborIds.length > 0) { - const rows = db - .select() - .from(schema.nodes) - .where(inArray(schema.nodes.id, neighborIds)) - .all() - neighborNodes.push(...rows.map(rowToNode)) + const rows = db.select().from(schema.nodes).where(inArray(schema.nodes.id, neighborIds)).all(); + neighborNodes.push(...rows.map(rowToNode)); } // Fetch collected edges - const edgeIdArr = [...collectedEdgeIds] - const edgeNodes: GraphEdge[] = [] + const edgeIdArr = [...collectedEdgeIds]; + const edgeNodes: GraphEdge[] = []; if (edgeIdArr.length > 0) { - const rows = db - .select() - .from(schema.edges) - .where(inArray(schema.edges.id, edgeIdArr)) - .all() - edgeNodes.push(...rows.map(rowToEdge)) + const rows = db.select().from(schema.edges).where(inArray(schema.edges.id, edgeIdArr)).all(); + edgeNodes.push(...rows.map(rowToEdge)); } return { - status: "success", + status: 'success', anchor, neighbors: neighborNodes, edges: edgeNodes, - } + }; } // --------------------------------------------------------------------------- // getOpenReconciliationNeeds // --------------------------------------------------------------------------- -function rowToReconNeed( - row: typeof schema.reconciliationNeed.$inferSelect, -): ReconciliationNeed { +function rowToReconNeed(row: typeof schema.reconciliationNeed.$inferSelect): ReconciliationNeed { const target: ReconciliationNeedTarget = - row.target_kind === "edge" - ? { kind: "edge", edgeId: row.target_edge_id! } - : { kind: "node_pair", aId: row.target_a_id!, bId: row.target_b_id! } + row.target_kind === 'edge' + ? { kind: 'edge', edgeId: row.target_edge_id! } + : { kind: 'node_pair', aId: row.target_a_id!, bId: row.target_b_id! }; return { id: String(row.id), - kind: row.kind as ReconciliationNeed["kind"], + kind: row.kind as ReconciliationNeed['kind'], target, ...(row.reason != null ? { rationale: row.reason } : {}), createdAtLsn: row.created_at_lsn, - ...(row.resolved_at_lsn != null - ? { resolvedAtLsn: row.resolved_at_lsn } - : {}), - } + ...(row.resolved_at_lsn != null ? { resolvedAtLsn: row.resolved_at_lsn } : {}), + }; } /** @@ -279,7 +251,7 @@ export function getOpenReconciliationNeeds(db: BrunchDb): ReconciliationNeed[] { const rows = db .select() .from(schema.reconciliationNeed) - .where(eq(schema.reconciliationNeed.status, "open")) - .all() - return rows.map(rowToReconNeed) + .where(eq(schema.reconciliationNeed.status, 'open')) + .all(); + return rows.map(rowToReconNeed); } diff --git a/src/jsonl-session-viability.test.ts b/src/jsonl-session-viability.test.ts index e438f501d..f7dc0a258 100644 --- a/src/jsonl-session-viability.test.ts +++ b/src/jsonl-session-viability.test.ts @@ -1,8 +1,6 @@ -import { mkdtempSync } from "node:fs" -import { tmpdir } from "node:os" -import { join } from "node:path" - -import { describe, expect, it } from "vitest" +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { SessionManager, @@ -10,291 +8,265 @@ import { type CustomMessageEntry, type SessionEntry, type SessionMessageEntry, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { assistantMessage, userMessage } from "./test-helpers.js" +import { assistantMessage, userMessage } from './test-helpers.js'; interface PersistedSessionFixture { - file: string - manager: SessionManager + file: string; + manager: SessionManager; } -describe("Pi JSONL transcript viability", () => { - it("jsonl raw user assistant payload survival", async () => { - const { file, manager } = createPersistedSession() - const userContent: (import("@earendil-works/pi-ai").TextContent | import("@earendil-works/pi-ai").ImageContent)[] = - [ - { type: "text", text: "Describe this image" }, - { - type: "image", - data: "data:image/png;base64,ZmFrZQ==", - mimeType: "image/png", - }, - ] - const assistantContent: import("@earendil-works/pi-ai").TextContent[] = [ - { type: "text", text: "Here is a structured answer." }, - ] - - manager.appendMessage(userMessage(userContent)) - manager.appendMessage(assistantMessage(assistantContent)) - - const reloaded = SessionManager.open(file) - const messages = reloaded.getEntries().filter(isMessageEntry) +describe('Pi JSONL transcript viability', () => { + it('jsonl raw user assistant payload survival', async () => { + const { file, manager } = createPersistedSession(); + const userContent: ( + | import('@earendil-works/pi-ai').TextContent + | import('@earendil-works/pi-ai').ImageContent + )[] = [ + { type: 'text', text: 'Describe this image' }, + { + type: 'image', + data: 'data:image/png;base64,ZmFrZQ==', + mimeType: 'image/png', + }, + ]; + const assistantContent: import('@earendil-works/pi-ai').TextContent[] = [ + { type: 'text', text: 'Here is a structured answer.' }, + ]; + + manager.appendMessage(userMessage(userContent)); + manager.appendMessage(assistantMessage(assistantContent)); + + const reloaded = SessionManager.open(file); + const messages = reloaded.getEntries().filter(isMessageEntry); expect(messages.map((entry) => entry.message)).toMatchObject([ - { role: "user", content: userContent }, - { role: "assistant", content: assistantContent }, - ]) - }) + { role: 'user', content: userContent }, + { role: 'assistant', content: assistantContent }, + ]); + }); - it("jsonl custom entry survival matrix", async () => { - const { file, manager } = createPersistedSession() + it('jsonl custom entry survival matrix', async () => { + const { file, manager } = createPersistedSession(); const customEntries = [ - ["brunch.lens_switch", { lens: "verification-design", reason: "test" }], + ['brunch.lens_switch', { lens: 'verification-design', reason: 'test' }], + ['brunch.mention', { entityId: 'node-1', snapshottedLsn: 7, title: 'Known node' }], + ['brunch.mention_staleness_hint', { entityId: 'node-1', seenLsn: 7, currentLsn: 9 }], [ - "brunch.mention", - { entityId: "node-1", snapshottedLsn: 7, title: "Known node" }, - ], - [ - "brunch.mention_staleness_hint", - { entityId: "node-1", seenLsn: 7, currentLsn: 9 }, - ], - [ - "brunch.continuity", + 'brunch.continuity', { lastSeenLsn: 9, - interestSet: ["node-1", "node-2"], - compactionAnchorIds: ["anchor-1"], + interestSet: ['node-1', 'node-2'], + compactionAnchorIds: ['anchor-1'], }, ], - ] as const + ] as const; for (const [customType, data] of customEntries) { - manager.appendCustomEntry(customType, data) + manager.appendCustomEntry(customType, data); } - flushPreAssistantEntries(manager) + flushPreAssistantEntries(manager); - const reloaded = SessionManager.open(file) + const reloaded = SessionManager.open(file); const customByType = new Map( reloaded .getEntries() .filter(isCustomEntry) .map((entry) => [entry.customType, entry.data]), - ) + ); for (const [customType, data] of customEntries) { - expect(customByType.get(customType)).toEqual(data) + expect(customByType.get(customType)).toEqual(data); } - }) + }); - it("jsonl custom message survival matrix", async () => { - const { file, manager } = createPersistedSession() + it('jsonl custom message survival matrix', async () => { + const { file, manager } = createPersistedSession(); const worldUpdate = { changedSinceLsn: 11, - items: [{ id: "node-1", lsn: 12, title: "Updated node" }], - } + items: [{ id: 'node-1', lsn: 12, title: 'Updated node' }], + }; const sideTaskResult = { - taskId: "side-task-1", - status: "succeeded", - summary: "Found related risk.", - } + taskId: 'side-task-1', + status: 'succeeded', + summary: 'Found related risk.', + }; const structuredPrompt = { - promptId: "prompt-1", - kind: "radio", - choices: ["A", "B"], - } + promptId: 'prompt-1', + kind: 'radio', + choices: ['A', 'B'], + }; manager.appendCustomMessageEntry( - "worldUpdate", - "Node node-1 changed since your last turn.", + 'worldUpdate', + 'Node node-1 changed since your last turn.', true, worldUpdate, - ) + ); manager.appendCustomMessageEntry( - "brunch.side_task_result", - [{ type: "text", text: "Side task result: Found related risk." }], + 'brunch.side_task_result', + [{ type: 'text', text: 'Side task result: Found related risk.' }], false, sideTaskResult, - ) + ); manager.appendCustomMessageEntry( - "brunch.elicitation_prompt", - "Choose the better framing.", + 'brunch.elicitation_prompt', + 'Choose the better framing.', true, structuredPrompt, - ) - flushPreAssistantEntries(manager) + ); + flushPreAssistantEntries(manager); - const reloaded = SessionManager.open(file) - const customMessages = reloaded.getEntries().filter(isCustomMessageEntry) + const reloaded = SessionManager.open(file); + const customMessages = reloaded.getEntries().filter(isCustomMessageEntry); expect(customMessages).toEqual([ expect.objectContaining({ - customType: "worldUpdate", - content: "Node node-1 changed since your last turn.", + customType: 'worldUpdate', + content: 'Node node-1 changed since your last turn.', display: true, details: worldUpdate, }), expect.objectContaining({ - customType: "brunch.side_task_result", - content: [ - { type: "text", text: "Side task result: Found related risk." }, - ], + customType: 'brunch.side_task_result', + content: [{ type: 'text', text: 'Side task result: Found related risk.' }], display: false, details: sideTaskResult, }), expect.objectContaining({ - customType: "brunch.elicitation_prompt", - content: "Choose the better framing.", + customType: 'brunch.elicitation_prompt', + content: 'Choose the better framing.', display: true, details: structuredPrompt, }), - ]) - }) - - it("jsonl custom messages re-enter pi context", async () => { - const { file, manager } = createPersistedSession() - manager.appendCustomMessageEntry( - "worldUpdate", - "World update: node-1 changed.", - true, - { changedSinceLsn: 3 }, - ) - manager.appendCustomEntry("brunch.lens_switch", { lens: "observer" }) - manager.appendCustomMessageEntry( - "brunch.side_task_result", - "Side task completed.", - false, - { taskId: "task-1" }, - ) - flushPreAssistantEntries(manager) + ]); + }); + + it('jsonl custom messages re-enter pi context', async () => { + const { file, manager } = createPersistedSession(); + manager.appendCustomMessageEntry('worldUpdate', 'World update: node-1 changed.', true, { + changedSinceLsn: 3, + }); + manager.appendCustomEntry('brunch.lens_switch', { lens: 'observer' }); + manager.appendCustomMessageEntry('brunch.side_task_result', 'Side task completed.', false, { + taskId: 'task-1', + }); + flushPreAssistantEntries(manager); const contextMessages = SessionManager.open(file) .buildSessionContext() - .messages.filter((message) => message.role === "custom") + .messages.filter((message) => message.role === 'custom'); expect(contextMessages).toEqual([ expect.objectContaining({ - role: "custom", - customType: "worldUpdate", - content: "World update: node-1 changed.", + role: 'custom', + customType: 'worldUpdate', + content: 'World update: node-1 changed.', }), expect.objectContaining({ - role: "custom", - customType: "brunch.side_task_result", - content: "Side task completed.", + role: 'custom', + customType: 'brunch.side_task_result', + content: 'Side task completed.', }), - ]) - }) + ]); + }); - it("jsonl continuity metadata survival", async () => { - const { file, manager } = createPersistedSession() - const anchorEntryId = manager.appendMessage( - assistantMessage("Anchor before compaction"), - ) + it('jsonl continuity metadata survival', async () => { + const { file, manager } = createPersistedSession(); + const anchorEntryId = manager.appendMessage(assistantMessage('Anchor before compaction')); const continuity = { lastSeenLsn: 42, - interestSet: ["node-a", "node-b"], - compactionAnchors: [{ entryId: anchorEntryId, graphNodeId: "node-a" }], - } + interestSet: ['node-a', 'node-b'], + compactionAnchors: [{ entryId: anchorEntryId, graphNodeId: 'node-a' }], + }; - manager.appendCustomEntry("brunch.continuity", continuity) - manager.appendCompaction("Compacted summary", anchorEntryId, 1_234, { + manager.appendCustomEntry('brunch.continuity', continuity); + manager.appendCompaction('Compacted summary', anchorEntryId, 1_234, { brunch: { continuity }, - }) - flushPreAssistantEntries(manager) + }); + flushPreAssistantEntries(manager); - const reloaded = SessionManager.open(file) + const reloaded = SessionManager.open(file); const customContinuity = reloaded .getEntries() .filter(isCustomEntry) - .find((entry) => entry.customType === "brunch.continuity") - const compaction = reloaded - .getEntries() - .find((entry) => entry.type === "compaction") + .find((entry) => entry.customType === 'brunch.continuity'); + const compaction = reloaded.getEntries().find((entry) => entry.type === 'compaction'); - expect(customContinuity?.data).toEqual(continuity) + expect(customContinuity?.data).toEqual(continuity); expect(compaction).toMatchObject({ details: { brunch: { continuity } }, - }) - }) + }); + }); - it("jsonl structured elicitation survival", async () => { - const { file, manager } = createPersistedSession() + it('jsonl structured elicitation survival', async () => { + const { file, manager } = createPersistedSession(); const promptDetails = { - promptId: "prompt-1", - surface: "checkbox", - choices: ["fast", "safe"], - } + promptId: 'prompt-1', + surface: 'checkbox', + choices: ['fast', 'safe'], + }; const responseData = { - promptId: "prompt-1", - selected: ["safe"], - freeform: "Prefer safety.", - } + promptId: 'prompt-1', + selected: ['safe'], + freeform: 'Prefer safety.', + }; - manager.appendCustomMessageEntry( - "brunch.elicitation_prompt", - "Select priorities.", - true, - promptDetails, - ) - manager.appendMessage(userMessage("I choose safety.")) - manager.appendCustomEntry("brunch.elicitation_response", responseData) - flushPreAssistantEntries(manager) + manager.appendCustomMessageEntry('brunch.elicitation_prompt', 'Select priorities.', true, promptDetails); + manager.appendMessage(userMessage('I choose safety.')); + manager.appendCustomEntry('brunch.elicitation_response', responseData); + flushPreAssistantEntries(manager); - const reloadedEntries = SessionManager.open(file).getEntries() + const reloadedEntries = SessionManager.open(file).getEntries(); const structuredPrompt = reloadedEntries.find( - (entry) => - isCustomMessageEntry(entry) && - entry.customType === "brunch.elicitation_prompt", - ) + (entry) => isCustomMessageEntry(entry) && entry.customType === 'brunch.elicitation_prompt', + ); const ordinaryUser = reloadedEntries.find( - (entry) => isMessageEntry(entry) && entry.message.role === "user", - ) + (entry) => isMessageEntry(entry) && entry.message.role === 'user', + ); const structuredResponse = reloadedEntries.find( - (entry) => - isCustomEntry(entry) && - entry.customType === "brunch.elicitation_response", - ) + (entry) => isCustomEntry(entry) && entry.customType === 'brunch.elicitation_response', + ); expect(structuredPrompt).toMatchObject({ - type: "custom_message", + type: 'custom_message', details: promptDetails, - }) + }); expect(ordinaryUser).toMatchObject({ - type: "message", - message: userMessage("I choose safety."), - }) + type: 'message', + message: userMessage('I choose safety.'), + }); expect(structuredResponse).toMatchObject({ - type: "custom", + type: 'custom', data: responseData, - }) - }) -}) + }); + }); +}); function createPersistedSession(): PersistedSessionFixture { - const cwd = mkdtempSync(join(tmpdir(), "brunch-jsonl-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - const file = manager.getSessionFile() + const cwd = mkdtempSync(join(tmpdir(), 'brunch-jsonl-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + const file = manager.getSessionFile(); if (!file) { - throw new Error("Expected persisted session file") + throw new Error('Expected persisted session file'); } - return { file, manager } + return { file, manager }; } function flushPreAssistantEntries(manager: SessionManager): void { - manager.appendMessage(assistantMessage("Persistence sentinel")) + manager.appendMessage(assistantMessage('Persistence sentinel')); } function isMessageEntry(entry: SessionEntry): entry is SessionMessageEntry { - return entry.type === "message" + return entry.type === 'message'; } function isCustomEntry(entry: SessionEntry): entry is CustomEntry { - return entry.type === "custom" + return entry.type === 'custom'; } -function isCustomMessageEntry( - entry: SessionEntry, -): entry is CustomMessageEntry { - return entry.type === "custom_message" +function isCustomMessageEntry(entry: SessionEntry): entry is CustomMessageEntry { + return entry.type === 'custom_message'; } diff --git a/src/print-snapshot.test.ts b/src/print-snapshot.test.ts index c7fea3d24..095379d37 100644 --- a/src/print-snapshot.test.ts +++ b/src/print-snapshot.test.ts @@ -1,70 +1,65 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { - renderWorkspaceSnapshot, - workspaceSnapshotFromState, -} from "./print-snapshot.js" -import type { WorkspaceSessionState } from "./workspace-session-coordinator.js" +import { renderWorkspaceSnapshot, workspaceSnapshotFromState } from './print-snapshot.js'; +import type { WorkspaceSessionState } from './workspace-session-coordinator.js'; -const cwd = "/tmp/brunch-project" +const cwd = '/tmp/brunch-project'; function readyState(): WorkspaceSessionState { return { - status: "ready", + status: 'ready', cwd, - spec: { id: "spec-1", title: "Alpha spec" }, + spec: { id: 'spec-1', title: 'Alpha spec' }, session: { - id: "session-1", - file: "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", + id: 'session-1', + file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', manager: {} as WorkspaceSessionState & never, }, chrome: { cwd, - spec: { id: "spec-1", title: "Alpha spec" }, - phase: "elicitation", - chatMode: "responding-to-elicitation", + spec: { id: 'spec-1', title: 'Alpha spec' }, + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - } + }; } -describe("print snapshot", () => { - it("projects and renders a ready workspace without exposing pi internals", () => { - const snapshot = workspaceSnapshotFromState(readyState()) +describe('print snapshot', () => { + it('projects and renders a ready workspace without exposing pi internals', () => { + const snapshot = workspaceSnapshotFromState(readyState()); expect(snapshot).toEqual({ - status: "ready", + status: 'ready', cwd, - spec: { id: "spec-1", title: "Alpha spec" }, + spec: { id: 'spec-1', title: 'Alpha spec' }, session: { - id: "session-1", - file: "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", + id: 'session-1', + file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', }, chrome: { - phase: "elicitation", - chatMode: "responding-to-elicitation", + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - }) - expect(renderWorkspaceSnapshot(snapshot)).toContain("status: ready") - expect(renderWorkspaceSnapshot(snapshot)).toContain( - "spec: Alpha spec (spec-1)", - ) - expect(renderWorkspaceSnapshot(snapshot)).toContain("session: session-1") - }) + }); + expect(renderWorkspaceSnapshot(snapshot)).toContain('status: ready'); + expect(renderWorkspaceSnapshot(snapshot)).toContain('spec: Alpha spec (spec-1)'); + expect(renderWorkspaceSnapshot(snapshot)).toContain('session: session-1'); + }); - it("renders select-spec as a snapshot instead of prompting", () => { + it('renders select-spec as a snapshot instead of prompting', () => { const snapshot = workspaceSnapshotFromState({ - status: "select_spec", + status: 'select_spec', cwd, chrome: { cwd, spec: null, - phase: "select_spec", - chatMode: "select-spec", + phase: 'select_spec', + chatMode: 'select-spec', }, - }) + }); - expect(renderWorkspaceSnapshot(snapshot)).toContain("status: select_spec") - expect(renderWorkspaceSnapshot(snapshot)).toContain("spec: ") - expect(renderWorkspaceSnapshot(snapshot)).not.toContain("session:") - }) -}) + expect(renderWorkspaceSnapshot(snapshot)).toContain('status: select_spec'); + expect(renderWorkspaceSnapshot(snapshot)).toContain('spec: '); + expect(renderWorkspaceSnapshot(snapshot)).not.toContain('session:'); + }); +}); diff --git a/src/print-snapshot.ts b/src/print-snapshot.ts index 5943ed6b9..fc94fef46 100644 --- a/src/print-snapshot.ts +++ b/src/print-snapshot.ts @@ -1,26 +1,24 @@ -import type { WorkspaceSessionState } from "./workspace-session-coordinator.js" +import type { WorkspaceSessionState } from './workspace-session-coordinator.js'; export interface WorkspaceSnapshot { - status: WorkspaceSessionState["status"] - cwd: string + status: WorkspaceSessionState['status']; + cwd: string; spec: { - id: string - title: string - } | null + id: string; + title: string; + } | null; session?: { - id: string - file: string - } + id: string; + file: string; + }; chrome: { - phase: "select_spec" | "elicitation" - chatMode: "select-spec" | "responding-to-elicitation" - } - reason?: string + phase: 'select_spec' | 'elicitation'; + chatMode: 'select-spec' | 'responding-to-elicitation'; + }; + reason?: string; } -export function workspaceSnapshotFromState( - state: WorkspaceSessionState, -): WorkspaceSnapshot { +export function workspaceSnapshotFromState(state: WorkspaceSessionState): WorkspaceSnapshot { const base = { status: state.status, cwd: state.cwd, @@ -29,44 +27,39 @@ export function workspaceSnapshotFromState( phase: state.chrome.phase, chatMode: state.chrome.chatMode, }, - } + }; - if (state.status === "ready") { + if (state.status === 'ready') { return { ...base, spec: state.spec, session: { id: state.session.id, file: state.session.file }, - } + }; } - if (state.status === "needs_human") { - return { ...base, reason: state.reason } + if (state.status === 'needs_human') { + return { ...base, reason: state.reason }; } - return base + return base; } export function renderWorkspaceSnapshot(snapshot: WorkspaceSnapshot): string { const lines = [ - "Brunch workspace snapshot", + 'Brunch workspace snapshot', `status: ${snapshot.status}`, `cwd: ${snapshot.cwd}`, - `spec: ${ - snapshot.spec ? `${snapshot.spec.title} (${snapshot.spec.id})` : "" - }`, + `spec: ${snapshot.spec ? `${snapshot.spec.title} (${snapshot.spec.id})` : ''}`, `phase: ${snapshot.chrome.phase}`, `chatMode: ${snapshot.chrome.chatMode}`, - ] + ]; if (snapshot.session) { - lines.push( - `session: ${snapshot.session.id}`, - `sessionFile: ${snapshot.session.file}`, - ) + lines.push(`session: ${snapshot.session.id}`, `sessionFile: ${snapshot.session.file}`); } if (snapshot.reason) { - lines.push(`reason: ${snapshot.reason}`) + lines.push(`reason: ${snapshot.reason}`); } - return `${lines.join("\n")}\n` + return `${lines.join('\n')}\n`; } diff --git a/src/probes/check-workspace-session-stores.ts b/src/probes/check-workspace-session-stores.ts index 410392fa0..f0835c8b3 100644 --- a/src/probes/check-workspace-session-stores.ts +++ b/src/probes/check-workspace-session-stores.ts @@ -1,26 +1,24 @@ #!/usr/bin/env node -import process from "node:process" +import process from 'node:process'; -import { verifyWorkspaceSessionStores } from "../workspace-session-coordinator.js" +import { verifyWorkspaceSessionStores } from '../workspace-session-coordinator.js'; -const cwd = process.argv[2] -const expectedSessionCount = process.argv[3] - ? Number(process.argv[3]) - : undefined +const cwd = process.argv[2]; +const expectedSessionCount = process.argv[3] ? Number(process.argv[3]) : undefined; if (!cwd || Number.isNaN(expectedSessionCount)) { process.stderr.write( - "Usage: tsx src/probes/check-workspace-session-stores.ts [expected-session-count]\n", - ) - process.exit(2) + 'Usage: tsx src/probes/check-workspace-session-stores.ts [expected-session-count]\n', + ); + process.exit(2); } const result = await verifyWorkspaceSessionStores( expectedSessionCount === undefined ? { cwd } : { cwd, expectedSessionCount }, -) +); if (!result.ok) { - process.stderr.write(`${result.errors.join("\n")}\n`) - process.exit(1) + process.stderr.write(`${result.errors.join('\n')}\n`); + process.exit(1); } process.stdout.write( @@ -38,4 +36,4 @@ process.stdout.write( null, 2, )}\n`, -) +); diff --git a/src/probes/public-rpc-parity-proof.test.ts b/src/probes/public-rpc-parity-proof.test.ts index ad924a4cf..42d99523a 100644 --- a/src/probes/public-rpc-parity-proof.test.ts +++ b/src/probes/public-rpc-parity-proof.test.ts @@ -1,120 +1,93 @@ -import { mkdtemp, readFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { basename, dirname, join } from "node:path" +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { basename, dirname, join } from 'node:path'; -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { runPublicRpcParityProof } from "./public-rpc-parity-proof.js" +import { runPublicRpcParityProof } from './public-rpc-parity-proof.js'; -describe("public Brunch RPC structured-exchange parity proof", () => { - it("drives each deterministic structured-exchange permutation from a fresh cwd", async () => { - const report = await runPublicRpcParityProof() +describe('public Brunch RPC structured-exchange parity proof', () => { + it('drives each deterministic structured-exchange permutation from a fresh cwd', async () => { + const report = await runPublicRpcParityProof(); expect(report).toMatchObject({ schemaVersion: 1, - probeId: "public-rpc-parity", + probeId: 'public-rpc-parity', runId: expect.any(String), generatedAt: expect.any(String), - mission: expect.stringContaining("public JSON-RPC only"), - evaluationFocus: expect.stringContaining( - "Tuple transcript/projection parity", - ), + mission: expect.stringContaining('public JSON-RPC only'), + evaluationFocus: expect.stringContaining('Tuple transcript/projection parity'), maxTurnBudget: 3, completedTurns: 3, friction: [], specId: expect.any(String), sessionId: expect.any(String), - }) - expect(Date.parse(report.generatedAt)).not.toBeNaN() + }); + expect(Date.parse(report.generatedAt)).not.toBeNaN(); expect(report.toolCoverage).toEqual([ - "present_options", - "present_question", - "request_answer", - "request_choice", - "request_choices", - ]) + 'present_options', + 'present_question', + 'request_answer', + 'request_choice', + 'request_choices', + ]); expect(report.exchangeIds).toEqual([ - "deterministic-grounding-choice-1", - "deterministic-grounding-text-2", - "deterministic-grounding-multi-3", - ]) - expect(new Set(report.exchangeIds).size).toBe(3) - expect(report.artifacts).toBeUndefined() - expect(report.transcriptDisplayRows).toBeGreaterThanOrEqual(6) - }) + 'deterministic-grounding-choice-1', + 'deterministic-grounding-text-2', + 'deterministic-grounding-multi-3', + ]); + expect(new Set(report.exchangeIds).size).toBe(3); + expect(report.artifacts).toBeUndefined(); + expect(report.transcriptDisplayRows).toBeGreaterThanOrEqual(6); + }); - it("writes a reviewable artifact bundle when given a fixture root", async () => { - const fixtureRoot = await mkdtemp(join(tmpdir(), "brunch-fixtures-")) + it('writes a reviewable artifact bundle when given a fixture root', async () => { + const fixtureRoot = await mkdtemp(join(tmpdir(), 'brunch-fixtures-')); const report = await runPublicRpcParityProof({ fixtureRoot, - runId: "artifact-test", - }) + runId: 'artifact-test', + }); - const artifacts = report.artifacts + const artifacts = report.artifacts; expect(artifacts).toEqual({ - runDir: join(fixtureRoot, "runs", "public-rpc-parity", report.runId), - sessionJsonl: join( - fixtureRoot, - "runs", - "public-rpc-parity", - report.runId, - "session.jsonl", - ), - transcriptMarkdown: join( - fixtureRoot, - "runs", - "public-rpc-parity", - report.runId, - "transcript.md", - ), - reportJson: join( - fixtureRoot, - "runs", - "public-rpc-parity", - report.runId, - "report.json", - ), - }) - if (artifacts === undefined) throw new Error("Expected artifact paths") + runDir: join(fixtureRoot, 'runs', 'public-rpc-parity', report.runId), + sessionJsonl: join(fixtureRoot, 'runs', 'public-rpc-parity', report.runId, 'session.jsonl'), + transcriptMarkdown: join(fixtureRoot, 'runs', 'public-rpc-parity', report.runId, 'transcript.md'), + reportJson: join(fixtureRoot, 'runs', 'public-rpc-parity', report.runId, 'report.json'), + }); + if (artifacts === undefined) throw new Error('Expected artifact paths'); - expect( - artifacts.runDir.endsWith(join("runs", report.probeId, report.runId)), - ).toBe(true) - expect(basename(artifacts.runDir)).toBe(report.runId) - expect(basename(dirname(artifacts.runDir))).toBe(report.probeId) + expect(artifacts.runDir.endsWith(join('runs', report.probeId, report.runId))).toBe(true); + expect(basename(artifacts.runDir)).toBe(report.runId); + expect(basename(dirname(artifacts.runDir))).toBe(report.probeId); - const sessionJsonl = await readFile(artifacts.sessionJsonl, "utf8") - const transcript = await readFile(artifacts.transcriptMarkdown, "utf8") - const persistedReport = JSON.parse( - await readFile(artifacts.reportJson, "utf8"), - ) as typeof report + const sessionJsonl = await readFile(artifacts.sessionJsonl, 'utf8'); + const transcript = await readFile(artifacts.transcriptMarkdown, 'utf8'); + const persistedReport = JSON.parse(await readFile(artifacts.reportJson, 'utf8')) as typeof report; - expect(sessionJsonl).toContain('"toolName":"present_options"') - expect(transcript).toContain("# Transcript — session.jsonl") - expect(transcript).toContain("## Exchange") - expect(transcript).toContain("— prompt (present_") - expect(transcript).toContain("— response (request_") + expect(sessionJsonl).toContain('"toolName":"present_options"'); + expect(transcript).toContain('# Transcript — session.jsonl'); + expect(transcript).toContain('## Exchange'); + expect(transcript).toContain('— prompt (present_'); + expect(transcript).toContain('— response (request_'); expect(persistedReport).toMatchObject({ schemaVersion: 1, - probeId: "public-rpc-parity", + probeId: 'public-rpc-parity', runId: report.runId, generatedAt: report.generatedAt, mission: report.mission, completedTurns: 3, exchangeIds: report.exchangeIds, artifacts: report.artifacts, - }) - expect(persistedReport.exchangeIds).toEqual(report.exchangeIds) - expect(persistedReport.exchangeIds).toHaveLength(3) - expect(new Set(persistedReport.exchangeIds).size).toBe(3) - expect( - transcript.match(/Is this a new product or feature from scratch\?/g) ?? - [], - ).toHaveLength(1) + }); + expect(persistedReport.exchangeIds).toEqual(report.exchangeIds); + expect(persistedReport.exchangeIds).toHaveLength(3); + expect(new Set(persistedReport.exchangeIds).size).toBe(3); + expect(transcript.match(/Is this a new product or feature from scratch\?/g) ?? []).toHaveLength(1); for (const exchangeId of persistedReport.exchangeIds) { - expect(sessionJsonl).toContain(exchangeId) - expect(transcript).toContain(exchangeId) + expect(sessionJsonl).toContain(exchangeId); + expect(transcript).toContain(exchangeId); } - }) -}) + }); +}); diff --git a/src/probes/public-rpc-parity-proof.ts b/src/probes/public-rpc-parity-proof.ts index 5f6794fc2..85b52c32c 100644 --- a/src/probes/public-rpc-parity-proof.ts +++ b/src/probes/public-rpc-parity-proof.ts @@ -1,377 +1,350 @@ -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { createRpcHandlers } from "../rpc/handlers.js" -import { renderSessionTranscript } from "../session-transcript.js" -import { createWorkspaceSessionCoordinator } from "../workspace-session-coordinator.js" +import { createRpcHandlers } from '../rpc/handlers.js'; +import { renderSessionTranscript } from '../session-transcript.js'; +import { createWorkspaceSessionCoordinator } from '../workspace-session-coordinator.js'; -const PUBLIC_RPC_PARITY_PERMUTATION_COUNT = 3 +const PUBLIC_RPC_PARITY_PERMUTATION_COUNT = 3; interface JsonRpcSuccess { - jsonrpc: "2.0" - id: number - result: T + jsonrpc: '2.0'; + id: number; + result: T; } interface PendingOption { - id: string - label: string - content?: string - rationale?: string + id: string; + label: string; + content?: string; + rationale?: string; } interface PendingExchange { - exchangeId: string - mode: "text" | "single-select" | "multi-select" - prompt: string - options: PendingOption[] + exchangeId: string; + mode: 'text' | 'single-select' | 'multi-select'; + prompt: string; + options: PendingOption[]; } interface RpcExchange { - promptEntryIds: string[] - responseEntryIds: string[] + promptEntryIds: string[]; + responseEntryIds: string[]; } interface RpcExchangeProjection { - status: string - exchanges: RpcExchange[] + status: string; + exchanges: RpcExchange[]; } interface TranscriptDisplayRow { - role: string - text: string + role: string; + text: string; } interface TranscriptDisplayProjection { - rows: TranscriptDisplayRow[] + rows: TranscriptDisplayRow[]; } interface WorkspaceSelectionResult { - requiresSelection: boolean + requiresSelection: boolean; } interface PendingResult { - status: "pending" - exchange: PendingExchange + status: 'pending'; + exchange: PendingExchange; } export interface PublicRpcParityProofArtifacts { - runDir: string - sessionJsonl: string - transcriptMarkdown: string - reportJson: string + runDir: string; + sessionJsonl: string; + transcriptMarkdown: string; + reportJson: string; } export interface PublicRpcParityProofOptions { - fixtureRoot?: string - runId?: string + fixtureRoot?: string; + runId?: string; } export interface PublicRpcParityProofReport { - schemaVersion: 1 - probeId: "public-rpc-parity" - runId: string - generatedAt: string - mission: string - evaluationFocus: string - maxTurnBudget: number - completedTurns: number - friction: string[] - cwd: string - specId: string - sessionId: string - toolCoverage: string[] - exchangeIds: string[] - transcriptDisplayRows: number - artifacts?: PublicRpcParityProofArtifacts + schemaVersion: 1; + probeId: 'public-rpc-parity'; + runId: string; + generatedAt: string; + mission: string; + evaluationFocus: string; + maxTurnBudget: number; + completedTurns: number; + friction: string[]; + cwd: string; + specId: string; + sessionId: string; + toolCoverage: string[]; + exchangeIds: string[]; + transcriptDisplayRows: number; + artifacts?: PublicRpcParityProofArtifacts; } function success(response: unknown): T { - if ( - typeof response === "object" && - response !== null && - "result" in response - ) { - return (response as JsonRpcSuccess).result + if (typeof response === 'object' && response !== null && 'result' in response) { + return (response as JsonRpcSuccess).result; } - throw new Error( - `Expected JSON-RPC success response: ${JSON.stringify(response)}`, - ) + throw new Error(`Expected JSON-RPC success response: ${JSON.stringify(response)}`); } interface ToolResultOptionDetails { - id?: string - label?: string - content?: string - rationale?: string + id?: string; + label?: string; + content?: string; + rationale?: string; } interface ToolResultDetails { - exchangeId?: string - schema?: string - requestTool?: string - presentTool?: string - prompt?: string - options?: ToolResultOptionDetails[] + exchangeId?: string; + schema?: string; + requestTool?: string; + presentTool?: string; + prompt?: string; + options?: ToolResultOptionDetails[]; } interface ToolResultEntry { - toolName: string - content: string - details?: ToolResultDetails + toolName: string; + content: string; + details?: ToolResultDetails; } interface JsonlMessageEntry { message?: { - role?: string - toolName?: string - content?: unknown - details?: unknown - } + role?: string; + toolName?: string; + content?: unknown; + details?: unknown; + }; } function toolResultEntries(sessionText: string): ToolResultEntry[] { return sessionText .trim() - .split("\n") + .split('\n') .map((line) => JSON.parse(line) as JsonlMessageEntry) - .filter((entry) => entry.message?.role === "toolResult") + .filter((entry) => entry.message?.role === 'toolResult') .map((entry) => ({ - toolName: entry.message?.toolName ?? "", + toolName: entry.message?.toolName ?? '', content: textContent(entry.message?.content), details: entry.message?.details as never, - })) + })); } function textContent(content: unknown): string { - if (typeof content === "string") return content - if (!Array.isArray(content)) return "" + if (typeof content === 'string') return content; + if (!Array.isArray(content)) return ''; return content .map((part) => - typeof part === "object" && - part !== null && - typeof (part as { text?: unknown }).text === "string" + typeof part === 'object' && part !== null && typeof (part as { text?: unknown }).text === 'string' ? (part as { text: string }).text - : "", + : '', ) - .join("\n") + .join('\n'); } interface ProofResponse { - answer: unknown - note?: string + answer: unknown; + note?: string; } function responseFor(exchange: PendingExchange): ProofResponse { - if (exchange.mode === "text") { - return { answer: { text: `Answer for ${exchange.exchangeId}` } } + if (exchange.mode === 'text') { + return { answer: { text: `Answer for ${exchange.exchangeId}` } }; } - if (exchange.mode === "multi-select") { + if (exchange.mode === 'multi-select') { return { - answer: { optionIds: ["transcript", "other"] }, - note: "Other: keep a compact blocker/friction report.", - } + answer: { optionIds: ['transcript', 'other'] }, + note: 'Other: keep a compact blocker/friction report.', + }; } return { - answer: { optionId: exchange.options[0]?.id ?? "new-from-scratch" }, - note: "Chosen by deterministic public-RPC proof.", - } + answer: { optionId: exchange.options[0]?.id ?? 'new-from-scratch' }, + note: 'Chosen by deterministic public-RPC proof.', + }; } export async function runPublicRpcParityProof( options: PublicRpcParityProofOptions = {}, ): Promise { - const runId = options.runId ?? defaultRunId() - const generatedAt = new Date().toISOString() - const cwd = await mkdtemp(join(tmpdir(), "brunch-public-rpc-parity-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const handlers = createRpcHandlers({ coordinator, cwd }) - const friction: string[] = [] + const runId = options.runId ?? defaultRunId(); + const generatedAt = new Date().toISOString(); + const cwd = await mkdtemp(join(tmpdir(), 'brunch-public-rpc-parity-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const handlers = createRpcHandlers({ coordinator, cwd }); + const friction: string[] = []; const discovery = success<{ methods: Array<{ method: string }> }>( - await handlers.handle({ jsonrpc: "2.0", id: 1, method: "rpc.discover" }), - ) + await handlers.handle({ jsonrpc: '2.0', id: 1, method: 'rpc.discover' }), + ); for (const method of [ - "workspace.selectionState", - "workspace.activate", - "session.startElicitation", - "session.pendingExchange", - "elicitation.respond", - "session.elicitationExchanges", - "session.transcriptDisplay", + 'workspace.selectionState', + 'workspace.activate', + 'session.startElicitation', + 'session.pendingExchange', + 'elicitation.respond', + 'session.elicitationExchanges', + 'session.transcriptDisplay', ]) { if (!discovery.methods.some((entry) => entry.method === method)) { - throw new Error(`rpc.discover did not include ${method}`) + throw new Error(`rpc.discover did not include ${method}`); } } const selection = success( await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 2, - method: "workspace.selectionState", + method: 'workspace.selectionState', }), - ) + ); if (!selection.requiresSelection) { - friction.push("Fresh cwd did not report selection-required state.") + friction.push('Fresh cwd did not report selection-required state.'); } await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 3, - method: "workspace.activate", + method: 'workspace.activate', params: { - decision: { action: "newSpec", title: "Public RPC parity spec" }, + decision: { action: 'newSpec', title: 'Public RPC parity spec' }, }, - }) - const workspace = await coordinator.openDefaultWorkspace() - if (workspace.status !== "ready") { - throw new Error( - "workspace.activate(newSpec) did not create a ready workspace", - ) + }); + const workspace = await coordinator.openDefaultWorkspace(); + if (workspace.status !== 'ready') { + throw new Error('workspace.activate(newSpec) did not create a ready workspace'); } - const exchangeIds: string[] = [] + const exchangeIds: string[] = []; for (let turn = 0; turn < PUBLIC_RPC_PARITY_PERMUTATION_COUNT; turn += 1) { const started = success( await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 10 + turn * 3, - method: "session.startElicitation", + method: 'session.startElicitation', }), - ) + ); const pending = success( await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 11 + turn * 3, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), - ) + ); if (pending.exchange.exchangeId !== started.exchange.exchangeId) { - friction.push( - `Turn ${turn + 1}: pendingExchange differed from startElicitation.`, - ) + friction.push(`Turn ${turn + 1}: pendingExchange differed from startElicitation.`); } - if (started.exchange.mode !== "text") { + if (started.exchange.mode !== 'text') { const richOption = started.exchange.options.find( - (option) => - option.content !== undefined && option.rationale !== undefined, - ) + (option) => option.content !== undefined && option.rationale !== undefined, + ); if (!richOption) { - throw new Error( - `Turn ${turn + 1}: pending options dropped content/rationale`, - ) + throw new Error(`Turn ${turn + 1}: pending options dropped content/rationale`); } if (richOption.content === richOption.label) { - throw new Error( - `Turn ${turn + 1}: pending option content collapsed into label`, - ) + throw new Error(`Turn ${turn + 1}: pending option content collapsed into label`); } } - exchangeIds.push(started.exchange.exchangeId) - const response = responseFor(started.exchange) + exchangeIds.push(started.exchange.exchangeId); + const response = responseFor(started.exchange); await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 12 + turn * 3, - method: "elicitation.respond", + method: 'elicitation.respond', params: { exchangeId: started.exchange.exchangeId, answer: response.answer, ...(response.note === undefined ? {} : { note: response.note }), }, - }) + }); } const exchanges = success( await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 50, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', }), - ) + ); const display = success( await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 51, - method: "session.transcriptDisplay", + method: 'session.transcriptDisplay', }), - ) + ); if (exchanges.exchanges.length !== PUBLIC_RPC_PARITY_PERMUTATION_COUNT) { throw new Error( `Expected ${PUBLIC_RPC_PARITY_PERMUTATION_COUNT} completed exchanges, got ${exchanges.exchanges.length}`, - ) + ); } - const sessionText = await readFile(workspace.session.file, "utf8") + const sessionText = await readFile(workspace.session.file, 'utf8'); if ( - sessionText.includes("brunch.elicitation_prompt") || - sessionText.includes("brunch.elicitation_response") + sessionText.includes('brunch.elicitation_prompt') || + sessionText.includes('brunch.elicitation_response') ) { - throw new Error( - "Public RPC parity transcript used the retired lightweight elicitation entries", - ) + throw new Error('Public RPC parity transcript used the retired lightweight elicitation entries'); } - const tools = toolResultEntries(sessionText) - const toolCoverage = [...new Set(tools.map((entry) => entry.toolName))].sort() + const tools = toolResultEntries(sessionText); + const toolCoverage = [...new Set(tools.map((entry) => entry.toolName))].sort(); for (const required of [ - "present_question", - "request_answer", - "present_options", - "request_choice", - "request_choices", + 'present_question', + 'request_answer', + 'present_options', + 'request_choice', + 'request_choices', ]) { if (!toolCoverage.includes(required)) { - throw new Error(`Missing tool coverage for ${required}`) + throw new Error(`Missing tool coverage for ${required}`); } } if (new Set(exchangeIds).size !== exchangeIds.length) { - throw new Error("Public RPC parity proof reused exchange IDs") + throw new Error('Public RPC parity proof reused exchange IDs'); } const presentPrompts = tools - .filter( - (entry) => entry.details?.schema === "brunch.structured_exchange.present", - ) + .filter((entry) => entry.details?.schema === 'brunch.structured_exchange.present') .map((entry) => entry.details?.prompt) - .filter((prompt): prompt is string => prompt !== undefined) + .filter((prompt): prompt is string => prompt !== undefined); if (new Set(presentPrompts).size !== presentPrompts.length) { - throw new Error("Public RPC parity proof repeated deterministic prompts") + throw new Error('Public RPC parity proof repeated deterministic prompts'); } - const optionPresentResults = tools.filter( - (entry) => entry.toolName === "present_options", - ) + const optionPresentResults = tools.filter((entry) => entry.toolName === 'present_options'); for (const entry of optionPresentResults) { const richOption = entry.details?.options?.find( - (option) => - option.content !== undefined && option.rationale !== undefined, - ) + (option) => option.content !== undefined && option.rationale !== undefined, + ); if (!richOption) { throw new Error( - `Exchange ${entry.details?.exchangeId ?? "unknown"} JSONL option details dropped content/rationale`, - ) + `Exchange ${entry.details?.exchangeId ?? 'unknown'} JSONL option details dropped content/rationale`, + ); } - const optionContent = richOption.content - const optionRationale = richOption.rationale + const optionContent = richOption.content; + const optionRationale = richOption.rationale; if (optionContent === undefined || optionRationale === undefined) { throw new Error( - `Exchange ${entry.details?.exchangeId ?? "unknown"} JSONL option details dropped content/rationale`, - ) + `Exchange ${entry.details?.exchangeId ?? 'unknown'} JSONL option details dropped content/rationale`, + ); } if (optionContent === richOption.label) { throw new Error( - `Exchange ${entry.details?.exchangeId ?? "unknown"} JSONL option content collapsed into label`, - ) + `Exchange ${entry.details?.exchangeId ?? 'unknown'} JSONL option content collapsed into label`, + ); } - if ( - !entry.content.includes(optionContent) || - !entry.content.includes(optionRationale) - ) { + if (!entry.content.includes(optionContent) || !entry.content.includes(optionRationale)) { throw new Error( - `Exchange ${entry.details?.exchangeId ?? "unknown"} transcript markdown dropped option artifacts`, - ) + `Exchange ${entry.details?.exchangeId ?? 'unknown'} transcript markdown dropped option artifacts`, + ); } } @@ -379,29 +352,26 @@ export async function runPublicRpcParityProof( const presentIndex = tools.findIndex( (entry) => entry.details?.exchangeId === exchangeId && - entry.details.schema === "brunch.structured_exchange.present", - ) + entry.details.schema === 'brunch.structured_exchange.present', + ); const requestIndex = tools.findIndex( (entry) => entry.details?.exchangeId === exchangeId && - entry.details.schema === "brunch.structured_exchange.request", - ) + entry.details.schema === 'brunch.structured_exchange.request', + ); if (presentIndex < 0 || requestIndex < 0 || presentIndex > requestIndex) { - throw new Error( - `Exchange ${exchangeId} did not preserve present-before-request order`, - ) + throw new Error(`Exchange ${exchangeId} did not preserve present-before-request order`); } } const report: PublicRpcParityProofReport = { schemaVersion: 1, - probeId: "public-rpc-parity", + probeId: 'public-rpc-parity', runId, generatedAt, - mission: - "Drive deterministic Brunch structured-exchange permutations through public JSON-RPC only.", + mission: 'Drive deterministic Brunch structured-exchange permutations through public JSON-RPC only.', evaluationFocus: - "Tuple transcript/projection parity for current structured-exchange modes without raw Pi RPC or legacy prompt/response entries.", + 'Tuple transcript/projection parity for current structured-exchange modes without raw Pi RPC or legacy prompt/response entries.', maxTurnBudget: PUBLIC_RPC_PARITY_PERMUTATION_COUNT, completedTurns: exchanges.exchanges.length, friction, @@ -411,7 +381,7 @@ export async function runPublicRpcParityProof( toolCoverage, exchangeIds, transcriptDisplayRows: display.rows.length, - } + }; if (options.fixtureRoot !== undefined) { report.artifacts = await writeProofArtifacts({ @@ -419,51 +389,42 @@ export async function runPublicRpcParityProof( runId, sessionText, report, - }) + }); } - return report + return report; } async function writeProofArtifacts(options: { - fixtureRoot: string - runId: string - sessionText: string - report: PublicRpcParityProofReport + fixtureRoot: string; + runId: string; + sessionText: string; + report: PublicRpcParityProofReport; }): Promise { - const runDir = join( - options.fixtureRoot, - "runs", - "public-rpc-parity", - options.runId, - ) + const runDir = join(options.fixtureRoot, 'runs', 'public-rpc-parity', options.runId); const artifacts: PublicRpcParityProofArtifacts = { runDir, - sessionJsonl: join(runDir, "session.jsonl"), - transcriptMarkdown: join(runDir, "transcript.md"), - reportJson: join(runDir, "report.json"), - } + sessionJsonl: join(runDir, 'session.jsonl'), + transcriptMarkdown: join(runDir, 'transcript.md'), + reportJson: join(runDir, 'report.json'), + }; const persistedReport: PublicRpcParityProofReport = { ...options.report, artifacts, - } + }; - await mkdir(runDir, { recursive: true }) - await writeFile(artifacts.sessionJsonl, options.sessionText, "utf8") + await mkdir(runDir, { recursive: true }); + await writeFile(artifacts.sessionJsonl, options.sessionText, 'utf8'); await writeFile( artifacts.transcriptMarkdown, - renderSessionTranscript(options.sessionText, { title: "session.jsonl" }), - "utf8", - ) - await writeFile( - artifacts.reportJson, - `${JSON.stringify(persistedReport, null, 2)}\n`, - "utf8", - ) + renderSessionTranscript(options.sessionText, { title: 'session.jsonl' }), + 'utf8', + ); + await writeFile(artifacts.reportJson, `${JSON.stringify(persistedReport, null, 2)}\n`, 'utf8'); - return artifacts + return artifacts; } function defaultRunId(): string { - return new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-") + return new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-'); } diff --git a/src/probes/startup-oracle-script.test.ts b/src/probes/startup-oracle-script.test.ts index 8fa5e8bfb..b5ee368ac 100644 --- a/src/probes/startup-oracle-script.test.ts +++ b/src/probes/startup-oracle-script.test.ts @@ -1,18 +1,15 @@ -import { readFile } from "node:fs/promises" +import { readFile } from 'node:fs/promises'; -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -describe("startup TUI oracle script", () => { - it("asserts Brunch identity markers without promoting the host-sensitive probe into verify", async () => { - const script = await readFile( - new URL("./scripts/verify-startup-no-resume.sh", import.meta.url), - "utf8", - ) +describe('startup TUI oracle script', () => { + it('asserts Brunch identity markers without promoting the host-sensitive probe into verify', async () => { + const script = await readFile(new URL('./scripts/verify-startup-no-resume.sh', import.meta.url), 'utf8'); - expect(script).toContain("BRUNCH_WORDMARK_TOP") - expect(script).toContain("built on Pi v") - expect(script).toContain("Choose or create the spec/session") - expect(script).toContain("manual/middle-loop oracle") - expect(script).not.toContain("npm run verify") - }) -}) + expect(script).toContain('BRUNCH_WORDMARK_TOP'); + expect(script).toContain('built on Pi v'); + expect(script).toContain('Choose or create the spec/session'); + expect(script).toContain('manual/middle-loop oracle'); + expect(script).not.toContain('npm run verify'); + }); +}); diff --git a/src/probes/structured-exchange-ordering-proof.test.ts b/src/probes/structured-exchange-ordering-proof.test.ts index 0be6579ee..6236b08d7 100644 --- a/src/probes/structured-exchange-ordering-proof.test.ts +++ b/src/probes/structured-exchange-ordering-proof.test.ts @@ -1,51 +1,46 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { runStructuredExchangeOrderingProof } from "./structured-exchange-ordering-proof.js" +import { runStructuredExchangeOrderingProof } from './structured-exchange-ordering-proof.js'; -describe("structured-exchange ordering proof", () => { - it("runs same-assistant-message present_options before request_choice with sequential tools", async () => { - const proof = await runStructuredExchangeOrderingProof() +describe('structured-exchange ordering proof', () => { + it('runs same-assistant-message present_options before request_choice with sequential tools', async () => { + const proof = await runStructuredExchangeOrderingProof(); expect(proof.scenario).toMatchObject({ - mission: - "Prove same-assistant-message present/request structured-exchange ordering.", - evaluationFocus: - "Verify sequential present_options persists before request_choice opens response UI.", + mission: 'Prove same-assistant-message present/request structured-exchange ordering.', + evaluationFocus: 'Verify sequential present_options persists before request_choice opens response UI.', maxTurns: 1, - }) + }); expect(proof.verdict).toEqual({ presentResultBeforeRequestUi: true, jsonlPresentBeforeRequest: true, - }) + }); expect(proof.eventOrder).toEqual([ - "present_options:start", - "present_options:end", - "ui:select", - "request_choice:start", - "ui:input", - "request_choice:end", - ]) - expect(proof.jsonlToolResultOrder).toEqual([ - "present_options", - "request_choice", - ]) + 'present_options:start', + 'present_options:end', + 'ui:select', + 'request_choice:start', + 'ui:input', + 'request_choice:end', + ]); + expect(proof.jsonlToolResultOrder).toEqual(['present_options', 'request_choice']); expect(proof.presentDetails).toMatchObject({ - schema: "brunch.structured_exchange.present", - exchangeId: "ordering-proof", - presentTool: "present_options", - expectedRequest: { tool: "request_choice", required: true }, - }) + schema: 'brunch.structured_exchange.present', + exchangeId: 'ordering-proof', + presentTool: 'present_options', + expectedRequest: { tool: 'request_choice', required: true }, + }); expect(proof.requestDetails).toMatchObject({ - schema: "brunch.structured_exchange.request", - exchangeId: "ordering-proof", - requestTool: "request_choice", - status: "answered", + schema: 'brunch.structured_exchange.request', + exchangeId: 'ordering-proof', + requestTool: 'request_choice', + status: 'answered', respondsTo: { - exchangeId: "ordering-proof", - presentTool: "present_options", + exchangeId: 'ordering-proof', + presentTool: 'present_options', }, - choice: { id: "tui", label: "Move under src/tui-client" }, - comment: "Sequential ordering looks safe for the next parity proof.", - }) - }, 20_000) -}) + choice: { id: 'tui', label: 'Move under src/tui-client' }, + comment: 'Sequential ordering looks safe for the next parity proof.', + }); + }, 20_000); +}); diff --git a/src/probes/structured-exchange-ordering-proof.ts b/src/probes/structured-exchange-ordering-proof.ts index 99b5c3236..b7730735a 100644 --- a/src/probes/structured-exchange-ordering-proof.ts +++ b/src/probes/structured-exchange-ordering-proof.ts @@ -1,157 +1,140 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process" -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join, resolve } from "node:path" -import { fileURLToPath } from "node:url" +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { StructuredExchangePresentDetails, StructuredExchangeRequestDetails, -} from "../.pi/extensions/structured-exchange/index.js" +} from '../.pi/extensions/structured-exchange/index.js'; interface OrderingScenario { - mission: string - evaluationFocus: string - maxTurns: number + mission: string; + evaluationFocus: string; + maxTurns: number; } interface OrderingVerdict { - presentResultBeforeRequestUi: boolean - jsonlPresentBeforeRequest: boolean + presentResultBeforeRequestUi: boolean; + jsonlPresentBeforeRequest: boolean; } interface ToolResultRecord { - toolName: string - details: unknown + toolName: string; + details: unknown; } export interface StructuredExchangeOrderingProofResult { - scenario: OrderingScenario - verdict: OrderingVerdict - eventOrder: string[] - jsonlToolResultOrder: string[] - presentDetails: StructuredExchangePresentDetails - requestDetails: StructuredExchangeRequestDetails - sessionFile: string - stdout: unknown[] + scenario: OrderingScenario; + verdict: OrderingVerdict; + eventOrder: string[]; + jsonlToolResultOrder: string[]; + presentDetails: StructuredExchangePresentDetails; + requestDetails: StructuredExchangeRequestDetails; + sessionFile: string; + stdout: unknown[]; } interface StructuredExchangeOrderingProofOptions { - cwd?: string - timeoutMs?: number + cwd?: string; + timeoutMs?: number; } const scenario: OrderingScenario = { - mission: - "Prove same-assistant-message present/request structured-exchange ordering.", - evaluationFocus: - "Verify sequential present_options persists before request_choice opens response UI.", + mission: 'Prove same-assistant-message present/request structured-exchange ordering.', + evaluationFocus: 'Verify sequential present_options persists before request_choice opens response UI.', maxTurns: 1, -} +}; export async function runStructuredExchangeOrderingProof( options: StructuredExchangeOrderingProofOptions = {}, ): Promise { - const cwd = - options.cwd ?? (await mkdtemp(join(tmpdir(), "brunch-exchange-ordering-"))) - const timeoutMs = options.timeoutMs ?? 10_000 - const extensionPath = await writeOrderingExtension(cwd) - const sessionDir = join(cwd, ".brunch", "sessions") - await mkdir(sessionDir, { recursive: true }) + const cwd = options.cwd ?? (await mkdtemp(join(tmpdir(), 'brunch-exchange-ordering-'))); + const timeoutMs = options.timeoutMs ?? 10_000; + const extensionPath = await writeOrderingExtension(cwd); + const sessionDir = join(cwd, '.brunch', 'sessions'); + await mkdir(sessionDir, { recursive: true }); const child = spawn( process.execPath, [ piCliPath(), - "--mode", - "rpc", - "--no-extensions", - "--no-builtin-tools", - "--extension", + '--mode', + 'rpc', + '--no-extensions', + '--no-builtin-tools', + '--extension', extensionPath, - "--session-dir", + '--session-dir', sessionDir, ], { cwd, - stdio: ["pipe", "pipe", "pipe"], - env: { ...process.env, NO_COLOR: "1", PI_OFFLINE: "1" }, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, NO_COLOR: '1', PI_OFFLINE: '1' }, }, - ) + ); - const client = new RpcProbeClient(child, timeoutMs) + const client = new RpcProbeClient(child, timeoutMs); try { const promptAccepted = client.waitFor( - (event): event is RpcResponse => - isRpcResponse(event) && event.command === "prompt", - ) + (event): event is RpcResponse => isRpcResponse(event) && event.command === 'prompt', + ); child.stdin.write( - `${JSON.stringify({ id: "ordering", type: "prompt", message: "/brunch-structured-exchange-ordering-proof" })}\n`, - ) + `${JSON.stringify({ id: 'ordering', type: 'prompt', message: '/brunch-structured-exchange-ordering-proof' })}\n`, + ); const selectRequest = await client.waitFor( - (event): event is ExtensionUiRequest => - isExtensionUiRequest(event) && event.method === "select", - ) + (event): event is ExtensionUiRequest => isExtensionUiRequest(event) && event.method === 'select', + ); child.stdin.write( - `${JSON.stringify({ type: "extension_ui_response", id: selectRequest.id, value: "Move under src/tui-client" })}\n`, - ) + `${JSON.stringify({ type: 'extension_ui_response', id: selectRequest.id, value: 'Move under src/tui-client' })}\n`, + ); const inputRequest = await client.waitFor( - (event): event is ExtensionUiRequest => - isExtensionUiRequest(event) && event.method === "input", - ) + (event): event is ExtensionUiRequest => isExtensionUiRequest(event) && event.method === 'input', + ); child.stdin.write( - `${JSON.stringify({ type: "extension_ui_response", id: inputRequest.id, value: "Sequential ordering looks safe for the next parity proof." })}\n`, - ) + `${JSON.stringify({ type: 'extension_ui_response', id: inputRequest.id, value: 'Sequential ordering looks safe for the next parity proof.' })}\n`, + ); - const promptResponse = await promptAccepted + const promptResponse = await promptAccepted; if (!promptResponse.success) { - throw new Error( - `Ordering proof prompt failed: ${promptResponse.error ?? "unknown error"}`, - ) + throw new Error(`Ordering proof prompt failed: ${promptResponse.error ?? 'unknown error'}`); } const stateResponse = client.waitFor( - (event): event is RpcResponse<{ sessionFile?: string }> => - isRpcResponse(event) && event.id === "state", - ) - child.stdin.write(`${JSON.stringify({ id: "state", type: "get_state" })}\n`) - const state = await stateResponse - const sessionFile = state.data?.sessionFile - if (!state.success || typeof sessionFile !== "string") { - throw new Error("Ordering proof did not expose a persisted session file") + (event): event is RpcResponse<{ sessionFile?: string }> => isRpcResponse(event) && event.id === 'state', + ); + child.stdin.write(`${JSON.stringify({ id: 'state', type: 'get_state' })}\n`); + const state = await stateResponse; + const sessionFile = state.data?.sessionFile; + if (!state.success || typeof sessionFile !== 'string') { + throw new Error('Ordering proof did not expose a persisted session file'); } - const toolResults = await readToolResults(sessionFile) - const present = toolResults.find( - (result) => result.toolName === "present_options", - ) - const request = toolResults.find( - (result) => result.toolName === "request_choice", - ) + const toolResults = await readToolResults(sessionFile); + const present = toolResults.find((result) => result.toolName === 'present_options'); + const request = toolResults.find((result) => result.toolName === 'request_choice'); if (!present || !request) { - throw new Error("Ordering proof did not persist both tool results") + throw new Error('Ordering proof did not persist both tool results'); } - const eventOrder = orderingEvents(client.events) - const jsonlToolResultOrder = toolResults.map((result) => result.toolName) - const presentIndex = eventOrder.indexOf("present_options:end") - const requestUiIndex = eventOrder.indexOf("ui:select") - const jsonlPresentIndex = jsonlToolResultOrder.indexOf("present_options") - const jsonlRequestIndex = jsonlToolResultOrder.indexOf("request_choice") + const eventOrder = orderingEvents(client.events); + const jsonlToolResultOrder = toolResults.map((result) => result.toolName); + const presentIndex = eventOrder.indexOf('present_options:end'); + const requestUiIndex = eventOrder.indexOf('ui:select'); + const jsonlPresentIndex = jsonlToolResultOrder.indexOf('present_options'); + const jsonlRequestIndex = jsonlToolResultOrder.indexOf('request_choice'); return { scenario, verdict: { presentResultBeforeRequestUi: - presentIndex !== -1 && - requestUiIndex !== -1 && - presentIndex < requestUiIndex, + presentIndex !== -1 && requestUiIndex !== -1 && presentIndex < requestUiIndex, jsonlPresentBeforeRequest: - jsonlPresentIndex !== -1 && - jsonlRequestIndex !== -1 && - jsonlPresentIndex < jsonlRequestIndex, + jsonlPresentIndex !== -1 && jsonlRequestIndex !== -1 && jsonlPresentIndex < jsonlRequestIndex, }, eventOrder, jsonlToolResultOrder, @@ -159,15 +142,15 @@ export async function runStructuredExchangeOrderingProof( requestDetails: request.details as StructuredExchangeRequestDetails, sessionFile, stdout: client.events, - } + }; } finally { - client.dispose() + client.dispose(); } } async function writeOrderingExtension(cwd: string): Promise { - const extensionPath = join(cwd, "structured-exchange-ordering-extension.ts") - const adapterPath = resolve("src/.pi/extensions/structured-exchange/index.ts") + const extensionPath = join(cwd, 'structured-exchange-ordering-extension.ts'); + const adapterPath = resolve('src/.pi/extensions/structured-exchange/index.ts'); const content = ` import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { @@ -236,164 +219,151 @@ async function writeOrderingExtension(cwd: string): Promise { }, }) } - ` - await writeFile(extensionPath, content, "utf8") - return extensionPath + `; + await writeFile(extensionPath, content, 'utf8'); + return extensionPath; } function orderingEvents(events: readonly unknown[]): string[] { return events.flatMap((event) => { - if (!isRecord(event)) return [] - if (event.type === "tool_execution_start") { - return [`${String(event.toolName)}:start`] + if (!isRecord(event)) return []; + if (event.type === 'tool_execution_start') { + return [`${String(event.toolName)}:start`]; } - if (event.type === "tool_execution_end") { - return [`${String(event.toolName)}:end`] + if (event.type === 'tool_execution_end') { + return [`${String(event.toolName)}:end`]; } - if (event.type === "extension_ui_request") { - return [`ui:${String(event.method)}`] + if (event.type === 'extension_ui_request') { + return [`ui:${String(event.method)}`]; } - return [] - }) + return []; + }); } -async function readToolResults( - sessionFile: string, -): Promise { - const entries = (await readFile(sessionFile, "utf8")) - .split("\n") +async function readToolResults(sessionFile: string): Promise { + const entries = (await readFile(sessionFile, 'utf8')) + .split('\n') .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as unknown) + .map((line) => JSON.parse(line) as unknown); return entries.flatMap((entry) => { - if (!isRecord(entry) || entry.type !== "message") return [] - const message = entry.message - if (!isRecord(message) || message.role !== "toolResult") return [] - if ( - message.toolName !== "present_options" && - message.toolName !== "request_choice" - ) { - return [] + if (!isRecord(entry) || entry.type !== 'message') return []; + const message = entry.message; + if (!isRecord(message) || message.role !== 'toolResult') return []; + if (message.toolName !== 'present_options' && message.toolName !== 'request_choice') { + return []; } - return [{ toolName: message.toolName, details: message.details }] - }) + return [{ toolName: message.toolName, details: message.details }]; + }); } function piCliPath(): string { return fileURLToPath( - new URL( - "../../node_modules/@earendil-works/pi-coding-agent/dist/cli.js", - import.meta.url, - ), - ) + new URL('../../node_modules/@earendil-works/pi-coding-agent/dist/cli.js', import.meta.url), + ); } interface RpcResponse { - type: "response" - id?: string - command: string - success: boolean - data?: T - error?: string + type: 'response'; + id?: string; + command: string; + success: boolean; + data?: T; + error?: string; } interface ExtensionUiRequest { - type: "extension_ui_request" - id: string - method: string + type: 'extension_ui_request'; + id: string; + method: string; } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === 'object' && value !== null; } function isRpcResponse(value: unknown): value is RpcResponse { return ( isRecord(value) && - value.type === "response" && - typeof value.command === "string" && - typeof value.success === "boolean" - ) + value.type === 'response' && + typeof value.command === 'string' && + typeof value.success === 'boolean' + ); } function isExtensionUiRequest(value: unknown): value is ExtensionUiRequest { return ( isRecord(value) && - value.type === "extension_ui_request" && - typeof value.id === "string" && - typeof value.method === "string" - ) + value.type === 'extension_ui_request' && + typeof value.id === 'string' && + typeof value.method === 'string' + ); } class RpcProbeClient { - readonly events: unknown[] = [] - readonly #child: ChildProcessWithoutNullStreams - readonly #timeoutMs: number - #stdout = "" - #stderr = "" + readonly events: unknown[] = []; + readonly #child: ChildProcessWithoutNullStreams; + readonly #timeoutMs: number; + #stdout = ''; + #stderr = ''; #waiters: Array<{ - predicate: (event: unknown) => boolean - resolve: (event: unknown) => void - }> = [] + predicate: (event: unknown) => boolean; + resolve: (event: unknown) => void; + }> = []; constructor(child: ChildProcessWithoutNullStreams, timeoutMs: number) { - this.#child = child - this.#timeoutMs = timeoutMs - child.stdout.on("data", (chunk) => this.#ingestStdout(String(chunk))) - child.stderr.on("data", (chunk) => { - this.#stderr += String(chunk) - }) + this.#child = child; + this.#timeoutMs = timeoutMs; + child.stdout.on('data', (chunk) => this.#ingestStdout(String(chunk))); + child.stderr.on('data', (chunk) => { + this.#stderr += String(chunk); + }); } - waitFor(predicate: (event: unknown) => event is T): Promise { - const existing = this.events.find(predicate) - if (existing) return Promise.resolve(existing) + waitFor(predicate: (event: unknown) => event is T): Promise { + const existing = this.events.find(predicate); + if (existing) return Promise.resolve(existing); return new Promise((resolve, reject) => { - const timeout = setTimeout( - () => { - reject( - new Error( - `Timed out waiting for ordering proof event. Events:\n${JSON.stringify(this.events, null, 2)}\nStderr:\n${this.#stderr}`, - ), - ) - }, - this.#timeoutMs, - ) + const timeout = setTimeout(() => { + reject( + new Error( + `Timed out waiting for ordering proof event. Events:\n${JSON.stringify(this.events, null, 2)}\nStderr:\n${this.#stderr}`, + ), + ); + }, this.#timeoutMs); this.#waiters.push({ predicate, resolve: (event) => { - clearTimeout(timeout) - resolve(event as T) + clearTimeout(timeout); + resolve(event as T); }, - }) - }) + }); + }); } dispose(): void { - this.#child.kill("SIGTERM") + this.#child.kill('SIGTERM'); } #ingestStdout(chunk: string): void { - this.#stdout += chunk + this.#stdout += chunk; for (;;) { - const newline = this.#stdout.indexOf("\n") - if (newline === -1) return - const line = this.#stdout.slice(0, newline).trim() - this.#stdout = this.#stdout.slice(newline + 1) - if (line.length === 0) continue - let parsed: unknown + const newline = this.#stdout.indexOf('\n'); + if (newline === -1) return; + const line = this.#stdout.slice(0, newline).trim(); + this.#stdout = this.#stdout.slice(newline + 1); + if (line.length === 0) continue; + let parsed: unknown; try { - parsed = JSON.parse(line) + parsed = JSON.parse(line); } catch { - continue + continue; } - this.events.push(parsed) + this.events.push(parsed); for (const waiter of this.#waiters) { if (waiter.predicate(parsed)) { - this.#waiters = this.#waiters.filter( - (candidate) => candidate !== waiter, - ) - waiter.resolve(parsed) + this.#waiters = this.#waiters.filter((candidate) => candidate !== waiter); + waiter.resolve(parsed); } } } diff --git a/src/probes/structured-exchange-rpc-proof.test.ts b/src/probes/structured-exchange-rpc-proof.test.ts index 0cb9d385a..378f991df 100644 --- a/src/probes/structured-exchange-rpc-proof.test.ts +++ b/src/probes/structured-exchange-rpc-proof.test.ts @@ -1,62 +1,62 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { runStructuredExchangeRpcProof } from "./structured-exchange-rpc-proof.js" +import { runStructuredExchangeRpcProof } from './structured-exchange-rpc-proof.js'; -describe("structured-exchange RPC proof", () => { - it("round-trips option answers and notes through Pi RPC editor fallback", async () => { - const proof = await runStructuredExchangeRpcProof() +describe('structured-exchange RPC proof', () => { + it('round-trips option answers and notes through Pi RPC editor fallback', async () => { + const proof = await runStructuredExchangeRpcProof(); expect(proof.scenario).toMatchObject({ - mission: expect.stringContaining("option-based structured exchange"), - evaluationFocus: expect.stringContaining("optional note"), + mission: expect.stringContaining('option-based structured exchange'), + evaluationFocus: expect.stringContaining('optional note'), maxTurns: 1, - }) + }); expect(proof.editorRequest).toMatchObject({ - type: "extension_ui_request", - method: "editor", - title: "Answer structured exchange as JSON", - }) - expect(JSON.parse(proof.editorRequest.prefill ?? "{}")).toMatchObject({ - schema: "brunch.structured_exchange.editor", + type: 'extension_ui_request', + method: 'editor', + title: 'Answer structured exchange as JSON', + }); + expect(JSON.parse(proof.editorRequest.prefill ?? '{}')).toMatchObject({ + schema: 'brunch.structured_exchange.editor', schemaVersion: 1, - question: "Which implementation path should the evaluator choose?", - mode: "multi-select", + question: 'Which implementation path should the evaluator choose?', + mode: 'multi-select', options: [ - { index: 1, label: "Ship RPC fallback", value: "rpc-fallback" }, - { index: 2, label: "Wait for web relay", value: "wait-web" }, - { index: 3, label: "Escalate blocker", value: "blocker" }, + { index: 1, label: 'Ship RPC fallback', value: 'rpc-fallback' }, + { index: 2, label: 'Wait for web relay', value: 'wait-web' }, + { index: 3, label: 'Escalate blocker', value: 'blocker' }, ], - }) + }); expect(proof.terminalDetails).toMatchObject({ - status: "answered", - mode: "multi-select", - question: "Which implementation path should the evaluator choose?", - context: "Scenario: prove option answers plus notes over Pi RPC.", + status: 'answered', + mode: 'multi-select', + question: 'Which implementation path should the evaluator choose?', + context: 'Scenario: prove option answers plus notes over Pi RPC.', options: [ - { label: "Ship RPC fallback", value: "rpc-fallback" }, - { label: "Wait for web relay", value: "wait-web" }, - { label: "Escalate blocker", value: "blocker" }, + { label: 'Ship RPC fallback', value: 'rpc-fallback' }, + { label: 'Wait for web relay', value: 'wait-web' }, + { label: 'Escalate blocker', value: 'blocker' }, ], answers: [ { - type: "option", - label: "Ship RPC fallback", - value: "rpc-fallback", + type: 'option', + label: 'Ship RPC fallback', + value: 'rpc-fallback', index: 1, }, ], rejectedOptions: [ - { label: "Wait for web relay", value: "wait-web" }, - { label: "Escalate blocker", value: "blocker" }, + { label: 'Wait for web relay', value: 'wait-web' }, + { label: 'Escalate blocker', value: 'blocker' }, ], - note: "Proceed, but report any relay friction separately.", - transport: { surface: "rpc-editor" }, + note: 'Proceed, but report any relay friction separately.', + transport: { surface: 'rpc-editor' }, probe: { - name: "structured-exchange-rpc-proof", - transport: "pi-rpc-editor", + name: 'structured-exchange-rpc-proof', + transport: 'pi-rpc-editor', }, frictionReport: { blockers: [], frictions: [] }, - }) - expect(proof.sessionFile).toContain(".brunch/sessions") - }, 20_000) -}) + }); + expect(proof.sessionFile).toContain('.brunch/sessions'); + }, 20_000); +}); diff --git a/src/probes/structured-exchange-rpc-proof.ts b/src/probes/structured-exchange-rpc-proof.ts index 965a2e26b..90c2a1c39 100644 --- a/src/probes/structured-exchange-rpc-proof.ts +++ b/src/probes/structured-exchange-rpc-proof.ts @@ -1,131 +1,124 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process" -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join, resolve } from "node:path" -import { fileURLToPath } from "node:url" +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import type { StructuredExchangeToolResultDetails } from "../.pi/extensions/structured-exchange/index.js" +import type { StructuredExchangeToolResultDetails } from '../.pi/extensions/structured-exchange/index.js'; interface ProbeMetadata { - name: string - transport: "pi-rpc-editor" + name: string; + transport: 'pi-rpc-editor'; } interface FrictionReport { - blockers: string[] - frictions: string[] + blockers: string[]; + frictions: string[]; } interface TerminalDetails extends StructuredExchangeToolResultDetails { - probe: ProbeMetadata - frictionReport: FrictionReport + probe: ProbeMetadata; + frictionReport: FrictionReport; } interface ProofResultEntry { - customType: string - data: unknown + customType: string; + data: unknown; } export interface StructuredExchangeRpcProofResult { scenario: { - mission: string - evaluationFocus: string - maxTurns: number - } + mission: string; + evaluationFocus: string; + maxTurns: number; + }; editorRequest: { - type: "extension_ui_request" - id: string - method: "editor" - title?: string - prefill?: string - } - terminalDetails: TerminalDetails - sessionFile: string - stdout: unknown[] + type: 'extension_ui_request'; + id: string; + method: 'editor'; + title?: string; + prefill?: string; + }; + terminalDetails: TerminalDetails; + sessionFile: string; + stdout: unknown[]; } interface StructuredExchangeRpcProofOptions { - cwd?: string - timeoutMs?: number + cwd?: string; + timeoutMs?: number; } -const PROOF_CUSTOM_TYPE = "brunch.structured_exchange_rpc_proof_result" +const PROOF_CUSTOM_TYPE = 'brunch.structured_exchange_rpc_proof_result'; const scenario = { - mission: - "Complete an option-based structured exchange as an agent-as-user evaluator.", + mission: 'Complete an option-based structured exchange as an agent-as-user evaluator.', evaluationFocus: - "Verify that selected option answers and an optional note survive the Pi RPC editor fallback as structured terminal details.", + 'Verify that selected option answers and an optional note survive the Pi RPC editor fallback as structured terminal details.', maxTurns: 1, -} +}; export async function runStructuredExchangeRpcProof( options: StructuredExchangeRpcProofOptions = {}, ): Promise { - const cwd = - options.cwd ?? (await mkdtemp(join(tmpdir(), "brunch-exchange-rpc-proof-"))) - const timeoutMs = options.timeoutMs ?? 10_000 - const extensionPath = await writeProofExtension(cwd) - const sessionDir = join(cwd, ".brunch", "sessions") - await mkdir(sessionDir, { recursive: true }) + const cwd = options.cwd ?? (await mkdtemp(join(tmpdir(), 'brunch-exchange-rpc-proof-'))); + const timeoutMs = options.timeoutMs ?? 10_000; + const extensionPath = await writeProofExtension(cwd); + const sessionDir = join(cwd, '.brunch', 'sessions'); + await mkdir(sessionDir, { recursive: true }); const child = spawn( process.execPath, [ piCliPath(), - "--mode", - "rpc", - "--no-extensions", - "--extension", + '--mode', + 'rpc', + '--no-extensions', + '--extension', extensionPath, - "--session-dir", + '--session-dir', sessionDir, ], { cwd, - stdio: ["pipe", "pipe", "pipe"], - env: { ...process.env, NO_COLOR: "1" }, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, NO_COLOR: '1' }, }, - ) + ); - const client = new RpcProbeClient(child, timeoutMs) + const client = new RpcProbeClient(child, timeoutMs); try { const promptAccepted = client.waitFor( - (event): event is RpcResponse => - isRpcResponse(event) && event.command === "prompt", - ) + (event): event is RpcResponse => isRpcResponse(event) && event.command === 'prompt', + ); child.stdin.write( - `${JSON.stringify({ id: "proof", type: "prompt", message: "/brunch-structured-exchange-rpc-proof" })}\n`, - ) + `${JSON.stringify({ id: 'proof', type: 'prompt', message: '/brunch-structured-exchange-rpc-proof' })}\n`, + ); const editorRequest = await client.waitFor( - (event): event is StructuredExchangeRpcProofResult["editorRequest"] => - isEditorRequest(event), - ) + (event): event is StructuredExchangeRpcProofResult['editorRequest'] => isEditorRequest(event), + ); child.stdin.write( `${JSON.stringify({ - type: "extension_ui_response", + type: 'extension_ui_response', id: editorRequest.id, value: answeredEditorPayload(editorRequest.prefill), })}\n`, - ) + ); - const promptResponse = await promptAccepted + const promptResponse = await promptAccepted; if (!promptResponse.success) { - throw new Error( - `Proof command failed: ${promptResponse.error ?? "unknown error"}`, - ) + throw new Error(`Proof command failed: ${promptResponse.error ?? 'unknown error'}`); } const stateResponse = client.waitFor( - (event): event is RpcResponse<{ sessionFile?: string }> => - isRpcResponse(event) && event.id === "state", - ) - child.stdin.write(`${JSON.stringify({ id: "state", type: "get_state" })}\n`) - const state = await stateResponse - const sessionFile = state.data?.sessionFile - if (!state.success || typeof sessionFile !== "string") { - throw new Error("RPC proof did not expose a persisted session file") + (event): event is RpcResponse<{ sessionFile?: string }> => isRpcResponse(event) && event.id === 'state', + ); + child.stdin.write(`${JSON.stringify({ id: 'state', type: 'get_state' })}\n`); + const state = await stateResponse; + const sessionFile = state.data?.sessionFile; + if (!state.success || typeof sessionFile !== 'string') { + throw new Error('RPC proof did not expose a persisted session file'); } return { @@ -134,15 +127,15 @@ export async function runStructuredExchangeRpcProof( terminalDetails: await readProofDetails(sessionFile), sessionFile, stdout: client.events, - } + }; } finally { - client.dispose() + client.dispose(); } } async function writeProofExtension(cwd: string): Promise { - const extensionPath = join(cwd, "structured-exchange-rpc-proof-extension.ts") - const adapterPath = resolve("src/.pi/extensions/structured-exchange/index.ts") + const extensionPath = join(cwd, 'structured-exchange-rpc-proof-extension.ts'); + const adapterPath = resolve('src/.pi/extensions/structured-exchange/index.ts'); const content = ` import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { @@ -197,160 +190,146 @@ async function writeProofExtension(cwd: string): Promise { }, }) } - ` - await writeFile(extensionPath, content, "utf8") - return extensionPath + `; + await writeFile(extensionPath, content, 'utf8'); + return extensionPath; } function answeredEditorPayload(prefill: string | undefined): string { - if (!prefill) throw new Error("RPC editor request did not include a prefill") - const payload = JSON.parse(prefill) as { response?: unknown } + if (!prefill) throw new Error('RPC editor request did not include a prefill'); + const payload = JSON.parse(prefill) as { response?: unknown }; payload.response = { - status: "answered", + status: 'answered', answers: [ { - type: "option", - label: "Ship RPC fallback", - value: "rpc-fallback", + type: 'option', + label: 'Ship RPC fallback', + value: 'rpc-fallback', index: 1, }, ], - note: "Proceed, but report any relay friction separately.", - } - return `${JSON.stringify(payload, null, 2)}\n` + note: 'Proceed, but report any relay friction separately.', + }; + return `${JSON.stringify(payload, null, 2)}\n`; } async function readProofDetails( sessionFile: string, -): Promise { - const entries = (await readFile(sessionFile, "utf8")) - .split("\n") +): Promise { + const entries = (await readFile(sessionFile, 'utf8')) + .split('\n') .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as unknown) + .map((line) => JSON.parse(line) as unknown); const proofEntry = entries.find( (entry): entry is ProofResultEntry => - typeof entry === "object" && + typeof entry === 'object' && entry !== null && (entry as { customType?: unknown }).customType === PROOF_CUSTOM_TYPE && - "data" in entry, - ) + 'data' in entry, + ); if (!proofEntry) { - throw new Error("RPC proof result entry was not written to the session") + throw new Error('RPC proof result entry was not written to the session'); } - return proofEntry.data as StructuredExchangeRpcProofResult["terminalDetails"] + return proofEntry.data as StructuredExchangeRpcProofResult['terminalDetails']; } function piCliPath(): string { return fileURLToPath( - new URL( - "../../node_modules/@earendil-works/pi-coding-agent/dist/cli.js", - import.meta.url, - ), - ) + new URL('../../node_modules/@earendil-works/pi-coding-agent/dist/cli.js', import.meta.url), + ); } interface RpcResponse { - type: "response" - id?: string - command: string - success: boolean - data?: T - error?: string + type: 'response'; + id?: string; + command: string; + success: boolean; + data?: T; + error?: string; } function isRpcResponse(value: unknown): value is RpcResponse { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type === "response" && - typeof (value as { command?: unknown }).command === "string" && - typeof (value as { success?: unknown }).success === "boolean" - ) + (value as { type?: unknown }).type === 'response' && + typeof (value as { command?: unknown }).command === 'string' && + typeof (value as { success?: unknown }).success === 'boolean' + ); } -function isEditorRequest( - value: unknown, -): value is StructuredExchangeRpcProofResult["editorRequest"] { +function isEditorRequest(value: unknown): value is StructuredExchangeRpcProofResult['editorRequest'] { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type === "extension_ui_request" && - typeof (value as { id?: unknown }).id === "string" && - (value as { method?: unknown }).method === "editor" - ) + (value as { type?: unknown }).type === 'extension_ui_request' && + typeof (value as { id?: unknown }).id === 'string' && + (value as { method?: unknown }).method === 'editor' + ); } class RpcProbeClient { - readonly events: unknown[] = [] - readonly #child: ChildProcessWithoutNullStreams - readonly #timeoutMs: number - #stdout = "" - #stderr = "" + readonly events: unknown[] = []; + readonly #child: ChildProcessWithoutNullStreams; + readonly #timeoutMs: number; + #stdout = ''; + #stderr = ''; #waiters: Array<{ - predicate: (event: unknown) => boolean - resolve: (event: unknown) => void - }> = [] + predicate: (event: unknown) => boolean; + resolve: (event: unknown) => void; + }> = []; constructor(child: ChildProcessWithoutNullStreams, timeoutMs: number) { - this.#child = child - this.#timeoutMs = timeoutMs - child.stdout.on("data", (chunk) => this.#ingestStdout(String(chunk))) - child.stderr.on("data", (chunk) => { - this.#stderr += String(chunk) - }) + this.#child = child; + this.#timeoutMs = timeoutMs; + child.stdout.on('data', (chunk) => this.#ingestStdout(String(chunk))); + child.stderr.on('data', (chunk) => { + this.#stderr += String(chunk); + }); } - waitFor(predicate: (event: unknown) => event is T): Promise { - const existing = this.events.find(predicate) - if (existing) return Promise.resolve(existing) + waitFor(predicate: (event: unknown) => event is T): Promise { + const existing = this.events.find(predicate); + if (existing) return Promise.resolve(existing); return new Promise((resolve, reject) => { - const timeout = setTimeout( - () => { - reject( - new Error( - `Timed out waiting for RPC proof event. Stderr:\n${this.#stderr}`, - ), - ) - }, - this.#timeoutMs, - ) + const timeout = setTimeout(() => { + reject(new Error(`Timed out waiting for RPC proof event. Stderr:\n${this.#stderr}`)); + }, this.#timeoutMs); this.#waiters.push({ predicate, resolve: (event) => { - clearTimeout(timeout) - resolve(event as T) + clearTimeout(timeout); + resolve(event as T); }, - }) - }) + }); + }); } dispose(): void { - this.#child.kill("SIGTERM") + this.#child.kill('SIGTERM'); } #ingestStdout(chunk: string): void { - this.#stdout += chunk + this.#stdout += chunk; while (true) { - const newline = this.#stdout.indexOf("\n") - if (newline === -1) return - const line = this.#stdout.slice(0, newline).replace(/\r$/, "") - this.#stdout = this.#stdout.slice(newline + 1) - if (line.trim().length === 0) continue - let event: unknown + const newline = this.#stdout.indexOf('\n'); + if (newline === -1) return; + const line = this.#stdout.slice(0, newline).replace(/\r$/, ''); + this.#stdout = this.#stdout.slice(newline + 1); + if (line.trim().length === 0) continue; + let event: unknown; try { - event = JSON.parse(line) + event = JSON.parse(line); } catch { - continue + continue; } - this.events.push(event) - const waiters = this.#waiters.slice() + this.events.push(event); + const waiters = this.#waiters.slice(); for (const waiter of waiters) { - if (!waiter.predicate(event)) continue - this.#waiters = this.#waiters.filter( - (candidate) => candidate !== waiter, - ) - waiter.resolve(event) + if (!waiter.predicate(event)) continue; + this.#waiters = this.#waiters.filter((candidate) => candidate !== waiter); + waiter.resolve(event); } } } diff --git a/src/project-identity.test.ts b/src/project-identity.test.ts index 169bdd690..a2eb765f6 100644 --- a/src/project-identity.test.ts +++ b/src/project-identity.test.ts @@ -1,147 +1,133 @@ -import { mkdtemp, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" -import { afterEach, beforeEach, describe, expect, it } from "vitest" +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { discoverProjectIdentity, slugify } from "./project-identity.js" +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -describe("slugify", () => { - it("lowercases and collapses non-alphanumeric runs to single dashes", () => { - expect(slugify("Acme Control Plane")).toBe("acme-control-plane") - expect(slugify("Foo___Bar Baz!!")).toBe("foo-bar-baz") - }) +import { discoverProjectIdentity, slugify } from './project-identity.js'; - it("strips leading and trailing dashes", () => { - expect(slugify("---wrap-around---")).toBe("wrap-around") - }) +describe('slugify', () => { + it('lowercases and collapses non-alphanumeric runs to single dashes', () => { + expect(slugify('Acme Control Plane')).toBe('acme-control-plane'); + expect(slugify('Foo___Bar Baz!!')).toBe('foo-bar-baz'); + }); - it("handles scoped npm package names", () => { - expect(slugify("@hashintel/brunch")).toBe("hashintel-brunch") - }) + it('strips leading and trailing dashes', () => { + expect(slugify('---wrap-around---')).toBe('wrap-around'); + }); + + it('handles scoped npm package names', () => { + expect(slugify('@hashintel/brunch')).toBe('hashintel-brunch'); + }); it("returns 'project' for inputs with no alphanumerics", () => { - expect(slugify("!!!")).toBe("project") - expect(slugify("")).toBe("project") - }) -}) + expect(slugify('!!!')).toBe('project'); + expect(slugify('')).toBe('project'); + }); +}); -describe("discoverProjectIdentity", () => { - let dir: string +describe('discoverProjectIdentity', () => { + let dir: string; beforeEach(async () => { - dir = await mkdtemp(join(tmpdir(), "brunch-project-identity-")) - }) + dir = await mkdtemp(join(tmpdir(), 'brunch-project-identity-')); + }); afterEach(() => { // Temp dirs are reaped by the OS; leaving them is acceptable for tests. - }) + }); - it("prefers package.json over every other signal", async () => { - await writeFile( - join(dir, "package.json"), - JSON.stringify({ name: "@hashintel/brunch" }), - ) - await writeFile( - join(dir, "pyproject.toml"), - '[project]\nname = "pythonic"\n', - ) - await writeFile(join(dir, "Cargo.toml"), '[package]\nname = "rusty"\n') - await writeFile(join(dir, "go.mod"), "module example.com/golang\n") + it('prefers package.json over every other signal', async () => { + await writeFile(join(dir, 'package.json'), JSON.stringify({ name: '@hashintel/brunch' })); + await writeFile(join(dir, 'pyproject.toml'), '[project]\nname = "pythonic"\n'); + await writeFile(join(dir, 'Cargo.toml'), '[package]\nname = "rusty"\n'); + await writeFile(join(dir, 'go.mod'), 'module example.com/golang\n'); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); expect(identity).toEqual({ - name: "@hashintel/brunch", - slug: "hashintel-brunch", - source: "package.json", - }) - }) + name: '@hashintel/brunch', + slug: 'hashintel-brunch', + source: 'package.json', + }); + }); - it("reads pyproject.toml [project].name when package.json is absent", async () => { + it('reads pyproject.toml [project].name when package.json is absent', async () => { await writeFile( - join(dir, "pyproject.toml"), + join(dir, 'pyproject.toml'), '# comment\n[build-system]\nrequires = ["hatch"]\n\n[project]\nname = "snake_case_app"\nversion = "0.1.0"\n', - ) + ); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); expect(identity).toEqual({ - name: "snake_case_app", - slug: "snake-case-app", - source: "pyproject.toml", - }) - }) + name: 'snake_case_app', + slug: 'snake-case-app', + source: 'pyproject.toml', + }); + }); - it("falls back to [tool.poetry].name in pyproject.toml", async () => { - await writeFile( - join(dir, "pyproject.toml"), - '[tool.poetry]\nname = "poetry-app"\n', - ) + it('falls back to [tool.poetry].name in pyproject.toml', async () => { + await writeFile(join(dir, 'pyproject.toml'), '[tool.poetry]\nname = "poetry-app"\n'); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); - expect(identity.name).toBe("poetry-app") - expect(identity.source).toBe("pyproject.toml") - }) + expect(identity.name).toBe('poetry-app'); + expect(identity.source).toBe('pyproject.toml'); + }); - it("reads Cargo.toml [package].name", async () => { + it('reads Cargo.toml [package].name', async () => { await writeFile( - join(dir, "Cargo.toml"), + join(dir, 'Cargo.toml'), '[package]\nname = "rustacean"\nversion = "0.1.0"\nedition = "2021"\n', - ) + ); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); expect(identity).toEqual({ - name: "rustacean", - slug: "rustacean", - source: "cargo.toml", - }) - }) + name: 'rustacean', + slug: 'rustacean', + source: 'cargo.toml', + }); + }); - it("uses the final segment of the module path in go.mod", async () => { - await writeFile( - join(dir, "go.mod"), - "module github.com/hashintel/widget-service\n\ngo 1.22\n", - ) + it('uses the final segment of the module path in go.mod', async () => { + await writeFile(join(dir, 'go.mod'), 'module github.com/hashintel/widget-service\n\ngo 1.22\n'); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); expect(identity).toEqual({ - name: "widget-service", - slug: "widget-service", - source: "go.mod", - }) - }) + name: 'widget-service', + slug: 'widget-service', + source: 'go.mod', + }); + }); - it("falls back to the directory basename when no manifest is present", async () => { - const identity = await discoverProjectIdentity(dir) + it('falls back to the directory basename when no manifest is present', async () => { + const identity = await discoverProjectIdentity(dir); - expect(identity.source).toBe("directory") - expect(identity.name).toBe(dir.split("/").pop()) - expect(identity.slug.length).toBeGreaterThan(0) - }) + expect(identity.source).toBe('directory'); + expect(identity.name).toBe(dir.split('/').pop()); + expect(identity.slug.length).toBeGreaterThan(0); + }); - it("falls back past a malformed package.json to the next signal", async () => { - await writeFile(join(dir, "package.json"), "{ this is not json") - await writeFile(join(dir, "Cargo.toml"), '[package]\nname = "rusty"\n') + it('falls back past a malformed package.json to the next signal', async () => { + await writeFile(join(dir, 'package.json'), '{ this is not json'); + await writeFile(join(dir, 'Cargo.toml'), '[package]\nname = "rusty"\n'); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); - expect(identity.name).toBe("rusty") - expect(identity.source).toBe("cargo.toml") - }) + expect(identity.name).toBe('rusty'); + expect(identity.source).toBe('cargo.toml'); + }); - it("ignores package.json with a missing or empty name field", async () => { - await writeFile( - join(dir, "package.json"), - JSON.stringify({ version: "1.0.0" }), - ) - await writeFile(join(dir, "go.mod"), "module example.com/fallback\n") + it('ignores package.json with a missing or empty name field', async () => { + await writeFile(join(dir, 'package.json'), JSON.stringify({ version: '1.0.0' })); + await writeFile(join(dir, 'go.mod'), 'module example.com/fallback\n'); - const identity = await discoverProjectIdentity(dir) + const identity = await discoverProjectIdentity(dir); - expect(identity.name).toBe("fallback") - expect(identity.source).toBe("go.mod") - }) -}) + expect(identity.name).toBe('fallback'); + expect(identity.source).toBe('go.mod'); + }); +}); diff --git a/src/project-identity.ts b/src/project-identity.ts index b419ae1dd..702ea12e6 100644 --- a/src/project-identity.ts +++ b/src/project-identity.ts @@ -1,15 +1,15 @@ -import { readFile } from "node:fs/promises" -import { basename, join } from "node:path" +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; -export type ProjectIdentitySource = "package.json" | "pyproject.toml" | "cargo.toml" | "go.mod" | "directory" +export type ProjectIdentitySource = 'package.json' | 'pyproject.toml' | 'cargo.toml' | 'go.mod' | 'directory'; export interface ProjectIdentity { /** Human-facing project name, as written in the source artifact. */ - name: string + name: string; /** Stable, filesystem/URL-safe identifier derived from `name`. */ - slug: string + slug: string; /** Which artifact in `cwd` produced `name`. */ - source: ProjectIdentitySource + source: ProjectIdentitySource; } /** @@ -28,25 +28,21 @@ export interface ProjectIdentity { * 4. go.mod — final segment of the `module` directive * 5. directory basename */ -export async function discoverProjectIdentity( - cwd: string, -): Promise { - const detectors: Array<() => Promise | null>> = - [ - () => readPackageJsonName(cwd), - () => readPyprojectName(cwd), - () => readCargoTomlName(cwd), - () => readGoModName(cwd), - ] +export async function discoverProjectIdentity(cwd: string): Promise { + const detectors: Array<() => Promise | null>> = [ + () => readPackageJsonName(cwd), + () => readPyprojectName(cwd), + () => readCargoTomlName(cwd), + () => readGoModName(cwd), + ]; for (const detect of detectors) { - const hit = await detect() - if (hit) - return { name: hit.name, slug: slugify(hit.name), source: hit.source } + const hit = await detect(); + if (hit) return { name: hit.name, slug: slugify(hit.name), source: hit.source }; } - const name = basename(cwd) - return { name, slug: slugify(name), source: "directory" } + const name = basename(cwd); + return { name, slug: slugify(name), source: 'directory' }; } /** @@ -61,79 +57,71 @@ export async function discoverProjectIdentity( export function slugify(name: string): string { const slug = name .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - return slug.length > 0 ? slug : "project" + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + return slug.length > 0 ? slug : 'project'; } async function readFileOrNull(path: string): Promise { try { - return await readFile(path, "utf8") + return await readFile(path, 'utf8'); } catch { - return null + return null; } } interface DetectedName { - name: string - source: S + name: string; + source: S; } -async function readPackageJsonName( - cwd: string, -): Promise | null> { - const raw = await readFileOrNull(join(cwd, "package.json")) - if (!raw) return null +async function readPackageJsonName(cwd: string): Promise | null> { + const raw = await readFileOrNull(join(cwd, 'package.json')); + if (!raw) return null; try { - const parsed = JSON.parse(raw) as { name?: unknown } - if (typeof parsed.name === "string" && parsed.name.trim().length > 0) { - return { name: parsed.name.trim(), source: "package.json" } + const parsed = JSON.parse(raw) as { name?: unknown }; + if (typeof parsed.name === 'string' && parsed.name.trim().length > 0) { + return { name: parsed.name.trim(), source: 'package.json' }; } } catch { // Malformed package.json — skip this signal rather than throwing. } - return null + return null; } -async function readPyprojectName( - cwd: string, -): Promise | null> { - const raw = await readFileOrNull(join(cwd, "pyproject.toml")) - if (!raw) return null - const fromProject = extractTomlNameInTable(raw, "project") - if (fromProject) return { name: fromProject, source: "pyproject.toml" } - const fromPoetry = extractTomlNameInTable(raw, "tool.poetry") - if (fromPoetry) return { name: fromPoetry, source: "pyproject.toml" } - return null +async function readPyprojectName(cwd: string): Promise | null> { + const raw = await readFileOrNull(join(cwd, 'pyproject.toml')); + if (!raw) return null; + const fromProject = extractTomlNameInTable(raw, 'project'); + if (fromProject) return { name: fromProject, source: 'pyproject.toml' }; + const fromPoetry = extractTomlNameInTable(raw, 'tool.poetry'); + if (fromPoetry) return { name: fromPoetry, source: 'pyproject.toml' }; + return null; } -async function readCargoTomlName( - cwd: string, -): Promise | null> { - const raw = await readFileOrNull(join(cwd, "Cargo.toml")) - if (!raw) return null - const name = extractTomlNameInTable(raw, "package") - return name ? { name, source: "cargo.toml" } : null +async function readCargoTomlName(cwd: string): Promise | null> { + const raw = await readFileOrNull(join(cwd, 'Cargo.toml')); + if (!raw) return null; + const name = extractTomlNameInTable(raw, 'package'); + return name ? { name, source: 'cargo.toml' } : null; } -async function readGoModName( - cwd: string, -): Promise | null> { - const raw = await readFileOrNull(join(cwd, "go.mod")) - if (!raw) return null +async function readGoModName(cwd: string): Promise | null> { + const raw = await readFileOrNull(join(cwd, 'go.mod')); + if (!raw) return null; for (const rawLine of raw.split(/\r?\n/)) { - const line = rawLine.trim() - if (!line.startsWith("module")) continue - const match = line.match(/^module\s+(\S+)/) - const captured = match?.[1] - if (!captured) continue - const modulePath = captured.replace(/^["']|["']$/g, "") - const tail = modulePath.split("/").filter(Boolean).pop() + const line = rawLine.trim(); + if (!line.startsWith('module')) continue; + const match = line.match(/^module\s+(\S+)/); + const captured = match?.[1]; + if (!captured) continue; + const modulePath = captured.replace(/^["']|["']$/g, ''); + const tail = modulePath.split('/').filter(Boolean).pop(); if (tail && tail.length > 0) { - return { name: tail, source: "go.mod" } + return { name: tail, source: 'go.mod' }; } } - return null + return null; } /** @@ -141,24 +129,21 @@ async function readGoModName( * at the next top-level table header. Not a real TOML parser — sufficient for * the well-formed manifests we care about and cheaper than a dependency. */ -function extractTomlNameInTable( - content: string, - tableName: string, -): string | null { - const lines = content.split(/\r?\n/) - const header = `[${tableName}]` - let inTable = false +function extractTomlNameInTable(content: string, tableName: string): string | null { + const lines = content.split(/\r?\n/); + const header = `[${tableName}]`; + let inTable = false; for (const rawLine of lines) { - const line = rawLine.trim() - if (line.startsWith("#") || line.length === 0) continue - if (line.startsWith("[") && line.endsWith("]")) { - inTable = line === header - continue + const line = rawLine.trim(); + if (line.startsWith('#') || line.length === 0) continue; + if (line.startsWith('[') && line.endsWith(']')) { + inTable = line === header; + continue; } - if (!inTable) continue - const match = line.match(/^name\s*=\s*(["'])(.*?)\1/) - const captured = match?.[2] - if (captured && captured.length > 0) return captured + if (!inTable) continue; + const match = line.match(/^name\s*=\s*(["'])(.*?)\1/); + const captured = match?.[2]; + if (captured && captured.length > 0) return captured; } - return null + return null; } diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index a78d27651..f0bbf3439 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -1,17 +1,15 @@ -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" -import { PassThrough } from "node:stream" -import { describe, expect, it } from "vitest" - -import { SessionManager } from "@earendil-works/pi-coding-agent" - -import { Value } from "typebox/value" - -import { createRpcHandlers, runJsonRpcLineServer } from "./handlers.js" -import { createSessionBindingData } from "../session-binding.js" -import { createWorkspaceSessionCoordinator } from "../workspace-session-coordinator.js" -import { assistantMessage, userMessage } from "../test-helpers.js" +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PassThrough } from 'node:stream'; + +import { SessionManager } from '@earendil-works/pi-coding-agent'; +import { Value } from 'typebox/value'; +import { describe, expect, it } from 'vitest'; + +import { createSessionBindingData } from '../session-binding.js'; +import { assistantMessage, userMessage } from '../test-helpers.js'; +import { createWorkspaceSessionCoordinator } from '../workspace-session-coordinator.js'; import type { DefaultWorkspaceCoordinator, WorkspaceActivationState, @@ -20,45 +18,42 @@ import type { WorkspaceSessionState, SpecSessionActivationCoordinator, SpecSessionActivationDecision, -} from "../workspace-session-coordinator.js" +} from '../workspace-session-coordinator.js'; +import { createRpcHandlers, runJsonRpcLineServer } from './handlers.js'; function coordinator( - state: WorkspaceSessionState = readyState( - "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", - ), + state: WorkspaceSessionState = readyState('/tmp/brunch-project/.brunch/sessions/session-1.jsonl'), ): DefaultWorkspaceCoordinator & SpecSessionActivationCoordinator { - const inventory = launchInventory() + const inventory = launchInventory(); return { async openDefaultWorkspace() { - return state + return state; }, async inspectWorkspace() { - return inventory + return inventory; }, - async activateWorkspace( - decision: SpecSessionActivationDecision, - ): Promise { - if (decision.action === "cancel") return cancelledState() - return readyState("/tmp/brunch-project/.brunch/sessions/session-1.jsonl") + async activateWorkspace(decision: SpecSessionActivationDecision): Promise { + if (decision.action === 'cancel') return cancelledState(); + return readyState('/tmp/brunch-project/.brunch/sessions/session-1.jsonl'); }, - } + }; } function launchInventory(): WorkspaceLaunchInventory { return { - cwd: "/tmp/brunch-project", - currentSpec: { id: "spec-1", title: "Alpha spec" }, - currentSessionFile: "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", + cwd: '/tmp/brunch-project', + currentSpec: { id: 'spec-1', title: 'Alpha spec' }, + currentSessionFile: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', needsNewSpec: false, specs: [ { - spec: { id: "spec-1", title: "Alpha spec" }, + spec: { id: 'spec-1', title: 'Alpha spec' }, sessions: [ { - id: "session-1", - file: "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", - specId: "spec-1", - specTitle: "Alpha spec", + id: 'session-1', + file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', + specId: 'spec-1', + specTitle: 'Alpha spec', available: true, }, ], @@ -66,1000 +61,985 @@ function launchInventory(): WorkspaceLaunchInventory { ], unavailableSessions: [ { - file: "/tmp/missing.jsonl", - reason: "missing_header", + file: '/tmp/missing.jsonl', + reason: 'missing_header', available: false, }, ], - } + }; } function cancelledState(): WorkspaceActivationState { return { - status: "cancelled", - cwd: "/tmp/brunch-project", + status: 'cancelled', + cwd: '/tmp/brunch-project', chrome: { - cwd: "/tmp/brunch-project", - spec: { id: "spec-1", title: "Alpha spec" }, - phase: "elicitation", - chatMode: "responding-to-elicitation", + cwd: '/tmp/brunch-project', + spec: { id: 'spec-1', title: 'Alpha spec' }, + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - } + }; } function readyState(sessionFile: string): WorkspaceSessionReadyState { return { - status: "ready", - cwd: "/tmp/brunch-project", - spec: { id: "spec-1", title: "Alpha spec" }, + status: 'ready', + cwd: '/tmp/brunch-project', + spec: { id: 'spec-1', title: 'Alpha spec' }, session: { - id: "session-1", + id: 'session-1', file: sessionFile, manager: {} as never, }, chrome: { - cwd: "/tmp/brunch-project", - spec: { id: "spec-1", title: "Alpha spec" }, - phase: "elicitation", - chatMode: "responding-to-elicitation", + cwd: '/tmp/brunch-project', + spec: { id: 'spec-1', title: 'Alpha spec' }, + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, - } + }; } function selectSpecState(): WorkspaceSessionState { return { - status: "select_spec", - cwd: "/tmp/brunch-project", + status: 'select_spec', + cwd: '/tmp/brunch-project', chrome: { - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', spec: null, - phase: "select_spec", - chatMode: "select-spec", + phase: 'select_spec', + chatMode: 'select-spec', }, - } + }; } async function createSessionFile(): Promise { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-session-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage(assistantMessage("Question")) - manager.appendMessage(userMessage("Answer")) - return manager.getSessionFile()! + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-session-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Question')); + manager.appendMessage(userMessage('Answer')); + return manager.getSessionFile()!; } async function createBranchedSessionFile(): Promise { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-branch-")) - const manager = SessionManager.create(cwd, join(cwd, ".brunch/sessions")) - appendBinding(manager) - manager.appendMessage(assistantMessage("Abandoned prompt")) - manager.appendMessage(userMessage("Abandoned answer")) - manager.resetLeaf() - manager.appendMessage(assistantMessage("Active prompt")) - manager.appendMessage(userMessage("Active answer")) - return manager.getSessionFile()! + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-branch-')); + const manager = SessionManager.create(cwd, join(cwd, '.brunch/sessions')); + appendBinding(manager); + manager.appendMessage(assistantMessage('Abandoned prompt')); + manager.appendMessage(userMessage('Abandoned answer')); + manager.resetLeaf(); + manager.appendMessage(assistantMessage('Active prompt')); + manager.appendMessage(userMessage('Active answer')); + return manager.getSessionFile()!; } -async function writeExplicitSessionFixture( - cwd: string, - entries: readonly unknown[], -): Promise { - const sessionRoot = join(cwd, ".brunch", "sessions") - await mkdir(sessionRoot, { recursive: true }) +async function writeExplicitSessionFixture(cwd: string, entries: readonly unknown[]): Promise { + const sessionRoot = join(cwd, '.brunch', 'sessions'); + await mkdir(sessionRoot, { recursive: true }); await writeFile( - join(sessionRoot, "session.jsonl"), - entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n", - ) + join(sessionRoot, 'session.jsonl'), + entries.map((entry) => JSON.stringify(entry)).join('\n') + '\n', + ); } function appendBinding(manager: SessionManager): void { manager.appendCustomEntry( - "brunch.session_binding", + 'brunch.session_binding', createSessionBindingData({ sessionId: manager.getSessionId(), - specId: "spec-1", - specTitle: "Spec", + specId: 'spec-1', + specTitle: 'Spec', }), - ) + ); } function presentQuestionEntry() { return { - id: "present-question-1", - type: "message", - parentId: "binding-session-1-spec-1", + id: 'present-question-1', + type: 'message', + parentId: 'binding-session-1-spec-1', message: { - role: "toolResult", - toolCallId: "present-call-1", - toolName: "present_question", - content: [ - { type: "text", text: "## Domain?\n\nWhat are we specifying?" }, - ], + role: 'toolResult', + toolCallId: 'present-call-1', + toolName: 'present_question', + content: [{ type: 'text', text: '## Domain?\n\nWhat are we specifying?' }], details: { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', schemaVersion: 1, - exchangeId: "domain", - presentTool: "present_question", - kind: "question", - status: "presented", - expectedRequest: { tool: "request_answer", required: true }, - createdAtToolCallId: "present-call-1", + exchangeId: 'domain', + presentTool: 'present_question', + kind: 'question', + status: 'presented', + expectedRequest: { tool: 'request_answer', required: true }, + createdAtToolCallId: 'present-call-1', }, isError: false, }, - } + }; } -function requestAnswerEntry(parentId = "present-question-1") { +function requestAnswerEntry(parentId = 'present-question-1') { return { - id: "request-answer-1", - type: "message", + id: 'request-answer-1', + type: 'message', parentId, message: { - role: "toolResult", - toolCallId: "request-call-1", - toolName: "request_answer", - content: [{ type: "text", text: "### Response\n\nDeveloper tooling" }], + role: 'toolResult', + toolCallId: 'request-call-1', + toolName: 'request_answer', + content: [{ type: 'text', text: '### Response\n\nDeveloper tooling' }], details: { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, - exchangeId: "domain", - requestTool: "request_answer", - status: "answered", + exchangeId: 'domain', + requestTool: 'request_answer', + status: 'answered', respondsTo: { - exchangeId: "domain", - presentTool: "present_question", + exchangeId: 'domain', + presentTool: 'present_question', }, - answer: "Developer tooling", - createdAtToolCallId: "request-call-1", + answer: 'Developer tooling', + createdAtToolCallId: 'request-call-1', }, isError: false, }, - } + }; } -function sessionBindingEntry(sessionId = "session-1", specId = "spec-1") { +function sessionBindingEntry(sessionId = 'session-1', specId = 'spec-1') { return { id: `binding-${sessionId}-${specId}`, - type: "custom", + type: 'custom', parentId: null, - customType: "brunch.session_binding", + customType: 'brunch.session_binding', data: createSessionBindingData({ sessionId, specId, - specTitle: "Spec", + specTitle: 'Spec', }), - } + }; } -describe("JSON-RPC handlers", () => { - it("discovers the current public Brunch JSON-RPC surface", async () => { +describe('JSON-RPC handlers', () => { + it('discovers the current public Brunch JSON-RPC surface', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); const response = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 30, - method: "rpc.discover", - }) - - expect(response).toMatchObject({ jsonrpc: "2.0", id: 30 }) - if (!("result" in response)) throw new Error("expected success response") - - const methods = (response.result as { - methods: Array<{ - method: string - description: string - paramsSchema: unknown - resultSchema: unknown - examples: Array> - }> - }).methods + method: 'rpc.discover', + }); + + expect(response).toMatchObject({ jsonrpc: '2.0', id: 30 }); + if (!('result' in response)) throw new Error('expected success response'); + + const methods = ( + response.result as { + methods: Array<{ + method: string; + description: string; + paramsSchema: unknown; + resultSchema: unknown; + examples: Array>; + }>; + } + ).methods; expect(methods.map((entry) => entry.method).sort()).toEqual([ - "elicitation.respond", - "rpc.discover", - "session.elicitationExchanges", - "session.pendingExchange", - "session.startElicitation", - "session.transcriptDisplay", - "workspace.activate", - "workspace.selectionState", - "workspace.snapshot", - ]) - - const discoveredNames = new Set(methods.map((entry) => entry.method)) + 'elicitation.respond', + 'rpc.discover', + 'session.elicitationExchanges', + 'session.pendingExchange', + 'session.startElicitation', + 'session.transcriptDisplay', + 'workspace.activate', + 'workspace.selectionState', + 'workspace.snapshot', + ]); + + const discoveredNames = new Set(methods.map((entry) => entry.method)); for (const entry of methods) { - expect(entry.description).toEqual(expect.any(String)) - expect(entry.description.length).toBeGreaterThan(10) - expect(entry.paramsSchema).toEqual(expect.any(Object)) - expect(entry.resultSchema).toEqual(expect.any(Object)) - expect(entry.examples.length).toBeGreaterThanOrEqual(1) + expect(entry.description).toEqual(expect.any(String)); + expect(entry.description.length).toBeGreaterThan(10); + expect(entry.paramsSchema).toEqual(expect.any(Object)); + expect(entry.resultSchema).toEqual(expect.any(Object)); + expect(entry.examples.length).toBeGreaterThanOrEqual(1); for (const example of entry.examples) { - expect(example).toMatchObject({ jsonrpc: "2.0", method: entry.method }) - expect(discoveredNames.has(String(example.method))).toBe(true) + expect(example).toMatchObject({ jsonrpc: '2.0', method: entry.method }); + expect(discoveredNames.has(String(example.method))).toBe(true); } } - }) + }); - it("rejects params on method discovery", async () => { + it('rejects params on method discovery', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 31, - method: "rpc.discover", + method: 'rpc.discover', params: {}, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 31, - error: { code: -32602, message: "Invalid params" }, - }) - }) + error: { code: -32602, message: 'Invalid params' }, + }); + }); - it("keeps discovery product-shaped and exposes workspace activation variants", async () => { + it('keeps discovery product-shaped and exposes workspace activation variants', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); const response = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 32, - method: "rpc.discover", - }) - if (!("result" in response)) throw new Error("expected success response") + method: 'rpc.discover', + }); + if (!('result' in response)) throw new Error('expected success response'); const result = response.result as { methods: Array<{ - method: string - paramsSchema: unknown - examples: unknown[] - }> - } - const methods = result.methods - const discoveryJson = JSON.stringify(result) - expect(discoveryJson).not.toContain("get_commands") - expect(discoveryJson).not.toContain("get_state") - expect(discoveryJson).not.toContain('"method":"prompt"') - expect(discoveryJson).not.toContain("/brunch") - - const activation = methods.find( - (entry) => entry.method === "workspace.activate", - ) - expect(activation).toBeDefined() - const activationSchema = JSON.stringify(activation?.paramsSchema) - for (const action of [ - "continue", - "openSession", - "newSession", - "newSpec", - "cancel", - ]) { - expect(activationSchema).toContain(action) + method: string; + paramsSchema: unknown; + examples: unknown[]; + }>; + }; + const methods = result.methods; + const discoveryJson = JSON.stringify(result); + expect(discoveryJson).not.toContain('get_commands'); + expect(discoveryJson).not.toContain('get_state'); + expect(discoveryJson).not.toContain('"method":"prompt"'); + expect(discoveryJson).not.toContain('/brunch'); + + const activation = methods.find((entry) => entry.method === 'workspace.activate'); + expect(activation).toBeDefined(); + const activationSchema = JSON.stringify(activation?.paramsSchema); + for (const action of ['continue', 'openSession', 'newSession', 'newSpec', 'cancel']) { + expect(activationSchema).toContain(action); } - }) + }); - it("serves discovery examples that are valid JSON-RPC requests for advertised methods", async () => { + it('serves discovery examples that are valid JSON-RPC requests for advertised methods', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); const response = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 33, - method: "rpc.discover", - }) - if (!("result" in response)) throw new Error("expected success response") - - const methods = (response.result as { - methods: Array<{ - method: string - examples: unknown[] - }> - }).methods - const discoveredNames = new Set(methods.map((entry) => entry.method)) + method: 'rpc.discover', + }); + if (!('result' in response)) throw new Error('expected success response'); + + const methods = ( + response.result as { + methods: Array<{ + method: string; + examples: unknown[]; + }>; + } + ).methods; + const discoveredNames = new Set(methods.map((entry) => entry.method)); const exampleRequestSchema = { - type: "object", + type: 'object', properties: { - jsonrpc: { const: "2.0" }, + jsonrpc: { const: '2.0' }, id: { - anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }], + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'null' }], }, - method: { type: "string" }, + method: { type: 'string' }, params: {}, }, - required: ["jsonrpc", "method"], + required: ['jsonrpc', 'method'], additionalProperties: false, - } + }; for (const entry of methods) { for (const example of entry.examples) { - expect(Value.Check(exampleRequestSchema, example)).toBe(true) - expect( - discoveredNames.has((example as { method: string }).method), - ).toBe(true) + expect(Value.Check(exampleRequestSchema, example)).toBe(true); + expect(discoveredNames.has((example as { method: string }).method)).toBe(true); } } - }) + }); - it("serves structured workspace selection state without invoking the TUI picker", async () => { + it('serves structured workspace selection state without invoking the TUI picker', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 20, - method: "workspace.selectionState", + method: 'workspace.selectionState', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 20, result: { - status: "select_spec", + status: 'select_spec', requiresSelection: true, - cwd: "/tmp/brunch-project", - currentSpec: { id: "spec-1", title: "Alpha spec" }, - currentSessionFile: - "/tmp/brunch-project/.brunch/sessions/session-1.jsonl", - specs: [{ spec: { id: "spec-1" }, sessions: [{ id: "session-1" }] }], - unavailableSessions: [{ reason: "missing_header" }], + cwd: '/tmp/brunch-project', + currentSpec: { id: 'spec-1', title: 'Alpha spec' }, + currentSessionFile: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', + specs: [{ spec: { id: 'spec-1' }, sessions: [{ id: 'session-1' }] }], + unavailableSessions: [{ reason: 'missing_header' }], }, - }) - }) + }); + }); - it("activates valid spec/session decisions and returns serializable product snapshots", async () => { - const decisions: SpecSessionActivationDecision[] = [] + it('activates valid spec/session decisions and returns serializable product snapshots', async () => { + const decisions: SpecSessionActivationDecision[] = []; const handlers = createRpcHandlers({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', coordinator: { ...coordinator(), async activateWorkspace(decision): Promise { - decisions.push(decision) - return decision.action === "cancel" + decisions.push(decision); + return decision.action === 'cancel' ? cancelledState() - : readyState("/tmp/brunch-project/.brunch/sessions/session-1.jsonl") + : readyState('/tmp/brunch-project/.brunch/sessions/session-1.jsonl'); }, }, - }) + }); const validDecisions: SpecSessionActivationDecision[] = [ - { action: "cancel" }, - { action: "newSpec", title: "New spec" }, - { action: "newSession", specId: "spec-1" }, + { action: 'cancel' }, + { action: 'newSpec', title: 'New spec' }, + { action: 'newSession', specId: 'spec-1' }, { - action: "continue", - specId: "spec-1", - sessionFile: "session-1.jsonl", + action: 'continue', + specId: 'spec-1', + sessionFile: 'session-1.jsonl', }, { - action: "openSession", - specId: "spec-1", - sessionFile: "session-2.jsonl", + action: 'openSession', + specId: 'spec-1', + sessionFile: 'session-2.jsonl', }, - ] + ]; for (const [index, decision] of validDecisions.entries()) { await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 21 + index, - method: "workspace.activate", + method: 'workspace.activate', params: { decision }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 21 + index, result: - decision.action === "cancel" - ? { status: "cancelled", spec: { id: "spec-1" } } + decision.action === 'cancel' + ? { status: 'cancelled', spec: { id: 'spec-1' } } : { - status: "ready", - spec: { id: "spec-1" }, - session: { id: "session-1" }, + status: 'ready', + spec: { id: 'spec-1' }, + session: { id: 'session-1' }, }, - }) - expect(decisions).toHaveLength(index + 1) - expect(decisions[index]).toEqual(decision) + }); + expect(decisions).toHaveLength(index + 1); + expect(decisions[index]).toEqual(decision); } - }) + }); - it("rejects invalid workspace activation params", async () => { + it('rejects invalid workspace activation params', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 22, - method: "workspace.activate", - params: { decision: { action: "openSession", specId: "spec-1" } }, + method: 'workspace.activate', + params: { decision: { action: 'openSession', specId: 'spec-1' } }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 22, - error: { code: -32602, message: "Invalid params" }, - }) - }) - - it("keeps RPC initial selection independent from TUI picker imports", async () => { - const source = await readFile( - new URL("./handlers.ts", import.meta.url), - "utf8", - ) - - expect(source).not.toContain("workspace-dialog") - expect(source).not.toContain("createWorkspaceDialogComponent") - expect(source).not.toContain("pi --mode rpc") - }) - - it("serves a named workspace snapshot method", async () => { + error: { code: -32602, message: 'Invalid params' }, + }); + }); + + it('keeps RPC initial selection independent from TUI picker imports', async () => { + const source = await readFile(new URL('./handlers.ts', import.meta.url), 'utf8'); + + expect(source).not.toContain('workspace-dialog'); + expect(source).not.toContain('createWorkspaceDialogComponent'); + expect(source).not.toContain('pi --mode rpc'); + }); + + it('serves a named workspace snapshot method', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); const result = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "workspace.snapshot", - }) + method: 'workspace.snapshot', + }); expect(result).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, result: { - status: "ready", - spec: { id: "spec-1", title: "Alpha spec" }, - session: { id: "session-1" }, + status: 'ready', + spec: { id: 'spec-1', title: 'Alpha spec' }, + session: { id: 'session-1' }, }, - }) - }) + }); + }); - it("serves session elicitation exchanges from the coordinator-selected session", async () => { - const sessionFile = await createSessionFile() + it('serves session elicitation exchanges from the coordinator-selected session', async () => { + const sessionFile = await createSessionFile(); const handlers = createRpcHandlers({ coordinator: coordinator(readyState(sessionFile)), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 3, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 3, result: { - status: "ready", + status: 'ready', exchanges: [{ promptEntryIds: [expect.any(String)] }], }, - }) - }) + }); + }); - it("starts a deterministic assistant-first elicitation prompt for the selected session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-start-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('starts a deterministic assistant-first elicitation prompt for the selected session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-start-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Start spec", - }) + specTitle: 'Start spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const start = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 40, - method: "session.startElicitation", - }) + method: 'session.startElicitation', + }); expect(start).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 40, result: { - status: "pending", + status: 'pending', exchange: { exchangeId: expect.any(String), - lens: "step-by-step", - mode: "single-select", - prompt: expect.stringContaining("new product or feature"), + lens: 'step-by-step', + mode: 'single-select', + prompt: expect.stringContaining('new product or feature'), options: expect.arrayContaining([ expect.objectContaining({ - id: "new-from-scratch", - label: "Yes — this is new from scratch", - content: "Start a new spec workspace from a blank slate.", - rationale: - "This keeps the parity run focused on initial grounding.", + id: 'new-from-scratch', + label: 'Yes — this is new from scratch', + content: 'Start a new spec workspace from a blank slate.', + rationale: 'This keeps the parity run focused on initial grounding.', }), ]), note: { allowed: true }, }, }, - }) - const exchangeId = (start as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId + }); + const exchangeId = ( + start as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; const exchanges = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 41, - method: "session.elicitationExchanges", - }) + method: 'session.elicitationExchanges', + }); expect(exchanges).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 41, - result: { status: "open_prompt", openPrompt: expect.any(Object) }, - }) + result: { status: 'open_prompt', openPrompt: expect.any(Object) }, + }); const display = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 42, - method: "session.transcriptDisplay", - }) + method: 'session.transcriptDisplay', + }); expect(display).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 42, result: { rows: [ { - role: "prompt", - text: expect.stringContaining("new product or feature"), + role: 'prompt', + text: expect.stringContaining('new product or feature'), }, ], }, - }) - const displayText = (display as { - result: { rows: Array<{ text: string }> } - }).result.rows[0]!.text - expect(displayText).toContain( - "Start a new spec workspace from a blank slate.", - ) - expect(displayText).toContain( - "This keeps the parity run focused on initial grounding.", - ) - - const sessionText = await readFile(workspace.session.file, "utf8") - expect(sessionText).toContain("brunch.structured_exchange.present") - expect(sessionText).toContain("present_options") - expect(sessionText).toContain(exchangeId) - expect(sessionText).toContain('"lens":"step-by-step"') - }) - - it("reads the selected pending elicitation exchange from transcript truth", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-pending-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + }); + const displayText = ( + display as { + result: { rows: Array<{ text: string }> }; + } + ).result.rows[0]!.text; + expect(displayText).toContain('Start a new spec workspace from a blank slate.'); + expect(displayText).toContain('This keeps the parity run focused on initial grounding.'); + + const sessionText = await readFile(workspace.session.file, 'utf8'); + expect(sessionText).toContain('brunch.structured_exchange.present'); + expect(sessionText).toContain('present_options'); + expect(sessionText).toContain(exchangeId); + expect(sessionText).toContain('"lens":"step-by-step"'); + }); + + it('reads the selected pending elicitation exchange from transcript truth', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-pending-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); await coordinatorInstance.createSetupSession({ - specTitle: "Pending spec", - }) + specTitle: 'Pending spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const start = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 46, - method: "session.startElicitation", - }) + method: 'session.startElicitation', + }); const pending = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 47, - method: "session.pendingExchange", - }) + method: 'session.pendingExchange', + }); expect(pending).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 47, result: { - status: "pending", + status: 'pending', exchange: { - exchangeId: (start as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId, - prompt: expect.stringContaining("new product or feature"), - lens: "step-by-step", + exchangeId: ( + start as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId, + prompt: expect.stringContaining('new product or feature'), + lens: 'step-by-step', options: expect.arrayContaining([ expect.objectContaining({ - id: "new-from-scratch", - content: "Start a new spec workspace from a blank slate.", - rationale: - "This keeps the parity run focused on initial grounding.", + id: 'new-from-scratch', + content: 'Start a new spec workspace from a blank slate.', + rationale: 'This keeps the parity run focused on initial grounding.', }), ]), note: { allowed: true }, }, }, - }) - }) + }); + }); - it("reads an explicit pending exchange without opening the selected workspace session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-explicit-pending-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('reads an explicit pending exchange without opening the selected workspace session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-explicit-pending-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Explicit pending spec", - }) + specTitle: 'Explicit pending spec', + }); const startHandlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); await startHandlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 48, - method: "session.startElicitation", - }) + method: 'session.startElicitation', + }); const handlers = createRpcHandlers({ coordinator: { ...coordinatorInstance, async openDefaultWorkspace() { - throw new Error( - "explicit pending reads must not open selected session", - ) + throw new Error('explicit pending reads must not open selected session'); }, }, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 49, - method: "session.pendingExchange", + method: 'session.pendingExchange', params: { sessionId: workspace.session.id, specId: workspace.spec.id }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 49, result: { - status: "pending", - exchange: { exchangeId: "deterministic-grounding-choice-1" }, + status: 'pending', + exchange: { exchangeId: 'deterministic-grounding-choice-1' }, }, - }) - }) + }); + }); - it("reads an explicit tuple-shaped pending exchange without a sidecar prompt store", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-tuple-pending-")) + it('reads an explicit tuple-shaped pending exchange without a sidecar prompt store', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-tuple-pending-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), presentQuestionEntry(), - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 149, - method: "session.pendingExchange", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.pendingExchange', + params: { sessionId: 'session-1', specId: 'spec-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 149, result: { - status: "pending", + status: 'pending', exchange: { - exchangeId: "domain", - mode: "text", - prompt: "Domain?", - details: expect.stringContaining("What are we specifying?"), + exchangeId: 'domain', + mode: 'text', + prompt: 'Domain?', + details: expect.stringContaining('What are we specifying?'), }, }, - }) - }) + }); + }); - it("serves tuple-shaped exchange and transcript projections explicitly", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-tuple-projection-")) + it('serves tuple-shaped exchange and transcript projections explicitly', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-tuple-projection-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), presentQuestionEntry(), requestAnswerEntry(), - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 150, - method: "session.elicitationExchanges", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.elicitationExchanges', + params: { sessionId: 'session-1', specId: 'spec-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 150, result: { - status: "ready", + status: 'ready', exchanges: [ { - promptEntryIds: ["present-question-1"], - responseEntryIds: ["request-answer-1"], + promptEntryIds: ['present-question-1'], + responseEntryIds: ['request-answer-1'], }, ], }, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 151, - method: "session.transcriptDisplay", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.transcriptDisplay', + params: { sessionId: 'session-1', specId: 'spec-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 151, result: { rows: [ - { role: "prompt", text: expect.stringContaining("Domain?") }, + { role: 'prompt', text: expect.stringContaining('Domain?') }, { - role: "user", - text: expect.stringContaining("Developer tooling"), + role: 'user', + text: expect.stringContaining('Developer tooling'), }, ], }, - }) - }) + }); + }); - it("reports idle pending state when the selected session has no open prompt", async () => { - const sessionFile = await createSessionFile() + it('reports idle pending state when the selected session has no open prompt', async () => { + const sessionFile = await createSessionFile(); const handlers = createRpcHandlers({ coordinator: coordinator(readyState(sessionFile)), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 50, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 50, - result: { status: "idle", exchange: null }, - }) - }) + result: { status: 'idle', exchange: null }, + }); + }); - it("reports idle pending state after selected and explicit terminal unavailable request tuples", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-unavailable-idle-")) - const sessionFile = join(cwd, ".brunch", "sessions", "session.jsonl") + it('reports idle pending state after selected and explicit terminal unavailable request tuples', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-unavailable-idle-')); + const sessionFile = join(cwd, '.brunch', 'sessions', 'session.jsonl'); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), presentQuestionEntry(), { ...requestAnswerEntry(), - id: "request-answer-unavailable", + id: 'request-answer-unavailable', message: { ...requestAnswerEntry().message, details: { ...requestAnswerEntry().message.details, - status: "unavailable", - message: "Editor unavailable.", + status: 'unavailable', + message: 'Editor unavailable.', }, }, }, - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(readyState(sessionFile)), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 152, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), ).resolves.toMatchObject({ - result: { status: "idle", exchange: null }, - }) + result: { status: 'idle', exchange: null }, + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 153, - method: "session.pendingExchange", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.pendingExchange', + params: { sessionId: 'session-1', specId: 'spec-1' }, }), ).resolves.toMatchObject({ - result: { status: "idle", exchange: null }, - }) - }) + result: { status: 'idle', exchange: null }, + }); + }); - it("reports idle pending state after an explicit terminal cancelled request_choices tuple", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-cancelled-idle-")) + it('reports idle pending state after an explicit terminal cancelled request_choices tuple', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-cancelled-idle-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), { ...presentQuestionEntry(), - id: "present-options-1", + id: 'present-options-1', message: { ...presentQuestionEntry().message, - toolName: "present_options", + toolName: 'present_options', details: { ...presentQuestionEntry().message.details, - presentTool: "present_options", - kind: "options", - expectedRequest: { tool: "request_choices", required: true }, + presentTool: 'present_options', + kind: 'options', + expectedRequest: { tool: 'request_choices', required: true }, }, }, }, { - id: "request-choices-cancelled", - type: "message", - parentId: "present-options-1", + id: 'request-choices-cancelled', + type: 'message', + parentId: 'present-options-1', message: { - role: "toolResult", - toolCallId: "request-call-choices-cancelled", - toolName: "request_choices", - content: [{ type: "text", text: "### Response\n\nCancelled." }], + role: 'toolResult', + toolCallId: 'request-call-choices-cancelled', + toolName: 'request_choices', + content: [{ type: 'text', text: '### Response\n\nCancelled.' }], details: { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, - exchangeId: "domain", - requestTool: "request_choices", - status: "cancelled", + exchangeId: 'domain', + requestTool: 'request_choices', + status: 'cancelled', respondsTo: { - exchangeId: "domain", - presentTool: "present_options", + exchangeId: 'domain', + presentTool: 'present_options', }, - message: "User cancelled the selection.", - createdAtToolCallId: "request-call-choices-cancelled", + message: 'User cancelled the selection.', + createdAtToolCallId: 'request-call-choices-cancelled', }, isError: false, }, }, - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 154, - method: "session.pendingExchange", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.pendingExchange', + params: { sessionId: 'session-1', specId: 'spec-1' }, }), ).resolves.toMatchObject({ - result: { status: "idle", exchange: null }, - }) - }) + result: { status: 'idle', exchange: null }, + }); + }); - it("returns a product-shaped no-session error when reading pending without a selected session", async () => { + it('returns a product-shaped no-session error when reading pending without a selected session', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 51, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 51, - error: { code: -32001, message: "No selected Brunch session" }, - }) - }) + error: { code: -32001, message: 'No selected Brunch session' }, + }); + }); - it("returns product-shaped non-linear errors when reading pending exchanges", async () => { - const sessionFile = await createBranchedSessionFile() + it('returns product-shaped non-linear errors when reading pending exchanges', async () => { + const sessionFile = await createBranchedSessionFile(); const handlers = createRpcHandlers({ coordinator: coordinator(readyState(sessionFile)), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 52, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 52, error: { code: -32002, - message: "Selected Brunch session transcript is non-linear", + message: 'Selected Brunch session transcript is non-linear', }, - }) - }) + }); + }); - it("responds to the deterministic listed-option exchange and closes the projection", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-respond-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('responds to the deterministic listed-option exchange and closes the projection', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Respond spec", - }) + specTitle: 'Respond spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const start = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 53, - method: "session.startElicitation", - }) - const exchangeId = (start as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId + method: 'session.startElicitation', + }); + const exchangeId = ( + start as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; const response = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 54, - method: "elicitation.respond", + method: 'elicitation.respond', params: { exchangeId, - answer: { optionId: "new-from-scratch" }, - note: "This is a greenfield product.", + answer: { optionId: 'new-from-scratch' }, + note: 'This is a greenfield product.', }, - }) + }); expect(response).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 54, result: { - status: "accepted", + status: 'accepted', exchangeId, answer: { - optionId: "new-from-scratch", - label: "Yes — this is new from scratch", + optionId: 'new-from-scratch', + label: 'Yes — this is new from scratch', }, - note: "This is a greenfield product.", + note: 'This is a greenfield product.', }, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 55, - method: "session.pendingExchange", + method: 'session.pendingExchange', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 55, - result: { status: "idle", exchange: null }, - }) + result: { status: 'idle', exchange: null }, + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 56, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 56, result: { - status: "ready", + status: 'ready', exchanges: [ { promptEntryIds: [expect.any(String)], @@ -1067,759 +1047,759 @@ describe("JSON-RPC handlers", () => { }, ], }, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 57, - method: "session.transcriptDisplay", + method: 'session.transcriptDisplay', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 57, result: { rows: [ { - role: "prompt", - text: expect.stringContaining("new product or feature"), + role: 'prompt', + text: expect.stringContaining('new product or feature'), }, { - role: "user", - text: expect.stringContaining("Yes — this is new from scratch"), + role: 'user', + text: expect.stringContaining('Yes — this is new from scratch'), }, ], }, - }) + }); - const sessionText = await readFile(workspace.session.file, "utf8") - expect(sessionText).toContain("brunch.structured_exchange.request") - expect(sessionText).toContain("request_choice") - expect(sessionText).toContain("This is a greenfield product.") - }) + const sessionText = await readFile(workspace.session.file, 'utf8'); + expect(sessionText).toContain('brunch.structured_exchange.request'); + expect(sessionText).toContain('request_choice'); + expect(sessionText).toContain('This is a greenfield product.'); + }); - it("responds to deterministic text and multi-choice tuple exchanges", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-respond-modes-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('responds to deterministic text and multi-choice tuple exchanges', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-modes-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Respond modes spec", - }) + specTitle: 'Respond modes spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const first = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 250, - method: "session.startElicitation", - }) - const firstExchangeId = (first as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId + method: 'session.startElicitation', + }); + const firstExchangeId = ( + first as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 251, - method: "elicitation.respond", + method: 'elicitation.respond', params: { exchangeId: firstExchangeId, - answer: { optionId: "new-from-scratch" }, + answer: { optionId: 'new-from-scratch' }, }, - }) + }); const textStart = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 252, - method: "session.startElicitation", - }) + method: 'session.startElicitation', + }); expect(textStart).toMatchObject({ result: { exchange: { - mode: "text", - exchangeId: "deterministic-grounding-text-2", + mode: 'text', + exchangeId: 'deterministic-grounding-text-2', }, }, - }) - const textExchangeId = (textStart as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId + }); + const textExchangeId = ( + textStart as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 253, - method: "elicitation.respond", + method: 'elicitation.respond', params: { exchangeId: textExchangeId, - answer: { text: "A local product specification workspace." }, + answer: { text: 'A local product specification workspace.' }, }, }), ).resolves.toMatchObject({ result: { - status: "accepted", - answer: { text: "A local product specification workspace." }, + status: 'accepted', + answer: { text: 'A local product specification workspace.' }, }, - }) + }); const multiStart = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 254, - method: "session.startElicitation", - }) + method: 'session.startElicitation', + }); expect(multiStart).toMatchObject({ result: { exchange: { - mode: "multi-select", - exchangeId: "deterministic-grounding-multi-3", + mode: 'multi-select', + exchangeId: 'deterministic-grounding-multi-3', }, }, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 255, - method: "elicitation.respond", + method: 'elicitation.respond', params: { - exchangeId: "deterministic-grounding-multi-3", - answer: { optionIds: ["transcript", "other"] }, - note: "Also verify friction reporting.", + exchangeId: 'deterministic-grounding-multi-3', + answer: { optionIds: ['transcript', 'other'] }, + note: 'Also verify friction reporting.', }, }), ).resolves.toMatchObject({ result: { - status: "accepted", - answer: { optionIds: ["transcript", "other"] }, + status: 'accepted', + answer: { optionIds: ['transcript', 'other'] }, }, - }) - - const sessionText = await readFile(workspace.session.file, "utf8") - expect(sessionText).toContain("request_answer") - expect(sessionText).toContain("request_choices") - expect(sessionText).not.toContain("brunch.elicitation_prompt") - expect(sessionText).not.toContain("brunch.elicitation_response") - }) - - it("rejects mismatched elicitation response ids without appending transcript entries", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-respond-bad-id-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + }); + + const sessionText = await readFile(workspace.session.file, 'utf8'); + expect(sessionText).toContain('request_answer'); + expect(sessionText).toContain('request_choices'); + expect(sessionText).not.toContain('brunch.elicitation_prompt'); + expect(sessionText).not.toContain('brunch.elicitation_response'); + }); + + it('rejects mismatched elicitation response ids without appending transcript entries', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-bad-id-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Bad id spec", - }) + specTitle: 'Bad id spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 58, - method: "session.startElicitation", - }) - const before = await readFile(workspace.session.file, "utf8") + method: 'session.startElicitation', + }); + const before = await readFile(workspace.session.file, 'utf8'); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 59, - method: "elicitation.respond", + method: 'elicitation.respond', params: { - exchangeId: "not-current", - answer: { optionId: "new-from-scratch" }, + exchangeId: 'not-current', + answer: { optionId: 'new-from-scratch' }, }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 59, error: { code: -32006, - message: "Pending elicitation exchange does not match request", + message: 'Pending elicitation exchange does not match request', }, - }) - await expect(readFile(workspace.session.file, "utf8")).resolves.toBe(before) - }) + }); + await expect(readFile(workspace.session.file, 'utf8')).resolves.toBe(before); + }); - it("rejects unknown elicitation option ids without appending transcript entries", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-respond-bad-option-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('rejects unknown elicitation option ids without appending transcript entries', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-bad-option-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Bad option spec", - }) + specTitle: 'Bad option spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const start = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 60, - method: "session.startElicitation", - }) - const exchangeId = (start as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId - const before = await readFile(workspace.session.file, "utf8") + method: 'session.startElicitation', + }); + const exchangeId = ( + start as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; + const before = await readFile(workspace.session.file, 'utf8'); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 61, - method: "elicitation.respond", - params: { exchangeId, answer: { optionId: "missing-option" } }, + method: 'elicitation.respond', + params: { exchangeId, answer: { optionId: 'missing-option' } }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 61, - error: { code: -32007, message: "Invalid elicitation option" }, - }) - await expect(readFile(workspace.session.file, "utf8")).resolves.toBe(before) - }) - - it("guards duplicate elicitation responses without appending transcript entries", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-respond-duplicate-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + error: { code: -32007, message: 'Invalid elicitation option' }, + }); + await expect(readFile(workspace.session.file, 'utf8')).resolves.toBe(before); + }); + + it('guards duplicate elicitation responses without appending transcript entries', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-respond-duplicate-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Duplicate spec", - }) + specTitle: 'Duplicate spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const start = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 62, - method: "session.startElicitation", - }) - const exchangeId = (start as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId + method: 'session.startElicitation', + }); + const exchangeId = ( + start as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId; await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 63, - method: "elicitation.respond", - params: { exchangeId, answer: { optionId: "existing-codebase" } }, - }) - const before = await readFile(workspace.session.file, "utf8") + method: 'elicitation.respond', + params: { exchangeId, answer: { optionId: 'existing-codebase' } }, + }); + const before = await readFile(workspace.session.file, 'utf8'); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 64, - method: "elicitation.respond", - params: { exchangeId, answer: { optionId: "existing-codebase" } }, + method: 'elicitation.respond', + params: { exchangeId, answer: { optionId: 'existing-codebase' } }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 64, - error: { code: -32008, message: "No pending elicitation exchange" }, - }) - await expect(readFile(workspace.session.file, "utf8")).resolves.toBe(before) - }) - - it("resumes an open deterministic elicitation prompt without duplicating transcript entries", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-resume-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + error: { code: -32008, message: 'No pending elicitation exchange' }, + }); + await expect(readFile(workspace.session.file, 'utf8')).resolves.toBe(before); + }); + + it('resumes an open deterministic elicitation prompt without duplicating transcript entries', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-resume-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Resume spec", - }) + specTitle: 'Resume spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); const first = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 43, - method: "session.startElicitation", - }) - const before = await readFile(workspace.session.file, "utf8") + method: 'session.startElicitation', + }); + const before = await readFile(workspace.session.file, 'utf8'); const second = await handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 44, - method: "session.startElicitation", - }) - const after = await readFile(workspace.session.file, "utf8") + method: 'session.startElicitation', + }); + const after = await readFile(workspace.session.file, 'utf8'); expect(second).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 44, result: { - status: "pending", + status: 'pending', exchange: { - exchangeId: (first as { - result: { exchange: { exchangeId: string } } - }).result.exchange.exchangeId, + exchangeId: ( + first as { + result: { exchange: { exchangeId: string } }; + } + ).result.exchange.exchangeId, }, }, - }) - expect(after).toBe(before) - }) + }); + expect(after).toBe(before); + }); - it("returns a product-shaped no-session error when starting elicitation without a selected session", async () => { + it('returns a product-shaped no-session error when starting elicitation without a selected session', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 45, - method: "session.startElicitation", + method: 'session.startElicitation', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 45, - error: { code: -32001, message: "No selected Brunch session" }, - }) - }) + error: { code: -32001, message: 'No selected Brunch session' }, + }); + }); - it("returns a product-shaped error for non-linear selected sessions", async () => { - const sessionFile = await createBranchedSessionFile() + it('returns a product-shaped error for non-linear selected sessions', async () => { + const sessionFile = await createBranchedSessionFile(); const handlers = createRpcHandlers({ coordinator: coordinator(readyState(sessionFile)), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 8, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 8, error: { code: -32002, - message: "Selected Brunch session transcript is non-linear", + message: 'Selected Brunch session transcript is non-linear', }, - }) - }) + }); + }); - it("serves session elicitation exchanges by durable session id without opening the selected workspace session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-explicit-session-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('serves session elicitation exchanges by durable session id without opening the selected workspace session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-explicit-session-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinatorInstance.createSetupSession({ - specTitle: "Explicit spec", - }) - first.session.manager.appendMessage(assistantMessage("First question")) - first.session.manager.appendMessage(userMessage("First answer")) - const second = await coordinatorInstance.createSetupSessionForCurrentSpec() - if (second.status !== "ready") { - throw new Error("expected a ready second session") + specTitle: 'Explicit spec', + }); + first.session.manager.appendMessage(assistantMessage('First question')); + first.session.manager.appendMessage(userMessage('First answer')); + const second = await coordinatorInstance.createSetupSessionForCurrentSpec(); + if (second.status !== 'ready') { + throw new Error('expected a ready second session'); } const handlers = createRpcHandlers({ coordinator: { ...coordinatorInstance, async openDefaultWorkspace() { - throw new Error("explicit reads must not open selected session") + throw new Error('explicit reads must not open selected session'); }, }, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 9, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', params: { sessionId: first.session.id }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 9, result: { - status: "ready", + status: 'ready', exchanges: [{ promptEntryIds: [expect.any(String)] }], }, - }) - }) + }); + }); - it("serves transcript display rows by durable session id without opening the selected workspace session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-display-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('serves transcript display rows by durable session id without opening the selected workspace session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-display-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Display spec", - }) - workspace.session.manager.appendMessage( - assistantMessage("Display question"), - ) - workspace.session.manager.appendMessage(userMessage("Display answer")) + specTitle: 'Display spec', + }); + workspace.session.manager.appendMessage(assistantMessage('Display question')); + workspace.session.manager.appendMessage(userMessage('Display answer')); const handlers = createRpcHandlers({ coordinator: { ...coordinatorInstance, async openDefaultWorkspace() { - throw new Error("explicit reads must not open selected session") + throw new Error('explicit reads must not open selected session'); }, }, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 13, - method: "session.transcriptDisplay", + method: 'session.transcriptDisplay', params: { sessionId: workspace.session.id, specId: workspace.spec.id }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 13, result: { rows: [ - { role: "assistant", text: "Display question" }, - { role: "user", text: "Display answer" }, + { role: 'assistant', text: 'Display question' }, + { role: 'user', text: 'Display answer' }, ], }, - }) - }) + }); + }); - it("validates explicit session projection against a requested spec id", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-explicit-spec-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('validates explicit session projection against a requested spec id', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-explicit-spec-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Explicit spec", - }) + specTitle: 'Explicit spec', + }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 10, - method: "session.elicitationExchanges", - params: { sessionId: workspace.session.id, specId: "spec-other" }, + method: 'session.elicitationExchanges', + params: { sessionId: workspace.session.id, specId: 'spec-other' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 10, error: { code: -32003, - message: "Brunch session does not belong to requested spec", + message: 'Brunch session does not belong to requested spec', }, - }) - }) + }); + }); - it("returns a product-shaped error for explicit sessions with duplicate durable bindings", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-duplicate-binding-")) + it('returns a product-shaped error for explicit sessions with duplicate durable bindings', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-duplicate-binding-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), sessionBindingEntry(), - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 16, - method: "session.elicitationExchanges", - params: { sessionId: "session-1" }, + method: 'session.elicitationExchanges', + params: { sessionId: 'session-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 16, error: { code: -32005, - message: "Brunch session self-description is invalid", + message: 'Brunch session self-description is invalid', }, - }) - }) + }); + }); - it("returns a product-shaped error for explicit sessions without exactly one Pi header", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-invalid-header-")) + it('returns a product-shaped error for explicit sessions without exactly one Pi header', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-invalid-header-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-1", cwd }, - { type: "session", id: "session-1", cwd }, + { type: 'session', id: 'session-1', cwd }, + { type: 'session', id: 'session-1', cwd }, sessionBindingEntry(), - ]) + ]); const handlers = createRpcHandlers({ coordinator: coordinator(), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 17, - method: "session.transcriptDisplay", - params: { sessionId: "session-1" }, + method: 'session.transcriptDisplay', + params: { sessionId: 'session-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 17, error: { code: -32005, - message: "Brunch session self-description is invalid", + message: 'Brunch session self-description is invalid', }, - }) + }); - const headerlessCwd = await mkdtemp( - join(tmpdir(), "brunch-rpc-missing-header-"), - ) - await writeExplicitSessionFixture(headerlessCwd, [sessionBindingEntry()]) + const headerlessCwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-missing-header-')); + await writeExplicitSessionFixture(headerlessCwd, [sessionBindingEntry()]); const headerlessHandlers = createRpcHandlers({ coordinator: coordinator(), cwd: headerlessCwd, - }) + }); await expect( headerlessHandlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 19, - method: "session.transcriptDisplay", - params: { sessionId: "session-1" }, + method: 'session.transcriptDisplay', + params: { sessionId: 'session-1' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 19, error: { code: -32005, - message: "Brunch session self-description is invalid", + message: 'Brunch session self-description is invalid', }, - }) - }) + }); + }); - it("returns a product-shaped error when explicit binding and Pi header session ids disagree", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-header-mismatch-")) + it('returns a product-shaped error when explicit binding and Pi header session ids disagree', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-header-mismatch-')); await writeExplicitSessionFixture(cwd, [ - { type: "session", id: "session-header", cwd }, - sessionBindingEntry("session-binding"), - ]) + { type: 'session', id: 'session-header', cwd }, + sessionBindingEntry('session-binding'), + ]); const handlers = createRpcHandlers({ coordinator: coordinator(), cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 18, - method: "session.elicitationExchanges", - params: { sessionId: "session-binding" }, + method: 'session.elicitationExchanges', + params: { sessionId: 'session-binding' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 18, error: { code: -32005, - message: "Brunch session self-description is invalid", + message: 'Brunch session self-description is invalid', }, - }) - }) + }); + }); - it("returns a product-shaped error for unknown explicit sessions", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-missing-session-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) - await coordinatorInstance.createSetupSession({ specTitle: "Explicit spec" }) + it('returns a product-shaped error for unknown explicit sessions', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-missing-session-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); + await coordinatorInstance.createSetupSession({ specTitle: 'Explicit spec' }); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 11, - method: "session.elicitationExchanges", - params: { sessionId: "session-does-not-exist" }, + method: 'session.elicitationExchanges', + params: { sessionId: 'session-does-not-exist' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 11, error: { code: -32004, - message: "Brunch session not found", + message: 'Brunch session not found', }, - }) - }) + }); + }); - it("returns a product-shaped error for non-linear explicit sessions", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-rpc-explicit-branch-")) - const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }) + it('returns a product-shaped error for non-linear explicit sessions', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-explicit-branch-')); + const coordinatorInstance = createWorkspaceSessionCoordinator({ cwd }); const workspace = await coordinatorInstance.createSetupSession({ - specTitle: "Explicit branch spec", - }) - const manager = SessionManager.open(workspace.session.file) - manager.appendMessage(assistantMessage("Abandoned prompt")) - manager.appendMessage(userMessage("Abandoned answer")) - manager.resetLeaf() - manager.appendMessage(assistantMessage("Active prompt")) + specTitle: 'Explicit branch spec', + }); + const manager = SessionManager.open(workspace.session.file); + manager.appendMessage(assistantMessage('Abandoned prompt')); + manager.appendMessage(userMessage('Abandoned answer')); + manager.resetLeaf(); + manager.appendMessage(assistantMessage('Active prompt')); const handlers = createRpcHandlers({ coordinator: coordinatorInstance, cwd, - }) + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 12, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', params: { sessionId: workspace.session.id }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 12, error: { code: -32002, - message: "Brunch session transcript is non-linear", + message: 'Brunch session transcript is non-linear', }, - }) - }) + }); + }); - it("rejects raw file params on session elicitation exchange RPC", async () => { + it('rejects raw file params on session elicitation exchange RPC', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 4, - method: "session.elicitationExchanges", - params: { file: "/tmp/not-a-product-param.jsonl" }, + method: 'session.elicitationExchanges', + params: { file: '/tmp/not-a-product-param.jsonl' }, }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 4, - error: { code: -32602, message: "Invalid params" }, - }) - }) + error: { code: -32602, message: 'Invalid params' }, + }); + }); - it("returns a product-shaped no-session error without creating a session", async () => { + it('returns a product-shaped no-session error without creating a session', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(selectSpecState()), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 5, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 5, - error: { code: -32001, message: "No selected Brunch session" }, - }) - }) + error: { code: -32001, message: 'No selected Brunch session' }, + }); + }); - it("rejects invalid request id shapes", async () => { + it('rejects invalid request id shapes', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); await expect( handlers.handle({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: { bad: true }, - method: "workspace.snapshot", + method: 'workspace.snapshot', }), ).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: null, - error: { code: -32600, message: "Invalid Request" }, - }) - }) + error: { code: -32600, message: 'Invalid Request' }, + }); + }); - it("returns structured errors for unknown methods", async () => { + it('returns structured errors for unknown methods', async () => { const handlers = createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", - }) + cwd: '/tmp/brunch-project', + }); - await expect( - handlers.handle({ jsonrpc: "2.0", id: 2, method: "records.list" }), - ).resolves.toMatchObject({ - jsonrpc: "2.0", + await expect(handlers.handle({ jsonrpc: '2.0', id: 2, method: 'records.list' })).resolves.toMatchObject({ + jsonrpc: '2.0', id: 2, - error: { code: -32601, message: "Method not found" }, - }) - }) + error: { code: -32601, message: 'Method not found' }, + }); + }); - it("returns parse errors over newline-delimited JSON-RPC streams", async () => { - const input = new PassThrough() - const output = new PassThrough() - const chunks: string[] = [] - output.on("data", (chunk) => chunks.push(String(chunk))) + it('returns parse errors over newline-delimited JSON-RPC streams', async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const chunks: string[] = []; + output.on('data', (chunk) => chunks.push(String(chunk))); const done = runJsonRpcLineServer({ input, output, handlers: createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', }), - }) + }); - input.end("not json\n") - await done + input.end('not json\n'); + await done; - expect(JSON.parse(chunks.join(""))).toEqual({ - jsonrpc: "2.0", + expect(JSON.parse(chunks.join(''))).toEqual({ + jsonrpc: '2.0', id: null, - error: { code: -32700, message: "Parse error" }, - }) - }) + error: { code: -32700, message: 'Parse error' }, + }); + }); - it("returns internal errors for thrown newline-delimited JSON-RPC handlers", async () => { - const input = new PassThrough() - const output = new PassThrough() - const chunks: string[] = [] - output.on("data", (chunk) => chunks.push(String(chunk))) + it('returns internal errors for thrown newline-delimited JSON-RPC handlers', async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const chunks: string[] = []; + output.on('data', (chunk) => chunks.push(String(chunk))); const done = runJsonRpcLineServer({ input, output, handlers: { async handle() { - throw new Error("boom") + throw new Error('boom'); }, }, - }) + }); - input.end( - `${JSON.stringify({ jsonrpc: "2.0", id: 15, method: "workspace.snapshot" })}\n`, - ) - await done + input.end(`${JSON.stringify({ jsonrpc: '2.0', id: 15, method: 'workspace.snapshot' })}\n`); + await done; - expect(JSON.parse(chunks.join(""))).toEqual({ - jsonrpc: "2.0", + expect(JSON.parse(chunks.join(''))).toEqual({ + jsonrpc: '2.0', id: 15, - error: { code: -32603, message: "Internal error" }, - }) - }) + error: { code: -32603, message: 'Internal error' }, + }); + }); - it("speaks newline-delimited JSON-RPC over streams", async () => { - const input = new PassThrough() - const output = new PassThrough() - const chunks: string[] = [] - output.on("data", (chunk) => chunks.push(String(chunk))) + it('speaks newline-delimited JSON-RPC over streams', async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const chunks: string[] = []; + output.on('data', (chunk) => chunks.push(String(chunk))); const done = runJsonRpcLineServer({ input, output, handlers: createRpcHandlers({ coordinator: coordinator(), - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', }), - }) + }); - input.end( - `${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "workspace.snapshot" })}\n`, - ) - await done + input.end(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'workspace.snapshot' })}\n`); + await done; - expect(JSON.parse(chunks.join(""))).toMatchObject({ - jsonrpc: "2.0", + expect(JSON.parse(chunks.join(''))).toMatchObject({ + jsonrpc: '2.0', id: 1, - result: { status: "ready" }, - }) - }) -}) + result: { status: 'ready' }, + }); + }); +}); diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index ba3ef6dee..6d8a43dd6 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -1,36 +1,26 @@ -import { createInterface } from "node:readline/promises" -import type { Readable, Writable } from "node:stream" +import { createInterface } from 'node:readline/promises'; +import type { Readable, Writable } from 'node:stream'; -import { Type, type Static } from "typebox" -import { Value } from "typebox/value" +import { Type, type Static } from 'typebox'; +import { Value } from 'typebox/value'; +import type { StructuredExchangePresentDetails } from '../.pi/extensions/structured-exchange/shared/model.js'; +import { isStructuredExchangePresentDetails } from '../.pi/extensions/structured-exchange/shared/recovery.js'; import { readBrunchSessionEnvelope, NonLinearTranscriptError, type BrunchSessionEnvelope, -} from "../brunch-session-envelope.js" +} from '../brunch-session-envelope.js'; import { projectLinearElicitationExchangeProjection, projectLinearTranscriptDisplayProjection, -} from "../elicitation-exchange.js" -import { isStructuredExchangePresentDetails } from "../.pi/extensions/structured-exchange/shared/recovery.js" -import type { StructuredExchangePresentDetails } from "../.pi/extensions/structured-exchange/shared/model.js" -import { - createJsonRpcFailure, - createJsonRpcSuccess, - isJsonRpcRequest, - jsonRpcRequestId, - dispatchJsonRpcMessage, - type JsonRpcId, - type JsonRpcRequest, - type JsonRpcResponse, -} from "./protocol.js" -import { workspaceSnapshotFromState } from "../print-snapshot.js" +} from '../elicitation-exchange.js'; +import { workspaceSnapshotFromState } from '../print-snapshot.js'; import { resolveExplicitSessionProjectionTarget, type ExplicitSessionProjectionParams, type SessionProjectionTarget, -} from "../session-projection-reader.js" +} from '../session-projection-reader.js'; import type { DefaultWorkspaceCoordinator, WorkspaceActivationState, @@ -38,158 +28,152 @@ import type { WorkspaceSessionState, SpecSessionActivationCoordinator, SpecSessionActivationDecision, -} from "../workspace-session-coordinator.js" +} from '../workspace-session-coordinator.js'; +import { + createJsonRpcFailure, + createJsonRpcSuccess, + isJsonRpcRequest, + jsonRpcRequestId, + dispatchJsonRpcMessage, + type JsonRpcId, + type JsonRpcRequest, + type JsonRpcResponse, +} from './protocol.js'; export interface RpcHandlers { - handle(request: unknown): Promise + handle(request: unknown): Promise; } export function createRpcHandlers(options: { - coordinator: DefaultWorkspaceCoordinator & SpecSessionActivationCoordinator - cwd: string + coordinator: DefaultWorkspaceCoordinator & SpecSessionActivationCoordinator; + cwd: string; }): RpcHandlers { return { async handle(request) { if (!isJsonRpcRequest(request)) { - return createJsonRpcFailure(null, -32600, "Invalid Request") + return createJsonRpcFailure(null, -32600, 'Invalid Request'); } - const requestId = jsonRpcRequestId(request) + const requestId = jsonRpcRequestId(request); - if (request.method === "rpc.discover") { + if (request.method === 'rpc.discover') { if (request.params !== undefined) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } - return createJsonRpcSuccess(requestId, discoverPublicRpcMethods()) + return createJsonRpcSuccess(requestId, discoverPublicRpcMethods()); } - if (request.method === "workspace.snapshot") { + if (request.method === 'workspace.snapshot') { if (request.params !== undefined) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } - const state = await options.coordinator.openDefaultWorkspace() - return createJsonRpcSuccess( - requestId, - workspaceSnapshotFromState(state), - ) + const state = await options.coordinator.openDefaultWorkspace(); + return createJsonRpcSuccess(requestId, workspaceSnapshotFromState(state)); } - if (request.method === "workspace.selectionState") { + if (request.method === 'workspace.selectionState') { if (request.params !== undefined) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } const [state, inventory] = await Promise.all([ options.coordinator.openDefaultWorkspace(), options.coordinator.inspectWorkspace(), - ]) - return createJsonRpcSuccess( - requestId, - workspaceSelectionStateFromInventory(state, inventory), - ) + ]); + return createJsonRpcSuccess(requestId, workspaceSelectionStateFromInventory(state, inventory)); } - if (request.method === "workspace.activate") { - const decision = parseWorkspaceActivationParams(request.params) + if (request.method === 'workspace.activate') { + const decision = parseWorkspaceActivationParams(request.params); if (!decision.ok) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } - const state = await options.coordinator.activateWorkspace( - decision.value, - ) - return createJsonRpcSuccess( - requestId, - workspaceActivationSnapshotFromState(state), - ) + const state = await options.coordinator.activateWorkspace(decision.value); + return createJsonRpcSuccess(requestId, workspaceActivationSnapshotFromState(state)); } - if (request.method === "session.startElicitation") { + if (request.method === 'session.startElicitation') { if (request.params !== undefined) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } - return handleStartElicitation(requestId, options) + return handleStartElicitation(requestId, options); } - if (request.method === "session.pendingExchange") { - return handleSessionProjection( - requestId, - request.params, - options, - projectPendingElicitationExchange, - ) + if (request.method === 'session.pendingExchange') { + return handleSessionProjection(requestId, request.params, options, projectPendingElicitationExchange); } - if (request.method === "elicitation.respond") { - return handleRespondToElicitation(requestId, request.params, options) + if (request.method === 'elicitation.respond') { + return handleRespondToElicitation(requestId, request.params, options); } - if (request.method === "session.elicitationExchanges") { + if (request.method === 'session.elicitationExchanges') { return handleSessionProjection( requestId, request.params, options, projectLinearElicitationExchangeProjection, - ) + ); } - if (request.method === "session.transcriptDisplay") { + if (request.method === 'session.transcriptDisplay') { return handleSessionProjection( requestId, request.params, options, projectLinearTranscriptDisplayProjection, - ) + ); } - return createJsonRpcFailure(requestId, -32601, "Method not found") + return createJsonRpcFailure(requestId, -32601, 'Method not found'); }, - } + }; } function workspaceSelectionStateFromInventory( state: WorkspaceSessionState, inventory: WorkspaceLaunchInventory, ): WorkspaceLaunchInventory & { - status: WorkspaceSessionState["status"] - requiresSelection: boolean + status: WorkspaceSessionState['status']; + requiresSelection: boolean; } { return { ...inventory, status: state.status, - requiresSelection: state.status !== "ready", - } + requiresSelection: state.status !== 'ready', + }; } -function workspaceActivationSnapshotFromState( - state: WorkspaceActivationState, -): ReturnType | { - status: "cancelled" - cwd: string - spec: WorkspaceActivationState["chrome"]["spec"] - chrome: { - phase: "select_spec" | "elicitation" - chatMode: "select-spec" | "responding-to-elicitation" - } -} { - if (state.status === "cancelled") { +function workspaceActivationSnapshotFromState(state: WorkspaceActivationState): + | ReturnType + | { + status: 'cancelled'; + cwd: string; + spec: WorkspaceActivationState['chrome']['spec']; + chrome: { + phase: 'select_spec' | 'elicitation'; + chatMode: 'select-spec' | 'responding-to-elicitation'; + }; + } { + if (state.status === 'cancelled') { return { - status: "cancelled", + status: 'cancelled', cwd: state.cwd, spec: state.chrome.spec, chrome: { phase: state.chrome.phase, chatMode: state.chrome.chatMode, }, - } + }; } - return workspaceSnapshotFromState(state) + return workspaceSnapshotFromState(state); } -const NonBlankStringSchema = Type.String({ minLength: 1, pattern: "\\S" }) +const NonBlankStringSchema = Type.String({ minLength: 1, pattern: '\\S' }); export const SpecSessionActivationDecisionSchema = Type.Union([ Type.Object( { - action: Type.Literal("continue"), + action: Type.Literal('continue'), specId: NonBlankStringSchema, sessionFile: NonBlankStringSchema, }, @@ -197,7 +181,7 @@ export const SpecSessionActivationDecisionSchema = Type.Union([ ), Type.Object( { - action: Type.Literal("openSession"), + action: Type.Literal('openSession'), specId: NonBlankStringSchema, sessionFile: NonBlankStringSchema, }, @@ -205,36 +189,36 @@ export const SpecSessionActivationDecisionSchema = Type.Union([ ), Type.Object( { - action: Type.Literal("newSession"), + action: Type.Literal('newSession'), specId: NonBlankStringSchema, }, { additionalProperties: false }, ), Type.Object( { - action: Type.Literal("newSpec"), + action: Type.Literal('newSpec'), title: NonBlankStringSchema, }, { additionalProperties: false }, ), Type.Object( { - action: Type.Literal("cancel"), + action: Type.Literal('cancel'), }, { additionalProperties: false }, ), -]) +]); const WorkspaceActivationParamsSchema = Type.Object( { decision: SpecSessionActivationDecisionSchema, }, { additionalProperties: false }, -) +); -type WorkspaceActivationParams = Static +type WorkspaceActivationParams = Static; -const NoParamsSchema = Type.Void({ description: "Omit JSON-RPC params." }) +const NoParamsSchema = Type.Void({ description: 'Omit JSON-RPC params.' }); const WorkspaceSnapshotResultSchema = Type.Object( { @@ -242,14 +226,17 @@ const WorkspaceSnapshotResultSchema = Type.Object( cwd: Type.String(), spec: Type.Union([ Type.Null(), - Type.Object({ id: Type.String(), title: Type.String() }, { - additionalProperties: true, - }), + Type.Object( + { id: Type.String(), title: Type.String() }, + { + additionalProperties: true, + }, + ), ]), chrome: Type.Object({}, { additionalProperties: true }), }, { additionalProperties: true }, -) +); const WorkspaceSelectionStateResultSchema = Type.Object( { @@ -257,42 +244,37 @@ const WorkspaceSelectionStateResultSchema = Type.Object( requiresSelection: Type.Boolean(), cwd: Type.String(), specs: Type.Array(Type.Object({}, { additionalProperties: true })), - unavailableSessions: Type.Array( - Type.Object({}, { additionalProperties: true }), - ), + unavailableSessions: Type.Array(Type.Object({}, { additionalProperties: true })), }, { additionalProperties: true }, -) +); const WorkspaceActivationResultSchema = Type.Union([ WorkspaceSnapshotResultSchema, Type.Object( { - status: Type.Literal("cancelled"), + status: Type.Literal('cancelled'), cwd: Type.String(), spec: Type.Union([ Type.Null(), - Type.Object({ id: Type.String(), title: Type.String() }, { - additionalProperties: true, - }), + Type.Object( + { id: Type.String(), title: Type.String() }, + { + additionalProperties: true, + }, + ), ]), chrome: Type.Object( { - phase: Type.Union([ - Type.Literal("select_spec"), - Type.Literal("elicitation"), - ]), - chatMode: Type.Union([ - Type.Literal("select-spec"), - Type.Literal("responding-to-elicitation"), - ]), + phase: Type.Union([Type.Literal('select_spec'), Type.Literal('elicitation')]), + chatMode: Type.Union([Type.Literal('select-spec'), Type.Literal('responding-to-elicitation')]), }, { additionalProperties: false }, ), }, { additionalProperties: false }, ), -]) +]); const SessionProjectionParamsSchema = Type.Object( { @@ -300,7 +282,7 @@ const SessionProjectionParamsSchema = Type.Object( specId: Type.Optional(NonBlankStringSchema), }, { additionalProperties: false }, -) +); const ElicitationExchangesResultSchema = Type.Object( { @@ -308,24 +290,20 @@ const ElicitationExchangesResultSchema = Type.Object( exchanges: Type.Array(Type.Object({}, { additionalProperties: true })), }, { additionalProperties: true }, -) +); const TranscriptDisplayResultSchema = Type.Object( { rows: Type.Array(Type.Object({}, { additionalProperties: true })), }, { additionalProperties: true }, -) +); const PendingElicitationExchangeSchema = Type.Object( { exchangeId: NonBlankStringSchema, - lens: Type.Literal("step-by-step"), - mode: Type.Union([ - Type.Literal("text"), - Type.Literal("single-select"), - Type.Literal("multi-select"), - ]), + lens: Type.Literal('step-by-step'), + mode: Type.Union([Type.Literal('text'), Type.Literal('single-select'), Type.Literal('multi-select')]), prompt: NonBlankStringSchema, details: Type.Optional(NonBlankStringSchema), options: Type.Array( @@ -339,42 +317,51 @@ const PendingElicitationExchangeSchema = Type.Object( { additionalProperties: false }, ), ), - note: Type.Object({ allowed: Type.Boolean() }, { - additionalProperties: false, - }), + note: Type.Object( + { allowed: Type.Boolean() }, + { + additionalProperties: false, + }, + ), }, { additionalProperties: false }, -) +); const StartElicitationResultSchema = Type.Object( { - status: Type.Literal("pending"), + status: Type.Literal('pending'), exchange: PendingElicitationExchangeSchema, }, { additionalProperties: false }, -) +); const PendingExchangeResultSchema = Type.Union([ StartElicitationResultSchema, Type.Object( { - status: Type.Literal("idle"), + status: Type.Literal('idle'), exchange: Type.Null(), }, { additionalProperties: false }, ), -]) +]); const ElicitationRespondParamsSchema = Type.Object( { exchangeId: NonBlankStringSchema, answer: Type.Union([ - Type.Object({ text: NonBlankStringSchema }, { - additionalProperties: false, - }), - Type.Object({ optionId: NonBlankStringSchema }, { - additionalProperties: false, - }), + Type.Object( + { text: NonBlankStringSchema }, + { + additionalProperties: false, + }, + ), + Type.Object( + { optionId: NonBlankStringSchema }, + { + additionalProperties: false, + }, + ), Type.Object( { optionIds: Type.Array(NonBlankStringSchema, { minItems: 1 }) }, { additionalProperties: false }, @@ -383,541 +370,497 @@ const ElicitationRespondParamsSchema = Type.Object( note: Type.Optional(Type.String()), }, { additionalProperties: false }, -) +); const ElicitationRespondResultSchema = Type.Object( { - status: Type.Literal("accepted"), + status: Type.Literal('accepted'), exchangeId: NonBlankStringSchema, answer: Type.Object({}, { additionalProperties: true }), note: Type.Optional(Type.String()), }, { additionalProperties: false }, -) +); -type ElicitationRespondParams = Static -type ElicitationRespondResult = Static +type ElicitationRespondParams = Static; +type ElicitationRespondResult = Static; type RpcMethodDiscovery = { - method: string - description: string - paramsSchema: unknown - resultSchema: unknown - examples: JsonRpcRequest[] -} + method: string; + description: string; + paramsSchema: unknown; + resultSchema: unknown; + examples: JsonRpcRequest[]; +}; function discoverPublicRpcMethods(): { methods: RpcMethodDiscovery[] } { - return { methods: PUBLIC_RPC_METHOD_DISCOVERY } + return { methods: PUBLIC_RPC_METHOD_DISCOVERY }; } const PUBLIC_RPC_METHOD_DISCOVERY: RpcMethodDiscovery[] = [ { - method: "rpc.discover", + method: 'rpc.discover', description: - "List the public Brunch JSON-RPC methods supported by this host with schemas and example calls.", + 'List the public Brunch JSON-RPC methods supported by this host with schemas and example calls.', paramsSchema: NoParamsSchema, resultSchema: Type.Object( { methods: Type.Array(Type.Object({}, { additionalProperties: true })) }, { additionalProperties: false }, ), - examples: [{ jsonrpc: "2.0", id: 1, method: "rpc.discover" }], + examples: [{ jsonrpc: '2.0', id: 1, method: 'rpc.discover' }], }, { - method: "workspace.snapshot", + method: 'workspace.snapshot', description: - "Return the current Brunch workspace/spec/session snapshot for the invocation cwd without changing activation state.", + 'Return the current Brunch workspace/spec/session snapshot for the invocation cwd without changing activation state.', paramsSchema: NoParamsSchema, resultSchema: WorkspaceSnapshotResultSchema, - examples: [{ jsonrpc: "2.0", id: 2, method: "workspace.snapshot" }], + examples: [{ jsonrpc: '2.0', id: 2, method: 'workspace.snapshot' }], }, { - method: "workspace.selectionState", + method: 'workspace.selectionState', description: - "Return the product-shaped workspace inventory and whether the client must choose or create a spec/session before an agent loop can run.", + 'Return the product-shaped workspace inventory and whether the client must choose or create a spec/session before an agent loop can run.', paramsSchema: NoParamsSchema, resultSchema: WorkspaceSelectionStateResultSchema, - examples: [{ jsonrpc: "2.0", id: 3, method: "workspace.selectionState" }], + examples: [{ jsonrpc: '2.0', id: 3, method: 'workspace.selectionState' }], }, { - method: "workspace.activate", + method: 'workspace.activate', description: - "Apply an explicit workspace→spec→session activation decision such as continuing, opening a session, creating a session, creating a spec, or cancelling.", + 'Apply an explicit workspace→spec→session activation decision such as continuing, opening a session, creating a session, creating a spec, or cancelling.', paramsSchema: WorkspaceActivationParamsSchema, resultSchema: WorkspaceActivationResultSchema, examples: [ { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 4, - method: "workspace.activate", - params: { decision: { action: "newSpec", title: "POC spec" } }, + method: 'workspace.activate', + params: { decision: { action: 'newSpec', title: 'POC spec' } }, }, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 5, - method: "workspace.activate", + method: 'workspace.activate', params: { decision: { - action: "openSession", - specId: "spec-1", - sessionFile: ".brunch/sessions/session-1.jsonl", + action: 'openSession', + specId: 'spec-1', + sessionFile: '.brunch/sessions/session-1.jsonl', }, }, }, ], }, { - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', description: - "Project structured elicitation exchanges from the selected or explicitly named linear Brunch session transcript.", + 'Project structured elicitation exchanges from the selected or explicitly named linear Brunch session transcript.', paramsSchema: SessionProjectionParamsSchema, resultSchema: ElicitationExchangesResultSchema, examples: [ { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 6, - method: "session.elicitationExchanges", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.elicitationExchanges', + params: { sessionId: 'session-1', specId: 'spec-1' }, }, ], }, { - method: "session.transcriptDisplay", + method: 'session.transcriptDisplay', description: - "Project transcript display rows from the selected or explicitly named linear Brunch session transcript.", + 'Project transcript display rows from the selected or explicitly named linear Brunch session transcript.', paramsSchema: SessionProjectionParamsSchema, resultSchema: TranscriptDisplayResultSchema, examples: [ { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 7, - method: "session.transcriptDisplay", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.transcriptDisplay', + params: { sessionId: 'session-1', specId: 'spec-1' }, }, ], }, { - method: "session.startElicitation", + method: 'session.startElicitation', description: "Start or resume the selected session's deterministic structured-exchange permutation loop and return the current pending exchange.", paramsSchema: NoParamsSchema, resultSchema: StartElicitationResultSchema, - examples: [{ jsonrpc: "2.0", id: 8, method: "session.startElicitation" }], + examples: [{ jsonrpc: '2.0', id: 8, method: 'session.startElicitation' }], }, { - method: "session.pendingExchange", + method: 'session.pendingExchange', description: - "Read the current transcript-backed pending elicitation exchange from the selected or explicitly named linear Brunch session.", + 'Read the current transcript-backed pending elicitation exchange from the selected or explicitly named linear Brunch session.', paramsSchema: SessionProjectionParamsSchema, resultSchema: PendingExchangeResultSchema, examples: [ - { jsonrpc: "2.0", id: 9, method: "session.pendingExchange" }, + { jsonrpc: '2.0', id: 9, method: 'session.pendingExchange' }, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 10, - method: "session.pendingExchange", - params: { sessionId: "session-1", specId: "spec-1" }, + method: 'session.pendingExchange', + params: { sessionId: 'session-1', specId: 'spec-1' }, }, ], }, { - method: "elicitation.respond", + method: 'elicitation.respond', description: "Submit a text, single-choice, or multi-choice answer for the selected session's current deterministic tuple-shaped pending structured exchange.", paramsSchema: ElicitationRespondParamsSchema, resultSchema: ElicitationRespondResultSchema, examples: [ { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 11, - method: "elicitation.respond", + method: 'elicitation.respond', params: { - exchangeId: "deterministic-grounding-choice", - answer: { optionId: "new-from-scratch" }, - note: "This is a greenfield product.", + exchangeId: 'deterministic-grounding-choice', + answer: { optionId: 'new-from-scratch' }, + note: 'This is a greenfield product.', }, }, ], }, -] +]; -type WorkspaceActivationParamsParseResult = { - ok: true - value: SpecSessionActivationDecision -} | { ok: false } +type WorkspaceActivationParamsParseResult = + | { + ok: true; + value: SpecSessionActivationDecision; + } + | { ok: false }; -function parseWorkspaceActivationParams( - value: unknown, -): WorkspaceActivationParamsParseResult { +function parseWorkspaceActivationParams(value: unknown): WorkspaceActivationParamsParseResult { if (!Value.Check(WorkspaceActivationParamsSchema, value)) { - return { ok: false } + return { ok: false }; } - const params: WorkspaceActivationParams = Value.Parse( - WorkspaceActivationParamsSchema, - value, - ) - return { ok: true, value: params.decision } + const params: WorkspaceActivationParams = Value.Parse(WorkspaceActivationParamsSchema, value); + return { ok: true, value: params.decision }; } async function handleSessionProjection( requestId: JsonRpcId, rawParams: unknown, options: { - coordinator: DefaultWorkspaceCoordinator - cwd: string + coordinator: DefaultWorkspaceCoordinator; + cwd: string; }, loadProjection: (envelope: BrunchSessionEnvelope) => T, ): Promise { - const params = parseSessionProjectionParams(rawParams) + const params = parseSessionProjectionParams(rawParams); if (!params.ok) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } const target = params.value ? await resolveExplicitSessionProjectionTarget(options.cwd, params.value) - : await selectedSessionFile( - await options.coordinator.openDefaultWorkspace(), - ) + : await selectedSessionFile(await options.coordinator.openDefaultWorkspace()); if (!target.ok) { - return createJsonRpcFailure(requestId, target.code, target.message) + return createJsonRpcFailure(requestId, target.code, target.message); } try { - return createJsonRpcSuccess(requestId, loadProjection(target.envelope)) + return createJsonRpcSuccess(requestId, loadProjection(target.envelope)); } catch (error) { if (error instanceof NonLinearTranscriptError) { - return createJsonRpcFailure(requestId, -32002, target.nonLinearMessage) + return createJsonRpcFailure(requestId, -32002, target.nonLinearMessage); } - throw error + throw error; } } async function handleStartElicitation( requestId: JsonRpcId, options: { - coordinator: DefaultWorkspaceCoordinator - cwd: string + coordinator: DefaultWorkspaceCoordinator; + cwd: string; }, ): Promise { - const state = await options.coordinator.openDefaultWorkspace() - if (state.status !== "ready") { - return createJsonRpcFailure(requestId, -32001, "No selected Brunch session") + const state = await options.coordinator.openDefaultWorkspace(); + if (state.status !== 'ready') { + return createJsonRpcFailure(requestId, -32001, 'No selected Brunch session'); } - const existingTarget = await selectedSessionFile(state) + const existingTarget = await selectedSessionFile(state); if (!existingTarget.ok) { - return createJsonRpcFailure( - requestId, - existingTarget.code, - existingTarget.message, - ) + return createJsonRpcFailure(requestId, existingTarget.code, existingTarget.message); } - const existing = pendingExchangeFromEnvelope(existingTarget.envelope) + const existing = pendingExchangeFromEnvelope(existingTarget.envelope); if (existing) { return createJsonRpcSuccess(requestId, { - status: "pending", + status: 'pending', exchange: existing, - }) + }); } const exchange = nextDeterministicElicitationExchange( - projectLinearElicitationExchangeProjection(existingTarget.envelope) - .exchanges.length, - ) - const manager = state.session.manager - manager.appendMessage(presentToolResultMessage(exchange)) - flushSessionEntries(manager, state.session.file) - - const reloadedTarget = await selectedSessionFile(state) + projectLinearElicitationExchangeProjection(existingTarget.envelope).exchanges.length, + ); + const manager = state.session.manager; + manager.appendMessage(presentToolResultMessage(exchange)); + flushSessionEntries(manager, state.session.file); + + const reloadedTarget = await selectedSessionFile(state); if (!reloadedTarget.ok) { - return createJsonRpcFailure( - requestId, - reloadedTarget.code, - reloadedTarget.message, - ) + return createJsonRpcFailure(requestId, reloadedTarget.code, reloadedTarget.message); } - const reloaded = pendingExchangeFromEnvelope(reloadedTarget.envelope) + const reloaded = pendingExchangeFromEnvelope(reloadedTarget.envelope); return createJsonRpcSuccess(requestId, { - status: "pending", + status: 'pending', exchange: reloaded ?? exchange, - }) + }); } async function handleRespondToElicitation( requestId: JsonRpcId, rawParams: unknown, options: { - coordinator: DefaultWorkspaceCoordinator - cwd: string + coordinator: DefaultWorkspaceCoordinator; + cwd: string; }, ): Promise { if (!Value.Check(ElicitationRespondParamsSchema, rawParams)) { - return createJsonRpcFailure(requestId, -32602, "Invalid params") + return createJsonRpcFailure(requestId, -32602, 'Invalid params'); } - const params: ElicitationRespondParams = Value.Parse( - ElicitationRespondParamsSchema, - rawParams, - ) - - const state = await options.coordinator.openDefaultWorkspace() - if (state.status !== "ready") { - return createJsonRpcFailure(requestId, -32001, "No selected Brunch session") + const params: ElicitationRespondParams = Value.Parse(ElicitationRespondParamsSchema, rawParams); + + const state = await options.coordinator.openDefaultWorkspace(); + if (state.status !== 'ready') { + return createJsonRpcFailure(requestId, -32001, 'No selected Brunch session'); } - const target = await selectedSessionFile(state) + const target = await selectedSessionFile(state); if (!target.ok) { - return createJsonRpcFailure(requestId, target.code, target.message) + return createJsonRpcFailure(requestId, target.code, target.message); } - let pending: PendingElicitationExchange | null + let pending: PendingElicitationExchange | null; try { - pending = pendingExchangeFromEnvelope(target.envelope) + pending = pendingExchangeFromEnvelope(target.envelope); } catch (error) { if (error instanceof NonLinearTranscriptError) { - return createJsonRpcFailure(requestId, -32002, target.nonLinearMessage) + return createJsonRpcFailure(requestId, -32002, target.nonLinearMessage); } - throw error + throw error; } if (!pending) { - return createJsonRpcFailure( - requestId, - -32008, - "No pending elicitation exchange", - ) + return createJsonRpcFailure(requestId, -32008, 'No pending elicitation exchange'); } if (params.exchangeId !== pending.exchangeId) { - return createJsonRpcFailure( - requestId, - -32006, - "Pending elicitation exchange does not match request", - ) + return createJsonRpcFailure(requestId, -32006, 'Pending elicitation exchange does not match request'); } - const accepted = acceptedResponseFromParams(pending, params) + const accepted = acceptedResponseFromParams(pending, params); if (!accepted.ok) { - return createJsonRpcFailure(requestId, -32007, accepted.message) + return createJsonRpcFailure(requestId, -32007, accepted.message); } const result: ElicitationRespondResult = { - status: "accepted", + status: 'accepted', exchangeId: pending.exchangeId, answer: accepted.answer, ...(params.note === undefined ? {} : { note: params.note }), - } + }; - state.session.manager.appendMessage(accepted.toolResultMessage) - flushSessionEntries(state.session.manager, state.session.file) + state.session.manager.appendMessage(accepted.toolResultMessage); + flushSessionEntries(state.session.manager, state.session.file); - return createJsonRpcSuccess(requestId, result) + return createJsonRpcSuccess(requestId, result); } interface AcceptedToolTextContent { - type: "text" - text: string + type: 'text'; + text: string; } interface AcceptedToolResultMessage { - role: "toolResult" - toolCallId: string - toolName: string - content: AcceptedToolTextContent[] - details: Record - isError: false - timestamp: 0 + role: 'toolResult'; + toolCallId: string; + toolName: string; + content: AcceptedToolTextContent[]; + details: Record; + isError: false; + timestamp: 0; } -type AcceptedResponse = { - ok: true - answer: Record - toolResultMessage: AcceptedToolResultMessage -} | { - ok: false - message: string -} +type AcceptedResponse = + | { + ok: true; + answer: Record; + toolResultMessage: AcceptedToolResultMessage; + } + | { + ok: false; + message: string; + }; function acceptedResponseFromParams( pending: PendingElicitationExchange, params: ElicitationRespondParams, ): AcceptedResponse { - if ("text" in params.answer) { - if (pending.mode !== "text") return invalidResponseMode() - const details = requestDetailsBase(pending, "request_answer") + if ('text' in params.answer) { + if (pending.mode !== 'text') return invalidResponseMode(); + const details = requestDetailsBase(pending, 'request_answer'); return { ok: true, answer: { text: params.answer.text }, toolResultMessage: { - ...toolResultMessageBase(pending, "request_answer"), - content: [ - { type: "text", text: `### Response\n\n${params.answer.text}` }, - ], + ...toolResultMessageBase(pending, 'request_answer'), + content: [{ type: 'text', text: `### Response\n\n${params.answer.text}` }], details: { ...details, answer: params.answer.text }, }, - } + }; } - if ("optionId" in params.answer) { - if (pending.mode !== "single-select") return invalidResponseMode() - const optionId = params.answer.optionId - const choice = pending.options.find((option) => option.id === optionId) - if (!choice) return { ok: false, message: "Invalid elicitation option" } - const details = requestDetailsBase(pending, "request_choice") + if ('optionId' in params.answer) { + if (pending.mode !== 'single-select') return invalidResponseMode(); + const optionId = params.answer.optionId; + const choice = pending.options.find((option) => option.id === optionId); + if (!choice) return { ok: false, message: 'Invalid elicitation option' }; + const details = requestDetailsBase(pending, 'request_choice'); if (params.note !== undefined && params.note.trim().length > 0) { - details.comment = params.note.trim() + details.comment = params.note.trim(); } return { ok: true, answer: { optionId: choice.id, label: choice.label }, toolResultMessage: { - ...toolResultMessageBase(pending, "request_choice"), - content: [ - { type: "text", text: choiceResponseMarkdown([choice], params.note) }, - ], + ...toolResultMessageBase(pending, 'request_choice'), + content: [{ type: 'text', text: choiceResponseMarkdown([choice], params.note) }], details: { ...details, choice }, }, - } + }; } - if (pending.mode !== "multi-select") return invalidResponseMode() - const selected = params.answer.optionIds.map((id) => - pending.options.find((option) => option.id === id), - ) + if (pending.mode !== 'multi-select') return invalidResponseMode(); + const selected = params.answer.optionIds.map((id) => pending.options.find((option) => option.id === id)); if (selected.some((choice) => choice === undefined)) { - return { ok: false, message: "Invalid elicitation option" } + return { ok: false, message: 'Invalid elicitation option' }; } - const choices = selected as PendingChoice[] + const choices = selected as PendingChoice[]; if ( - choices.some((choice) => choice.id === "other" || choice.id === "none") && + choices.some((choice) => choice.id === 'other' || choice.id === 'none') && (params.note === undefined || params.note.trim().length === 0) ) { return { ok: false, - message: - "Elicitation response requires a comment for Other or None selections", - } + message: 'Elicitation response requires a comment for Other or None selections', + }; } - const details = requestDetailsBase(pending, "request_choices") + const details = requestDetailsBase(pending, 'request_choices'); if (params.note !== undefined && params.note.trim().length > 0) { - details.comment = params.note.trim() + details.comment = params.note.trim(); } return { ok: true, answer: { optionIds: choices.map((choice) => choice.id), choices }, toolResultMessage: { - ...toolResultMessageBase(pending, "request_choices"), - content: [ - { type: "text", text: choiceResponseMarkdown(choices, params.note) }, - ], + ...toolResultMessageBase(pending, 'request_choices'), + content: [{ type: 'text', text: choiceResponseMarkdown(choices, params.note) }], details: { ...details, choices }, }, - } + }; } function invalidResponseMode(): AcceptedResponse { return { ok: false, - message: "Elicitation response mode does not match pending exchange", - } + message: 'Elicitation response mode does not match pending exchange', + }; } function requestDetailsBase( pending: PendingElicitationExchange, - requestTool: "request_answer" | "request_choice" | "request_choices", + requestTool: 'request_answer' | 'request_choice' | 'request_choices', ): Record { return { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, exchangeId: pending.exchangeId, requestTool, - status: "answered", + status: 'answered', respondsTo: { exchangeId: pending.exchangeId, - presentTool: - pending.mode === "text" ? "present_question" : "present_options", + presentTool: pending.mode === 'text' ? 'present_question' : 'present_options', }, createdAtToolCallId: `${pending.exchangeId}:${requestTool}`, - } + }; } function toolResultMessageBase( pending: PendingElicitationExchange, - requestTool: "request_answer" | "request_choice" | "request_choices", + requestTool: 'request_answer' | 'request_choice' | 'request_choices', ) { return { - role: "toolResult" as const, + role: 'toolResult' as const, toolCallId: `${pending.exchangeId}:${requestTool}`, toolName: requestTool, isError: false as const, timestamp: 0 as const, - } + }; } -function choiceResponseMarkdown( - choices: Array<{ label: string }>, - comment: string | undefined, -): string { - const lines = [ - "### Response", - "", - ...choices.map((choice) => `- ${choice.label}`), - ] +function choiceResponseMarkdown(choices: Array<{ label: string }>, comment: string | undefined): string { + const lines = ['### Response', '', ...choices.map((choice) => `- ${choice.label}`)]; if (comment !== undefined && comment.trim().length > 0) { - lines.push("", "Comment:", "", `> ${comment.trim()}`) + lines.push('', 'Comment:', '', `> ${comment.trim()}`); } - return lines.join("\n") + return lines.join('\n'); } interface PendingChoice { - id: string - label: string - content: string - rationale?: string + id: string; + label: string; + content: string; + rationale?: string; } -type PendingElicitationExchange = Static +type PendingElicitationExchange = Static; -function nextDeterministicElicitationExchange( - completedCount: number, -): PendingElicitationExchange { - const turnNumber = completedCount + 1 +function nextDeterministicElicitationExchange(completedCount: number): PendingElicitationExchange { + const turnNumber = completedCount + 1; const script: PendingElicitationExchange[] = [ { exchangeId: `deterministic-grounding-choice-${turnNumber}`, - lens: "step-by-step", - mode: "single-select", - prompt: "Is this a new product or feature from scratch?", - details: - "Choose the best starting context so later elicitation can ask useful follow-ups.", + lens: 'step-by-step', + mode: 'single-select', + prompt: 'Is this a new product or feature from scratch?', + details: 'Choose the best starting context so later elicitation can ask useful follow-ups.', options: [ { - id: "new-from-scratch", - label: "Yes — this is new from scratch", - content: "Start a new spec workspace from a blank slate.", - rationale: "This keeps the parity run focused on initial grounding.", + id: 'new-from-scratch', + label: 'Yes — this is new from scratch', + content: 'Start a new spec workspace from a blank slate.', + rationale: 'This keeps the parity run focused on initial grounding.', }, { - id: "existing-codebase", - label: "No — this builds on existing code", - content: "Ground the spec in existing implementation constraints.", - rationale: - "Existing code changes what the elicitor should inspect next.", + id: 'existing-codebase', + label: 'No — this builds on existing code', + content: 'Ground the spec in existing implementation constraints.', + rationale: 'Existing code changes what the elicitor should inspect next.', }, { - id: "relates-to-existing-spec", - label: "It relates to an existing spec", - content: "Connect this work to a prior specification thread.", - rationale: "Continuity matters when prior graph intent exists.", + id: 'relates-to-existing-spec', + label: 'It relates to an existing spec', + content: 'Connect this work to a prior specification thread.', + rationale: 'Continuity matters when prior graph intent exists.', }, ], note: { allowed: true }, }, { exchangeId: `deterministic-grounding-text-${turnNumber}`, - lens: "step-by-step", - mode: "text", - prompt: "What are we specifying?", + lens: 'step-by-step', + mode: 'text', + prompt: 'What are we specifying?', details: "This covers the text-answer permutation in Brunch's deterministic public-RPC structured-exchange parity proof.", options: [], @@ -925,67 +868,64 @@ function nextDeterministicElicitationExchange( }, { exchangeId: `deterministic-grounding-multi-${turnNumber}`, - lens: "step-by-step", - mode: "multi-select", - prompt: "Which proof qualities matter for this parity run?", + lens: 'step-by-step', + mode: 'multi-select', + prompt: 'Which proof qualities matter for this parity run?', details: - "Select all qualities the deterministic structured-exchange permutation proof should preserve.", + 'Select all qualities the deterministic structured-exchange permutation proof should preserve.', options: [ { - id: "transcript", - label: "Transcript fidelity", - content: "Pi JSONL keeps every present/request tuple recoverable.", - rationale: "The transcript is the durable source of truth.", + id: 'transcript', + label: 'Transcript fidelity', + content: 'Pi JSONL keeps every present/request tuple recoverable.', + rationale: 'The transcript is the durable source of truth.', }, { - id: "projection", - label: "Projection fidelity", - content: "Brunch projections preserve semantic option artifacts.", - rationale: - "Public clients depend on projected structured exchange data.", + id: 'projection', + label: 'Projection fidelity', + content: 'Brunch projections preserve semantic option artifacts.', + rationale: 'Public clients depend on projected structured exchange data.', }, { - id: "other", - label: "Other", - content: "Another proof quality should be captured in the note.", - rationale: - "Other requires a comment so the transcript stays explicit.", + id: 'other', + label: 'Other', + content: 'Another proof quality should be captured in the note.', + rationale: 'Other requires a comment so the transcript stays explicit.', }, { - id: "none", - label: "None", - content: "No additional proof qualities matter for this run.", - rationale: "None requires a comment to avoid silent dismissal.", + id: 'none', + label: 'None', + content: 'No additional proof qualities matter for this run.', + rationale: 'None requires a comment to avoid silent dismissal.', }, ], note: { allowed: true }, }, - ] - return script[completedCount % script.length]! + ]; + return script[completedCount % script.length]!; } function presentToolResultMessage(exchange: PendingElicitationExchange) { - const presentTool = - exchange.mode === "text" ? "present_question" : "present_options" + const presentTool = exchange.mode === 'text' ? 'present_question' : 'present_options'; const requestTool = - exchange.mode === "text" - ? "request_answer" - : exchange.mode === "multi-select" - ? "request_choices" - : "request_choice" - const toolCallId = `${exchange.exchangeId}:${presentTool}` + exchange.mode === 'text' + ? 'request_answer' + : exchange.mode === 'multi-select' + ? 'request_choices' + : 'request_choice'; + const toolCallId = `${exchange.exchangeId}:${presentTool}`; return { - role: "toolResult" as const, + role: 'toolResult' as const, toolCallId, toolName: presentTool, - content: [{ type: "text" as const, text: presentMarkdown(exchange) }], + content: [{ type: 'text' as const, text: presentMarkdown(exchange) }], details: { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', schemaVersion: 1, exchangeId: exchange.exchangeId, presentTool, - kind: exchange.mode === "text" ? "question" : "options", - status: "presented", + kind: exchange.mode === 'text' ? 'question' : 'options', + status: 'presented', expectedRequest: { tool: requestTool, required: true }, createdAtToolCallId: toolCallId, prompt: exchange.prompt, @@ -995,61 +935,55 @@ function presentToolResultMessage(exchange: PendingElicitationExchange) { }, isError: false as const, timestamp: 0 as const, - } + }; } function presentMarkdown(exchange: PendingElicitationExchange): string { - if (exchange.mode === "text") { - return [`## ${exchange.prompt}`, exchange.details] - .filter(Boolean) - .join("\n\n") + if (exchange.mode === 'text') { + return [`## ${exchange.prompt}`, exchange.details].filter(Boolean).join('\n\n'); } - const lines = [`## ${exchange.prompt}`] - if (exchange.details) lines.push("", exchange.details) + const lines = [`## ${exchange.prompt}`]; + if (exchange.details) lines.push('', exchange.details); exchange.options.forEach((option, index) => { - lines.push("", `### ${index + 1}. ${option.content}`) + lines.push('', `### ${index + 1}. ${option.content}`); if (option.rationale) { - lines.push("", `**Rationale:** ${option.rationale}`) + lines.push('', `**Rationale:** ${option.rationale}`); } - lines.push("", ``) - }) - return lines.join("\n") + lines.push('', ``); + }); + return lines.join('\n'); } -function pendingExchangeFromEnvelope( - envelope: BrunchSessionEnvelope, -): PendingElicitationExchange | null { - const projection = projectLinearElicitationExchangeProjection(envelope) +function pendingExchangeFromEnvelope(envelope: BrunchSessionEnvelope): PendingElicitationExchange | null { + const projection = projectLinearElicitationExchangeProjection(envelope); if (!projection.openPrompt) { - return null + return null; } for (const entryId of projection.openPrompt.promptEntryIds) { const entry = envelope.entries.find( (candidate) => - candidate.type === "custom_message" && + candidate.type === 'custom_message' && candidate.id === entryId && - candidate.customType === "brunch.elicitation_prompt" && + candidate.customType === 'brunch.elicitation_prompt' && Value.Check(PendingElicitationExchangeSchema, candidate.details), - ) - if (entry?.type === "custom_message") { - return Value.Parse(PendingElicitationExchangeSchema, entry.details) + ); + if (entry?.type === 'custom_message') { + return Value.Parse(PendingElicitationExchangeSchema, entry.details); } } for (const entryId of projection.openPrompt.promptEntryIds) { const entry = envelope.entries.find( - (candidate) => candidate.type === "message" && candidate.id === entryId, - ) - const details = structuredExchangePresentDetails(entry) - if (!details) continue - const text = textContent( - (entry as { message: { content?: unknown } }).message.content, - ) - return pendingExchangeFromStructuredPresent(details, text) + (candidate) => candidate.type === 'message' && candidate.id === entryId, + ); + const details = structuredExchangePresentDetails(entry); + if (!details) continue; + const text = textContent((entry as { message: { content?: unknown } }).message.content); + return pendingExchangeFromStructuredPresent(details, text); } - return null + return null; } function pendingExchangeFromStructuredPresent( @@ -1057,240 +991,221 @@ function pendingExchangeFromStructuredPresent( markdown: string, ): PendingElicitationExchange { const richDetails = details as StructuredExchangePresentDetails & { - prompt?: unknown - details?: unknown - options?: unknown - } + prompt?: unknown; + details?: unknown; + options?: unknown; + }; const prompt = - typeof richDetails.prompt === "string" + typeof richDetails.prompt === 'string' ? richDetails.prompt - : (firstNonEmptyMarkdownLine(markdown) ?? markdown) - const detailsText = - typeof richDetails.details === "string" ? richDetails.details : markdown + : (firstNonEmptyMarkdownLine(markdown) ?? markdown); + const detailsText = typeof richDetails.details === 'string' ? richDetails.details : markdown; return { exchangeId: details.exchangeId, - lens: "step-by-step", + lens: 'step-by-step', mode: - details.expectedRequest?.tool === "request_choices" - ? "multi-select" - : details.presentTool === "present_question" - ? "text" - : "single-select", + details.expectedRequest?.tool === 'request_choices' + ? 'multi-select' + : details.presentTool === 'present_question' + ? 'text' + : 'single-select', prompt, ...(detailsText.length > 0 ? { details: detailsText } : {}), options: parsePendingOptions(richDetails.options, markdown), note: { allowed: true }, - } + }; } -function parsePendingOptions( - value: unknown, - markdown: string = "", -): PendingChoice[] { - if (!Array.isArray(value)) return parseMarkdownPendingOptions(markdown) +function parsePendingOptions(value: unknown, markdown: string = ''): PendingChoice[] { + if (!Array.isArray(value)) return parseMarkdownPendingOptions(markdown); const options = value.flatMap((option) => { - if (typeof option !== "object" || option === null) return [] - const id = (option as { id?: unknown }).id - const label = (option as { label?: unknown }).label - const content = (option as { content?: unknown }).content - const rationale = (option as { rationale?: unknown }).rationale - if (typeof id !== "string") return [] + if (typeof option !== 'object' || option === null) return []; + const id = (option as { id?: unknown }).id; + const label = (option as { label?: unknown }).label; + const content = (option as { content?: unknown }).content; + const rationale = (option as { rationale?: unknown }).rationale; + if (typeof id !== 'string') return []; const optionContent = - typeof content === "string" - ? content - : typeof label === "string" - ? label - : undefined - if (optionContent === undefined) return [] + typeof content === 'string' ? content : typeof label === 'string' ? label : undefined; + if (optionContent === undefined) return []; return [ { id, - label: typeof label === "string" ? label : optionContent, + label: typeof label === 'string' ? label : optionContent, content: optionContent, - ...(typeof rationale === "string" ? { rationale } : {}), + ...(typeof rationale === 'string' ? { rationale } : {}), }, - ] - }) - return options.length > 0 ? options : parseMarkdownPendingOptions(markdown) + ]; + }); + return options.length > 0 ? options : parseMarkdownPendingOptions(markdown); } function parseMarkdownPendingOptions(markdown: string): PendingChoice[] { - const options: PendingChoice[] = [] - let pending: { - content: string - rationale?: string - } | undefined - - for (const line of markdown.split("\n")) { - const heading = /^###\s+\d+\.\s+(.+)$/.exec(line.trim()) + const options: PendingChoice[] = []; + let pending: + | { + content: string; + rationale?: string; + } + | undefined; + + for (const line of markdown.split('\n')) { + const heading = /^###\s+\d+\.\s+(.+)$/.exec(line.trim()); if (heading) { - pending = { content: heading[1]!.trim() } - continue + pending = { content: heading[1]!.trim() }; + continue; } - const rationale = /^\*\*Rationale:\*\*\s+(.+)$/.exec(line.trim()) + const rationale = /^\*\*Rationale:\*\*\s+(.+)$/.exec(line.trim()); if (rationale && pending) { - pending.rationale = rationale[1]!.trim() - continue + pending.rationale = rationale[1]!.trim(); + continue; } - const optionId = //.exec(line.trim()) + const optionId = //.exec(line.trim()); if (optionId && pending) { - const content = pending.content + const content = pending.content; options.push({ id: optionId[1]!.trim(), label: content, content, - ...(pending.rationale === undefined - ? {} - : { rationale: pending.rationale }), - }) - pending = undefined + ...(pending.rationale === undefined ? {} : { rationale: pending.rationale }), + }); + pending = undefined; } } - return options + return options; } -function structuredExchangePresentDetails( - entry: unknown, -): StructuredExchangePresentDetails | undefined { - if ( - typeof entry !== "object" || - entry === null || - (entry as { type?: unknown }).type !== "message" - ) { - return undefined +function structuredExchangePresentDetails(entry: unknown): StructuredExchangePresentDetails | undefined { + if (typeof entry !== 'object' || entry === null || (entry as { type?: unknown }).type !== 'message') { + return undefined; } - const message = (entry as { message?: unknown }).message + const message = (entry as { message?: unknown }).message; if ( - typeof message !== "object" || + typeof message !== 'object' || message === null || - (message as { role?: unknown }).role !== "toolResult" + (message as { role?: unknown }).role !== 'toolResult' ) { - return undefined + return undefined; } - const details = (message as { details?: unknown }).details + const details = (message as { details?: unknown }).details; return isStructuredExchangePresentDetails(details) - ? details as StructuredExchangePresentDetails - : undefined + ? (details as StructuredExchangePresentDetails) + : undefined; } function firstNonEmptyMarkdownLine(markdown: string): string | undefined { return markdown - .split("\n") - .map((line) => line.replace(/^#+\s*/, "").trim()) - .find((line) => line.length > 0) + .split('\n') + .map((line) => line.replace(/^#+\s*/, '').trim()) + .find((line) => line.length > 0); } function textContent(content: unknown): string { - if (typeof content === "string") return content - if (!Array.isArray(content)) return "" + if (typeof content === 'string') return content; + if (!Array.isArray(content)) return ''; return content .map((part) => - typeof part === "object" && - part !== null && - typeof (part as { text?: unknown }).text === "string" + typeof part === 'object' && part !== null && typeof (part as { text?: unknown }).text === 'string' ? (part as { text: string }).text - : "", + : '', ) .filter((text) => text.length > 0) - .join("\n") + .join('\n'); } function projectPendingElicitationExchange( envelope: BrunchSessionEnvelope, ): Static { - const exchange = pendingExchangeFromEnvelope(envelope) + const exchange = pendingExchangeFromEnvelope(envelope); if (!exchange) { - return { status: "idle", exchange: null } + return { status: 'idle', exchange: null }; } - return { status: "pending", exchange } + return { status: 'pending', exchange }; } interface FlushableSessionManager { - _rewriteFile(): void - setSessionFile(file: string): void + _rewriteFile(): void; + setSessionFile(file: string): void; } function flushSessionEntries(manager: unknown, sessionFile: string): void { - const flushable = manager as FlushableSessionManager - flushable._rewriteFile() - flushable.setSessionFile(sessionFile) + const flushable = manager as FlushableSessionManager; + flushable._rewriteFile(); + flushable.setSessionFile(sessionFile); } -type SessionProjectionParamsParseResult = { - ok: true - value: ExplicitSessionProjectionParams | null -} | { ok: false } +type SessionProjectionParamsParseResult = + | { + ok: true; + value: ExplicitSessionProjectionParams | null; + } + | { ok: false }; -function parseSessionProjectionParams( - value: unknown, -): SessionProjectionParamsParseResult { +function parseSessionProjectionParams(value: unknown): SessionProjectionParamsParseResult { if (value === undefined) { - return { ok: true, value: null } + return { ok: true, value: null }; } - if (typeof value !== "object" || value === null || Array.isArray(value)) { - return { ok: false } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return { ok: false }; } - const keys = Object.keys(value) - if (!keys.every((key) => key === "sessionId" || key === "specId")) { - return { ok: false } + const keys = Object.keys(value); + if (!keys.every((key) => key === 'sessionId' || key === 'specId')) { + return { ok: false }; } - const sessionId = (value as { sessionId?: unknown }).sessionId - const specId = (value as { specId?: unknown }).specId + const sessionId = (value as { sessionId?: unknown }).sessionId; + const specId = (value as { specId?: unknown }).specId; if ( - typeof sessionId !== "string" || + typeof sessionId !== 'string' || sessionId.length === 0 || - (specId !== undefined && - (typeof specId !== "string" || specId.length === 0)) + (specId !== undefined && (typeof specId !== 'string' || specId.length === 0)) ) { - return { ok: false } + return { ok: false }; } return { ok: true, value: specId === undefined ? { sessionId } : { sessionId, specId }, - } + }; } -async function selectedSessionFile( - state: WorkspaceSessionState, -): Promise { - if (state.status !== "ready") { - return { ok: false, code: -32001, message: "No selected Brunch session" } +async function selectedSessionFile(state: WorkspaceSessionState): Promise { + if (state.status !== 'ready') { + return { ok: false, code: -32001, message: 'No selected Brunch session' }; } - const readResult = await readBrunchSessionEnvelope(state.session.file) + const readResult = await readBrunchSessionEnvelope(state.session.file); if (!readResult.ok) { return { ok: false, code: -32005, - message: "Brunch session self-description is invalid", - } + message: 'Brunch session self-description is invalid', + }; } return { ok: true, envelope: readResult.envelope, - nonLinearMessage: "Selected Brunch session transcript is non-linear", - } + nonLinearMessage: 'Selected Brunch session transcript is non-linear', + }; } export async function runJsonRpcLineServer(options: { - input: Readable - output: Writable - handlers: RpcHandlers + input: Readable; + output: Writable; + handlers: RpcHandlers; }): Promise { - const lines = createInterface({ input: options.input }) + const lines = createInterface({ input: options.input }); for await (const line of lines) { if (line.trim().length === 0) { - continue + continue; } - const response = await dispatchJsonRpcMessage(line, options.handlers) - options.output.write(`${JSON.stringify(response)}\n`) + const response = await dispatchJsonRpcMessage(line, options.handlers); + options.output.write(`${JSON.stringify(response)}\n`); } } diff --git a/src/rpc/protocol.test.ts b/src/rpc/protocol.test.ts index 0cfa3c870..c0296bc5f 100644 --- a/src/rpc/protocol.test.ts +++ b/src/rpc/protocol.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; import { createJsonRpcFailure, @@ -7,94 +7,83 @@ import { dispatchJsonRpcMessage, isJsonRpcRequest, parseJsonRpcMessage, -} from "./protocol.js" +} from './protocol.js'; -describe("JSON-RPC protocol helpers", () => { - it("recognizes valid request IDs and rejects invalid request shapes", () => { +describe('JSON-RPC protocol helpers', () => { + it('recognizes valid request IDs and rejects invalid request shapes', () => { expect( isJsonRpcRequest({ - jsonrpc: "2.0", - id: "abc", - method: "workspace.snapshot", + jsonrpc: '2.0', + id: 'abc', + method: 'workspace.snapshot', }), - ).toBe(true) - expect( - isJsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "workspace.snapshot" }), - ).toBe(true) + ).toBe(true); + expect(isJsonRpcRequest({ jsonrpc: '2.0', id: 1, method: 'workspace.snapshot' })).toBe(true); expect( isJsonRpcRequest({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: null, - method: "workspace.snapshot", + method: 'workspace.snapshot', }), - ).toBe(true) + ).toBe(true); expect( isJsonRpcRequest({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: { bad: true }, - method: "workspace.snapshot", + method: 'workspace.snapshot', }), - ).toBe(false) - expect(isJsonRpcRequest({ jsonrpc: "2.0", id: 1 })).toBe(false) - }) + ).toBe(false); + expect(isJsonRpcRequest({ jsonrpc: '2.0', id: 1 })).toBe(false); + }); - it("creates success, failure, method-not-found, and parse-error responses", () => { + it('creates success, failure, method-not-found, and parse-error responses', () => { expect(createJsonRpcSuccess(1, { ok: true })).toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, result: { ok: true }, - }) - expect( - createJsonRpcFailure("request-1", -32601, "Method not found"), - ).toEqual({ - jsonrpc: "2.0", - id: "request-1", - error: { code: -32601, message: "Method not found" }, - }) + }); + expect(createJsonRpcFailure('request-1', -32601, 'Method not found')).toEqual({ + jsonrpc: '2.0', + id: 'request-1', + error: { code: -32601, message: 'Method not found' }, + }); expect(createJsonRpcParseError()).toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: null, - error: { code: -32700, message: "Parse error" }, - }) - }) + error: { code: -32700, message: 'Parse error' }, + }); + }); - it("dispatches parse failures and handler throws without attaching product semantics", async () => { + it('dispatches parse failures and handler throws without attaching product semantics', async () => { await expect( - dispatchJsonRpcMessage("not json", { + dispatchJsonRpcMessage('not json', { async handle() { - throw new Error("should not handle malformed JSON") + throw new Error('should not handle malformed JSON'); }, }), - ).resolves.toEqual(createJsonRpcParseError()) + ).resolves.toEqual(createJsonRpcParseError()); await expect( - dispatchJsonRpcMessage( - '{"jsonrpc":"2.0","id":7,"method":"workspace.snapshot"}', - { - async handle() { - throw new Error("boom") - }, + dispatchJsonRpcMessage('{"jsonrpc":"2.0","id":7,"method":"workspace.snapshot"}', { + async handle() { + throw new Error('boom'); }, - ), + }), ).resolves.toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 7, - error: { code: -32603, message: "Internal error" }, - }) - }) + error: { code: -32603, message: 'Internal error' }, + }); + }); - it("parses protocol messages without attaching product semantics", () => { - expect( - parseJsonRpcMessage( - '{"jsonrpc":"2.0","id":1,"method":"workspace.snapshot"}', - ), - ).toEqual({ + it('parses protocol messages without attaching product semantics', () => { + expect(parseJsonRpcMessage('{"jsonrpc":"2.0","id":1,"method":"workspace.snapshot"}')).toEqual({ ok: true, - value: { jsonrpc: "2.0", id: 1, method: "workspace.snapshot" }, - }) - expect(parseJsonRpcMessage("not json")).toEqual({ + value: { jsonrpc: '2.0', id: 1, method: 'workspace.snapshot' }, + }); + expect(parseJsonRpcMessage('not json')).toEqual({ ok: false, response: createJsonRpcParseError(), - }) - }) -}) + }); + }); +}); diff --git a/src/rpc/protocol.ts b/src/rpc/protocol.ts index e9438d7dd..d68ee9185 100644 --- a/src/rpc/protocol.ts +++ b/src/rpc/protocol.ts @@ -1,65 +1,60 @@ -export type JsonRpcId = string | number | null +export type JsonRpcId = string | number | null; export interface JsonRpcRequest { - jsonrpc: "2.0" - id?: JsonRpcId - method: string - params?: unknown + jsonrpc: '2.0'; + id?: JsonRpcId; + method: string; + params?: unknown; } export interface JsonRpcSuccess { - jsonrpc: "2.0" - id: JsonRpcId - result: T + jsonrpc: '2.0'; + id: JsonRpcId; + result: T; } export interface JsonRpcFailure { - jsonrpc: "2.0" - id: JsonRpcId + jsonrpc: '2.0'; + id: JsonRpcId; error: { - code: number - message: string - } + code: number; + message: string; + }; } -export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure +export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure; -export type JsonRpcParseResult = { - ok: true - value: unknown -} | { - ok: false - response: JsonRpcFailure -} +export type JsonRpcParseResult = + | { + ok: true; + value: unknown; + } + | { + ok: false; + response: JsonRpcFailure; + }; export interface JsonRpcMessageHandler { - handle(request: unknown): Promise + handle(request: unknown): Promise; } -export function createJsonRpcSuccess( - id: JsonRpcId, - result: T, -): JsonRpcSuccess { - return { jsonrpc: "2.0", id, result } +export function createJsonRpcSuccess(id: JsonRpcId, result: T): JsonRpcSuccess { + return { jsonrpc: '2.0', id, result }; } -export function createJsonRpcFailure( - id: JsonRpcId, - code: number, - message: string, -): JsonRpcFailure { - return { jsonrpc: "2.0", id, error: { code, message } } +export function createJsonRpcFailure(id: JsonRpcId, code: number, message: string): JsonRpcFailure { + return { jsonrpc: '2.0', id, error: { code, message } }; } export function createJsonRpcParseError(): JsonRpcFailure { - return createJsonRpcFailure(null, -32700, "Parse error") + return createJsonRpcFailure(null, -32700, 'Parse error'); } export function parseJsonRpcMessage(message: string): JsonRpcParseResult { try { - return { ok: true, value: JSON.parse(message) as unknown } + return { ok: true, value: JSON.parse(message) as unknown }; } catch { - return { ok: false, response: createJsonRpcParseError() } + return { ok: false, response: createJsonRpcParseError() }; } } @@ -67,40 +62,33 @@ export async function dispatchJsonRpcMessage( message: string, handler: JsonRpcMessageHandler, ): Promise { - const parsed = parseJsonRpcMessage(message) + const parsed = parseJsonRpcMessage(message); if (!parsed.ok) { - return parsed.response + return parsed.response; } try { - return await handler.handle(parsed.value) + return await handler.handle(parsed.value); } catch { - const id = isJsonRpcRequest(parsed.value) - ? jsonRpcRequestId(parsed.value) - : null - return createJsonRpcFailure(id, -32603, "Internal error") + const id = isJsonRpcRequest(parsed.value) ? jsonRpcRequestId(parsed.value) : null; + return createJsonRpcFailure(id, -32603, 'Internal error'); } } export function jsonRpcRequestId(request: JsonRpcRequest): JsonRpcId { - return request.id ?? null + return request.id ?? null; } export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest { if ( - typeof value !== "object" || + typeof value !== 'object' || value === null || - (value as { jsonrpc?: unknown }).jsonrpc !== "2.0" || - typeof (value as { method?: unknown }).method !== "string" + (value as { jsonrpc?: unknown }).jsonrpc !== '2.0' || + typeof (value as { method?: unknown }).method !== 'string' ) { - return false + return false; } - const id = (value as { id?: unknown }).id - return ( - id === undefined || - id === null || - typeof id === "string" || - typeof id === "number" - ) + const id = (value as { id?: unknown }).id; + return id === undefined || id === null || typeof id === 'string' || typeof id === 'number'; } diff --git a/src/rpc/websocket.ts b/src/rpc/websocket.ts index 1830aa20f..a04791575 100644 --- a/src/rpc/websocket.ts +++ b/src/rpc/websocket.ts @@ -1,118 +1,116 @@ -import type { Server as HttpServer } from "node:http" +import type { Server as HttpServer } from 'node:http'; -import { WebSocketServer, type RawData } from "ws" +import { WebSocketServer, type RawData } from 'ws'; -import { dispatchJsonRpcMessage } from "./protocol.js" -import type { RpcHandlers } from "./handlers.js" +import type { RpcHandlers } from './handlers.js'; +import { dispatchJsonRpcMessage } from './protocol.js'; export interface WebRpcTransport { - close(): Promise + close(): Promise; } export function attachWebRpcTransport(options: { - server: HttpServer - path: string - handlers: RpcHandlers + server: HttpServer; + path: string; + handlers: RpcHandlers; }): WebRpcTransport { - const webSocketServer = new WebSocketServer({ noServer: true }) + const webSocketServer = new WebSocketServer({ noServer: true }); - options.server.on("upgrade", (request, socket, head) => { + options.server.on('upgrade', (request, socket, head) => { if (request.url !== options.path) { - socket.destroy() - return + socket.destroy(); + return; } webSocketServer.handleUpgrade(request, socket, head, (webSocket) => { - webSocketServer.emit("connection", webSocket, request) - }) - }) + webSocketServer.emit('connection', webSocket, request); + }); + }); - webSocketServer.on("connection", (webSocket) => { - webSocket.on("message", (data) => { - void handleMessage(options.handlers, data).then( - ({ response, method }) => { - webSocket.send(JSON.stringify(response)) - if (isProductMutation(method) && !Object.hasOwn(response, "error")) { - broadcastProductUpdate() - } - }, - ) - }) - }) + webSocketServer.on('connection', (webSocket) => { + webSocket.on('message', (data) => { + void handleMessage(options.handlers, data).then(({ response, method }) => { + webSocket.send(JSON.stringify(response)); + if (isProductMutation(method) && !Object.hasOwn(response, 'error')) { + broadcastProductUpdate(); + } + }); + }); + }); return { async close() { for (const client of webSocketServer.clients) { - client.close() + client.close(); } await new Promise((resolve, reject) => { webSocketServer.close((error) => { if (error) { - reject(error) - return + reject(error); + return; } - resolve() - }) - }) + resolve(); + }); + }); }, - } + }; function broadcastProductUpdate(): void { const notification = JSON.stringify({ - jsonrpc: "2.0", - method: "brunch.updated", + jsonrpc: '2.0', + method: 'brunch.updated', params: { topics: [ - "workspace.snapshot", - "session.pendingExchange", - "session.elicitationExchanges", - "session.transcriptDisplay", + 'workspace.snapshot', + 'session.pendingExchange', + 'session.elicitationExchanges', + 'session.transcriptDisplay', ], }, - }) + }); for (const client of webSocketServer.clients) { - client.send(notification) + client.send(notification); } } } async function handleMessage(handlers: RpcHandlers, data: RawData) { - const message = websocketMessageToString(data) + const message = websocketMessageToString(data); return { response: await dispatchJsonRpcMessage(message, handlers), method: requestMethod(message), - } + }; } function requestMethod(message: string): string | undefined { try { - const value = JSON.parse(message) as unknown - return typeof value === "object" && + const value = JSON.parse(message) as unknown; + return typeof value === 'object' && value !== null && - typeof (value as { method?: unknown }).method === "string" + typeof (value as { method?: unknown }).method === 'string' ? (value as { method: string }).method - : undefined + : undefined; } catch { - return undefined + return undefined; } } function isProductMutation(method: string | undefined): boolean { return ( - method === "workspace.activate" || - method === "session.startElicitation" || - method === "elicitation.respond" - ) + method === 'workspace.activate' || + method === 'session.startElicitation' || + method === 'elicitation.respond' + ); } function websocketMessageToString(data: RawData): string { - if (typeof data === "string") { - return data + if (typeof data === 'string') { + return data; } if (Array.isArray(data)) { - return Buffer.concat(data).toString("utf8") + return Buffer.concat(data).toString('utf8'); } if (data instanceof ArrayBuffer) { - return Buffer.from(new Uint8Array(data)).toString("utf8") + return Buffer.from(new Uint8Array(data)).toString('utf8'); } - return Buffer.from(data).toString("utf8") + return Buffer.from(data).toString('utf8'); } diff --git a/src/session-binding.ts b/src/session-binding.ts index 0a1ac6e1f..24cc97831 100644 --- a/src/session-binding.ts +++ b/src/session-binding.ts @@ -1,59 +1,54 @@ -import type { CustomEntry } from "@earendil-works/pi-coding-agent" +import type { CustomEntry } from '@earendil-works/pi-coding-agent'; -export const SESSION_BINDING_TYPE = "brunch.session_binding" -export const SESSION_BINDING_SCHEMA_VERSION = 1 +export const SESSION_BINDING_TYPE = 'brunch.session_binding'; +export const SESSION_BINDING_SCHEMA_VERSION = 1; export interface SessionBindingData { - schemaVersion: typeof SESSION_BINDING_SCHEMA_VERSION - sessionId: string - specId: string - specTitle: string + schemaVersion: typeof SESSION_BINDING_SCHEMA_VERSION; + sessionId: string; + specId: string; + specTitle: string; } export type SessionBindingEntry = CustomEntry & { - customType: typeof SESSION_BINDING_TYPE - data: SessionBindingData -} + customType: typeof SESSION_BINDING_TYPE; + data: SessionBindingData; +}; export function createSessionBindingData(options: { - sessionId: string - specId: string - specTitle: string + sessionId: string; + specId: string; + specTitle: string; }): SessionBindingData { return { schemaVersion: SESSION_BINDING_SCHEMA_VERSION, sessionId: options.sessionId, specId: options.specId, specTitle: options.specTitle, - } + }; } -export function isSessionBindingEntry( - value: unknown, -): value is SessionBindingEntry { +export function isSessionBindingEntry(value: unknown): value is SessionBindingEntry { if ( - typeof value !== "object" || + typeof value !== 'object' || value === null || - (value as { type?: unknown }).type !== "custom" || + (value as { type?: unknown }).type !== 'custom' || (value as { customType?: unknown }).customType !== SESSION_BINDING_TYPE ) { - return false + return false; } - const data = (value as { data?: unknown }).data - return isSessionBindingData(data) + const data = (value as { data?: unknown }).data; + return isSessionBindingData(data); } -export function isSessionBindingData( - value: unknown, -): value is SessionBindingData { +export function isSessionBindingData(value: unknown): value is SessionBindingData { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { schemaVersion?: unknown }).schemaVersion === - SESSION_BINDING_SCHEMA_VERSION && - typeof (value as { sessionId?: unknown }).sessionId === "string" && - typeof (value as { specId?: unknown }).specId === "string" && - typeof (value as { specTitle?: unknown }).specTitle === "string" - ) + (value as { schemaVersion?: unknown }).schemaVersion === SESSION_BINDING_SCHEMA_VERSION && + typeof (value as { sessionId?: unknown }).sessionId === 'string' && + typeof (value as { specId?: unknown }).specId === 'string' && + typeof (value as { specTitle?: unknown }).specTitle === 'string' + ); } diff --git a/src/session-projection-reader.ts b/src/session-projection-reader.ts index 825bea235..4492d235a 100644 --- a/src/session-projection-reader.ts +++ b/src/session-projection-reader.ts @@ -1,86 +1,81 @@ -import { readdir } from "node:fs/promises" -import { join, resolve } from "node:path" +import { readdir } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; -import { - readBrunchSessionEnvelope, - type BrunchSessionEnvelope, -} from "./brunch-session-envelope.js" +import { readBrunchSessionEnvelope, type BrunchSessionEnvelope } from './brunch-session-envelope.js'; export interface ExplicitSessionProjectionParams { - sessionId: string - specId?: string + sessionId: string; + specId?: string; } -export type SessionProjectionTarget = { - ok: true - envelope: BrunchSessionEnvelope - nonLinearMessage: string -} | { - ok: false - code: number - message: string -} +export type SessionProjectionTarget = + | { + ok: true; + envelope: BrunchSessionEnvelope; + nonLinearMessage: string; + } + | { + ok: false; + code: number; + message: string; + }; export async function resolveExplicitSessionProjectionTarget( cwd: string, params: ExplicitSessionProjectionParams, ): Promise { - const files = await listSessionFiles(cwd) + const files = await listSessionFiles(cwd); for (const file of files) { - const readResult = await readBrunchSessionEnvelope(file) + const readResult = await readBrunchSessionEnvelope(file); if (!sessionIds(readResult).includes(params.sessionId)) { - continue + continue; } if (!readResult.ok) { - return invalidSessionSelfDescription() + return invalidSessionSelfDescription(); } - const binding = readResult.envelope.binding + const binding = readResult.envelope.binding; if (params.specId && binding.specId !== params.specId) { return { ok: false, code: -32003, - message: "Brunch session does not belong to requested spec", - } + message: 'Brunch session does not belong to requested spec', + }; } return { ok: true, envelope: readResult.envelope, - nonLinearMessage: "Brunch session transcript is non-linear", - } + nonLinearMessage: 'Brunch session transcript is non-linear', + }; } - return { ok: false, code: -32004, message: "Brunch session not found" } + return { ok: false, code: -32004, message: 'Brunch session not found' }; } -function sessionIds( - readResult: Awaited>, -): string[] { - return readResult.ok - ? [readResult.envelope.binding.sessionId] - : readResult.observedSessionIds +function sessionIds(readResult: Awaited>): string[] { + return readResult.ok ? [readResult.envelope.binding.sessionId] : readResult.observedSessionIds; } function invalidSessionSelfDescription(): SessionProjectionTarget { return { ok: false, code: -32005, - message: "Brunch session self-description is invalid", - } + message: 'Brunch session self-description is invalid', + }; } async function listSessionFiles(cwd: string): Promise { - const sessionRoot = join(resolve(cwd), ".brunch", "sessions") + const sessionRoot = join(resolve(cwd), '.brunch', 'sessions'); try { - const entries = await readdir(sessionRoot, { withFileTypes: true }) + const entries = await readdir(sessionRoot, { withFileTypes: true }); return entries - .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")) + .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) .map((entry) => join(sessionRoot, entry.name)) - .sort() + .sort(); } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return [] + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; } - throw error + throw error; } } diff --git a/src/session-transcript.test.ts b/src/session-transcript.test.ts index ce67a9361..fbc2e2c3e 100644 --- a/src/session-transcript.test.ts +++ b/src/session-transcript.test.ts @@ -1,102 +1,98 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { renderSessionTranscript } from "./session-transcript.js" +import { renderSessionTranscript } from './session-transcript.js'; function line(value: unknown): string { - return JSON.stringify(value) + return JSON.stringify(value); } -describe("session transcript renderer", () => { - it("renders structured-exchange tuple JSONL as a readable transcript", () => { +describe('session transcript renderer', () => { + it('renders structured-exchange tuple JSONL as a readable transcript', () => { const jsonl = [ - line({ type: "session", id: "session-1", cwd: "/tmp/brunch" }), + line({ type: 'session', id: 'session-1', cwd: '/tmp/brunch' }), line({ - id: "binding-1", - type: "custom", - customType: "brunch.session_binding", - data: { specId: "spec-1", specTitle: "Demo spec" }, + id: 'binding-1', + type: 'custom', + customType: 'brunch.session_binding', + data: { specId: 'spec-1', specTitle: 'Demo spec' }, }), line({ - id: "generic-tool-1", - type: "message", + id: 'generic-tool-1', + type: 'message', message: { - role: "toolResult", - toolName: "read", - content: [{ type: "text", text: "Generic file contents" }], - details: { path: "notes.txt" }, + role: 'toolResult', + toolName: 'read', + content: [{ type: 'text', text: 'Generic file contents' }], + details: { path: 'notes.txt' }, }, }), line({ - id: "present-1", - type: "message", + id: 'present-1', + type: 'message', message: { - role: "toolResult", - toolName: "present_options", + role: 'toolResult', + toolName: 'present_options', content: [ { - type: "text", - text: "## Which direction?\n\n### 1. Fast\n\n**Rationale:** validates the seam.", + type: 'text', + text: '## Which direction?\n\n### 1. Fast\n\n**Rationale:** validates the seam.', }, ], details: { - schema: "brunch.structured_exchange.present", + schema: 'brunch.structured_exchange.present', schemaVersion: 1, - exchangeId: "turn-1", - presentTool: "present_options", - kind: "options", - status: "presented", - expectedRequest: { tool: "request_choice", required: true }, - createdAtToolCallId: "present-call-1", + exchangeId: 'turn-1', + presentTool: 'present_options', + kind: 'options', + status: 'presented', + expectedRequest: { tool: 'request_choice', required: true }, + createdAtToolCallId: 'present-call-1', }, }, }), line({ - id: "request-1", - type: "message", + id: 'request-1', + type: 'message', message: { - role: "toolResult", - toolName: "request_choice", + role: 'toolResult', + toolName: 'request_choice', content: [ { - type: "text", - text: "### Response\n\n- Fast\n\nComment:\n\n> Keep it deterministic.", + type: 'text', + text: '### Response\n\n- Fast\n\nComment:\n\n> Keep it deterministic.', }, ], details: { - schema: "brunch.structured_exchange.request", + schema: 'brunch.structured_exchange.request', schemaVersion: 1, - exchangeId: "turn-1", - requestTool: "request_choice", - status: "answered", + exchangeId: 'turn-1', + requestTool: 'request_choice', + status: 'answered', respondsTo: { - exchangeId: "turn-1", - presentTool: "present_options", + exchangeId: 'turn-1', + presentTool: 'present_options', }, - choice: { id: "fast", label: "Fast" }, - comment: "Keep it deterministic.", - createdAtToolCallId: "request-call-1", + choice: { id: 'fast', label: 'Fast' }, + comment: 'Keep it deterministic.', + createdAtToolCallId: 'request-call-1', }, }, }), - ].join("\n") + ].join('\n'); const transcript = renderSessionTranscript(jsonl, { - title: "session.jsonl", - }) + title: 'session.jsonl', + }); - expect(transcript).toContain("# Transcript — session.jsonl") - expect(transcript).toContain("## Session") - expect(transcript).toContain("- session: session-1") - expect(transcript).toContain("## Session binding") - expect(transcript).toContain( - "## Exchange turn-1 — prompt (present_options → request_choice)", - ) - expect(transcript).toContain("**Rationale:** validates the seam.") - expect(transcript).toContain( - "## Exchange turn-1 — response (request_choice, answered)", - ) - expect(transcript).toContain("Keep it deterministic.") - expect(transcript).not.toContain("## Tool result: read") - expect(transcript).not.toContain("Generic file contents") - }) -}) + expect(transcript).toContain('# Transcript — session.jsonl'); + expect(transcript).toContain('## Session'); + expect(transcript).toContain('- session: session-1'); + expect(transcript).toContain('## Session binding'); + expect(transcript).toContain('## Exchange turn-1 — prompt (present_options → request_choice)'); + expect(transcript).toContain('**Rationale:** validates the seam.'); + expect(transcript).toContain('## Exchange turn-1 — response (request_choice, answered)'); + expect(transcript).toContain('Keep it deterministic.'); + expect(transcript).not.toContain('## Tool result: read'); + expect(transcript).not.toContain('Generic file contents'); + }); +}); diff --git a/src/session-transcript.ts b/src/session-transcript.ts index 15d9391c7..74695dfac 100644 --- a/src/session-transcript.ts +++ b/src/session-transcript.ts @@ -1,240 +1,195 @@ -import { readFile } from "node:fs/promises" -import { basename, resolve } from "node:path" -import { fileURLToPath } from "node:url" +import { readFile } from 'node:fs/promises'; +import { basename, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import type { ToolResultMessage } from "@earendil-works/pi-ai" +import type { ToolResultMessage } from '@earendil-works/pi-ai'; import type { CustomEntry, CustomMessageEntry, FileEntry, SessionHeader, SessionMessageEntry, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-coding-agent'; import { isStructuredExchangePresentDetails, isStructuredExchangeRequestDetails, -} from "./.pi/extensions/structured-exchange/shared/recovery.js" +} from './.pi/extensions/structured-exchange/shared/recovery.js'; -type TranscriptEntry = FileEntry +type TranscriptEntry = FileEntry; -type TranscriptToolResultMessage = ToolResultMessage +type TranscriptToolResultMessage = ToolResultMessage; -export async function renderSessionTranscriptFile( - sessionFile: string, -): Promise { - const text = await readFile(sessionFile, "utf8") - return renderSessionTranscript(text, { title: basename(sessionFile) }) +export async function renderSessionTranscriptFile(sessionFile: string): Promise { + const text = await readFile(sessionFile, 'utf8'); + return renderSessionTranscript(text, { title: basename(sessionFile) }); } -export function renderSessionTranscript( - jsonl: string, - options: { title?: string } = {}, -): string { - const entries = parseJsonl(jsonl) - const lines: string[] = [ - `# Transcript${options.title ? ` — ${options.title}` : ""}`, - ] +export function renderSessionTranscript(jsonl: string, options: { title?: string } = {}): string { + const entries = parseJsonl(jsonl); + const lines: string[] = [`# Transcript${options.title ? ` — ${options.title}` : ''}`]; for (const entry of entries) { - lines.push("", ...renderEntry(entry)) + lines.push('', ...renderEntry(entry)); } - return `${lines.join("\n").trimEnd()}\n` + return `${lines.join('\n').trimEnd()}\n`; } function parseJsonl(jsonl: string): FileEntry[] { return jsonl - .split("\n") + .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line, index) => { try { - return JSON.parse(line) as TranscriptEntry + return JSON.parse(line) as TranscriptEntry; } catch (error) { - throw new Error( - `Invalid JSONL at line ${index + 1}: ${(error as Error).message}`, - ) + throw new Error(`Invalid JSONL at line ${index + 1}: ${(error as Error).message}`); } - }) + }); } function renderEntry(entry: TranscriptEntry): string[] { if (isSessionHeaderEntry(entry)) { - return renderSessionHeader(entry) + return renderSessionHeader(entry); } if (isCustomTranscriptEntry(entry)) { - return renderCustomEntry(entry) + return renderCustomEntry(entry); } if (isMessageEntry(entry)) { - return renderMessageEntry(entry) + return renderMessageEntry(entry); } - return [ - `## Entry ${entryId(entry)}`, - "", - "```json", - JSON.stringify(entry, null, 2), - "```", - ] + return [`## Entry ${entryId(entry)}`, '', '```json', JSON.stringify(entry, null, 2), '```']; } function renderSessionHeader(entry: SessionHeader): string[] { const fields = [ - typeof entry.id === "string" ? `- session: ${entry.id}` : undefined, - typeof entry.cwd === "string" ? `- cwd: ${entry.cwd}` : undefined, - ].filter((line): line is string => line !== undefined) - return [ - "## Session", - "", - ...(fields.length > 0 ? fields : ["- session metadata present"]), - ] + typeof entry.id === 'string' ? `- session: ${entry.id}` : undefined, + typeof entry.cwd === 'string' ? `- cwd: ${entry.cwd}` : undefined, + ].filter((line): line is string => line !== undefined); + return ['## Session', '', ...(fields.length > 0 ? fields : ['- session metadata present'])]; } function renderCustomEntry(entry: CustomEntry | CustomMessageEntry): string[] { - const customType = - typeof entry.customType === "string" ? entry.customType : "custom" - const title = - customType === "brunch.session_binding" - ? "Session binding" - : `Custom: ${customType}` - const payload = entry.type === "custom_message" ? entry.details : entry.data - const text = textContent( - entry.type === "custom_message" ? entry.content : undefined, - ) - const body: string[] = [] - if (text.length > 0) body.push(text) + const customType = typeof entry.customType === 'string' ? entry.customType : 'custom'; + const title = customType === 'brunch.session_binding' ? 'Session binding' : `Custom: ${customType}`; + const payload = entry.type === 'custom_message' ? entry.details : entry.data; + const text = textContent(entry.type === 'custom_message' ? entry.content : undefined); + const body: string[] = []; + if (text.length > 0) body.push(text); if (payload !== undefined) { - body.push("```json", JSON.stringify(payload, null, 2), "```") + body.push('```json', JSON.stringify(payload, null, 2), '```'); } - return [ - `## ${title}`, - "", - ...(body.length > 0 ? body : ["_(no display content)_"]), - ] + return [`## ${title}`, '', ...(body.length > 0 ? body : ['_(no display content)_'])]; } function renderMessageEntry(entry: SessionMessageEntry): string[] { - const message = entry.message - if (!message || typeof message !== "object") { - return [`## Message ${entryId(entry)}`, "", "_(missing message payload)_"] + const message = entry.message; + if (!message || typeof message !== 'object') { + return [`## Message ${entryId(entry)}`, '', '_(missing message payload)_']; } if (isToolResultMessage(message)) { - return renderToolResult(entry, message) + return renderToolResult(entry, message); } - const role = - typeof message.role === "string" ? titleCase(message.role) : "Message" - const text = textContent( - (message as unknown as Record).content, - ) - return [`## ${role}`, "", text.length > 0 ? text : "_(empty)_"] + const role = typeof message.role === 'string' ? titleCase(message.role) : 'Message'; + const text = textContent((message as unknown as Record).content); + return [`## ${role}`, '', text.length > 0 ? text : '_(empty)_']; } -function renderToolResult( - _entry: SessionMessageEntry, - message: TranscriptToolResultMessage, -): string[] { - const details = message.details - const present = structuredPresent(details) +function renderToolResult(_entry: SessionMessageEntry, message: TranscriptToolResultMessage): string[] { + const details = message.details; + const present = structuredPresent(details); if (present) { const expected = - present.expectedRequest && - typeof present.expectedRequest.tool === "string" + present.expectedRequest && typeof present.expectedRequest.tool === 'string' ? ` → ${present.expectedRequest.tool}` - : "" + : ''; return [ `## Exchange ${present.exchangeId} — prompt (${present.presentTool}${expected})`, - "", - textContent(message.content) || "_(empty prompt)_", - ] + '', + textContent(message.content) || '_(empty prompt)_', + ]; } - const request = structuredRequest(details) + const request = structuredRequest(details); if (request) { return [ `## Exchange ${request.exchangeId} — response (${request.requestTool}, ${request.status})`, - "", - textContent(message.content) || "_(empty response)_", - ] + '', + textContent(message.content) || '_(empty response)_', + ]; } - return [] + return []; } function structuredPresent(value: unknown) { - return isStructuredExchangePresentDetails(value) ? value : null + return isStructuredExchangePresentDetails(value) ? value : null; } function structuredRequest(value: unknown) { - return isStructuredExchangeRequestDetails(value) ? value : null + return isStructuredExchangeRequestDetails(value) ? value : null; } function textContent(content: unknown): string { - if (typeof content === "string") return content.trim() - if (!Array.isArray(content)) return "" + if (typeof content === 'string') return content.trim(); + if (!Array.isArray(content)) return ''; return content - .map((part) => - isRecord(part) && typeof part.text === "string" ? part.text : "", - ) + .map((part) => (isRecord(part) && typeof part.text === 'string' ? part.text : '')) .filter((text) => text.length > 0) - .join("\n") - .trim() + .join('\n') + .trim(); } function isSessionHeaderEntry(entry: TranscriptEntry): entry is SessionHeader { - return entry.type === "session" + return entry.type === 'session'; } -function isCustomTranscriptEntry( - entry: TranscriptEntry, -): entry is CustomEntry | CustomMessageEntry { - return entry.type === "custom" || entry.type === "custom_message" +function isCustomTranscriptEntry(entry: TranscriptEntry): entry is CustomEntry | CustomMessageEntry { + return entry.type === 'custom' || entry.type === 'custom_message'; } function isMessageEntry(entry: TranscriptEntry): entry is SessionMessageEntry { - return entry.type === "message" + return entry.type === 'message'; } function isToolResultMessage( - message: SessionMessageEntry["message"], + message: SessionMessageEntry['message'], ): message is TranscriptToolResultMessage { - return message.role === "toolResult" + return message.role === 'toolResult'; } function entryId(entry: TranscriptEntry): string { - return typeof entry.id === "string" ? entry.id : "(unknown)" + return typeof entry.id === 'string' ? entry.id : '(unknown)'; } function titleCase(value: string): string { - return value.charAt(0).toUpperCase() + value.slice(1) + return value.charAt(0).toUpperCase() + value.slice(1); } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === 'object' && value !== null; } async function main(): Promise { - const [, , sessionFile] = process.argv + const [, , sessionFile] = process.argv; if (!sessionFile) { - process.stderr.write( - "Usage: tsx src/session-transcript.ts \n", - ) - process.exitCode = 1 - return + process.stderr.write('Usage: tsx src/session-transcript.ts \n'); + process.exitCode = 1; + return; } - process.stdout.write(await renderSessionTranscriptFile(sessionFile)) + process.stdout.write(await renderSessionTranscriptFile(sessionFile)); } -if ( - process.argv[1] && - resolve(process.argv[1]) === fileURLToPath(import.meta.url) -) { +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { void main().catch((error) => { - process.stderr.write(`${(error as Error).stack ?? String(error)}\n`) - process.exitCode = 1 - }) + process.stderr.write(`${(error as Error).stack ?? String(error)}\n`); + process.exitCode = 1; + }); } diff --git a/src/structured-exchange.ts b/src/structured-exchange.ts index 8330e88e9..03d3f14a6 100644 --- a/src/structured-exchange.ts +++ b/src/structured-exchange.ts @@ -1,72 +1,67 @@ -export const STRUCTURED_EXCHANGE_RESULT_SCHEMA = - "brunch.structured_exchange.result" as const +export const STRUCTURED_EXCHANGE_RESULT_SCHEMA = 'brunch.structured_exchange.result' as const; -export type StructuredExchangeStatus = "answered" | "cancelled" | "unavailable" -export type StructuredExchangeMode = "text" | "single-select" | "multi-select" +export type StructuredExchangeStatus = 'answered' | 'cancelled' | 'unavailable'; +export type StructuredExchangeMode = 'text' | 'single-select' | 'multi-select'; export interface StructuredExchangeOption { - label: string - value: string - description?: string + label: string; + value: string; + description?: string; } -export type StructuredExchangeAnswer = { - type: "text" - label: string - value: string -} | { - type: "option" - label: string - value: string - index: number -} | { - type: "other" - label: string - value: string -} +export type StructuredExchangeAnswer = + | { + type: 'text'; + label: string; + value: string; + } + | { + type: 'option'; + label: string; + value: string; + index: number; + } + | { + type: 'other'; + label: string; + value: string; + }; export interface StructuredExchangeResultDetails { - schema: typeof STRUCTURED_EXCHANGE_RESULT_SCHEMA - schemaVersion: 1 - status: StructuredExchangeStatus - question: string - context?: string - mode: StructuredExchangeMode - options?: StructuredExchangeOption[] - answers: StructuredExchangeAnswer[] - rejectedOptions?: StructuredExchangeOption[] - note?: string - transport?: { surface: "tui-custom" | "rpc-editor" | "headless" } - message?: string + schema: typeof STRUCTURED_EXCHANGE_RESULT_SCHEMA; + schemaVersion: 1; + status: StructuredExchangeStatus; + question: string; + context?: string; + mode: StructuredExchangeMode; + options?: StructuredExchangeOption[]; + answers: StructuredExchangeAnswer[]; + rejectedOptions?: StructuredExchangeOption[]; + note?: string; + transport?: { surface: 'tui-custom' | 'rpc-editor' | 'headless' }; + message?: string; } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null + return typeof value === 'object' && value !== null; } -export function isStructuredExchangeResultDetails( - value: unknown, -): value is StructuredExchangeResultDetails { - if (!isRecord(value)) return false +export function isStructuredExchangeResultDetails(value: unknown): value is StructuredExchangeResultDetails { + if (!isRecord(value)) return false; return ( value.schema === STRUCTURED_EXCHANGE_RESULT_SCHEMA && value.schemaVersion === 1 && - (value.status === "answered" || - value.status === "cancelled" || - value.status === "unavailable") && - typeof value.question === "string" && - (value.mode === "text" || - value.mode === "single-select" || - value.mode === "multi-select") && + (value.status === 'answered' || value.status === 'cancelled' || value.status === 'unavailable') && + typeof value.question === 'string' && + (value.mode === 'text' || value.mode === 'single-select' || value.mode === 'multi-select') && Array.isArray(value.answers) - ) + ); } export function isTerminalStructuredExchangeResultDetails( value: unknown, ): value is StructuredExchangeResultDetails { return ( - isStructuredExchangeResultDetails(value) && - (value.status === "answered" || value.status === "cancelled") - ) + isStructuredExchangeResultDetails(value) && (value.status === 'answered' || value.status === 'cancelled') + ); } diff --git a/src/test-helpers.ts b/src/test-helpers.ts index aa6ce2162..a504fa774 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -15,12 +15,8 @@ import type { ThinkingContent, ToolCall, UserMessage, -} from "@earendil-works/pi-ai" -import type { - CustomEntry, - CustomMessageEntry, - SessionEntry, -} from "@earendil-works/pi-coding-agent" +} from '@earendil-works/pi-ai'; +import type { CustomEntry, CustomMessageEntry, SessionEntry } from '@earendil-works/pi-coding-agent'; const ZERO_USAGE = { input: 0, @@ -29,13 +25,10 @@ const ZERO_USAGE = { cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -} as const +} as const; -export function userMessage( - content: string | (TextContent | ImageContent)[], - timestamp = 0, -): UserMessage { - return { role: "user", content, timestamp } +export function userMessage(content: string | (TextContent | ImageContent)[], timestamp = 0): UserMessage { + return { role: 'user', content, timestamp }; } export function assistantMessage( @@ -43,25 +36,23 @@ export function assistantMessage( timestamp = 0, ): AssistantMessage { const content: (TextContent | ThinkingContent | ToolCall)[] = - typeof text === "string" ? [{ type: "text", text }] : text + typeof text === 'string' ? [{ type: 'text', text }] : text; return { - role: "assistant", + role: 'assistant', content, - api: "openai-completions", - provider: "openai", - model: "test-model", + api: 'openai-completions', + provider: 'openai', + model: 'test-model', usage: { ...ZERO_USAGE, cost: { ...ZERO_USAGE.cost } }, - stopReason: "stop", + stopReason: 'stop', timestamp, - } + }; } export function isCustomEntry(entry: SessionEntry): entry is CustomEntry { - return entry.type === "custom" + return entry.type === 'custom'; } -export function isCustomMessageEntry( - entry: SessionEntry, -): entry is CustomMessageEntry { - return entry.type === "custom_message" +export function isCustomMessageEntry(entry: SessionEntry): entry is CustomMessageEntry { + return entry.type === 'custom_message'; } diff --git a/src/web-client/app.test.tsx b/src/web-client/app.test.tsx index 8cb58c6b3..a558d7420 100644 --- a/src/web-client/app.test.tsx +++ b/src/web-client/app.test.tsx @@ -1,84 +1,82 @@ // @vitest-environment jsdom -import { cleanup, render, screen, waitFor } from "@testing-library/react" -import { afterEach, describe, expect, it, vi } from "vitest" +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { TranscriptDisplayProjection } from "../elicitation-exchange.js" -import type { WorkspaceSnapshot } from "../print-snapshot.js" -import { BrunchWebApp, createBrunchWebRuntime } from "./app.js" +import type { TranscriptDisplayProjection } from '../elicitation-exchange.js'; +import type { WorkspaceSnapshot } from '../print-snapshot.js'; +import { BrunchWebApp, createBrunchWebRuntime } from './app.js'; import type { WebSocketRpcClient, WebSocketRpcNotification, WebSocketRpcNotificationListener, -} from "./rpc-client.js" +} from './rpc-client.js'; interface RpcCall { - method: string - params?: unknown + method: string; + params?: unknown; } const readySnapshot: WorkspaceSnapshot = { - status: "ready", - cwd: "/tmp/brunch-project", - spec: { id: "spec-1", title: "Web spec" }, - session: { id: "session-1", file: "/tmp/session.jsonl" }, + status: 'ready', + cwd: '/tmp/brunch-project', + spec: { id: 'spec-1', title: 'Web spec' }, + session: { id: 'session-1', file: '/tmp/session.jsonl' }, chrome: { - phase: "elicitation", - chatMode: "responding-to-elicitation", + phase: 'elicitation', + chatMode: 'responding-to-elicitation', }, -} +}; const selectSpecSnapshot: WorkspaceSnapshot = { - status: "select_spec", - cwd: "/tmp/brunch-project", + status: 'select_spec', + cwd: '/tmp/brunch-project', spec: null, chrome: { - phase: "select_spec", - chatMode: "select-spec", + phase: 'select_spec', + chatMode: 'select-spec', }, -} +}; const readyProjection: TranscriptDisplayProjection = { rows: [ - { id: "prompt-1", role: "prompt", text: "Choose the better framing." }, - { id: "assistant-1", role: "assistant", text: "What should we build?" }, - { id: "user-1", role: "user", text: "A read-only dashboard." }, + { id: 'prompt-1', role: 'prompt', text: 'Choose the better framing.' }, + { id: 'assistant-1', role: 'assistant', text: 'What should we build?' }, + { id: 'user-1', role: 'user', text: 'A read-only dashboard.' }, ], -} +}; function rpcClient(options?: { - snapshot?: WorkspaceSnapshot - projection?: TranscriptDisplayProjection | (() => TranscriptDisplayProjection) - projectionError?: Error - calls?: RpcCall[] - listeners?: Set + snapshot?: WorkspaceSnapshot; + projection?: TranscriptDisplayProjection | (() => TranscriptDisplayProjection); + projectionError?: Error; + calls?: RpcCall[]; + listeners?: Set; }): WebSocketRpcClient { - const snapshot = options?.snapshot ?? readySnapshot - const projection = options?.projection ?? readyProjection - const calls = options?.calls - const listeners = options?.listeners ?? new Set() + const snapshot = options?.snapshot ?? readySnapshot; + const projection = options?.projection ?? readyProjection; + const calls = options?.calls; + const listeners = options?.listeners ?? new Set(); return { - async request(method: string, params?: unknown): Promise { - calls?.push(params === undefined ? { method } : { method, params }) - if (method === "workspace.snapshot") { - return snapshot as T + async request(method: string, params?: unknown): Promise { + calls?.push(params === undefined ? { method } : { method, params }); + if (method === 'workspace.snapshot') { + return snapshot as T; } - if (method === "session.transcriptDisplay") { + if (method === 'session.transcriptDisplay') { if (options?.projectionError) { - throw options.projectionError + throw options.projectionError; } - return ( - typeof projection === "function" ? projection() : projection - ) as T + return (typeof projection === 'function' ? projection() : projection) as T; } - throw new Error(`unexpected RPC method ${method}`) + throw new Error(`unexpected RPC method ${method}`); }, subscribe(listener: WebSocketRpcNotificationListener) { - listeners.add(listener) - return () => listeners.delete(listener) + listeners.add(listener); + return () => listeners.delete(listener); }, close: vi.fn(), - } as unknown as WebSocketRpcClient + } as unknown as WebSocketRpcClient; } function emitNotification( @@ -86,135 +84,131 @@ function emitNotification( notification: WebSocketRpcNotification, ): void { for (const listener of listeners) { - listener(notification) + listener(notification); } } -afterEach(() => cleanup()) +afterEach(() => cleanup()); -describe("Brunch React web app", () => { - it("renders workspace chrome from workspace.snapshot via the RPC client", async () => { - const runtime = createBrunchWebRuntime({ rpcClient: rpcClient() }) +describe('Brunch React web app', () => { + it('renders workspace chrome from workspace.snapshot via the RPC client', async () => { + const runtime = createBrunchWebRuntime({ rpcClient: rpcClient() }); - render() + render(); - expect(await screen.findByText("/tmp/brunch-project")).toBeTruthy() - expect(screen.getByText("Web spec")).toBeTruthy() - expect(screen.getByText("session-1")).toBeTruthy() - expect(screen.getByText("elicitation")).toBeTruthy() - expect(screen.getByText("responding-to-elicitation")).toBeTruthy() - }) + expect(await screen.findByText('/tmp/brunch-project')).toBeTruthy(); + expect(screen.getByText('Web spec')).toBeTruthy(); + expect(screen.getByText('session-1')).toBeTruthy(); + expect(screen.getByText('elicitation')).toBeTruthy(); + expect(screen.getByText('responding-to-elicitation')).toBeTruthy(); + }); - it("requests the selected session projection explicitly", async () => { - const calls: RpcCall[] = [] - const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ calls }) }) + it('requests the selected session projection explicitly', async () => { + const calls: RpcCall[] = []; + const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ calls }) }); - render() + render(); - expect(await screen.findByText("Choose the better framing.")).toBeTruthy() - expect(screen.getByText("What should we build?")).toBeTruthy() - expect(screen.getByText("A read-only dashboard.")).toBeTruthy() - expect(screen.getByLabelText("prompt message")).toBeTruthy() - expect(calls).toContainEqual({ method: "workspace.snapshot" }) + expect(await screen.findByText('Choose the better framing.')).toBeTruthy(); + expect(screen.getByText('What should we build?')).toBeTruthy(); + expect(screen.getByText('A read-only dashboard.')).toBeTruthy(); + expect(screen.getByLabelText('prompt message')).toBeTruthy(); + expect(calls).toContainEqual({ method: 'workspace.snapshot' }); expect(calls).toContainEqual({ - method: "session.transcriptDisplay", - params: { sessionId: "session-1", specId: "spec-1" }, - }) - }) + method: 'session.transcriptDisplay', + params: { sessionId: 'session-1', specId: 'spec-1' }, + }); + }); - it("renders an empty transcript display state", async () => { + it('renders an empty transcript display state', async () => { const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ projection: { rows: [] } }), - }) + }); - render() + render(); - expect(await screen.findByText("No transcript messages yet.")).toBeTruthy() - }) + expect(await screen.findByText('No transcript messages yet.')).toBeTruthy(); + }); - it("refetches selected session transcript when the RPC client reports a product update", async () => { - const listeners = new Set() - let projection: TranscriptDisplayProjection = { rows: [] } + it('refetches selected session transcript when the RPC client reports a product update', async () => { + const listeners = new Set(); + let projection: TranscriptDisplayProjection = { rows: [] }; const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ listeners, projection: () => projection, }), - }) + }); - render() + render(); - expect(await screen.findByText("No transcript messages yet.")).toBeTruthy() + expect(await screen.findByText('No transcript messages yet.')).toBeTruthy(); projection = { rows: [ { - id: "prompt-2", - role: "prompt", - text: "Is this a new product or feature from scratch?", + id: 'prompt-2', + role: 'prompt', + text: 'Is this a new product or feature from scratch?', }, ], - } + }; emitNotification(listeners, { - jsonrpc: "2.0", - method: "brunch.updated", - params: { topics: ["session.transcriptDisplay"] }, - }) + jsonrpc: '2.0', + method: 'brunch.updated', + params: { topics: ['session.transcriptDisplay'] }, + }); await waitFor(() => - expect( - screen.getByText("Is this a new product or feature from scratch?"), - ).toBeTruthy(), - ) - }) - - it("does not request session projection when no session is selected", async () => { - const calls: RpcCall[] = [] + expect(screen.getByText('Is this a new product or feature from scratch?')).toBeTruthy(), + ); + }); + + it('does not request session projection when no session is selected', async () => { + const calls: RpcCall[] = []; const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ snapshot: selectSpecSnapshot, calls }), - }) + }); - render() + render(); - expect(await screen.findByText("No Brunch session selected.")).toBeTruthy() - expect(calls).toEqual([{ method: "workspace.snapshot" }]) - }) + expect(await screen.findByText('No Brunch session selected.')).toBeTruthy(); + expect(calls).toEqual([{ method: 'workspace.snapshot' }]); + }); - it("renders read-only session projection errors", async () => { + it('renders read-only session projection errors', async () => { const runtime = createBrunchWebRuntime({ rpcClient: rpcClient({ - projectionError: new Error("Brunch session transcript is non-linear"), + projectionError: new Error('Brunch session transcript is non-linear'), }), - }) + }); - render() + render(); expect( - await screen.findByText( - "Transcript unavailable: Brunch session transcript is non-linear", - ), - ).toBeTruthy() - }) - - it("keeps one router and QueryClient across BrunchWebApp re-renders", async () => { - const runtime = createBrunchWebRuntime({ rpcClient: rpcClient() }) - const initialRouter = runtime.router - const initialQueryClient = runtime.queryClient - const { rerender } = render() - await screen.findAllByText("Web spec") - - rerender() - - expect(runtime.router).toBe(initialRouter) - expect(runtime.queryClient).toBe(initialQueryClient) - }) - - it("disposes the root-owned RPC client", () => { - const client = rpcClient() - const runtime = createBrunchWebRuntime({ rpcClient: client }) - - runtime.dispose() - - expect(client.close).toHaveBeenCalledOnce() - }) -}) + await screen.findByText('Transcript unavailable: Brunch session transcript is non-linear'), + ).toBeTruthy(); + }); + + it('keeps one router and QueryClient across BrunchWebApp re-renders', async () => { + const runtime = createBrunchWebRuntime({ rpcClient: rpcClient() }); + const initialRouter = runtime.router; + const initialQueryClient = runtime.queryClient; + const { rerender } = render(); + await screen.findAllByText('Web spec'); + + rerender(); + + expect(runtime.router).toBe(initialRouter); + expect(runtime.queryClient).toBe(initialQueryClient); + }); + + it('disposes the root-owned RPC client', () => { + const client = rpcClient(); + const runtime = createBrunchWebRuntime({ rpcClient: client }); + + runtime.dispose(); + + expect(client.close).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/web-client/app.tsx b/src/web-client/app.tsx index db9a53cde..908abf6ca 100644 --- a/src/web-client/app.tsx +++ b/src/web-client/app.tsx @@ -4,111 +4,94 @@ import { queryOptions, useQuery, useSuspenseQuery, -} from "@tanstack/react-query" -import { - RouterProvider, - createRootRouteWithContext, - createRouter, -} from "@tanstack/react-router" -import { Suspense, useEffect } from "react" +} from '@tanstack/react-query'; +import { RouterProvider, createRootRouteWithContext, createRouter } from '@tanstack/react-router'; +import { Suspense, useEffect } from 'react'; -import type { TranscriptDisplayProjection } from "../elicitation-exchange.js" -import type { WorkspaceSnapshot } from "../print-snapshot.js" -import type { WebSocketRpcClient } from "./rpc-client.js" +import type { TranscriptDisplayProjection } from '../elicitation-exchange.js'; +import type { WorkspaceSnapshot } from '../print-snapshot.js'; +import type { WebSocketRpcClient } from './rpc-client.js'; type RouterContext = { - queryClient: QueryClient - rpcClient: WebSocketRpcClient -} + queryClient: QueryClient; + rpcClient: WebSocketRpcClient; +}; type SessionProjectionTarget = { - sessionId: string - specId: string -} + sessionId: string; + specId: string; +}; export interface BrunchWebRuntime { - queryClient: QueryClient - rpcClient: WebSocketRpcClient - router: ReturnType - dispose(): void + queryClient: QueryClient; + rpcClient: WebSocketRpcClient; + router: ReturnType; + dispose(): void; } const rootRoute = createRootRouteWithContext()({ loader: ({ context }) => - context.queryClient.ensureQueryData( - workspaceSnapshotQueryOptions(context.rpcClient), - ), + context.queryClient.ensureQueryData(workspaceSnapshotQueryOptions(context.rpcClient)), component: WorkspaceSnapshotPage, -}) +}); -const routeTree = rootRoute +const routeTree = rootRoute; -export function createBrunchWebRouter(options: { - queryClient: QueryClient - rpcClient: WebSocketRpcClient -}) { +export function createBrunchWebRouter(options: { queryClient: QueryClient; rpcClient: WebSocketRpcClient }) { return createRouter({ routeTree, defaultPreloadStaleTime: 0, context: options, Wrap: ({ children }) => ( - - {children} - + {children} ), - }) + }); } -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface Register { - router: ReturnType + router: ReturnType; } } -export function createBrunchWebRuntime(options: { - rpcClient: WebSocketRpcClient -}): BrunchWebRuntime { - const queryClient = new QueryClient() +export function createBrunchWebRuntime(options: { rpcClient: WebSocketRpcClient }): BrunchWebRuntime { + const queryClient = new QueryClient(); const router = createBrunchWebRouter({ queryClient, rpcClient: options.rpcClient, - }) + }); return { queryClient, rpcClient: options.rpcClient, router, dispose() { - options.rpcClient.close() - queryClient.clear() + options.rpcClient.close(); + queryClient.clear(); }, - } + }; } export function BrunchWebApp(options: { runtime: BrunchWebRuntime }) { return ( - Loading Brunch workspace…} - > + Loading Brunch workspace…}> - ) + ); } function workspaceSnapshotQueryOptions(rpcClient: WebSocketRpcClient) { return queryOptions({ - queryKey: ["workspace.snapshot"], - queryFn: () => rpcClient.request("workspace.snapshot"), - }) + queryKey: ['workspace.snapshot'], + queryFn: () => rpcClient.request('workspace.snapshot'), + }); } -function sessionProjectionTargetFromSnapshot( - snapshot: WorkspaceSnapshot, -): SessionProjectionTarget | null { +function sessionProjectionTargetFromSnapshot(snapshot: WorkspaceSnapshot): SessionProjectionTarget | null { if (!snapshot.session || !snapshot.spec) { - return null + return null; } - return { sessionId: snapshot.session.id, specId: snapshot.spec.id } + return { sessionId: snapshot.session.id, specId: snapshot.spec.id }; } function sessionTranscriptDisplayQueryOptions( @@ -116,47 +99,39 @@ function sessionTranscriptDisplayQueryOptions( target: SessionProjectionTarget | null, ) { return { - queryKey: [ - "session.transcriptDisplay", - target?.sessionId ?? null, - target?.specId ?? null, - ], + queryKey: ['session.transcriptDisplay', target?.sessionId ?? null, target?.specId ?? null], queryFn: () => rpcClient.request( - "session.transcriptDisplay", + 'session.transcriptDisplay', target ?? unreachableSessionProjectionTarget(), ), enabled: target !== null, retry: false, - } + }; } function unreachableSessionProjectionTarget(): never { - throw new Error("Session transcript query is disabled without a target") + throw new Error('Session transcript query is disabled without a target'); } function WorkspaceSnapshotPage() { - const { queryClient, rpcClient } = rootRoute.useRouteContext() + const { queryClient, rpcClient } = rootRoute.useRouteContext(); useEffect( () => rpcClient.subscribe((notification) => { - if (notification.method !== "brunch.updated") return + if (notification.method !== 'brunch.updated') return; void queryClient.invalidateQueries({ - queryKey: ["workspace.snapshot"], - }) + queryKey: ['workspace.snapshot'], + }); void queryClient.invalidateQueries({ - queryKey: ["session.transcriptDisplay"], - }) + queryKey: ['session.transcriptDisplay'], + }); }), [queryClient, rpcClient], - ) - const { data: snapshot } = useSuspenseQuery( - workspaceSnapshotQueryOptions(rpcClient), - ) - const target = sessionProjectionTargetFromSnapshot(snapshot) - const projection = useQuery( - sessionTranscriptDisplayQueryOptions(rpcClient, target), - ) + ); + const { data: snapshot } = useSuspenseQuery(workspaceSnapshotQueryOptions(rpcClient)); + const target = sessionProjectionTargetFromSnapshot(snapshot); + const projection = useQuery(sessionTranscriptDisplayQueryOptions(rpcClient, target)); return (
@@ -168,11 +143,11 @@ function WorkspaceSnapshotPage() {
spec
-
{snapshot.spec?.title ?? ""}
+
{snapshot.spec?.title ?? ''}
session
-
{snapshot.session?.id ?? ""}
+
{snapshot.session?.id ?? ''}
phase
@@ -185,12 +160,12 @@ function WorkspaceSnapshotPage() {
- ) + ); } function TranscriptPanel(options: { - snapshot: WorkspaceSnapshot - projection: ReturnType> + snapshot: WorkspaceSnapshot; + projection: ReturnType>; }) { if (!options.snapshot.session || !options.snapshot.spec) { return ( @@ -198,7 +173,7 @@ function TranscriptPanel(options: {

Session transcript

No Brunch session selected.

- ) + ); } if (options.projection.isError) { @@ -207,7 +182,7 @@ function TranscriptPanel(options: {

Session transcript

{`Transcript unavailable: ${errorMessage(options.projection.error)}`}

- ) + ); } if (!options.projection.data) { @@ -216,10 +191,10 @@ function TranscriptPanel(options: {

Session transcript

Loading transcript…

- ) + ); } - const projection = options.projection.data + const projection = options.projection.data; return (

Session transcript

@@ -235,9 +210,9 @@ function TranscriptPanel(options: { ))}
- ) + ); } function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error) + return error instanceof Error ? error.message : String(error); } diff --git a/src/web-client/main.tsx b/src/web-client/main.tsx index 2d2a46a56..c3a41b414 100644 --- a/src/web-client/main.tsx +++ b/src/web-client/main.tsx @@ -1,21 +1,21 @@ -import { StrictMode } from "react" -import { createRoot } from "react-dom/client" +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; -import { BrunchWebApp, createBrunchWebRuntime } from "./app.js" -import { createWebSocketRpcClient } from "./rpc-client.js" +import { BrunchWebApp, createBrunchWebRuntime } from './app.js'; +import { createWebSocketRpcClient } from './rpc-client.js'; -const rootElement = document.getElementById("root") +const rootElement = document.getElementById('root'); if (!rootElement) { - throw new Error("Brunch web shell requires a #root element") + throw new Error('Brunch web shell requires a #root element'); } const runtime = createBrunchWebRuntime({ rpcClient: createWebSocketRpcClient({}), -}) -window.addEventListener("pagehide", () => runtime.dispose(), { once: true }) +}); +window.addEventListener('pagehide', () => runtime.dispose(), { once: true }); createRoot(rootElement).render( , -) +); diff --git a/src/web-client/rpc-client.test.ts b/src/web-client/rpc-client.test.ts index dedc724e3..60e8e2e4c 100644 --- a/src/web-client/rpc-client.test.ts +++ b/src/web-client/rpc-client.test.ts @@ -1,275 +1,233 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest'; -import { JsonRpcClientError, createWebSocketRpcClient } from "./rpc-client.js" +import { JsonRpcClientError, createWebSocketRpcClient } from './rpc-client.js'; -type Listener = (event: { data?: string }) => void +type Listener = (event: { data?: string }) => void; class FakeWebSocket { - static instances: FakeWebSocket[] = [] + static instances: FakeWebSocket[] = []; - readonly sent: string[] = [] - readonly listeners = new Map() - closed = false + readonly sent: string[] = []; + readonly listeners = new Map(); + closed = false; constructor(readonly url: string) { - FakeWebSocket.instances.push(this) + FakeWebSocket.instances.push(this); } send(message: string) { - this.sent.push(message) + this.sent.push(message); } close() { - this.closed = true + this.closed = true; } addEventListener(event: string, listener: Listener) { - const listeners = this.listeners.get(event) ?? [] - listeners.push(listener) - this.listeners.set(event, listeners) + const listeners = this.listeners.get(event) ?? []; + listeners.push(listener); + this.listeners.set(event, listeners); } emit(event: string, data?: string) { for (const listener of this.listeners.get(event) ?? []) { - listener(data === undefined ? {} : { data }) + listener(data === undefined ? {} : { data }); } } } function rpcClient() { - FakeWebSocket.instances = [] + FakeWebSocket.instances = []; return createWebSocketRpcClient({ - url: "ws://brunch.test/rpc", + url: 'ws://brunch.test/rpc', WebSocketImpl: FakeWebSocket, - }) + }); } -describe("browser WebSocket RPC client", () => { - it("opens one persistent socket and queues requests until open", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - - expect(FakeWebSocket.instances).toHaveLength(1) - const socket = FakeWebSocket.instances[0]! - expect(socket.sent).toHaveLength(0) - - socket.emit("open") - - expect(socket.sent).toHaveLength(2) - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 1, result: "first" }), - ) - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 2, result: "second" }), - ) - await expect(first).resolves.toBe("first") - await expect(second).resolves.toBe("second") - }) - - it("resolves concurrent requests by response id, not response order", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("workspace.snapshot") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 2, result: "second" }), - ) - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 1, result: "first" }), - ) - - await expect(first).resolves.toBe("first") - await expect(second).resolves.toBe("second") - }) - - it("delivers JSON-RPC notifications without disturbing pending requests", async () => { - const client = rpcClient() - const notifications: unknown[] = [] - client.subscribe((notification) => notifications.push(notification)) - const request = client.request("workspace.snapshot") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") +describe('browser WebSocket RPC client', () => { + it('opens one persistent socket and queues requests until open', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + + expect(FakeWebSocket.instances).toHaveLength(1); + const socket = FakeWebSocket.instances[0]!; + expect(socket.sent).toHaveLength(0); + + socket.emit('open'); + + expect(socket.sent).toHaveLength(2); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'first' })); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 2, result: 'second' })); + await expect(first).resolves.toBe('first'); + await expect(second).resolves.toBe('second'); + }); + + it('resolves concurrent requests by response id, not response order', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('workspace.snapshot'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 2, result: 'second' })); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'first' })); + + await expect(first).resolves.toBe('first'); + await expect(second).resolves.toBe('second'); + }); + + it('delivers JSON-RPC notifications without disturbing pending requests', async () => { + const client = rpcClient(); + const notifications: unknown[] = []; + client.subscribe((notification) => notifications.push(notification)); + const request = client.request('workspace.snapshot'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); socket.emit( - "message", + 'message', JSON.stringify({ - jsonrpc: "2.0", - method: "brunch.updated", - params: { topics: ["session.transcriptDisplay"] }, + jsonrpc: '2.0', + method: 'brunch.updated', + params: { topics: ['session.transcriptDisplay'] }, }), - ) - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 1, result: "snapshot" }), - ) + ); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'snapshot' })); - await expect(request).resolves.toBe("snapshot") + await expect(request).resolves.toBe('snapshot'); expect(notifications).toEqual([ { - jsonrpc: "2.0", - method: "brunch.updated", - params: { topics: ["session.transcriptDisplay"] }, + jsonrpc: '2.0', + method: 'brunch.updated', + params: { topics: ['session.transcriptDisplay'] }, }, - ]) - }) - - it("unsubscribes notification listeners", () => { - const client = rpcClient() - const notifications: unknown[] = [] - const unsubscribe = client.subscribe((notification) => - notifications.push(notification), - ) - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - unsubscribe() - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", method: "brunch.updated" }), - ) + ]); + }); + + it('unsubscribes notification listeners', () => { + const client = rpcClient(); + const notifications: unknown[] = []; + const unsubscribe = client.subscribe((notification) => notifications.push(notification)); + const socket = FakeWebSocket.instances[0]!; - expect(notifications).toEqual([]) - }) + socket.emit('open'); + unsubscribe(); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', method: 'brunch.updated' })); - it("rejects JSON-RPC failures with code and message", async () => { - const client = rpcClient() - const request = client.request("workspace.snapshot") - const socket = FakeWebSocket.instances[0]! + expect(notifications).toEqual([]); + }); - socket.emit("open") + it('rejects JSON-RPC failures with code and message', async () => { + const client = rpcClient(); + const request = client.request('workspace.snapshot'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); socket.emit( - "message", + 'message', JSON.stringify({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - error: { code: -32603, message: "Internal error" }, + error: { code: -32603, message: 'Internal error' }, }), - ) + ); await expect(request).rejects.toMatchObject({ - name: "JsonRpcClientError", + name: 'JsonRpcClientError', code: -32603, - message: "Internal error", - } satisfies Partial) - }) - - it("rejects all pending requests and later calls on malformed response frames", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit("message", "not json") - - await expect(first).rejects.toThrow("Brunch WebSocket RPC protocol failure") - await expect(second).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - await expect(client.request("workspace.snapshot")).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - }) - - it("rejects all pending requests and later calls on invalid response frames", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", result: "missing id" }), - ) - - await expect(first).rejects.toThrow("Brunch WebSocket RPC protocol failure") - await expect(second).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - await expect(client.request("workspace.snapshot")).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - }) - - it("rejects all pending requests and later calls on unknown response IDs", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit( - "message", - JSON.stringify({ jsonrpc: "2.0", id: 999, result: "unknown" }), - ) - - await expect(first).rejects.toThrow("Brunch WebSocket RPC protocol failure") - await expect(second).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - await expect(client.request("workspace.snapshot")).rejects.toThrow( - "Brunch WebSocket RPC protocol failure", - ) - }) - - it("rejects all pending requests on socket close", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit("close") - - await expect(first).rejects.toThrow( - "Brunch WebSocket RPC connection closed", - ) - await expect(second).rejects.toThrow( - "Brunch WebSocket RPC connection closed", - ) - }) - - it("treats socket errors as terminal connection failures", async () => { - const client = rpcClient() - const first = client.request("workspace.snapshot") - const second = client.request("session.elicitationExchanges") - const socket = FakeWebSocket.instances[0]! - - socket.emit("open") - socket.emit("error") - socket.emit("close") - - await expect(first).rejects.toThrow( - "Brunch WebSocket RPC connection failed", - ) - await expect(second).rejects.toThrow( - "Brunch WebSocket RPC connection failed", - ) - await expect(client.request("workspace.snapshot")).rejects.toThrow( - "Brunch WebSocket RPC connection failed", - ) - }) - - it("exposes close and rejects later requests", async () => { - const client = rpcClient() - const pending = client.request("workspace.snapshot") - const socket = FakeWebSocket.instances[0]! - socket.emit("open") - - client.close() - - expect(socket.closed).toBe(true) - await expect(pending).rejects.toThrow("Brunch WebSocket RPC client closed") - await expect(client.request("workspace.snapshot")).rejects.toThrow( - "Brunch WebSocket RPC client closed", - ) - }) -}) + message: 'Internal error', + } satisfies Partial); + }); + + it('rejects all pending requests and later calls on malformed response frames', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('message', 'not json'); + + await expect(first).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(second).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(client.request('workspace.snapshot')).rejects.toThrow( + 'Brunch WebSocket RPC protocol failure', + ); + }); + + it('rejects all pending requests and later calls on invalid response frames', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', result: 'missing id' })); + + await expect(first).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(second).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(client.request('workspace.snapshot')).rejects.toThrow( + 'Brunch WebSocket RPC protocol failure', + ); + }); + + it('rejects all pending requests and later calls on unknown response IDs', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('message', JSON.stringify({ jsonrpc: '2.0', id: 999, result: 'unknown' })); + + await expect(first).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(second).rejects.toThrow('Brunch WebSocket RPC protocol failure'); + await expect(client.request('workspace.snapshot')).rejects.toThrow( + 'Brunch WebSocket RPC protocol failure', + ); + }); + + it('rejects all pending requests on socket close', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('close'); + + await expect(first).rejects.toThrow('Brunch WebSocket RPC connection closed'); + await expect(second).rejects.toThrow('Brunch WebSocket RPC connection closed'); + }); + + it('treats socket errors as terminal connection failures', async () => { + const client = rpcClient(); + const first = client.request('workspace.snapshot'); + const second = client.request('session.elicitationExchanges'); + const socket = FakeWebSocket.instances[0]!; + + socket.emit('open'); + socket.emit('error'); + socket.emit('close'); + + await expect(first).rejects.toThrow('Brunch WebSocket RPC connection failed'); + await expect(second).rejects.toThrow('Brunch WebSocket RPC connection failed'); + await expect(client.request('workspace.snapshot')).rejects.toThrow( + 'Brunch WebSocket RPC connection failed', + ); + }); + + it('exposes close and rejects later requests', async () => { + const client = rpcClient(); + const pending = client.request('workspace.snapshot'); + const socket = FakeWebSocket.instances[0]!; + socket.emit('open'); + + client.close(); + + expect(socket.closed).toBe(true); + await expect(pending).rejects.toThrow('Brunch WebSocket RPC client closed'); + await expect(client.request('workspace.snapshot')).rejects.toThrow('Brunch WebSocket RPC client closed'); + }); +}); diff --git a/src/web-client/rpc-client.ts b/src/web-client/rpc-client.ts index a31d62e17..d39bb1d83 100644 --- a/src/web-client/rpc-client.ts +++ b/src/web-client/rpc-client.ts @@ -1,246 +1,233 @@ -import type { - JsonRpcFailure, - JsonRpcId, - JsonRpcRequest, - JsonRpcResponse, -} from "../rpc/protocol.js" +import type { JsonRpcFailure, JsonRpcId, JsonRpcRequest, JsonRpcResponse } from '../rpc/protocol.js'; -export type { JsonRpcRequest, JsonRpcResponse } from "../rpc/protocol.js" +export type { JsonRpcRequest, JsonRpcResponse } from '../rpc/protocol.js'; -type WebSocketEventListener = (event: { data?: unknown }) => void +type WebSocketEventListener = (event: { data?: unknown }) => void; -type WebSocketLike = Pick & { - addEventListener(event: string, listener: WebSocketEventListener): void -} +type WebSocketLike = Pick & { + addEventListener(event: string, listener: WebSocketEventListener): void; +}; -type WebSocketConstructor = new (url: string) => WebSocketLike +type WebSocketConstructor = new (url: string) => WebSocketLike; export interface WebSocketRpcClient { - request(method: string, params?: unknown): Promise - subscribe(listener: WebSocketRpcNotificationListener): () => void - close(): void + request(method: string, params?: unknown): Promise; + subscribe(listener: WebSocketRpcNotificationListener): () => void; + close(): void; } export interface WebSocketRpcNotification { - jsonrpc: "2.0" - method: string - params?: unknown + jsonrpc: '2.0'; + method: string; + params?: unknown; } -export type WebSocketRpcNotificationListener = ( - notification: WebSocketRpcNotification, -) => void +export type WebSocketRpcNotificationListener = (notification: WebSocketRpcNotification) => void; export class JsonRpcClientError extends Error { - readonly code: number + readonly code: number; - constructor(error: JsonRpcFailure["error"]) { - super(error.message) - this.name = "JsonRpcClientError" - this.code = error.code + constructor(error: JsonRpcFailure['error']) { + super(error.message); + this.name = 'JsonRpcClientError'; + this.code = error.code; } } type PendingRequest = { - resolve(value: unknown): void - reject(error: Error): void -} + resolve(value: unknown): void; + reject(error: Error): void; +}; interface ResponseFrameSuccess { - ok: true - value: JsonRpcResponse | WebSocketRpcNotification + ok: true; + value: JsonRpcResponse | WebSocketRpcNotification; } interface ResponseFrameFailure { - ok: false + ok: false; } -type ResponseFrameParseResult = ResponseFrameSuccess | ResponseFrameFailure +type ResponseFrameParseResult = ResponseFrameSuccess | ResponseFrameFailure; export function createWebSocketRpcClient(options: { - url?: string - WebSocketImpl?: WebSocketConstructor + url?: string; + WebSocketImpl?: WebSocketConstructor; }): WebSocketRpcClient { - const WebSocketImpl = options.WebSocketImpl ?? WebSocket - const url = options.url ?? defaultRpcUrl() - const socket = new WebSocketImpl(url) - const pending = new Map() - const notificationListeners = new Set() - const queued: string[] = [] - let nextId = 1 - let isOpen = false - let isClosed = false - let terminalError: Error | null = null - - socket.addEventListener("open", () => { - isOpen = true + const WebSocketImpl = options.WebSocketImpl ?? WebSocket; + const url = options.url ?? defaultRpcUrl(); + const socket = new WebSocketImpl(url); + const pending = new Map(); + const notificationListeners = new Set(); + const queued: string[] = []; + let nextId = 1; + let isOpen = false; + let isClosed = false; + let terminalError: Error | null = null; + + socket.addEventListener('open', () => { + isOpen = true; for (const message of queued.splice(0)) { - socket.send(message) + socket.send(message); } - }) + }); - socket.addEventListener("message", (event) => { - const parsed = parseResponseFrame(event.data) + socket.addEventListener('message', (event) => { + const parsed = parseResponseFrame(event.data); if (!parsed.ok) { - failProtocol() - return + failProtocol(); + return; } if (isJsonRpcNotification(parsed.value)) { for (const listener of notificationListeners) { - listener(parsed.value) + listener(parsed.value); } - return + return; } - const response = parsed.value - const request = pending.get(response.id) + const response = parsed.value; + const request = pending.get(response.id); if (!request) { - failProtocol() - return + failProtocol(); + return; } - pending.delete(response.id) - if ("error" in response) { - request.reject(new JsonRpcClientError(response.error)) - return + pending.delete(response.id); + if ('error' in response) { + request.reject(new JsonRpcClientError(response.error)); + return; } - request.resolve(response.result) - }) + request.resolve(response.result); + }); - socket.addEventListener("close", () => { + socket.addEventListener('close', () => { if (!isClosed) { - rejectPending(new Error("Brunch WebSocket RPC connection closed")) + rejectPending(new Error('Brunch WebSocket RPC connection closed')); } - isClosed = true - }) + isClosed = true; + }); - socket.addEventListener("error", () => { - terminalError = new Error("Brunch WebSocket RPC connection failed") - isClosed = true - rejectPending(terminalError) - }) + socket.addEventListener('error', () => { + terminalError = new Error('Brunch WebSocket RPC connection failed'); + isClosed = true; + rejectPending(terminalError); + }); function failProtocol(): void { // A malformed, uncorrelatable, or otherwise invalid server frame means the // client cannot trust response correlation anymore. Close this attachment, // reject pending calls, and make future requests fail immediately. - terminalError = new Error("Brunch WebSocket RPC protocol failure") - isClosed = true - rejectPending(terminalError) - socket.close() + terminalError = new Error('Brunch WebSocket RPC protocol failure'); + isClosed = true; + rejectPending(terminalError); + socket.close(); } function rejectPending(error: Error): void { for (const request of pending.values()) { - request.reject(error) + request.reject(error); } - pending.clear() - queued.length = 0 + pending.clear(); + queued.length = 0; } return { - request(method: string, params?: unknown): Promise { + request(method: string, params?: unknown): Promise { if (terminalError) { - return Promise.reject(terminalError) + return Promise.reject(terminalError); } if (isClosed) { - return Promise.reject(new Error("Brunch WebSocket RPC client closed")) + return Promise.reject(new Error('Brunch WebSocket RPC client closed')); } - const id = nextId - nextId += 1 + const id = nextId; + nextId += 1; const request: JsonRpcRequest = { - jsonrpc: "2.0", + jsonrpc: '2.0', id, method, ...(params === undefined ? {} : { params }), - } - const message = JSON.stringify(request) + }; + const message = JSON.stringify(request); return new Promise((resolve, reject) => { pending.set(id, { resolve: (value) => resolve(value as T), reject, - }) + }); if (isOpen) { - socket.send(message) - return + socket.send(message); + return; } - queued.push(message) - }) + queued.push(message); + }); }, subscribe(listener: WebSocketRpcNotificationListener) { - notificationListeners.add(listener) + notificationListeners.add(listener); return () => { - notificationListeners.delete(listener) - } + notificationListeners.delete(listener); + }; }, close() { if (isClosed) { - return + return; } - isClosed = true - rejectPending(new Error("Brunch WebSocket RPC client closed")) - socket.close() + isClosed = true; + rejectPending(new Error('Brunch WebSocket RPC client closed')); + socket.close(); }, - } + }; } function parseResponseFrame(data: unknown): ResponseFrameParseResult { try { - const value = JSON.parse(String(data)) as unknown - return isJsonRpcResponse(value) || isJsonRpcNotification(value) - ? { ok: true, value } - : { ok: false } + const value = JSON.parse(String(data)) as unknown; + return isJsonRpcResponse(value) || isJsonRpcNotification(value) ? { ok: true, value } : { ok: false }; } catch { - return { ok: false } + return { ok: false }; } } -function isJsonRpcNotification( - value: unknown, -): value is WebSocketRpcNotification { +function isJsonRpcNotification(value: unknown): value is WebSocketRpcNotification { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { jsonrpc?: unknown }).jsonrpc === "2.0" && - typeof (value as { method?: unknown }).method === "string" && - !Object.hasOwn(value, "id") - ) + (value as { jsonrpc?: unknown }).jsonrpc === '2.0' && + typeof (value as { method?: unknown }).method === 'string' && + !Object.hasOwn(value, 'id') + ); } function isJsonRpcResponse(value: unknown): value is JsonRpcResponse { if ( - typeof value !== "object" || + typeof value !== 'object' || value === null || - (value as { jsonrpc?: unknown }).jsonrpc !== "2.0" || + (value as { jsonrpc?: unknown }).jsonrpc !== '2.0' || !isJsonRpcId((value as { id?: unknown }).id) ) { - return false + return false; } - if ("error" in value) { - const error = (value as { error?: unknown }).error + if ('error' in value) { + const error = (value as { error?: unknown }).error; return ( - typeof error === "object" && + typeof error === 'object' && error !== null && - typeof (error as { code?: unknown }).code === "number" && - typeof (error as { message?: unknown }).message === "string" - ) + typeof (error as { code?: unknown }).code === 'number' && + typeof (error as { message?: unknown }).message === 'string' + ); } - return Object.hasOwn(value, "result") + return Object.hasOwn(value, 'result'); } function isJsonRpcId(value: unknown): value is JsonRpcId { - return ( - value === null || typeof value === "string" || typeof value === "number" - ) + return value === null || typeof value === 'string' || typeof value === 'number'; } function defaultRpcUrl(): string { - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" - return `${protocol}//${window.location.host}/rpc` + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/rpc`; } diff --git a/src/web-host.test.ts b/src/web-host.test.ts index b423d0c24..85c56f030 100644 --- a/src/web-host.test.ts +++ b/src/web-host.test.ts @@ -1,611 +1,569 @@ -import { request } from "node:http" -import { mkdir, mkdtemp, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; +import { request } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { describe, expect, it } from "vitest" - -import { SessionManager } from "@earendil-works/pi-coding-agent" +import { SessionManager } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; +import { assistantMessage, userMessage } from './test-helpers.js'; +import { startWebHost } from './web-host.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from "./workspace-session-coordinator.js" -import { startWebHost } from "./web-host.js" -import { assistantMessage, userMessage } from "./test-helpers.js" +} from './workspace-session-coordinator.js'; function text(response: Response): Promise { - return response.text() + return response.text(); } async function rawGet(url: string, path: string): Promise { - const base = new URL(url) + const base = new URL(url); return new Promise((resolve, reject) => { const req = request( { hostname: base.hostname, port: base.port, - method: "GET", + method: 'GET', path, }, (res) => { - const chunks: Uint8Array[] = [] - res.on("data", (chunk: Uint8Array) => chunks.push(chunk)) - res.on("end", () => { + const chunks: Uint8Array[] = []; + res.on('data', (chunk: Uint8Array) => chunks.push(chunk)); + res.on('end', () => { resolve( new Response(Buffer.concat(chunks), { - ...(res.statusCode !== undefined - ? { status: res.statusCode } - : {}), + ...(res.statusCode !== undefined ? { status: res.statusCode } : {}), headers: res.headers as Record, }), - ) - }) + ); + }); }, - ) - req.on("error", reject) - req.end() - }) + ); + req.on('error', reject); + req.end(); + }); } async function builtWebAssets(): Promise { - const assetRoot = await mkdtemp(join(tmpdir(), "brunch-web-assets-")) - await mkdir(join(assetRoot, "assets")) + const assetRoot = await mkdtemp(join(tmpdir(), 'brunch-web-assets-')); + await mkdir(join(assetRoot, 'assets')); await writeFile( - join(assetRoot, "index.html"), + join(assetRoot, 'index.html'), 'Brunch
', - ) - await writeFile( - join(assetRoot, "assets", "brunch-web.js"), - "console.log('built web')", - ) - return assetRoot + ); + await writeFile(join(assetRoot, 'assets', 'brunch-web.js'), "console.log('built web')"); + return assetRoot; } -describe("web host", () => { - it("serves built Vite index.html as the native Brunch HTML shell", async () => { - const assetRoot = await builtWebAssets() +describe('web host', () => { + it('serves built Vite index.html as the native Brunch HTML shell', async () => { + const assetRoot = await builtWebAssets(); const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, webAssetRoot: assetRoot, - }) + }); try { - const response = await fetch(host.url) - const html = await text(response) - - expect(response.status).toBe(200) - expect(response.headers.get("content-type")).toContain("text/html") - expect(html).toContain('data-built-shell="true"') - expect(html).toContain("/assets/brunch-web.js") - expect(html).not.toContain("pi-web-ui") + const response = await fetch(host.url); + const html = await text(response); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toContain('text/html'); + expect(html).toContain('data-built-shell="true"'); + expect(html).toContain('/assets/brunch-web.js'); + expect(html).not.toContain('pi-web-ui'); } finally { - await host.close() + await host.close(); } - }) + }); - it("serves built Vite JavaScript assets", async () => { - const assetRoot = await builtWebAssets() + it('serves built Vite JavaScript assets', async () => { + const assetRoot = await builtWebAssets(); const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, webAssetRoot: assetRoot, - }) + }); try { - const response = await fetch(`${host.url}/assets/brunch-web.js`) - const body = await text(response) + const response = await fetch(`${host.url}/assets/brunch-web.js`); + const body = await text(response); - expect(response.status).toBe(200) - expect(response.headers.get("content-type")).toContain("text/javascript") - expect(body).toContain("console.log('built web')") + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toContain('text/javascript'); + expect(body).toContain("console.log('built web')"); } finally { - await host.close() + await host.close(); } - }) + }); - it("rejects asset traversal without reading outside the web asset root", async () => { - const assetRoot = await builtWebAssets() - await writeFile(join(assetRoot, "secret.txt"), "outside asset root") + it('rejects asset traversal without reading outside the web asset root', async () => { + const assetRoot = await builtWebAssets(); + await writeFile(join(assetRoot, 'secret.txt'), 'outside asset root'); const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, webAssetRoot: assetRoot, - }) + }); try { - const traversal = await rawGet(host.url, "/assets/../secret.txt") - const encodedTraversal = await rawGet( - host.url, - "/assets/%2e%2e/secret.txt", - ) - const absoluteLike = await rawGet(host.url, "/assets/%2Ftmp/secret.txt") - - expect(traversal.status).toBe(404) - expect(await text(traversal)).not.toContain("outside asset root") - expect(encodedTraversal.status).toBe(404) - expect(await text(encodedTraversal)).not.toContain("outside asset root") - expect(absoluteLike.status).toBe(404) + const traversal = await rawGet(host.url, '/assets/../secret.txt'); + const encodedTraversal = await rawGet(host.url, '/assets/%2e%2e/secret.txt'); + const absoluteLike = await rawGet(host.url, '/assets/%2Ftmp/secret.txt'); + + expect(traversal.status).toBe(404); + expect(await text(traversal)).not.toContain('outside asset root'); + expect(encodedTraversal.status).toBe(404); + expect(await text(encodedTraversal)).not.toContain('outside asset root'); + expect(absoluteLike.status).toBe(404); } finally { - await host.close() + await host.close(); } - }) + }); - it("returns an explicit build-web error when the web bundle is missing", async () => { - const assetRoot = await mkdtemp( - join(tmpdir(), "brunch-web-assets-missing-"), - ) + it('returns an explicit build-web error when the web bundle is missing', async () => { + const assetRoot = await mkdtemp(join(tmpdir(), 'brunch-web-assets-missing-')); const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, webAssetRoot: assetRoot, - }) + }); try { - const response = await fetch(host.url) - const body = await text(response) + const response = await fetch(host.url); + const body = await text(response); - expect(response.status).toBe(500) - expect(body).toContain("npm run build:web") + expect(response.status).toBe(500); + expect(body).toContain('npm run build:web'); } finally { - await host.close() + await host.close(); } - }) + }); - it("serves a native Brunch HTML shell on an ephemeral port", async () => { - const assetRoot = await builtWebAssets() + it('serves a native Brunch HTML shell on an ephemeral port', async () => { + const assetRoot = await builtWebAssets(); const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, webAssetRoot: assetRoot, - }) + }); try { - const response = await fetch(host.url) - const html = await text(response) + const response = await fetch(host.url); + const html = await text(response); - expect(response.status).toBe(200) - expect(response.headers.get("content-type")).toContain("text/html") - expect(html).toContain("Brunch") - expect(html).not.toContain("pi-web-ui") + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toContain('text/html'); + expect(html).toContain('Brunch'); + expect(html).not.toContain('pi-web-ui'); } finally { - await host.close() + await host.close(); } - }) + }); - it("serves workspace and session JSON-RPC over WebSocket using shared handlers", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-")) + it('serves workspace and session JSON-RPC over WebSocket using shared handlers', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-')); const workspace = await createWorkspaceSessionCoordinator({ cwd, }).createSetupSession({ - specTitle: "Web spec", - }) - workspace.session.manager.appendMessage(assistantMessage("Question")) - workspace.session.manager.appendMessage(userMessage("Answer")) + specTitle: 'Web spec', + }); + workspace.session.manager.appendMessage(assistantMessage('Question')); + workspace.session.manager.appendMessage(userMessage('Answer')); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) + }); try { const snapshot = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "workspace.snapshot", - }) + method: 'workspace.snapshot', + }); const exchanges = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 2, - method: "session.elicitationExchanges", - }) + method: 'session.elicitationExchanges', + }); expect(snapshot).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - result: { status: "ready", spec: { title: "Web spec" } }, - }) + result: { status: 'ready', spec: { title: 'Web spec' } }, + }); expect(exchanges).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 2, result: { - status: "ready", + status: 'ready', exchanges: [{ promptEntryIds: [expect.any(String)] }], }, - }) + }); } finally { - await host.close() + await host.close(); } - }) + }); - it("serves explicit session projection over WebSocket", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-explicit-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('serves explicit session projection over WebSocket', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-explicit-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinator.createSetupSession({ - specTitle: "Explicit web spec", - }) - first.session.manager.appendMessage(assistantMessage("First question")) + specTitle: 'Explicit web spec', + }); + first.session.manager.appendMessage(assistantMessage('First question')); first.session.manager.appendCustomMessageEntry( - "brunch.elicitation_prompt", - "Pick an explicit session direction.", + 'brunch.elicitation_prompt', + 'Pick an explicit session direction.', true, - ) - first.session.manager.appendMessage(userMessage("First answer")) - await coordinator.createSetupSessionForCurrentSpec() + ); + first.session.manager.appendMessage(userMessage('First answer')); + await coordinator.createSetupSessionForCurrentSpec(); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) + }); try { const response = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 14, - method: "session.elicitationExchanges", + method: 'session.elicitationExchanges', params: { sessionId: first.session.id, specId: first.spec.id }, - }) + }); const display = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 15, - method: "session.transcriptDisplay", + method: 'session.transcriptDisplay', params: { sessionId: first.session.id, specId: first.spec.id }, - }) + }); expect(response).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 14, result: { - status: "ready", - exchanges: [ - { promptEntryIds: expect.arrayContaining([expect.any(String)]) }, - ], + status: 'ready', + exchanges: [{ promptEntryIds: expect.arrayContaining([expect.any(String)]) }], }, - }) + }); expect(display).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 15, result: { rows: [ - { role: "assistant", text: "First question" }, - { role: "prompt", text: "Pick an explicit session direction." }, - { role: "user", text: "First answer" }, + { role: 'assistant', text: 'First question' }, + { role: 'prompt', text: 'Pick an explicit session direction.' }, + { role: 'user', text: 'First answer' }, ], }, - }) + }); } finally { - await host.close() + await host.close(); } - }) + }); - it("notifies attached web observers after RPC structured-exchange mutations", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-live-")) + it('notifies attached web observers after RPC structured-exchange mutations', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-live-')); await createWorkspaceSessionCoordinator({ cwd }).createSetupSession({ - specTitle: "Live web spec", - }) + specTitle: 'Live web spec', + }); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) - const observer = await openWebSocket( - `${host.url.replace(/^http/u, "ws")}/rpc`, - ) - const actor = await openWebSocket(`${host.url.replace(/^http/u, "ws")}/rpc`) + }); + const observer = await openWebSocket(`${host.url.replace(/^http/u, 'ws')}/rpc`); + const actor = await openWebSocket(`${host.url.replace(/^http/u, 'ws')}/rpc`); try { - const observerNotification = nextWebSocketMessage(observer) - const actorResponse = nextWebSocketMessage(actor) + const observerNotification = nextWebSocketMessage(observer); + const actorResponse = nextWebSocketMessage(actor); actor.send( JSON.stringify({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 21, - method: "session.startElicitation", + method: 'session.startElicitation', }), - ) + ); await expect(actorResponse).resolves.toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 21, result: { - status: "pending", - exchange: { exchangeId: "deterministic-grounding-choice-1" }, + status: 'pending', + exchange: { exchangeId: 'deterministic-grounding-choice-1' }, }, - }) + }); await expect(observerNotification).resolves.toEqual({ - jsonrpc: "2.0", - method: "brunch.updated", + jsonrpc: '2.0', + method: 'brunch.updated', params: { topics: [ - "workspace.snapshot", - "session.pendingExchange", - "session.elicitationExchanges", - "session.transcriptDisplay", + 'workspace.snapshot', + 'session.pendingExchange', + 'session.elicitationExchanges', + 'session.transcriptDisplay', ], }, - }) + }); - const responseNotification = nextWebSocketMessage(observer) + const responseNotification = nextWebSocketMessage(observer); const respond = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 23, - method: "elicitation.respond", + method: 'elicitation.respond', params: { - exchangeId: "deterministic-grounding-choice-1", - answer: { optionId: "new-from-scratch" }, - note: "Observed by the web live-update proof.", + exchangeId: 'deterministic-grounding-choice-1', + answer: { optionId: 'new-from-scratch' }, + note: 'Observed by the web live-update proof.', }, - }) + }); expect(respond).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 23, - result: { status: "accepted" }, - }) + result: { status: 'accepted' }, + }); await expect(responseNotification).resolves.toMatchObject({ - jsonrpc: "2.0", - method: "brunch.updated", - }) + jsonrpc: '2.0', + method: 'brunch.updated', + }); const display = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 22, - method: "session.transcriptDisplay", - }) + method: 'session.transcriptDisplay', + }); expect(display).toMatchObject({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 22, result: { rows: [ { - role: "prompt", - text: expect.stringContaining( - "Is this a new product or feature from scratch?", - ), + role: 'prompt', + text: expect.stringContaining('Is this a new product or feature from scratch?'), }, { - role: "user", - text: expect.stringContaining( - "Observed by the web live-update proof.", - ), + role: 'user', + text: expect.stringContaining('Observed by the web live-update proof.'), }, ], }, - }) + }); } finally { - observer.close() - actor.close() - await host.close() + observer.close(); + actor.close(); + await host.close(); } - }) + }); - it("multiplexes two JSON-RPC requests over one WebSocket", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-multiplex-")) + it('multiplexes two JSON-RPC requests over one WebSocket', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-multiplex-')); await createWorkspaceSessionCoordinator({ cwd }).createSetupSession({ - specTitle: "Multiplex spec", - }) + specTitle: 'Multiplex spec', + }); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) + }); try { const responses = await websocketRpcBatch(host.url, [ - { jsonrpc: "2.0", id: 10, method: "workspace.snapshot" }, - { jsonrpc: "2.0", id: 11, method: "workspace.snapshot" }, - ]) + { jsonrpc: '2.0', id: 10, method: 'workspace.snapshot' }, + { jsonrpc: '2.0', id: 11, method: 'workspace.snapshot' }, + ]); - expect(responses).toHaveLength(2) + expect(responses).toHaveLength(2); expect(responses).toEqual( expect.arrayContaining([ - expect.objectContaining({ jsonrpc: "2.0", id: 10 }), - expect.objectContaining({ jsonrpc: "2.0", id: 11 }), + expect.objectContaining({ jsonrpc: '2.0', id: 10 }), + expect.objectContaining({ jsonrpc: '2.0', id: 11 }), ]), - ) + ); } finally { - await host.close() + await host.close(); } - }) + }); - it("returns a parse error for malformed WebSocket JSON without killing the host", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-malformed-")) + it('returns a parse error for malformed WebSocket JSON without killing the host', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-malformed-')); await createWorkspaceSessionCoordinator({ cwd }).createSetupSession({ - specTitle: "Malformed spec", - }) + specTitle: 'Malformed spec', + }); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) + }); try { - const response = await websocketRaw(host.url, "not json") + const response = await websocketRaw(host.url, 'not json'); expect(response).toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: null, - error: { code: -32700, message: "Parse error" }, - }) + error: { code: -32700, message: 'Parse error' }, + }); await expect( websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 12, - method: "workspace.snapshot", + method: 'workspace.snapshot', }), - ).resolves.toMatchObject({ jsonrpc: "2.0", id: 12 }) + ).resolves.toMatchObject({ jsonrpc: '2.0', id: 12 }); } finally { - await host.close() + await host.close(); } - }) + }); - it("returns an internal error for WebSocket handler failures", async () => { + it('returns an internal error for WebSocket handler failures', async () => { const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, coordinator: throwingCoordinator(), - }) + }); try { const response = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 13, - method: "workspace.snapshot", - }) + method: 'workspace.snapshot', + }); expect(response).toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 13, - error: { code: -32603, message: "Internal error" }, - }) + error: { code: -32603, message: 'Internal error' }, + }); } finally { - await host.close() + await host.close(); } - }) + }); - it("rejects non-rpc WebSocket upgrade paths", async () => { + it('rejects non-rpc WebSocket upgrade paths', async () => { const host = await startWebHost({ - cwd: "/tmp/brunch-project", + cwd: '/tmp/brunch-project', port: 0, coordinator: throwingCoordinator(), - }) + }); try { - await expect( - openWebSocket(`${host.url.replace(/^http/u, "ws")}/not-rpc`), - ).rejects.toThrow("WebSocket failed to open") + await expect(openWebSocket(`${host.url.replace(/^http/u, 'ws')}/not-rpc`)).rejects.toThrow( + 'WebSocket failed to open', + ); } finally { - await host.close() + await host.close(); } - }) + }); - it("propagates the non-linear transcript JSON-RPC error over WebSocket", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-web-rpc-branch-")) + it('propagates the non-linear transcript JSON-RPC error over WebSocket', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-web-rpc-branch-')); const workspace = await createWorkspaceSessionCoordinator({ cwd, }).createSetupSession({ - specTitle: "Branch spec", - }) - const manager = SessionManager.open(workspace.session.file) - manager.appendMessage(assistantMessage("Abandoned prompt")) - manager.appendMessage(userMessage("Abandoned answer")) - manager.resetLeaf() - manager.appendMessage(assistantMessage("Active prompt")) + specTitle: 'Branch spec', + }); + const manager = SessionManager.open(workspace.session.file); + manager.appendMessage(assistantMessage('Abandoned prompt')); + manager.appendMessage(userMessage('Abandoned answer')); + manager.resetLeaf(); + manager.appendMessage(assistantMessage('Active prompt')); const host = await startWebHost({ cwd, port: 0, coordinator: createWorkspaceSessionCoordinator({ cwd }), - }) + }); try { const response = await websocketRpc(host.url, { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 4, - method: "session.elicitationExchanges", - }) + method: 'session.elicitationExchanges', + }); expect(response).toEqual({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 4, error: { code: -32002, - message: "Selected Brunch session transcript is non-linear", + message: 'Selected Brunch session transcript is non-linear', }, - }) + }); } finally { - await host.close() + await host.close(); } - }) + }); - it("does not expose product read endpoints over HTTP GET", async () => { - const host = await startWebHost({ cwd: "/tmp/brunch-project", port: 0 }) + it('does not expose product read endpoints over HTTP GET', async () => { + const host = await startWebHost({ cwd: '/tmp/brunch-project', port: 0 }); try { - const response = await fetch(`${host.url}/workspace.snapshot`) + const response = await fetch(`${host.url}/workspace.snapshot`); - expect(response.status).toBe(404) + expect(response.status).toBe(404); } finally { - await host.close() + await host.close(); } - }) -}) + }); +}); async function websocketRpc(url: string, request: unknown): Promise { - const [response] = await websocketRpcBatch(url, [request]) - return response + const [response] = await websocketRpcBatch(url, [request]); + return response; } -async function websocketRpcBatch( - url: string, - requests: readonly unknown[], -): Promise { - const socket = await openWebSocket(`${url.replace(/^http/u, "ws")}/rpc`) - const responses: unknown[] = [] +async function websocketRpcBatch(url: string, requests: readonly unknown[]): Promise { + const socket = await openWebSocket(`${url.replace(/^http/u, 'ws')}/rpc`); + const responses: unknown[] = []; try { const done = new Promise((resolve, reject) => { - socket.addEventListener("message", (event) => { - responses.push(JSON.parse(String(event.data)) as unknown) + socket.addEventListener('message', (event) => { + responses.push(JSON.parse(String(event.data)) as unknown); if (responses.length === requests.length) { - resolve(responses) + resolve(responses); } - }) - socket.addEventListener( - "error", - () => reject(new Error("WebSocket error")), - { once: true }, - ) - }) + }); + socket.addEventListener('error', () => reject(new Error('WebSocket error')), { once: true }); + }); for (const request of requests) { - socket.send(JSON.stringify(request)) + socket.send(JSON.stringify(request)); } - return await done + return await done; } finally { - socket.close() + socket.close(); } } async function websocketRaw(url: string, message: string): Promise { - const socket = await openWebSocket(`${url.replace(/^http/u, "ws")}/rpc`) + const socket = await openWebSocket(`${url.replace(/^http/u, 'ws')}/rpc`); try { const response = new Promise((resolve, reject) => { - socket.addEventListener( - "message", - (event) => resolve(JSON.parse(String(event.data)) as unknown), - { once: true }, - ) - socket.addEventListener( - "error", - () => reject(new Error("WebSocket error")), - { once: true }, - ) - }) - socket.send(message) - return await response + socket.addEventListener('message', (event) => resolve(JSON.parse(String(event.data)) as unknown), { + once: true, + }); + socket.addEventListener('error', () => reject(new Error('WebSocket error')), { once: true }); + }); + socket.send(message); + return await response; } finally { - socket.close() + socket.close(); } } function nextWebSocketMessage(socket: WebSocket): Promise { return new Promise((resolve, reject) => { - socket.addEventListener( - "message", - (event) => resolve(JSON.parse(String(event.data)) as unknown), - { once: true }, - ) - socket.addEventListener( - "error", - () => reject(new Error("WebSocket error")), - { once: true }, - ) - }) + socket.addEventListener('message', (event) => resolve(JSON.parse(String(event.data)) as unknown), { + once: true, + }); + socket.addEventListener('error', () => reject(new Error('WebSocket error')), { once: true }); + }); } function openWebSocket(url: string): Promise { - const socket = new WebSocket(url) + const socket = new WebSocket(url); return new Promise((resolve, reject) => { - socket.addEventListener("open", () => resolve(socket), { once: true }) - socket.addEventListener( - "error", - () => reject(new Error("WebSocket failed to open")), - { once: true }, - ) - }) + socket.addEventListener('open', () => resolve(socket), { once: true }); + socket.addEventListener('error', () => reject(new Error('WebSocket failed to open')), { once: true }); + }); } function throwingCoordinator(): WorkspaceSessionCoordinator { return { - ...createWorkspaceSessionCoordinator({ cwd: "/tmp/brunch-project" }), + ...createWorkspaceSessionCoordinator({ cwd: '/tmp/brunch-project' }), async openDefaultWorkspace() { - throw new Error("boom") + throw new Error('boom'); }, - } + }; } diff --git a/src/web-host.ts b/src/web-host.ts index f53bfddee..b03134c0a 100644 --- a/src/web-host.ts +++ b/src/web-host.ts @@ -1,185 +1,176 @@ -import { readFile } from "node:fs/promises" -import { createServer, type Server } from "node:http" -import { dirname, resolve, sep } from "node:path" -import { fileURLToPath } from "node:url" +import { readFile } from 'node:fs/promises'; +import { createServer, type Server } from 'node:http'; +import { dirname, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import { createRpcHandlers } from "./rpc/handlers.js" -import { attachWebRpcTransport } from "./rpc/websocket.js" -import type { WorkspaceSessionCoordinator } from "./workspace-session-coordinator.js" +import { createRpcHandlers } from './rpc/handlers.js'; +import { attachWebRpcTransport } from './rpc/websocket.js'; +import type { WorkspaceSessionCoordinator } from './workspace-session-coordinator.js'; export interface WebHostOptions { - cwd: string - port?: number - hostname?: string - coordinator?: WorkspaceSessionCoordinator - webAssetRoot?: string + cwd: string; + port?: number; + hostname?: string; + coordinator?: WorkspaceSessionCoordinator; + webAssetRoot?: string; } export interface RunningWebHost { - url: string - close(): Promise + url: string; + close(): Promise; } const MISSING_WEB_BUNDLE_MESSAGE = - "Brunch web bundle is missing. Run npm run build:web before starting web mode." + 'Brunch web bundle is missing. Run npm run build:web before starting web mode.'; -export async function startWebHost( - options: WebHostOptions, -): Promise { - void options.cwd - const webAssetRoot = options.webAssetRoot ?? defaultWebAssetRoot() +export async function startWebHost(options: WebHostOptions): Promise { + void options.cwd; + const webAssetRoot = options.webAssetRoot ?? defaultWebAssetRoot(); const server = createServer((request, response) => { - if (request.method === "GET" && request.url === "/") { - void readFile(resolve(webAssetRoot, "index.html")).then( + if (request.method === 'GET' && request.url === '/') { + void readFile(resolve(webAssetRoot, 'index.html')).then( (asset) => { response.writeHead(200, { - "content-type": "text/html; charset=utf-8", - "cache-control": "no-store", - }) - response.end(asset) + 'content-type': 'text/html; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(asset); }, () => { response.writeHead(500, { - "content-type": "text/plain; charset=utf-8", - "cache-control": "no-store", - }) - response.end(MISSING_WEB_BUNDLE_MESSAGE) + 'content-type': 'text/plain; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(MISSING_WEB_BUNDLE_MESSAGE); }, - ) - return + ); + return; } - if (request.method === "GET" && request.url?.startsWith("/assets/")) { - const assetPath = resolveAssetRequest(webAssetRoot, request.url) + if (request.method === 'GET' && request.url?.startsWith('/assets/')) { + const assetPath = resolveAssetRequest(webAssetRoot, request.url); if (!assetPath) { response.writeHead(404, { - "content-type": "text/plain; charset=utf-8", - }) - response.end("Not found") - return + 'content-type': 'text/plain; charset=utf-8', + }); + response.end('Not found'); + return; } void readFile(assetPath.file).then( (asset) => { response.writeHead(200, { - "content-type": contentTypeForAsset(assetPath.relativePath), - "cache-control": "no-store", - }) - response.end(asset) + 'content-type': contentTypeForAsset(assetPath.relativePath), + 'cache-control': 'no-store', + }); + response.end(asset); }, () => { response.writeHead(404, { - "content-type": "text/plain; charset=utf-8", - }) - response.end("Not found") + 'content-type': 'text/plain; charset=utf-8', + }); + response.end('Not found'); }, - ) - return + ); + return; } - response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }) - response.end("Not found") - }) + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }); + response.end('Not found'); + }); const rpcTransport = options.coordinator ? attachWebRpcTransport({ server, - path: "/rpc", + path: '/rpc', handlers: createRpcHandlers({ coordinator: options.coordinator, cwd: options.cwd, }), }) - : null + : null; - const hostname = options.hostname ?? "127.0.0.1" - await listen(server, options.port ?? 0, hostname) - const address = server.address() - if (address === null || typeof address === "string") { - throw new Error("Expected Brunch web host to listen on a TCP address") + const hostname = options.hostname ?? '127.0.0.1'; + await listen(server, options.port ?? 0, hostname); + const address = server.address(); + if (address === null || typeof address === 'string') { + throw new Error('Expected Brunch web host to listen on a TCP address'); } return { url: `http://${hostname}:${address.port}`, async close() { - await rpcTransport?.close() - await close(server) + await rpcTransport?.close(); + await close(server); }, - } + }; } function defaultWebAssetRoot(): string { - return resolve(dirname(fileURLToPath(import.meta.url)), "..", "dist-web") + return resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist-web'); } interface ResolvedAssetRequest { - file: string - relativePath: string + file: string; + relativePath: string; } -function resolveAssetRequest( - webAssetRoot: string, - requestUrl: string, -): ResolvedAssetRequest | null { - let pathname: string +function resolveAssetRequest(webAssetRoot: string, requestUrl: string): ResolvedAssetRequest | null { + let pathname: string; try { - pathname = new URL(requestUrl, "http://brunch.local").pathname + pathname = new URL(requestUrl, 'http://brunch.local').pathname; } catch { - return null + return null; } - let suffix: string + let suffix: string; try { - suffix = decodeURIComponent(pathname.slice("/assets/".length)) + suffix = decodeURIComponent(pathname.slice('/assets/'.length)); } catch { - return null + return null; } - if ( - suffix.length === 0 || - suffix.startsWith("/") || - /^[a-zA-Z]:/u.test(suffix) - ) { - return null + if (suffix.length === 0 || suffix.startsWith('/') || /^[a-zA-Z]:/u.test(suffix)) { + return null; } - const assetRoot = resolve(webAssetRoot, "assets") - const file = resolve(assetRoot, suffix) + const assetRoot = resolve(webAssetRoot, 'assets'); + const file = resolve(assetRoot, suffix); if (file !== assetRoot && !file.startsWith(`${assetRoot}${sep}`)) { - return null + return null; } - return { file, relativePath: `assets/${suffix}` } + return { file, relativePath: `assets/${suffix}` }; } function contentTypeForAsset(relativePath: string): string { - if (relativePath.endsWith(".js")) { - return "text/javascript; charset=utf-8" + if (relativePath.endsWith('.js')) { + return 'text/javascript; charset=utf-8'; } - if (relativePath.endsWith(".css")) { - return "text/css; charset=utf-8" + if (relativePath.endsWith('.css')) { + return 'text/css; charset=utf-8'; } - return "application/octet-stream" + return 'application/octet-stream'; } function listen(server: Server, port: number, hostname: string): Promise { return new Promise((resolve, reject) => { - server.once("error", reject) + server.once('error', reject); server.listen(port, hostname, () => { - server.off("error", reject) - resolve() - }) - }) + server.off('error', reject); + resolve(); + }); + }); } function close(server: Server): Promise { return new Promise((resolve, reject) => { server.close((error) => { if (error) { - reject(error) - return + reject(error); + return; } - resolve() - }) - }) + resolve(); + }); + }); } diff --git a/src/workspace-session-coordinator.test.ts b/src/workspace-session-coordinator.test.ts index cdf40239d..d8a7f2531 100644 --- a/src/workspace-session-coordinator.test.ts +++ b/src/workspace-session-coordinator.test.ts @@ -1,137 +1,111 @@ -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" -import { join } from "node:path" +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import { describe, expect, it } from "vitest" +import { SessionManager, type SessionEntry } from '@earendil-works/pi-coding-agent'; +import { describe, expect, it } from 'vitest'; -import { - SessionManager, - type SessionEntry, -} from "@earendil-works/pi-coding-agent" - -import { projectElicitationExchanges } from "./elicitation-exchange.js" -import { SESSION_BINDING_TYPE } from "./session-binding.js" -import { assistantMessage, userMessage, isCustomEntry } from "./test-helpers.js" +import { projectElicitationExchanges } from './elicitation-exchange.js'; +import { SESSION_BINDING_TYPE } from './session-binding.js'; +import { assistantMessage, userMessage, isCustomEntry } from './test-helpers.js'; import { createWorkspaceSessionCoordinator, verifyWorkspaceSessionStores, -} from "./workspace-session-coordinator.js" +} from './workspace-session-coordinator.js'; type JsonlLine = { - type?: string - customType?: string -} + type?: string; + customType?: string; +}; -describe("WorkspaceSessionCoordinator", () => { - it("creates scoped state, a bound pi session, and derivable chrome state", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) +describe('WorkspaceSessionCoordinator', () => { + it('creates scoped state, a bound pi session, and derivable chrome state', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) + specTitle: 'Scratch spec', + }); - expect(result.status).toBe("ready") - expect(result.chrome.cwd).toBe(cwd) - expect(result.chrome.spec?.id).toMatch(/^spec-/u) - expect(result.chrome.spec?.title).toBe("Scratch spec") - expect(result.chrome.phase).toBe("elicitation") - expect(result.chrome.chatMode).toBe("responding-to-elicitation") + expect(result.status).toBe('ready'); + expect(result.chrome.cwd).toBe(cwd); + expect(result.chrome.spec?.id).toMatch(/^spec-/u); + expect(result.chrome.spec?.title).toBe('Scratch spec'); + expect(result.chrome.phase).toBe('elicitation'); + expect(result.chrome.chatMode).toBe('responding-to-elicitation'); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 1, - }) - expect(oracle.ok).toBe(true) + }); + expect(oracle.ok).toBe(true); if (!oracle.ok) { - expect(oracle.errors).toEqual([]) - return + expect(oracle.errors).toEqual([]); + return; } - expect(oracle.specId).toBe(result.spec.id) - expect(oracle.sessions).toHaveLength(1) - expect(oracle.sessions[0]?.binding.specId).toBe(result.spec.id) - }) + expect(oracle.specId).toBe(result.spec.id); + expect(oracle.sessions).toHaveLength(1); + expect(oracle.sessions[0]?.binding.specId).toBe(result.spec.id); + }); - it("jsonl coordinator new session reloads same spec", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('jsonl coordinator new session reloads same spec', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - const second = await coordinator.createSetupSessionForCurrentSpec() + specTitle: 'Scratch spec', + }); + const second = await coordinator.createSetupSessionForCurrentSpec(); - expect(second.status).toBe("ready") - if (second.status !== "ready") { - return + expect(second.status).toBe('ready'); + if (second.status !== 'ready') { + return; } - expect(second.spec.id).toBe(first.spec.id) - expect(second.session.id).not.toBe(first.session.id) + expect(second.spec.id).toBe(first.spec.id); + expect(second.session.id).not.toBe(first.session.id); - const reloadedFirst = SessionManager.open( - first.session.file, - undefined, - cwd, - ) - const reloadedSecond = SessionManager.open( - second.session.file, - undefined, - cwd, - ) + const reloadedFirst = SessionManager.open(first.session.file, undefined, cwd); + const reloadedSecond = SessionManager.open(second.session.file, undefined, cwd); const firstBinding = reloadedFirst .getEntries() - .find( - (entry) => - isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE, - ) + .find((entry) => isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE); const secondBinding = reloadedSecond .getEntries() - .find( - (entry) => - isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE, - ) + .find((entry) => isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE); expect(firstBinding).toMatchObject({ - data: { specId: first.spec.id, specTitle: "Scratch spec" }, - }) + data: { specId: first.spec.id, specTitle: 'Scratch spec' }, + }); expect(secondBinding).toMatchObject({ - data: { specId: first.spec.id, specTitle: "Scratch spec" }, - }) + data: { specId: first.spec.id, specTitle: 'Scratch spec' }, + }); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 2, - }) - expect(oracle.ok).toBe(true) + }); + expect(oracle.ok).toBe(true); if (!oracle.ok) { - expect(oracle.errors).toEqual([]) - return + expect(oracle.errors).toEqual([]); + return; } - expect(oracle.sessions.map((session) => session.binding.specId)).toEqual([ - first.spec.id, - first.spec.id, - ]) - expect(oracle.sessions.every((session) => session.bindingCount === 1)).toBe( - true, - ) - }) - - it("jsonl binding-only coordinator session reloads", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + expect(oracle.sessions.map((session) => session.binding.specId)).toEqual([first.spec.id, first.spec.id]); + expect(oracle.sessions.every((session) => session.bindingCount === 1)).toBe(true); + }); + + it('jsonl binding-only coordinator session reloads', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - const reloaded = SessionManager.open(result.session.file, undefined, cwd) + specTitle: 'Scratch spec', + }); + const reloaded = SessionManager.open(result.session.file, undefined, cwd); const bindings = reloaded .getEntries() - .filter( - (entry) => - isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE, - ) + .filter((entry) => isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE); - expect(bindings).toHaveLength(1) + expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ customType: SESSION_BINDING_TYPE, data: { @@ -139,211 +113,182 @@ describe("WorkspaceSessionCoordinator", () => { specId: result.spec.id, specTitle: result.spec.title, }, - }) - }) + }); + }); - it("jsonl coordinator pre-assistant flush does not duplicate prefix", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('jsonl coordinator pre-assistant flush does not duplicate prefix', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - const reloaded = SessionManager.open(result.session.file, undefined, cwd) - reloaded.appendMessage(assistantMessage("hello")) - reloaded.appendMessage(userMessage("hi")) + specTitle: 'Scratch spec', + }); + const reloaded = SessionManager.open(result.session.file, undefined, cwd); + reloaded.appendMessage(assistantMessage('hello')); + reloaded.appendMessage(userMessage('hi')); - const content = await readFile(result.session.file, "utf8") + const content = await readFile(result.session.file, 'utf8'); const lines = content - .split("\n") + .split('\n') .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as JsonlLine) + .map((line) => JSON.parse(line) as JsonlLine); - expect(lines.filter((entry) => entry.type === "session")).toHaveLength(1) + expect(lines.filter((entry) => entry.type === 'session')).toHaveLength(1); expect( lines.filter( (entry) => isCustomEntry(entry as unknown as SessionEntry) && (entry as JsonlLine).customType === SESSION_BINDING_TYPE, ), - ).toHaveLength(1) - }) + ).toHaveLength(1); + }); - it("jsonl session reload preserves coordinator binding", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('jsonl session reload preserves coordinator binding', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - result.session.manager.appendMessage(assistantMessage("hello")) - result.session.manager.appendMessage(userMessage("answer")) + specTitle: 'Scratch spec', + }); + result.session.manager.appendMessage(assistantMessage('hello')); + result.session.manager.appendMessage(userMessage('answer')); - const reloaded = SessionManager.open(result.session.file, undefined, cwd) + const reloaded = SessionManager.open(result.session.file, undefined, cwd); const bindings = reloaded .getEntries() - .filter( - (entry) => - isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE, - ) + .filter((entry) => isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE); - expect(bindings).toHaveLength(1) + expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ data: { sessionId: result.session.id, specId: result.spec.id, - specTitle: "Scratch spec", + specTitle: 'Scratch spec', }, - }) - }) + }); + }); - it("does not duplicate pre-assistant entries when flushed after the user message and before assistant persistence", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('does not duplicate pre-assistant entries when flushed after the user message and before assistant persistence', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - result.session.manager.appendModelChange("test-provider", "test-model") - result.session.manager.appendThinkingLevelChange("high") - await coordinator.bindCurrentSpecToReplacementSession( - result.session.manager, - ) - result.session.manager.appendMessage(userMessage("hello")) - await coordinator.bindCurrentSpecToReplacementSession( - result.session.manager, - ) - result.session.manager.appendMessage(assistantMessage("hi")) - - const content = await readFile(result.session.file, "utf8") - const sessionHeaderCount = content - .split("\n") - .filter((line) => line.includes('"type":"session"')).length + specTitle: 'Scratch spec', + }); + result.session.manager.appendModelChange('test-provider', 'test-model'); + result.session.manager.appendThinkingLevelChange('high'); + await coordinator.bindCurrentSpecToReplacementSession(result.session.manager); + result.session.manager.appendMessage(userMessage('hello')); + await coordinator.bindCurrentSpecToReplacementSession(result.session.manager); + result.session.manager.appendMessage(assistantMessage('hi')); + + const content = await readFile(result.session.file, 'utf8'); + const sessionHeaderCount = content.split('\n').filter((line) => line.includes('"type":"session"')).length; const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 1, - }) + }); - expect(sessionHeaderCount).toBe(1) - expect(oracle.ok).toBe(true) + expect(sessionHeaderCount).toBe(1); + expect(oracle.ok).toBe(true); if (!oracle.ok) { - expect(oracle.errors).toEqual([]) + expect(oracle.errors).toEqual([]); } - }) + }); - it("jsonl session reload projects the same simple exchange", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('jsonl session reload projects the same simple exchange', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - result.session.manager.appendMessage(assistantMessage("Question")) - result.session.manager.appendMessage(userMessage("Answer")) - - const beforeReload = projectElicitationExchanges( - result.session.manager.getBranch(), - ) + specTitle: 'Scratch spec', + }); + result.session.manager.appendMessage(assistantMessage('Question')); + result.session.manager.appendMessage(userMessage('Answer')); + + const beforeReload = projectElicitationExchanges(result.session.manager.getBranch()); const afterReload = projectElicitationExchanges( SessionManager.open(result.session.file, undefined, cwd).getBranch(), - ) + ); - expect(afterReload).toEqual(beforeReload) - expect(afterReload.exchanges).toHaveLength(1) - }) + expect(afterReload).toEqual(beforeReload); + expect(afterReload.exchanges).toHaveLength(1); + }); - it("binds a pi-created replacement session to the current spec", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('binds a pi-created replacement session to the current spec', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) - const replacementFile = first.session.manager.newSession() - await coordinator.bindCurrentSpecToReplacementSession(first.session.manager) + specTitle: 'Scratch spec', + }); + const replacementFile = first.session.manager.newSession(); + await coordinator.bindCurrentSpecToReplacementSession(first.session.manager); - expect(replacementFile).toBeDefined() + expect(replacementFile).toBeDefined(); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 2, - }) - expect(oracle.ok).toBe(true) + }); + expect(oracle.ok).toBe(true); if (!oracle.ok) { - expect(oracle.errors).toEqual([]) - return + expect(oracle.errors).toEqual([]); + return; } - expect( - oracle.sessions.every( - (session) => session.binding.specId === first.spec.id, - ), - ).toBe(true) - expect(oracle.sessions.every((session) => session.bindingCount === 1)).toBe( - true, - ) - }) - - it("inspects current defaults, bound specs, and sessions without activation writes", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - - const first = await coordinator.createSetupSession({ specTitle: "Alpha" }) - first.session.manager.appendMessage(userMessage("first")) + expect(oracle.sessions.every((session) => session.binding.specId === first.spec.id)).toBe(true); + expect(oracle.sessions.every((session) => session.bindingCount === 1)).toBe(true); + }); + + it('inspects current defaults, bound specs, and sessions without activation writes', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + + const first = await coordinator.createSetupSession({ specTitle: 'Alpha' }); + first.session.manager.appendMessage(userMessage('first')); const second = await coordinator.createSetupSession({ - specTitle: "Beta", + specTitle: 'Beta', createNewSpec: true, - }) - const beforeState = await readFile( - join(cwd, ".brunch", "state.json"), - "utf8", - ) - const beforeFirst = await readFile(first.session.file, "utf8") - const beforeSecond = await readFile(second.session.file, "utf8") - - const inventory = await coordinator.inspectWorkspace() - - expect(inventory.cwd).toBe(cwd) - expect(inventory.needsNewSpec).toBe(false) - expect(inventory.currentSpec).toEqual(second.spec) - expect(inventory.currentSessionFile).toBe(second.session.file) - expect(inventory.specs.map(({ spec }) => spec.title)).toEqual([ - "Alpha", - "Beta", - ]) + }); + const beforeState = await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'); + const beforeFirst = await readFile(first.session.file, 'utf8'); + const beforeSecond = await readFile(second.session.file, 'utf8'); + + const inventory = await coordinator.inspectWorkspace(); + + expect(inventory.cwd).toBe(cwd); + expect(inventory.needsNewSpec).toBe(false); + expect(inventory.currentSpec).toEqual(second.spec); + expect(inventory.currentSessionFile).toBe(second.session.file); + expect(inventory.specs.map(({ spec }) => spec.title)).toEqual(['Alpha', 'Beta']); expect(inventory.specs[0]?.sessions).toEqual([ expect.objectContaining({ id: first.session.id, file: first.session.file, specId: first.spec.id, - specTitle: "Alpha", + specTitle: 'Alpha', available: true, }), - ]) + ]); expect(inventory.specs[1]?.sessions).toEqual([ expect.objectContaining({ id: second.session.id, file: second.session.file, specId: second.spec.id, - specTitle: "Beta", + specTitle: 'Beta', available: true, }), - ]) - expect(inventory.unavailableSessions).toEqual([]) - await expect( - readFile(join(cwd, ".brunch", "state.json"), "utf8"), - ).resolves.toBe(beforeState) - await expect(readFile(first.session.file, "utf8")).resolves.toBe( - beforeFirst, - ) - await expect(readFile(second.session.file, "utf8")).resolves.toBe( - beforeSecond, - ) - }) - - it("inspects an empty workspace without creating session files", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - - const inventory = await coordinator.inspectWorkspace() + ]); + expect(inventory.unavailableSessions).toEqual([]); + await expect(readFile(join(cwd, '.brunch', 'state.json'), 'utf8')).resolves.toBe(beforeState); + await expect(readFile(first.session.file, 'utf8')).resolves.toBe(beforeFirst); + await expect(readFile(second.session.file, 'utf8')).resolves.toBe(beforeSecond); + }); + + it('inspects an empty workspace without creating session files', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + + const inventory = await coordinator.inspectWorkspace(); expect(inventory).toMatchObject({ cwd, @@ -352,256 +297,236 @@ describe("WorkspaceSessionCoordinator", () => { needsNewSpec: true, specs: [], unavailableSessions: [], - }) - await expect( - readFile(join(cwd, ".brunch", "sessions", "missing.jsonl"), "utf8"), - ).rejects.toMatchObject({ code: "ENOENT" }) - }) - - it("marks unbound or incompatible sessions unavailable during inventory", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const ready = await coordinator.createSetupSession({ specTitle: "Alpha" }) - const unboundFile = join(cwd, ".brunch", "sessions", "unbound.jsonl") - const mismatchedFile = join(cwd, ".brunch", "sessions", "mismatched.jsonl") + }); + await expect(readFile(join(cwd, '.brunch', 'sessions', 'missing.jsonl'), 'utf8')).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + + it('marks unbound or incompatible sessions unavailable during inventory', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const ready = await coordinator.createSetupSession({ specTitle: 'Alpha' }); + const unboundFile = join(cwd, '.brunch', 'sessions', 'unbound.jsonl'); + const mismatchedFile = join(cwd, '.brunch', 'sessions', 'mismatched.jsonl'); await writeFile( unboundFile, - `${JSON.stringify({ type: "session", id: "unbound-session", cwd })}\n`, - "utf8", - ) + `${JSON.stringify({ type: 'session', id: 'unbound-session', cwd })}\n`, + 'utf8', + ); await writeFile( mismatchedFile, - `${JSON.stringify({ type: "session", id: "header-session", cwd })}\n${JSON.stringify( - { - type: "custom", - customType: SESSION_BINDING_TYPE, - data: { - schemaVersion: 1, - sessionId: "other-session", - specId: ready.spec.id, - specTitle: ready.spec.title, - }, + `${JSON.stringify({ type: 'session', id: 'header-session', cwd })}\n${JSON.stringify({ + type: 'custom', + customType: SESSION_BINDING_TYPE, + data: { + schemaVersion: 1, + sessionId: 'other-session', + specId: ready.spec.id, + specTitle: ready.spec.title, }, - )}\n`, - "utf8", - ) - const beforeUnbound = await readFile(unboundFile, "utf8") - const beforeMismatched = await readFile(mismatchedFile, "utf8") + })}\n`, + 'utf8', + ); + const beforeUnbound = await readFile(unboundFile, 'utf8'); + const beforeMismatched = await readFile(mismatchedFile, 'utf8'); - const inventory = await coordinator.inspectWorkspace() + const inventory = await coordinator.inspectWorkspace(); - expect(inventory.specs).toHaveLength(1) - expect(inventory.specs[0]?.sessions).toHaveLength(1) + expect(inventory.specs).toHaveLength(1); + expect(inventory.specs[0]?.sessions).toHaveLength(1); expect(inventory.unavailableSessions).toEqual([ expect.objectContaining({ file: mismatchedFile, - reason: "incompatible_binding", + reason: 'incompatible_binding', }), - expect.objectContaining({ file: unboundFile, reason: "missing_binding" }), - ]) - await expect(readFile(unboundFile, "utf8")).resolves.toBe(beforeUnbound) - await expect(readFile(mismatchedFile, "utf8")).resolves.toBe( - beforeMismatched, - ) - }) - - it("activates explicit open and continue decisions as the current workspace", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const first = await coordinator.createSetupSession({ specTitle: "Alpha" }) + expect.objectContaining({ file: unboundFile, reason: 'missing_binding' }), + ]); + await expect(readFile(unboundFile, 'utf8')).resolves.toBe(beforeUnbound); + await expect(readFile(mismatchedFile, 'utf8')).resolves.toBe(beforeMismatched); + }); + + it('activates explicit open and continue decisions as the current workspace', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const first = await coordinator.createSetupSession({ specTitle: 'Alpha' }); const second = await coordinator.createSetupSession({ - specTitle: "Beta", + specTitle: 'Beta', createNewSpec: true, - }) + }); const opened = await coordinator.activateWorkspace({ - action: "openSession", + action: 'openSession', specId: first.spec.id, sessionFile: first.session.file, - }) + }); - expect(opened.status).toBe("ready") - if (opened.status !== "ready") { - return + expect(opened.status).toBe('ready'); + if (opened.status !== 'ready') { + return; } - expect(opened.spec).toEqual(first.spec) - expect(opened.session.id).toBe(first.session.id) - expect(opened.session.file).toBe(first.session.file) - expect(opened.chrome.spec).toEqual(first.spec) + expect(opened.spec).toEqual(first.spec); + expect(opened.session.id).toBe(first.session.id); + expect(opened.session.file).toBe(first.session.file); + expect(opened.chrome.spec).toEqual(first.spec); const continued = await coordinator.activateWorkspace({ - action: "continue", + action: 'continue', specId: second.spec.id, sessionFile: second.session.file, - }) + }); - expect(continued.status).toBe("ready") - if (continued.status !== "ready") { - return + expect(continued.status).toBe('ready'); + if (continued.status !== 'ready') { + return; } - expect(continued.spec).toEqual(second.spec) - expect(continued.session.id).toBe(second.session.id) - expect( - JSON.parse(await readFile(join(cwd, ".brunch", "state.json"), "utf8")), - ).toMatchObject({ + expect(continued.spec).toEqual(second.spec); + expect(continued.session.id).toBe(second.session.id); + expect(JSON.parse(await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'))).toMatchObject({ currentSpec: second.spec, currentSessionFile: second.session.file, - }) - }) + }); + }); - it("activates a new session decision as a binding-only session for the selected spec", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const first = await coordinator.createSetupSession({ specTitle: "Alpha" }) - first.session.manager.appendMessage(userMessage("preserve me")) - const beforeFirst = await readFile(first.session.file, "utf8") + it('activates a new session decision as a binding-only session for the selected spec', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const first = await coordinator.createSetupSession({ specTitle: 'Alpha' }); + first.session.manager.appendMessage(userMessage('preserve me')); + const beforeFirst = await readFile(first.session.file, 'utf8'); const created = await coordinator.activateWorkspace({ - action: "newSession", + action: 'newSession', specId: first.spec.id, - }) + }); - expect(created.status).toBe("ready") - if (created.status !== "ready") { - return + expect(created.status).toBe('ready'); + if (created.status !== 'ready') { + return; } - expect(created.spec).toEqual(first.spec) - expect(created.session.id).not.toBe(first.session.id) - await expect(readFile(first.session.file, "utf8")).resolves.toBe( - beforeFirst, - ) - const createdContent = await readFile(created.session.file, "utf8") - expect(createdContent).toContain(SESSION_BINDING_TYPE) - expect(createdContent).not.toContain("preserve me") + expect(created.spec).toEqual(first.spec); + expect(created.session.id).not.toBe(first.session.id); + await expect(readFile(first.session.file, 'utf8')).resolves.toBe(beforeFirst); + const createdContent = await readFile(created.session.file, 'utf8'); + expect(createdContent).toContain(SESSION_BINDING_TYPE); + expect(createdContent).not.toContain('preserve me'); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 2, - }) - expect(oracle.ok).toBe(true) - }) + }); + expect(oracle.ok).toBe(true); + }); - it("activates a new spec decision by creating a bound current session", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('activates a new spec decision by creating a bound current session', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const created = await coordinator.activateWorkspace({ - action: "newSpec", - title: "Gamma", - }) + action: 'newSpec', + title: 'Gamma', + }); - expect(created.status).toBe("ready") - if (created.status !== "ready") { - return + expect(created.status).toBe('ready'); + if (created.status !== 'ready') { + return; } - expect(created.spec.title).toBe("Gamma") - expect(created.session.id).toMatch(/[\da-f-]+/iu) + expect(created.spec.title).toBe('Gamma'); + expect(created.session.id).toMatch(/[\da-f-]+/iu); const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 1, - }) - expect(oracle.ok).toBe(true) - }) - - it("activates cancel without mutating workspace state or session files", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const ready = await coordinator.createSetupSession({ specTitle: "Alpha" }) - const beforeState = await readFile( - join(cwd, ".brunch", "state.json"), - "utf8", - ) - const beforeSession = await readFile(ready.session.file, "utf8") - - const result = await coordinator.activateWorkspace({ action: "cancel" }) - - expect(result.status).toBe("cancelled") - await expect( - readFile(join(cwd, ".brunch", "state.json"), "utf8"), - ).resolves.toBe(beforeState) - await expect(readFile(ready.session.file, "utf8")).resolves.toBe( - beforeSession, - ) - }) - - it("refuses to activate mismatched or unavailable sessions", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const ready = await coordinator.createSetupSession({ specTitle: "Alpha" }) - const unavailableFile = join( - cwd, - ".brunch", - "sessions", - "unavailable.jsonl", - ) + }); + expect(oracle.ok).toBe(true); + }); + + it('activates cancel without mutating workspace state or session files', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const ready = await coordinator.createSetupSession({ specTitle: 'Alpha' }); + const beforeState = await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'); + const beforeSession = await readFile(ready.session.file, 'utf8'); + + const result = await coordinator.activateWorkspace({ action: 'cancel' }); + + expect(result.status).toBe('cancelled'); + await expect(readFile(join(cwd, '.brunch', 'state.json'), 'utf8')).resolves.toBe(beforeState); + await expect(readFile(ready.session.file, 'utf8')).resolves.toBe(beforeSession); + }); + + it('refuses to activate mismatched or unavailable sessions', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const ready = await coordinator.createSetupSession({ specTitle: 'Alpha' }); + const unavailableFile = join(cwd, '.brunch', 'sessions', 'unavailable.jsonl'); await writeFile( unavailableFile, - `${JSON.stringify({ type: "session", id: "unavailable-session", cwd })}\n`, - "utf8", - ) + `${JSON.stringify({ type: 'session', id: 'unavailable-session', cwd })}\n`, + 'utf8', + ); const unavailable = await coordinator.activateWorkspace({ - action: "openSession", + action: 'openSession', specId: ready.spec.id, sessionFile: unavailableFile, - }) + }); const mismatched = await coordinator.activateWorkspace({ - action: "openSession", - specId: "spec-missing", + action: 'openSession', + specId: 'spec-missing', sessionFile: ready.session.file, - }) + }); - expect(unavailable.status).toBe("needs_human") - expect(mismatched.status).toBe("needs_human") - }) + expect(unavailable.status).toBe('needs_human'); + expect(mismatched.status).toBe('needs_human'); + }); - it("asks for spec selection when no current spec exists and creation is not allowed", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - await mkdir(join(cwd, ".brunch"), { recursive: true }) + it('asks for spec selection when no current spec exists and creation is not allowed', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + await mkdir(join(cwd, '.brunch'), { recursive: true }); - const coordinator = createWorkspaceSessionCoordinator({ cwd }) - const result = await coordinator.openDefaultWorkspace() + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const result = await coordinator.openDefaultWorkspace(); - expect(result.status).toBe("select_spec") - expect(result.chrome.cwd).toBe(cwd) - expect(result.chrome.spec).toBeNull() - }) + expect(result.status).toBe('select_spec'); + expect(result.chrome.cwd).toBe(cwd); + expect(result.chrome.spec).toBeNull(); + }); - it("generates a display name for new sessions and persists it as session_info", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('generates a display name for new sessions and persists it as session_info', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); const first = await coordinator.createSetupSession({ - specTitle: "Scratch spec", - }) + specTitle: 'Scratch spec', + }); // Session should have a display name derived from spec title - const manager1 = SessionManager.open(first.session.file, undefined, cwd) - expect(manager1.getSessionName()).toBe("Scratch spec — session 1") + const manager1 = SessionManager.open(first.session.file, undefined, cwd); + expect(manager1.getSessionName()).toBe('Scratch spec — session 1'); // Second session for same spec gets ordinal 2 - const second = await coordinator.createSetupSessionForCurrentSpec() - expect(second.status).toBe("ready") - if (second.status !== "ready") return + const second = await coordinator.createSetupSessionForCurrentSpec(); + expect(second.status).toBe('ready'); + if (second.status !== 'ready') return; - const manager2 = SessionManager.open(second.session.file, undefined, cwd) - expect(manager2.getSessionName()).toBe("Scratch spec — session 2") - }) + const manager2 = SessionManager.open(second.session.file, undefined, cwd); + expect(manager2.getSessionName()).toBe('Scratch spec — session 2'); + }); - it("preserves existing display name on session resume", async () => { - const cwd = await mkdtemp(join(tmpdir(), "brunch-ws-")) - const coordinator = createWorkspaceSessionCoordinator({ cwd }) + it('preserves existing display name on session resume', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); await coordinator.createSetupSession({ - specTitle: "My spec", - }) + specTitle: 'My spec', + }); // Reopen the same session - const reopened = await coordinator.openDefaultWorkspace() - expect(reopened.status).toBe("ready") - if (reopened.status !== "ready") return + const reopened = await coordinator.openDefaultWorkspace(); + expect(reopened.status).toBe('ready'); + if (reopened.status !== 'ready') return; // Name should be unchanged - const manager = SessionManager.open(reopened.session.file, undefined, cwd) - expect(manager.getSessionName()).toBe("My spec — session 1") - }) -}) + const manager = SessionManager.open(reopened.session.file, undefined, cwd); + expect(manager.getSessionName()).toBe('My spec — session 1'); + }); +}); diff --git a/src/workspace-session-coordinator.ts b/src/workspace-session-coordinator.ts index 1c8a544b7..98b3692ee 100644 --- a/src/workspace-session-coordinator.ts +++ b/src/workspace-session-coordinator.ts @@ -1,382 +1,363 @@ -import { randomUUID } from "node:crypto" -import { readdir, readFile, writeFile, mkdir } from "node:fs/promises" -import { join, resolve } from "node:path" +import { randomUUID } from 'node:crypto'; +import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; -import { - SessionManager, - type SessionHeader, -} from "@earendil-works/pi-coding-agent" +import { SessionManager, type SessionHeader } from '@earendil-works/pi-coding-agent'; import { createSessionBindingData, isSessionBindingEntry, SESSION_BINDING_TYPE, type SessionBindingData, -} from "./session-binding.js" +} from './session-binding.js'; -const BRUNCH_DIR = ".brunch" -const STATE_FILE = "state.json" -const SESSION_DIR = "sessions" -const STATE_SCHEMA_VERSION = 1 +const BRUNCH_DIR = '.brunch'; +const STATE_FILE = 'state.json'; +const SESSION_DIR = 'sessions'; +const STATE_SCHEMA_VERSION = 1; export interface WorkspaceSpecState { - id: string - title: string + id: string; + title: string; } interface WorkspaceStateFile { - schemaVersion: 1 - currentSpec: WorkspaceSpecState - currentSessionFile?: string + schemaVersion: 1; + currentSpec: WorkspaceSpecState; + currentSessionFile?: string; } export interface WorkspaceSessionChromeState { - cwd: string - spec: WorkspaceSpecState | null - phase: "select_spec" | "elicitation" - chatMode: "select-spec" | "responding-to-elicitation" + cwd: string; + spec: WorkspaceSpecState | null; + phase: 'select_spec' | 'elicitation'; + chatMode: 'select-spec' | 'responding-to-elicitation'; } export interface WorkspaceSessionReadyState { - status: "ready" - cwd: string - spec: WorkspaceSpecState + status: 'ready'; + cwd: string; + spec: WorkspaceSpecState; session: { - id: string - file: string - name?: string - manager: SessionManager - } - chrome: WorkspaceSessionChromeState + id: string; + file: string; + name?: string; + manager: SessionManager; + }; + chrome: WorkspaceSessionChromeState; } export interface WorkspaceSessionSelectSpecState { - status: "select_spec" - cwd: string - chrome: WorkspaceSessionChromeState + status: 'select_spec'; + cwd: string; + chrome: WorkspaceSessionChromeState; } export interface WorkspaceSessionNeedsHumanState { - status: "needs_human" - cwd: string - reason: string - chrome: WorkspaceSessionChromeState + status: 'needs_human'; + cwd: string; + reason: string; + chrome: WorkspaceSessionChromeState; } export interface WorkspaceSessionCancelledState { - status: "cancelled" - cwd: string - chrome: WorkspaceSessionChromeState + status: 'cancelled'; + cwd: string; + chrome: WorkspaceSessionChromeState; } -export type WorkspaceSessionState = WorkspaceSessionReadyState | WorkspaceSessionSelectSpecState | WorkspaceSessionNeedsHumanState +export type WorkspaceSessionState = + | WorkspaceSessionReadyState + | WorkspaceSessionSelectSpecState + | WorkspaceSessionNeedsHumanState; export interface WorkspaceContinueDecision { - action: "continue" - specId: string - sessionFile: string + action: 'continue'; + specId: string; + sessionFile: string; } export interface WorkspaceOpenSessionDecision { - action: "openSession" - specId: string - sessionFile: string + action: 'openSession'; + specId: string; + sessionFile: string; } export interface WorkspaceNewSessionDecision { - action: "newSession" - specId: string + action: 'newSession'; + specId: string; } export interface WorkspaceNewSpecDecision { - action: "newSpec" - title: string + action: 'newSpec'; + title: string; } export interface WorkspaceCancelDecision { - action: "cancel" + action: 'cancel'; } -export type SpecSessionActivationDecision = WorkspaceContinueDecision | WorkspaceOpenSessionDecision | WorkspaceNewSessionDecision | WorkspaceNewSpecDecision | WorkspaceCancelDecision +export type SpecSessionActivationDecision = + | WorkspaceContinueDecision + | WorkspaceOpenSessionDecision + | WorkspaceNewSessionDecision + | WorkspaceNewSpecDecision + | WorkspaceCancelDecision; -export type WorkspaceActivationState = WorkspaceSessionReadyState | WorkspaceSessionNeedsHumanState | WorkspaceSessionCancelledState +export type WorkspaceActivationState = + | WorkspaceSessionReadyState + | WorkspaceSessionNeedsHumanState + | WorkspaceSessionCancelledState; export interface WorkspaceLaunchSession { - id: string - file: string - specId: string - specTitle: string - name?: string - available: true + id: string; + file: string; + specId: string; + specTitle: string; + name?: string; + available: true; } export interface WorkspaceLaunchSpec { - spec: WorkspaceSpecState - sessions: WorkspaceLaunchSession[] + spec: WorkspaceSpecState; + sessions: WorkspaceLaunchSession[]; } -export type WorkspaceUnavailableSessionReason = "missing_header" | "missing_binding" | "incompatible_binding" +export type WorkspaceUnavailableSessionReason = 'missing_header' | 'missing_binding' | 'incompatible_binding'; export interface WorkspaceUnavailableSession { - file: string - reason: WorkspaceUnavailableSessionReason - available: false + file: string; + reason: WorkspaceUnavailableSessionReason; + available: false; } export interface WorkspaceLaunchInventory { - cwd: string - currentSpec: WorkspaceSpecState | null - currentSessionFile: string | null - needsNewSpec: boolean - specs: WorkspaceLaunchSpec[] - unavailableSessions: WorkspaceUnavailableSession[] + cwd: string; + currentSpec: WorkspaceSpecState | null; + currentSessionFile: string | null; + needsNewSpec: boolean; + specs: WorkspaceLaunchSpec[]; + unavailableSessions: WorkspaceUnavailableSession[]; } export interface SpecSessionActivationCoordinator { - inspectWorkspace(): Promise - activateWorkspace( - decision: SpecSessionActivationDecision, - ): Promise + inspectWorkspace(): Promise; + activateWorkspace(decision: SpecSessionActivationDecision): Promise; } export interface DefaultWorkspaceCoordinator { - openDefaultWorkspace(): Promise + openDefaultWorkspace(): Promise; } export interface WorkspaceSetupCoordinator { createSetupSession(options?: { - specTitle?: string - createNewSpec?: boolean - }): Promise - createSetupSessionForCurrentSpec(): Promise + specTitle?: string; + createNewSpec?: boolean; + }): Promise; + createSetupSessionForCurrentSpec(): Promise; } export interface WorkspaceSessionBoundaryCoordinator { - bindCurrentSpecToReplacementSession( - manager: SessionManager, - ): Promise + bindCurrentSpecToReplacementSession(manager: SessionManager): Promise; } export interface WorkspaceDefaultChromeCoordinator { - deriveDefaultChromeState(): Promise + deriveDefaultChromeState(): Promise; } export interface WorkspaceSessionCoordinator - extends SpecSessionActivationCoordinator, + extends + SpecSessionActivationCoordinator, DefaultWorkspaceCoordinator, WorkspaceSetupCoordinator, WorkspaceSessionBoundaryCoordinator, WorkspaceDefaultChromeCoordinator {} -export function createWorkspaceSessionCoordinator(options?: { - cwd?: string -}): WorkspaceSessionCoordinator { - const cwd = resolve(options?.cwd ?? process.cwd()) - return new FileWorkspaceSessionCoordinator(cwd) +export function createWorkspaceSessionCoordinator(options?: { cwd?: string }): WorkspaceSessionCoordinator { + const cwd = resolve(options?.cwd ?? process.cwd()); + return new FileWorkspaceSessionCoordinator(cwd); } class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { - readonly #cwd: string + readonly #cwd: string; constructor(cwd: string) { - this.#cwd = cwd + this.#cwd = cwd; } async inspectWorkspace(): Promise { - return inspectWorkspaceInventory(this.#cwd) + return inspectWorkspaceInventory(this.#cwd); } - async activateWorkspace( - decision: SpecSessionActivationDecision, - ): Promise { - if (decision.action === "cancel") { - const state = await readWorkspaceState(this.#cwd) + async activateWorkspace(decision: SpecSessionActivationDecision): Promise { + if (decision.action === 'cancel') { + const state = await readWorkspaceState(this.#cwd); return { - status: "cancelled", + status: 'cancelled', cwd: this.#cwd, chrome: chromeState(this.#cwd, state?.currentSpec ?? null), - } + }; } - if (decision.action === "newSpec") { + if (decision.action === 'newSpec') { return this.createSetupSession({ specTitle: decision.title, createNewSpec: true, - }) + }); } - const inventory = await inspectWorkspaceInventory(this.#cwd) - const spec = inventory.specs.find( - (candidate) => candidate.spec.id === decision.specId, - ) + const inventory = await inspectWorkspaceInventory(this.#cwd); + const spec = inventory.specs.find((candidate) => candidate.spec.id === decision.specId); if (!spec) { return needsHumanState( this.#cwd, inventory.currentSpec, - "Selected spec is not available in this workspace.", - ) + 'Selected spec is not available in this workspace.', + ); } - if (decision.action === "newSession") { - const session = await createBoundSession(this.#cwd, spec.spec) - await writeCurrentWorkspaceState(this.#cwd, spec.spec, session.file) - return readyState(this.#cwd, spec.spec, session) + if (decision.action === 'newSession') { + const session = await createBoundSession(this.#cwd, spec.spec); + await writeCurrentWorkspaceState(this.#cwd, spec.spec, session.file); + return readyState(this.#cwd, spec.spec, session); } - const session = spec.sessions.find( - (candidate) => candidate.file === decision.sessionFile, - ) + const session = spec.sessions.find((candidate) => candidate.file === decision.sessionFile); if (!session) { return needsHumanState( this.#cwd, inventory.currentSpec, - "Selected session is not available for the selected spec.", - ) + 'Selected session is not available for the selected spec.', + ); } - const manager = SessionManager.open( - session.file, - sessionDir(this.#cwd), - this.#cwd, - ) - const opened = bindSessionToSpec(manager, spec.spec) - await writeCurrentWorkspaceState(this.#cwd, spec.spec, opened.file) - return readyState(this.#cwd, spec.spec, opened) + const manager = SessionManager.open(session.file, sessionDir(this.#cwd), this.#cwd); + const opened = bindSessionToSpec(manager, spec.spec); + await writeCurrentWorkspaceState(this.#cwd, spec.spec, opened.file); + return readyState(this.#cwd, spec.spec, opened); } async openDefaultWorkspace(): Promise { - const state = await readWorkspaceState(this.#cwd) + const state = await readWorkspaceState(this.#cwd); if (!state) { return { - status: "select_spec", + status: 'select_spec', cwd: this.#cwd, chrome: chromeState(this.#cwd, null), - } + }; } - const session = await openCurrentSession( - this.#cwd, - state.currentSpec, - state.currentSessionFile, - ) - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file) - return readyState(this.#cwd, state.currentSpec, session) + const session = await openCurrentSession(this.#cwd, state.currentSpec, state.currentSessionFile); + await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); + return readyState(this.#cwd, state.currentSpec, session); } async createSetupSession(options?: { - specTitle?: string - createNewSpec?: boolean + specTitle?: string; + createNewSpec?: boolean; }): Promise { - await ensureWorkspaceDirs(this.#cwd) - const existing = await readWorkspaceState(this.#cwd) - const spec = - existing && !options?.createNewSpec - ? existing.currentSpec - : createSpec(options?.specTitle) - const session = await createBoundSession(this.#cwd, spec) - await writeCurrentWorkspaceState(this.#cwd, spec, session.file) - return readyState(this.#cwd, spec, session) + await ensureWorkspaceDirs(this.#cwd); + const existing = await readWorkspaceState(this.#cwd); + const spec = existing && !options?.createNewSpec ? existing.currentSpec : createSpec(options?.specTitle); + const session = await createBoundSession(this.#cwd, spec); + await writeCurrentWorkspaceState(this.#cwd, spec, session.file); + return readyState(this.#cwd, spec, session); } async createSetupSessionForCurrentSpec(): Promise { - const state = await readWorkspaceState(this.#cwd) + const state = await readWorkspaceState(this.#cwd); if (!state) { return { - status: "needs_human", + status: 'needs_human', cwd: this.#cwd, - reason: "No current spec is selected for this workspace.", + reason: 'No current spec is selected for this workspace.', chrome: chromeState(this.#cwd, null), - } + }; } - const session = await createBoundSession(this.#cwd, state.currentSpec) - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file) - return readyState(this.#cwd, state.currentSpec, session) + const session = await createBoundSession(this.#cwd, state.currentSpec); + await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); + return readyState(this.#cwd, state.currentSpec, session); } - async bindCurrentSpecToReplacementSession( - manager: SessionManager, - ): Promise { - const state = await readWorkspaceState(this.#cwd) + async bindCurrentSpecToReplacementSession(manager: SessionManager): Promise { + const state = await readWorkspaceState(this.#cwd); if (!state) { - throw new Error("No current spec is selected for this workspace.") + throw new Error('No current spec is selected for this workspace.'); } - const session = bindSessionToSpec(manager, state.currentSpec) - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file) - return readyState(this.#cwd, state.currentSpec, session) + const session = bindSessionToSpec(manager, state.currentSpec); + await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); + return readyState(this.#cwd, state.currentSpec, session); } async deriveDefaultChromeState(): Promise { - const state = await readWorkspaceState(this.#cwd) - return chromeState(this.#cwd, state?.currentSpec ?? null) + const state = await readWorkspaceState(this.#cwd); + return chromeState(this.#cwd, state?.currentSpec ?? null); } } -function createSpec(title = "Untitled spec"): WorkspaceSpecState { - return { id: `spec-${randomUUID()}`, title } +function createSpec(title = 'Untitled spec'): WorkspaceSpecState { + return { id: `spec-${randomUUID()}`, title }; } async function createBoundSession( cwd: string, spec: WorkspaceSpecState, -): Promise { - await ensureWorkspaceDirs(cwd) - const existingSessionCount = await countSessionsForSpec(cwd, spec.id) - const manager = SessionManager.create(cwd, sessionDir(cwd)) - const sessionFile = manager.getSessionFile() +): Promise { + await ensureWorkspaceDirs(cwd); + const existingSessionCount = await countSessionsForSpec(cwd, spec.id); + const manager = SessionManager.create(cwd, sessionDir(cwd)); + const sessionFile = manager.getSessionFile(); if (!sessionFile) { - throw new Error("Pi SessionManager did not create a persisted session file") + throw new Error('Pi SessionManager did not create a persisted session file'); } - return bindSessionToSpec(manager, spec, existingSessionCount + 1) + return bindSessionToSpec(manager, spec, existingSessionCount + 1); } -async function countSessionsForSpec( - cwd: string, - specId: string, -): Promise { - const files = await listSessionFiles(cwd) - let count = 0 +async function countSessionsForSpec(cwd: string, specId: string): Promise { + const files = await listSessionFiles(cwd); + let count = 0; for (const file of files) { - const session = await inspectSessionFile(file) + const session = await inspectSessionFile(file); if (session.available && session.specId === specId) { - count++ + count++; } } - return count + return count; } async function openCurrentSession( cwd: string, spec: WorkspaceSpecState, currentSessionFile: string | undefined, -): Promise { - await ensureWorkspaceDirs(cwd) - const files = await listSessionFiles(cwd) +): Promise { + await ensureWorkspaceDirs(cwd); + const files = await listSessionFiles(cwd); const manager = currentSessionFile ? SessionManager.open(currentSessionFile, sessionDir(cwd), cwd) : files.length === 0 ? SessionManager.create(cwd, sessionDir(cwd)) - : SessionManager.continueRecent(cwd, sessionDir(cwd)) - const sessionFile = manager.getSessionFile() + : SessionManager.continueRecent(cwd, sessionDir(cwd)); + const sessionFile = manager.getSessionFile(); if (!sessionFile) { - throw new Error("Pi SessionManager did not open a persisted session file") + throw new Error('Pi SessionManager did not open a persisted session file'); } - return bindSessionToSpec(manager, spec) + return bindSessionToSpec(manager, spec); } function bindSessionToSpec( manager: SessionManager, spec: WorkspaceSpecState, sessionOrdinal?: number, -): WorkspaceSessionReadyState["session"] { - const sessionFile = manager.getSessionFile() +): WorkspaceSessionReadyState['session'] { + const sessionFile = manager.getSessionFile(); if (!sessionFile) { - throw new Error("Pi SessionManager did not create a persisted session file") + throw new Error('Pi SessionManager did not create a persisted session file'); } - const existingBindings = manager.getEntries().filter(isSessionBindingEntry) + const existingBindings = manager.getEntries().filter(isSessionBindingEntry); if (existingBindings.length === 0) { manager.appendCustomEntry( SESSION_BINDING_TYPE, @@ -385,123 +366,113 @@ function bindSessionToSpec( specId: spec.id, specTitle: spec.title, }), - ) + ); // Generate and persist a display name for new sessions if (sessionOrdinal !== undefined) { - const displayName = sessionDisplayName(spec.title, sessionOrdinal) - manager.appendSessionInfo(displayName) + const displayName = sessionDisplayName(spec.title, sessionOrdinal); + manager.appendSessionInfo(displayName); } } else if ( existingBindings.length !== 1 || existingBindings[0]?.data.sessionId !== manager.getSessionId() || existingBindings[0].data.specId !== spec.id ) { - throw new Error( - "Session already has an incompatible Brunch session binding", - ) + throw new Error('Session already has an incompatible Brunch session binding'); } - flushSessionWithoutAssistant(manager) - const sessionName = manager.getSessionName() + flushSessionWithoutAssistant(manager); + const sessionName = manager.getSessionName(); return { id: manager.getSessionId(), file: sessionFile, ...(sessionName != null ? { name: sessionName } : {}), manager, - } + }; } export function sessionDisplayName(specTitle: string, ordinal: number): string { - return `${specTitle} — session ${ordinal}` + return `${specTitle} — session ${ordinal}`; } interface FlushableSessionManager { - _rewriteFile(): void + _rewriteFile(): void; } function flushSessionWithoutAssistant(manager: SessionManager): void { - const sessionFile = manager.getSessionFile() - ;(manager as unknown as FlushableSessionManager)._rewriteFile() + const sessionFile = manager.getSessionFile(); + (manager as unknown as FlushableSessionManager)._rewriteFile(); if (sessionFile) { - manager.setSessionFile(sessionFile) + manager.setSessionFile(sessionFile); } } async function ensureWorkspaceDirs(cwd: string): Promise { - await mkdir(sessionDir(cwd), { recursive: true }) + await mkdir(sessionDir(cwd), { recursive: true }); } function brunchDir(cwd: string): string { - return join(cwd, BRUNCH_DIR) + return join(cwd, BRUNCH_DIR); } function sessionDir(cwd: string): string { - return join(brunchDir(cwd), SESSION_DIR) + return join(brunchDir(cwd), SESSION_DIR); } function statePath(cwd: string): string { - return join(brunchDir(cwd), STATE_FILE) + return join(brunchDir(cwd), STATE_FILE); } -async function readWorkspaceState( - cwd: string, -): Promise { +async function readWorkspaceState(cwd: string): Promise { try { - const parsed = JSON.parse( - await readFile(statePath(cwd), "utf8"), - ) as Partial + const parsed = JSON.parse(await readFile(statePath(cwd), 'utf8')) as Partial; if ( parsed.schemaVersion === STATE_SCHEMA_VERSION && - typeof parsed.currentSpec?.id === "string" && - typeof parsed.currentSpec.title === "string" + typeof parsed.currentSpec?.id === 'string' && + typeof parsed.currentSpec.title === 'string' ) { - return parsed as WorkspaceStateFile + return parsed as WorkspaceStateFile; } - return null + return null; } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return null + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; } - throw error + throw error; } } -async function inspectWorkspaceInventory( - cwd: string, -): Promise { - const state = await readWorkspaceState(cwd) - const files = await listSessionFiles(cwd) - const specsById = new Map() - const unavailableSessions: WorkspaceUnavailableSession[] = [] +async function inspectWorkspaceInventory(cwd: string): Promise { + const state = await readWorkspaceState(cwd); + const files = await listSessionFiles(cwd); + const specsById = new Map(); + const unavailableSessions: WorkspaceUnavailableSession[] = []; if (state) { specsById.set(state.currentSpec.id, { spec: state.currentSpec, sessions: [], - }) + }); } for (const file of files) { - const session = await inspectSessionFile(file) + const session = await inspectSessionFile(file); if (session.available) { const spec = getOrCreateLaunchSpec(specsById, { id: session.specId, title: session.specTitle, - }) - spec.sessions.push(session) + }); + spec.sessions.push(session); } else { - unavailableSessions.push(session) + unavailableSessions.push(session); } } const specs = [...specsById.values()] .map((spec) => ({ ...spec, - sessions: spec.sessions.sort((left, right) => - left.file.localeCompare(right.file), - ), + sessions: spec.sessions.sort((left, right) => left.file.localeCompare(right.file)), })) - .sort((left, right) => left.spec.title.localeCompare(right.spec.title)) + .sort((left, right) => left.spec.title.localeCompare(right.spec.title)); return { cwd, @@ -509,37 +480,35 @@ async function inspectWorkspaceInventory( currentSessionFile: state?.currentSessionFile ?? null, needsNewSpec: specs.length === 0, specs, - unavailableSessions: unavailableSessions.sort((left, right) => - left.file.localeCompare(right.file), - ), - } + unavailableSessions: unavailableSessions.sort((left, right) => left.file.localeCompare(right.file)), + }; } -type InspectedSessionFile = WorkspaceLaunchSession | WorkspaceUnavailableSession +type InspectedSessionFile = WorkspaceLaunchSession | WorkspaceUnavailableSession; async function inspectSessionFile(file: string): Promise { - const entries = await readJsonl(file) - const header = entries.find(isSessionHeader) + const entries = await readJsonl(file); + const header = entries.find(isSessionHeader); if (!header) { - return { file, reason: "missing_header", available: false } + return { file, reason: 'missing_header', available: false }; } - const bindings = entries.filter(isSessionBindingEntry) + const bindings = entries.filter(isSessionBindingEntry); if (bindings.length === 0) { - return { file, reason: "missing_binding", available: false } + return { file, reason: 'missing_binding', available: false }; } - const binding = bindings[0]! + const binding = bindings[0]!; if (bindings.length !== 1 || binding.data.sessionId !== header.id) { - return { file, reason: "incompatible_binding", available: false } + return { file, reason: 'incompatible_binding', available: false }; } - const sessionInfoEntries = entries.filter(isSessionInfoEntry) + const sessionInfoEntries = entries.filter(isSessionInfoEntry); const lastInfo = sessionInfoEntries.length > 0 - ? sessionInfoEntries[sessionInfoEntries.length - 1] as { name?: string } - : undefined - const name = lastInfo?.name + ? (sessionInfoEntries[sessionInfoEntries.length - 1] as { name?: string }) + : undefined; + const name = lastInfo?.name; return { id: header.id, @@ -548,28 +517,25 @@ async function inspectSessionFile(file: string): Promise { specTitle: binding.data.specTitle, ...(name != null ? { name } : {}), available: true, - } + }; } function getOrCreateLaunchSpec( specsById: Map, spec: WorkspaceSpecState, ): WorkspaceLaunchSpec { - const existing = specsById.get(spec.id) + const existing = specsById.get(spec.id); if (existing) { - return existing + return existing; } - const created = { spec, sessions: [] } - specsById.set(spec.id, created) - return created + const created = { spec, sessions: [] }; + specsById.set(spec.id, created); + return created; } -async function writeWorkspaceState( - cwd: string, - state: WorkspaceStateFile, -): Promise { - await ensureWorkspaceDirs(cwd) - await writeFile(statePath(cwd), `${JSON.stringify(state, null, 2)}\n`, "utf8") +async function writeWorkspaceState(cwd: string, state: WorkspaceStateFile): Promise { + await ensureWorkspaceDirs(cwd); + await writeFile(statePath(cwd), `${JSON.stringify(state, null, 2)}\n`, 'utf8'); } async function writeCurrentWorkspaceState( @@ -581,21 +547,21 @@ async function writeCurrentWorkspaceState( schemaVersion: STATE_SCHEMA_VERSION, currentSpec: spec, currentSessionFile, - }) + }); } function readyState( cwd: string, spec: WorkspaceSpecState, - session: WorkspaceSessionReadyState["session"], + session: WorkspaceSessionReadyState['session'], ): WorkspaceSessionReadyState { return { - status: "ready", + status: 'ready', cwd, spec, session, chrome: chromeState(cwd, spec), - } + }; } function needsHumanState( @@ -604,144 +570,124 @@ function needsHumanState( reason: string, ): WorkspaceSessionNeedsHumanState { return { - status: "needs_human", + status: 'needs_human', cwd, reason, chrome: chromeState(cwd, spec), - } + }; } -function chromeState( - cwd: string, - spec: WorkspaceSpecState | null, -): WorkspaceSessionChromeState { +function chromeState(cwd: string, spec: WorkspaceSpecState | null): WorkspaceSessionChromeState { return { cwd, spec, - phase: spec ? "elicitation" : "select_spec", - chatMode: spec ? "responding-to-elicitation" : "select-spec", - } + phase: spec ? 'elicitation' : 'select_spec', + chatMode: spec ? 'responding-to-elicitation' : 'select-spec', + }; } export interface WorkspaceStoreOracleOptions { - cwd: string - expectedSessionCount?: number + cwd: string; + expectedSessionCount?: number; } export interface WorkspaceStoreOracleSuccess { - ok: true - specId: string + ok: true; + specId: string; sessions: Array<{ - file: string - sessionId: string - bindingCount: number - binding: SessionBindingData - }> + file: string; + sessionId: string; + bindingCount: number; + binding: SessionBindingData; + }>; } export interface WorkspaceStoreOracleFailure { - ok: false - errors: string[] + ok: false; + errors: string[]; } -export type WorkspaceStoreOracleResult = WorkspaceStoreOracleSuccess | WorkspaceStoreOracleFailure +export type WorkspaceStoreOracleResult = WorkspaceStoreOracleSuccess | WorkspaceStoreOracleFailure; export async function verifyWorkspaceSessionStores( options: WorkspaceStoreOracleOptions, ): Promise { - const cwd = resolve(options.cwd) - const errors: string[] = [] - const state = await readWorkspaceState(cwd) + const cwd = resolve(options.cwd); + const errors: string[] = []; + const state = await readWorkspaceState(cwd); if (!state) { - return { ok: false, errors: ["Missing or invalid .brunch/state.json"] } + return { ok: false, errors: ['Missing or invalid .brunch/state.json'] }; } - const files = await listSessionFiles(cwd) - if ( - options.expectedSessionCount !== undefined && - files.length !== options.expectedSessionCount - ) { - errors.push( - `Expected ${options.expectedSessionCount} session file(s), found ${files.length}`, - ) + const files = await listSessionFiles(cwd); + if (options.expectedSessionCount !== undefined && files.length !== options.expectedSessionCount) { + errors.push(`Expected ${options.expectedSessionCount} session file(s), found ${files.length}`); } - const sessions: WorkspaceStoreOracleSuccess["sessions"] = [] + const sessions: WorkspaceStoreOracleSuccess['sessions'] = []; for (const file of files) { - const entries = await readJsonl(file) - const header = entries.find(isSessionHeader) - const bindings = entries.filter(isSessionBindingEntry) + const entries = await readJsonl(file); + const header = entries.find(isSessionHeader); + const bindings = entries.filter(isSessionBindingEntry); if (!header) { - errors.push(`${file} has no session header`) - continue + errors.push(`${file} has no session header`); + continue; } if (bindings.length !== 1) { - errors.push( - `${file} has ${bindings.length} ${SESSION_BINDING_TYPE} entries`, - ) - continue + errors.push(`${file} has ${bindings.length} ${SESSION_BINDING_TYPE} entries`); + continue; } - const binding = bindings[0]!.data + const binding = bindings[0]!.data; if (binding.specId !== state.currentSpec.id) { - errors.push( - `${file} binding spec ${binding.specId} does not match state ${state.currentSpec.id}`, - ) + errors.push(`${file} binding spec ${binding.specId} does not match state ${state.currentSpec.id}`); } if (binding.sessionId !== header.id) { - errors.push( - `${file} binding session ${binding.sessionId} does not match header ${header.id}`, - ) + errors.push(`${file} binding session ${binding.sessionId} does not match header ${header.id}`); } sessions.push({ file, sessionId: header.id, bindingCount: bindings.length, binding, - }) + }); } - return errors.length === 0 - ? { ok: true, specId: state.currentSpec.id, sessions } - : { ok: false, errors } + return errors.length === 0 ? { ok: true, specId: state.currentSpec.id, sessions } : { ok: false, errors }; } async function listSessionFiles(cwd: string): Promise { try { - const entries = await readdir(sessionDir(cwd), { withFileTypes: true }) + const entries = await readdir(sessionDir(cwd), { withFileTypes: true }); return entries - .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")) + .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) .map((entry) => join(sessionDir(cwd), entry.name)) - .sort() + .sort(); } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return [] + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; } - throw error + throw error; } } async function readJsonl(file: string): Promise { - const content = await readFile(file, "utf8") + const content = await readFile(file, 'utf8'); return content - .split("\n") + .split('\n') .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as unknown) + .map((line) => JSON.parse(line) as unknown); } function isSessionHeader(value: unknown): value is SessionHeader { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && - (value as { type?: unknown }).type === "session" && - typeof (value as { id?: unknown }).id === "string" - ) + (value as { type?: unknown }).type === 'session' && + typeof (value as { id?: unknown }).id === 'string' + ); } function isSessionInfoEntry(value: unknown): boolean { - return ( - typeof value === "object" && - value !== null && - (value as { type?: unknown }).type === "session_info" - ) + return typeof value === 'object' && value !== null && (value as { type?: unknown }).type === 'session_info'; } diff --git a/tsconfig.build.json b/tsconfig.build.json index 76f631465..a6133e942 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,13 +1,13 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "rootDir": "./src", - "outDir": "./dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "archive", "src/**/*.test.ts", "src/**/*.test.tsx", ".pi"] + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "archive", "src/**/*.test.ts", "src/**/*.test.tsx", ".pi"] } diff --git a/tsconfig.json b/tsconfig.json index ba1bf396f..c3b619488 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,37 +1,26 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], - "jsx": "react-jsx", - "noEmit": true, - "strict": true, - "noImplicitAny": true, - "noImplicitOverride": true, - "noUncheckedIndexedAccess": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "useUnknownInCatchVariables": true, - "exactOptionalPropertyTypes": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "isolatedModules": true, - "verbatimModuleSyntax": true - }, - "include": [ - "src/**/*", - ".pi/extensions/**/*.ts" - ], - "exclude": [ - "node_modules", - "dist", - "archive" - ] + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "strict": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "useUnknownInCatchVariables": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*", ".pi/extensions/**/*.ts"], + "exclude": ["node_modules", "dist", "archive"] } diff --git a/vite.config.ts b/vite.config.ts index a3609e588..7e4ffa328 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,17 +1,17 @@ -import react from "@vitejs/plugin-react" -import { defineConfig } from "vite" +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; export default defineConfig({ plugins: [react()], build: { - outDir: "dist-web", + outDir: 'dist-web', emptyOutDir: true, rollupOptions: { output: { - entryFileNames: "assets/brunch-web.js", - chunkFileNames: "assets/[name].js", - assetFileNames: "assets/[name][extname]", + entryFileNames: 'assets/brunch-web.js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name][extname]', }, }, }, -}) +}); From 73380286dbb3468e02fec44961da31ee6477d52f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 09:32:33 +0200 Subject: [PATCH 08/34] vite react plugin v6 --- package-lock.json | 13720 +++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 6588 insertions(+), 7134 deletions(-) diff --git a/package-lock.json b/package-lock.json index c77098ab1..f344b3d18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7135 +1,6589 @@ { - "name": "brunch-next", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "brunch-next", - "version": "0.0.0", - "dependencies": { - "@earendil-works/pi-coding-agent": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.4", - "@tanstack/react-query": "^5.100.11", - "@tanstack/react-router": "^1.170.6", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.20.1", - "zod": "^4.4.3" - }, - "bin": { - "brunch-next": "bin/brunch.js" - }, - "devDependencies": { - "@sinclair/typebox": "^0.34.14", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", - "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "@types/react": "^19.2.15", - "@types/react-dom": "^19.2.3", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.7.0", - "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.18.1", - "drizzle-orm": "^0.45.2", - "drizzle-typebox": "^0.3.3", - "jsdom": "^29.1.1", - "oxfmt": "latest", - "oxlint": "latest", - "tsx": "^4.19.0", - "typescript": "^5.7.0", - "vite": "^8.0.16", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1050.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1050.0.tgz", - "integrity": "sha512-KbQqWGSyXh1c0opFTEcwNu6PcGd/IRyTnihDh8fpdiVCu62/53469AN+Xe6cKSuM6W2oOBbY12Pbj3zrdRK5mA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/credential-provider-node": "^3.972.43", - "@aws-sdk/eventstream-handler-node": "^3.972.16", - "@aws-sdk/middleware-eventstream": "^3.972.12", - "@aws-sdk/middleware-websocket": "^3.972.20", - "@aws-sdk/token-providers": "3.1050.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.974.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.12.tgz", - "integrity": "sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.38.tgz", - "integrity": "sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.40.tgz", - "integrity": "sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.42.tgz", - "integrity": "sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/credential-provider-env": "^3.972.38", - "@aws-sdk/credential-provider-http": "^3.972.40", - "@aws-sdk/credential-provider-login": "^3.972.42", - "@aws-sdk/credential-provider-process": "^3.972.38", - "@aws-sdk/credential-provider-sso": "^3.972.42", - "@aws-sdk/credential-provider-web-identity": "^3.972.42", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.42.tgz", - "integrity": "sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.43.tgz", - "integrity": "sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.38", - "@aws-sdk/credential-provider-http": "^3.972.40", - "@aws-sdk/credential-provider-ini": "^3.972.42", - "@aws-sdk/credential-provider-process": "^3.972.38", - "@aws-sdk/credential-provider-sso": "^3.972.42", - "@aws-sdk/credential-provider-web-identity": "^3.972.42", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.38.tgz", - "integrity": "sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.42.tgz", - "integrity": "sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/token-providers": "3.1049.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1049.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1049.0.tgz", - "integrity": "sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.42.tgz", - "integrity": "sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", - "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", - "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.20.tgz", - "integrity": "sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.10.tgz", - "integrity": "sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", - "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1050.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1050.0.tgz", - "integrity": "sha512-LVw+bW8LKWdus3U4v7Ojm5XmIXv1ZlQ3rsQrlkEt5fss+SsWfTTzVxoo8kl6ZCY5gl5kL8lPGluHPIDGR8bntQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", - "license": "Apache-2.0", - "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", - "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@earendil-works/pi-agent-core": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", - "integrity": "sha512-azg09GSrckQa3ffbH09YEZC7DyHgmNSX+vmWEoEhQvp4icbzqbqLfIeMayMNEK/aGusm1SghZC4bPlDdagDALg==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", - "ignore": "^7.0.5", - "typebox": "^1.1.24", - "yaml": "^2.8.2" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/@earendil-works/pi-ai": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.3.tgz", - "integrity": "sha512-UKccS+ADlkSVJ49a00346jUfXmUi6zzzB+pPWotsyA6SxhKr2ejjkGQksGyR1DyNVrsEP/WWlsOSTUUwVlzNaA==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "openai": "6.26.0", - "partial-json": "^0.1.7", - "typebox": "^1.1.24" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.3.tgz", - "integrity": "sha512-LIi5/CdUBfcLp3BAtpLx1BfnHDLmDOQVdzYfS1H9fjjCw2dcPr9voSI5ncrhvZdgyFSnfHck4BCbNcfZk+TEHQ==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.3", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cross-spawn": "^7.0.6", - "diff": "^8.0.2", - "glob": "^13.0.1", - "highlight.js": "^10.7.3", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "jiti": "^2.7.0", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "typebox": "^1.1.24", - "undici": "^8.3.0", - "yaml": "^2.8.2" - }, - "bin": { - "pi": "dist/cli.js" - }, - "engines": { - "node": ">=22.19.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.6" - } - }, - "node_modules/@earendil-works/pi-tui": { - "version": "0.75.4", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.4.tgz", - "integrity": "sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "1.6.0", - "marked": "15.0.12" - }, - "engines": { - "node": ">=22.19.0" - }, - "optionalDependencies": { - "koffi": "2.16.2" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", - "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.6", - "@mariozechner/clipboard-darwin-universal": "0.3.6", - "@mariozechner/clipboard-darwin-x64": "0.3.6", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-musl": "0.3.6", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" - } - }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", - "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", - "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", - "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", - "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", - "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", - "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", - "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", - "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", - "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", - "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", - "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", - "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", - "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", - "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", - "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", - "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", - "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", - "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", - "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", - "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", - "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", - "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", - "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", - "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", - "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", - "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", - "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", - "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.53.0.tgz", - "integrity": "sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/darwin-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.18.1.tgz", - "integrity": "sha512-FqDrcQJmEGNkgmZgI4wbCrGyJl1tiRZa3udxvyYaXag8W80A0zLFNCyWVvHIgUJ0DHlZjRc7O72xUGjiyvQrqQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/darwin-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-0.18.1.tgz", - "integrity": "sha512-YUcyWBJdNuMcJxAwdV/i25/kvnKrVsA+vLn7SsL87cAwiD//rqGdOixk0r8sKUYa71Kx3h0Fg2ToUOjdE6ddYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/linux-arm64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-0.18.1.tgz", - "integrity": "sha512-ol3jhmUv5VI/omMrt6DkwY/jVTSVJlflFyU1SnSb/BuVVf3TyBiCHmZ4wVtcrcT5k3sWjrvYWw2kSozvmuE4tg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-arm64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-0.18.1.tgz", - "integrity": "sha512-iKDj1ZwlU4KpXuIL1qkVP6NJzri2VSJreqXCIAe1Bf5RZXMAGSO3xjldgiX+HBvFOKSBIarLcqONYDbYco9uaQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-0.18.1.tgz", - "integrity": "sha512-A3g+fZhlOivUdK7xU/IrbhBcMHig5GLrfMX0HYjXL1fiSqKYu9n1o1p42WpT6KfPL3L2uncSg/iyg7hspcN6qA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-0.18.1.tgz", - "integrity": "sha512-LA02SdATWZEZBy8ZZpR2GlUbDg7+Jq1/WKkywMXqxdClkcoyyFozj8aQD2iTMKELSra4OSyqqZpOYroqjSSKmw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/win32-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-0.18.1.tgz", - "integrity": "sha512-FNL+OxDflqLGXRgLxfBM/X4RnLYgtOKTsb1mNSqsjSCEfUi1Oqivh7KvZ09IfAMZeJ85/fL6EI6hSOyY7nNYUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint/win32-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-0.18.1.tgz", - "integrity": "sha512-W+aVE9Siqs6Oe3NDaDOTTOYsN9X3znl+whfqWK1EcLpqJXX1kdB8Hf45HkGjqnHoFoP96GRgUnXQHQvmUybjvg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.14", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.14.tgz", - "integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@smithy/core": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", - "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", - "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", - "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tanstack/history": { - "version": "1.162.0", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", - "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", - "license": "MIT", - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.11.tgz", - "integrity": "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.11.tgz", - "integrity": "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.11" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-router": { - "version": "1.170.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.6.tgz", - "integrity": "sha512-tGQkOjcMESBbfw+iz9zE/ivcuw4D2zdhW8PA4wMmpOyA2bLiqpc6bg5MnJPxamdXoO3GZBiHYOOoEwi5qxpPgA==", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.162.0", - "@tanstack/react-store": "^0.9.3", - "@tanstack/router-core": "1.171.4", - "isbot": "^5.1.22" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - } - }, - "node_modules/@tanstack/react-store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", - "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", - "license": "MIT", - "dependencies": { - "@tanstack/store": "0.9.3", - "use-sync-external-store": "^1.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/router-core": { - "version": "1.171.4", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.4.tgz", - "integrity": "sha512-LU9YuVdgaP+h4MEXRvokyjIEelulylgsromHMfYwVfgo1PF9oJP3NHyy7qtjxGLJq6zoZMCfoa1frDJlPo7I8g==", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.162.0", - "cookie-es": "^3.0.0", - "seroval": "^1.5.4", - "seroval-plugins": "^1.5.4" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/store": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", - "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", - "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/cli-color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", - "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.15", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie-es": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", - "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/difflib": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", - "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", - "dev": true, - "dependencies": { - "heap": ">= 0.2.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dreamopt": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", - "integrity": "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==", - "dev": true, - "dependencies": { - "wordwrap": ">=0.0.2" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/drizzle-kit": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.18.1.tgz", - "integrity": "sha512-Oqie227W2Dd7FuqX4pvQWeClSvnoPCIn2cO9JueeLWZqj3tpdBhnbgt4nLHhBbOdWRlTLYwXnkTDW3hYym/gGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "commander": "^9.4.1", - "esbuild": "^0.15.18", - "esbuild-register": "^3.4.2", - "glob": "^8.1.0", - "hanji": "^0.0.5", - "json-diff": "0.9.0", - "minimatch": "^7.4.3", - "zod": "^3.20.2" - }, - "bin": { - "drizzle-kit": "index.js" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", - "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", - "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/drizzle-kit/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/drizzle-kit/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/drizzle-kit/node_modules/esbuild": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", - "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.15.18", - "@esbuild/linux-loong64": "0.15.18", - "esbuild-android-64": "0.15.18", - "esbuild-android-arm64": "0.15.18", - "esbuild-darwin-64": "0.15.18", - "esbuild-darwin-arm64": "0.15.18", - "esbuild-freebsd-64": "0.15.18", - "esbuild-freebsd-arm64": "0.15.18", - "esbuild-linux-32": "0.15.18", - "esbuild-linux-64": "0.15.18", - "esbuild-linux-arm": "0.15.18", - "esbuild-linux-arm64": "0.15.18", - "esbuild-linux-mips64le": "0.15.18", - "esbuild-linux-ppc64le": "0.15.18", - "esbuild-linux-riscv64": "0.15.18", - "esbuild-linux-s390x": "0.15.18", - "esbuild-netbsd-64": "0.15.18", - "esbuild-openbsd-64": "0.15.18", - "esbuild-sunos-64": "0.15.18", - "esbuild-windows-32": "0.15.18", - "esbuild-windows-64": "0.15.18", - "esbuild-windows-arm64": "0.15.18" - } - }, - "node_modules/drizzle-kit/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/drizzle-kit/node_modules/glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/drizzle-kit/node_modules/minimatch": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", - "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/drizzle-kit/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=4", - "@electric-sql/pglite": ">=0.2.0", - "@libsql/client": ">=0.10.0", - "@libsql/client-wasm": ">=0.10.0", - "@neondatabase/serverless": ">=0.10.0", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=14.0.0", - "gel": ">=2", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "sql.js": ">=1", - "sqlite3": ">=5" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@libsql/client-wasm": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { - "optional": true - }, - "expo-sqlite": { - "optional": true - }, - "gel": { - "optional": true - }, - "knex": { - "optional": true - }, - "kysely": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "postgres": { - "optional": true - }, - "prisma": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - } - } - }, - "node_modules/drizzle-typebox": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/drizzle-typebox/-/drizzle-typebox-0.3.3.tgz", - "integrity": "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "@sinclair/typebox": ">=0.34.8", - "drizzle-orm": ">=0.36.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.360", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", - "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", - "dev": true, - "license": "ISC" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "dev": true, - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dev": true, - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/esbuild-android-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", - "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", - "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", - "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", - "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", - "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", - "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", - "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", - "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", - "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", - "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", - "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", - "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", - "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", - "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", - "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", - "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", - "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", - "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "license": "ISC", - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hanji": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", - "integrity": "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lodash.throttle": "^4.1.1", - "sisteransi": "^1.0.5" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", - "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", - "dev": true, - "license": "MIT" - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isbot": { - "version": "5.1.40", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", - "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", - "license": "Unlicense", - "engines": { - "node": ">=18" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-diff": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.9.0.tgz", - "integrity": "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-color": "^2.0.0", - "difflib": "~0.2.1", - "dreamopt": "~0.8.0" - }, - "bin": { - "json-diff": "bin/json-diff.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es5-ext": "~0.10.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/memoizee": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", - "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "es5-ext": "^0.10.64", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-releases": { - "version": "2.0.45", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", - "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/oxfmt": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.53.0.tgz", - "integrity": "sha512-9cB5glS3Ip6NMuZ+6NYTao9FCWkDhRtPYCtR3QBu/NxHoFbgzzTvi41N4jxz/GqGfuLKspui1qb/LlSu2IbMcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinypool": "2.1.0" - }, - "bin": { - "oxfmt": "bin/oxfmt" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.53.0", - "@oxfmt/binding-android-arm64": "0.53.0", - "@oxfmt/binding-darwin-arm64": "0.53.0", - "@oxfmt/binding-darwin-x64": "0.53.0", - "@oxfmt/binding-freebsd-x64": "0.53.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.53.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.53.0", - "@oxfmt/binding-linux-arm64-gnu": "0.53.0", - "@oxfmt/binding-linux-arm64-musl": "0.53.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.53.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.53.0", - "@oxfmt/binding-linux-riscv64-musl": "0.53.0", - "@oxfmt/binding-linux-s390x-gnu": "0.53.0", - "@oxfmt/binding-linux-x64-gnu": "0.53.0", - "@oxfmt/binding-linux-x64-musl": "0.53.0", - "@oxfmt/binding-openharmony-arm64": "0.53.0", - "@oxfmt/binding-win32-arm64-msvc": "0.53.0", - "@oxfmt/binding-win32-ia32-msvc": "0.53.0", - "@oxfmt/binding-win32-x64-msvc": "0.53.0" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite-plus": "*" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - }, - "vite-plus": { - "optional": true - } - } - }, - "node_modules/oxfmt/node_modules/tinypool": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.0.0 || >=22.0.0" - } - }, - "node_modules/oxlint": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-0.18.1.tgz", - "integrity": "sha512-JGcQvbhd00Qb+nq4f9sYYRh7mZIb0K/7rbMepNdJDMzo8pbmBpx1N2XOG61RjHDsNnY6ImAmVk3h4QVwFenwUQ==", - "dev": true, - "license": "MIT", - "bin": { - "oxc_language_server": "bin/oxc_language_server", - "oxlint": "bin/oxlint" - }, - "engines": { - "node": ">=8.*" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxlint/darwin-arm64": "0.18.1", - "@oxlint/darwin-x64": "0.18.1", - "@oxlint/linux-arm64-gnu": "0.18.1", - "@oxlint/linux-arm64-musl": "0.18.1", - "@oxlint/linux-x64-gnu": "0.18.1", - "@oxlint/linux-x64-musl": "0.18.1", - "@oxlint/win32-arm64": "0.18.1", - "@oxlint/win32-x64": "0.18.1" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/partial-json": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT" - }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/protobufjs": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", - "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.6" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/seroval": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", - "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/seroval-plugins": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", - "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "seroval": "^1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/timers-ext": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", - "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", - "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.30" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } + "name": "brunch-next", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "brunch-next", + "version": "0.0.0", + "dependencies": { + "@earendil-works/pi-coding-agent": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.4", + "@tanstack/react-query": "^5.100.11", + "@tanstack/react-router": "^1.170.6", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "ws": "^8.20.1", + "zod": "^4.4.3" + }, + "bin": { + "brunch-next": "bin/brunch.js" + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.14", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.10.0", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^6.0.2", + "better-sqlite3": "^12.8.0", + "drizzle-kit": "^0.18.1", + "drizzle-orm": "^0.45.2", + "drizzle-typebox": "^0.3.3", + "jsdom": "^29.1.1", + "oxfmt": "latest", + "oxlint": "latest", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vite": "^8.0.16", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1050.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1050.0.tgz", + "integrity": "sha512-KbQqWGSyXh1c0opFTEcwNu6PcGd/IRyTnihDh8fpdiVCu62/53469AN+Xe6cKSuM6W2oOBbY12Pbj3zrdRK5mA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/credential-provider-node": "^3.972.43", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.20", + "@aws-sdk/token-providers": "3.1050.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.12.tgz", + "integrity": "sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.38.tgz", + "integrity": "sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.40.tgz", + "integrity": "sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.42.tgz", + "integrity": "sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/credential-provider-env": "^3.972.38", + "@aws-sdk/credential-provider-http": "^3.972.40", + "@aws-sdk/credential-provider-login": "^3.972.42", + "@aws-sdk/credential-provider-process": "^3.972.38", + "@aws-sdk/credential-provider-sso": "^3.972.42", + "@aws-sdk/credential-provider-web-identity": "^3.972.42", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.42.tgz", + "integrity": "sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.43.tgz", + "integrity": "sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.38", + "@aws-sdk/credential-provider-http": "^3.972.40", + "@aws-sdk/credential-provider-ini": "^3.972.42", + "@aws-sdk/credential-provider-process": "^3.972.38", + "@aws-sdk/credential-provider-sso": "^3.972.42", + "@aws-sdk/credential-provider-web-identity": "^3.972.42", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.38.tgz", + "integrity": "sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.42.tgz", + "integrity": "sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/token-providers": "3.1049.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1049.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1049.0.tgz", + "integrity": "sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.42.tgz", + "integrity": "sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.20.tgz", + "integrity": "sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.10.tgz", + "integrity": "sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1050.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1050.0.tgz", + "integrity": "sha512-LVw+bW8LKWdus3U4v7Ojm5XmIXv1ZlQ3rsQrlkEt5fss+SsWfTTzVxoo8kl6ZCY5gl5kL8lPGluHPIDGR8bntQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.12", + "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@earendil-works/pi-agent-core": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", + "integrity": "sha512-azg09GSrckQa3ffbH09YEZC7DyHgmNSX+vmWEoEhQvp4icbzqbqLfIeMayMNEK/aGusm1SghZC4bPlDdagDALg==", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.75.3", + "ignore": "^7.0.5", + "typebox": "^1.1.24", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.3.tgz", + "integrity": "sha512-UKccS+ADlkSVJ49a00346jUfXmUi6zzzB+pPWotsyA6SxhKr2ejjkGQksGyR1DyNVrsEP/WWlsOSTUUwVlzNaA==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.91.1", + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", + "@google/genai": "^1.40.0", + "@mistralai/mistralai": "^2.2.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "openai": "6.26.0", + "partial-json": "^0.1.7", + "typebox": "^1.1.24" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.3.tgz", + "integrity": "sha512-LIi5/CdUBfcLp3BAtpLx1BfnHDLmDOQVdzYfS1H9fjjCw2dcPr9voSI5ncrhvZdgyFSnfHck4BCbNcfZk+TEHQ==", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", + "@silvia-odwyer/photon-node": "^0.3.4", + "chalk": "^5.5.0", + "cross-spawn": "^7.0.6", + "diff": "^8.0.2", + "glob": "^13.0.1", + "highlight.js": "^10.7.3", + "hosted-git-info": "^9.0.2", + "ignore": "^7.0.5", + "jiti": "^2.7.0", + "minimatch": "^10.2.3", + "proper-lockfile": "^4.1.2", + "typebox": "^1.1.24", + "undici": "^8.3.0", + "yaml": "^2.8.2" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.6" + } + }, + "node_modules/@earendil-works/pi-tui": { + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.4.tgz", + "integrity": "sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "koffi": "2.16.2" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + } + }, + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", + "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", + "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", + "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", + "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", + "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", + "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", + "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", + "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", + "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", + "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", + "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", + "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", + "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", + "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", + "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", + "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", + "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", + "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.53.0.tgz", + "integrity": "sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/darwin-arm64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.18.1.tgz", + "integrity": "sha512-FqDrcQJmEGNkgmZgI4wbCrGyJl1tiRZa3udxvyYaXag8W80A0zLFNCyWVvHIgUJ0DHlZjRc7O72xUGjiyvQrqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/darwin-x64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-0.18.1.tgz", + "integrity": "sha512-YUcyWBJdNuMcJxAwdV/i25/kvnKrVsA+vLn7SsL87cAwiD//rqGdOixk0r8sKUYa71Kx3h0Fg2ToUOjdE6ddYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/linux-arm64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-0.18.1.tgz", + "integrity": "sha512-ol3jhmUv5VI/omMrt6DkwY/jVTSVJlflFyU1SnSb/BuVVf3TyBiCHmZ4wVtcrcT5k3sWjrvYWw2kSozvmuE4tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-arm64-musl": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-0.18.1.tgz", + "integrity": "sha512-iKDj1ZwlU4KpXuIL1qkVP6NJzri2VSJreqXCIAe1Bf5RZXMAGSO3xjldgiX+HBvFOKSBIarLcqONYDbYco9uaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-0.18.1.tgz", + "integrity": "sha512-A3g+fZhlOivUdK7xU/IrbhBcMHig5GLrfMX0HYjXL1fiSqKYu9n1o1p42WpT6KfPL3L2uncSg/iyg7hspcN6qA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-musl": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-0.18.1.tgz", + "integrity": "sha512-LA02SdATWZEZBy8ZZpR2GlUbDg7+Jq1/WKkywMXqxdClkcoyyFozj8aQD2iTMKELSra4OSyqqZpOYroqjSSKmw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/win32-arm64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-0.18.1.tgz", + "integrity": "sha512-FNL+OxDflqLGXRgLxfBM/X4RnLYgtOKTsb1mNSqsjSCEfUi1Oqivh7KvZ09IfAMZeJ85/fL6EI6hSOyY7nNYUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/win32-x64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-0.18.1.tgz", + "integrity": "sha512-W+aVE9Siqs6Oe3NDaDOTTOYsN9X3znl+whfqWK1EcLpqJXX1kdB8Hf45HkGjqnHoFoP96GRgUnXQHQvmUybjvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.14", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.14.tgz", + "integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/history": { + "version": "1.162.0", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", + "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.11.tgz", + "integrity": "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.11.tgz", + "integrity": "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.170.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.6.tgz", + "integrity": "sha512-tGQkOjcMESBbfw+iz9zE/ivcuw4D2zdhW8PA4wMmpOyA2bLiqpc6bg5MnJPxamdXoO3GZBiHYOOoEwi5qxpPgA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.171.4", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.171.4", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.4.tgz", + "integrity": "sha512-LU9YuVdgaP+h4MEXRvokyjIEelulylgsromHMfYwVfgo1PF9oJP3NHyy7qtjxGLJq6zoZMCfoa1frDJlPo7I8g==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dreamopt": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", + "integrity": "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==", + "dev": true, + "dependencies": { + "wordwrap": ">=0.0.2" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/drizzle-kit": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.18.1.tgz", + "integrity": "sha512-Oqie227W2Dd7FuqX4pvQWeClSvnoPCIn2cO9JueeLWZqj3tpdBhnbgt4nLHhBbOdWRlTLYwXnkTDW3hYym/gGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "commander": "^9.4.1", + "esbuild": "^0.15.18", + "esbuild-register": "^3.4.2", + "glob": "^8.1.0", + "hanji": "^0.0.5", + "json-diff": "0.9.0", + "minimatch": "^7.4.3", + "zod": "^3.20.2" + }, + "bin": { + "drizzle-kit": "index.js" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/drizzle-kit/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/drizzle-kit/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/drizzle-kit/node_modules/glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/drizzle-kit/node_modules/minimatch": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/drizzle-kit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/drizzle-typebox": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/drizzle-typebox/-/drizzle-typebox-0.3.3.tgz", + "integrity": "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@sinclair/typebox": ">=0.34.8", + "drizzle-orm": ">=0.36.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hanji": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", + "integrity": "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash.throttle": "^4.1.1", + "sisteransi": "^1.0.5" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbot": { + "version": "5.1.40", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", + "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-diff": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.9.0.tgz", + "integrity": "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.0", + "difflib": "~0.2.1", + "dreamopt": "~0.8.0" + }, + "bin": { + "json-diff": "bin/json-diff.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/koffi": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/oxfmt": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.53.0.tgz", + "integrity": "sha512-9cB5glS3Ip6NMuZ+6NYTao9FCWkDhRtPYCtR3QBu/NxHoFbgzzTvi41N4jxz/GqGfuLKspui1qb/LlSu2IbMcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.53.0", + "@oxfmt/binding-android-arm64": "0.53.0", + "@oxfmt/binding-darwin-arm64": "0.53.0", + "@oxfmt/binding-darwin-x64": "0.53.0", + "@oxfmt/binding-freebsd-x64": "0.53.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.53.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.53.0", + "@oxfmt/binding-linux-arm64-gnu": "0.53.0", + "@oxfmt/binding-linux-arm64-musl": "0.53.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.53.0", + "@oxfmt/binding-linux-riscv64-musl": "0.53.0", + "@oxfmt/binding-linux-s390x-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-gnu": "0.53.0", + "@oxfmt/binding-linux-x64-musl": "0.53.0", + "@oxfmt/binding-openharmony-arm64": "0.53.0", + "@oxfmt/binding-win32-arm64-msvc": "0.53.0", + "@oxfmt/binding-win32-ia32-msvc": "0.53.0", + "@oxfmt/binding-win32-x64-msvc": "0.53.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxfmt/node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/oxlint": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-0.18.1.tgz", + "integrity": "sha512-JGcQvbhd00Qb+nq4f9sYYRh7mZIb0K/7rbMepNdJDMzo8pbmBpx1N2XOG61RjHDsNnY6ImAmVk3h4QVwFenwUQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxc_language_server": "bin/oxc_language_server", + "oxlint": "bin/oxlint" + }, + "engines": { + "node": ">=8.*" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/darwin-arm64": "0.18.1", + "@oxlint/darwin-x64": "0.18.1", + "@oxlint/linux-arm64-gnu": "0.18.1", + "@oxlint/linux-arm64-musl": "0.18.1", + "@oxlint/linux-x64-gnu": "0.18.1", + "@oxlint/linux-x64-musl": "0.18.1", + "@oxlint/win32-arm64": "0.18.1", + "@oxlint/win32-x64": "0.18.1" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/protobufjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/seroval": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } } diff --git a/package.json b/package.json index afdd8e9f4..abc545f68 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^6.0.2", "better-sqlite3": "^12.8.0", "drizzle-kit": "^0.18.1", "drizzle-orm": "^0.45.2", From 4834d1fdfd1bfe27867e67106ed95be097143fd8 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 11:15:42 +0200 Subject: [PATCH 09/34] prep for spec-persistence-and-startup batch 1 --- memory/CARDS.md | 175 +++++++++++++++++++++++++++++ memory/PLAN.md | 27 ++++- src/.pi/extensions/snapshot-cwd.ts | 16 +++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 memory/CARDS.md diff --git a/memory/CARDS.md b/memory/CARDS.md new file mode 100644 index 000000000..befaf59dd --- /dev/null +++ b/memory/CARDS.md @@ -0,0 +1,175 @@ + + +# Cards — `spec-persistence-and-startup` + +Frontier: **spec-persistence-and-startup** (PLAN.md). Branch: to create. +Two cards: an additive persistence foundation, then the integration that makes +startup correct end-to-end. Card 2 consumes Card 1's spec API; the model is +fully locked, so Card 2's shape does not depend on Card 1's build findings. + +Locked state-model decisions this queue implements (from the survey session): + +```yml +specs[] # lives: .brunch/data.db + id: integer # autoincrement; replaces 'spec-${uuid}' + name: string + slug: string + readiness_grade: enum # rename optional; elicitation_posture + commitment_focus RETIRED + +workspace # lives: .brunch/workspace.json (renamed from state.json) + project: { name, slug } # 'source' DROPPED (dead) + current: { specId:int, sessionId } + posture: { certainty, stakes, audience, horizon, migration, sourcing } # POC stub, empty strings + # activation, chrome: coordinator-derived at startup, NOT persisted + +binding # lives: Pi JSONL (brunch.session_binding) + { schemaVersion, specId:int } # sessionId + specTitle DROPPED; self-id guard removed +``` + +--- + +## Card 1 — Spec entity + DB-on-startup foundation [status: next] + +### Target Behavior + +A Brunch process opening a workspace creates `.brunch/data.db` when absent and can create, read, and grade-update a `specs` row exclusively through the `CommandExecutor`. + +### Boundary Crossings + +``` +→ createDb(path) (creates file if missing, idempotent) +→ db/schema.ts + initSchema DDL (new `specs` table) +→ graph/ CommandExecutor (createSpec / getSpec / updateReadinessGrade) +→ specs row persisted; integer id returned +``` + +### Risks and Assumptions + +``` +- RISK: should spec writes consume a graph LSN + change_log entry, or are they a + separate control-plane write? + → MITIGATION: default — route through CommandExecutor for the no-bypass invariant + and append a change_log entry for audit; reuse the single graph_clock LSN unless + the build surfaces a concrete reason to separate the clocks. Settle in build. +- RISK: graph nodes remain unscoped to a spec (no nodes.spec_id this slice). + → MITIGATION: out of scope — spec-row only; node↔spec scoping is a separate + decision, deliberately not opened here. +- ASSUMPTION: Drizzle + better-sqlite3 line is settled. + → IMPACT IF FALSE: low — already validated. + → VALIDATE: existing M4 A20-L spike. [→ memory/SPEC.md §Assumptions A20-L] +``` + +### Tracer-bullet check + +Scores on **uncertainty** (retires the spec-row persistence hole) and **proof of life** (first real `specs` row written to a real `.brunch/data.db` outside `:memory:` tests). Build it as-is. + +### Acceptance Criteria + +``` +✓ schema/initSchema — `specs` table created (Drizzle table def + CREATE IF NOT EXISTS DDL), + columns {id INTEGER PK AUTOINCREMENT, name, slug, readiness_grade}; NO elicitation_posture, + NO commitment_focus +✓ createSpec — writes a row through CommandExecutor, returns integer id +✓ getSpec — returns the row by integer id +✓ updateReadinessGrade — mutates grade through the command boundary; invalid grade rejected +✓ createDb on a non-existent path — creates the file; reopening the same path is idempotent +✓ no-bypass — a direct ORM write to `specs` outside CommandExecutor is caught by the + architectural boundary test (I26-L extension) +``` + +### Verification Approach + +``` +- Inner: vitest unit — CommandExecutor spec ops + createDb file-creation/idempotence +- Middle: architectural no-bypass test extended to `specs` +- Outer: n/a (boot integration is Card 2) +``` + +### Cross-cutting obligations + +``` +- All spec writes route through CommandExecutor (D16-L / D20-L no-bypass). +- SPEC reconciliation lands with this card (the model omits the dead fields): + retire the elicitation_posture half of D45-L; gut/retire D46-L; trim requirement #22; + simplify I31-L to grade-only. +``` + +--- + +## Card 2 — Coordinator startup on the corrected model [status: queued — after Card 1] + +### Target Behavior + +Brunch boots end-to-end against the DB-backed spec model: it reads the workspace pointer from `.brunch/workspace.json`, the spec from the DB, and the session→spec link from the collapsed binding, and reaches a correct activation state under every foreseeable startup condition. + +### Boundary Crossings + +``` +→ runBrunchCli → openDefaultWorkspace / activateWorkspace +→ .brunch/workspace.json (renamed, reshaped) read / write / first-run scaffold +→ .brunch/data.db (create-if-missing) + spec row create/read (Card 1 API) +→ brunch.session_binding {specId:int} append/read; spec name resolved from DB +→ WorkspaceSessionState returned; activation/chrome DERIVED, not stored +``` + +### Risks and Assumptions + +``` +- RISK: specId string→int flip touches workspace.json + binding + coordinator + many + test fixtures at once. + → MITIGATION: pre-release posture — regenerate fixtures; land as one coherent change + that keeps `npm run verify` green; no compat shim for old spec-${uuid} data. +- RISK: the spec picker/inventory now needs a DB read (new coupling; today it reads JSONL only). + → MITIGATION: thread the DB/CommandExecutor into inspectWorkspaceInventory; ensure the + DB is created before inventory runs. +- ASSUMPTION: Card 1's spec API shape is stable. + → IMPACT IF FALSE: rework limited to the coordinator call sites. +``` + +### Tracer-bullet check + +Scores on **proof of life** (lights up the real boot path: a live `.brunch/data.db` created and read by an actual `brunch` run) and **invariants** (stabilizes "init/startup correct under all conditions" — the frontier's north-star oracle). + +### Acceptance Criteria + +``` +startup condition matrix (each leaf = one assertion): +├── no .brunch/ → scaffolds workspace.json (empty posture) + data.db +├── workspace.json + data.db, valid → ready; spec read from DB by specId +├── workspace.json present, db missing → recreates data.db; reconciles current spec +├── current specId absent from DB → needs_human(reason) +├── current sessionId/file missing/stale → needs_human or new-session path +├── multiple specs → picker lists names resolved from DB +├── new-spec → writes specs row, ready +├── new-session-for-current-spec → appends binding {specId}, ready +├── cancel → cancelled +└── forked session (binding inherited) → still resolves spec linkage (no self-id reject) +shape assertions: +├── workspace.json has no `source`; persists {project, current:{specId:int,sessionId}, posture} +└── brunch.session_binding is {schemaVersion, specId:int} — no sessionId, no specTitle +``` + +### Verification Approach + +``` +- Inner: workspace.json read/write/scaffold unit tests; binding schema test +- Middle: startup-condition-matrix integration tests (NORTH STAR); coordinator↔DB; + picker-name-from-DB; no-bypass +- Outer: real `brunch --mode print` (and TUI boot) against a regenerated `.brunch/`, + proving a live `.brunch/data.db` is created and read +``` + +### Cross-cutting obligations + +``` +- Session identity stays Pi-owned; Brunch contributes only {specId}; binding is fork-portable. +- Rename .brunch/state.json → .brunch/workspace.json (constant + tests). +- SPEC reconciliation: delete prompt-assembly layer 7 (elicitation posture); flip the + SPEC §Vocabulary-evolution `posture` note spec-level → workspace-level (POC-stubbed). +- README updates: src/README.md (state.json → workspace.json), src/db/README.md (specs table), + src/session/README.md (binding collapse). +- Do NOT wire graph-agent tools here — this frontier owns DB lifecycle only. +``` diff --git a/memory/PLAN.md b/memory/PLAN.md index 900f1464c..60661ad48 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -51,7 +51,7 @@ The POC should maximize assumption falsification rather than merely implement mi ### Active -(none — `graph-data-plane` just completed; `agent-graph-integration` is next) +1. `spec-persistence-and-startup` — restore DB-on-startup + specs-in-DB, reshape `.brunch/workspace.json`, collapse the session binding; init/startup correct under all foreseeable conditions. Precedes the runtime portion of `agent-graph-integration`. ### Next @@ -75,6 +75,28 @@ The POC should maximize assumption falsification rather than merely implement mi ## Frontier Definitions +### spec-persistence-and-startup + +- **Name:** Spec persistence and startup integrity +- **Linear:** unassigned (create in FE / brunch when branch opens) +- **Branch:** to create — `ln/-spec-persistence-startup` +- **Kind:** structural (persistence-model correction + startup-path regression repair) +- **Status:** active +- **Objective:** Restore the DB-on-startup path lost in the rebuild and correct the workspace/spec/session persistence model so Brunch initializes and boots correctly under all foreseeable conditions, reading each fact from its canonical home. Concretely: **(a)** model specs as DB rows (`specs{id:int, name, slug, readiness_grade}`), retiring `elicitation_posture` and `commitment_focus`; **(b)** create `.brunch/data.db` if absent at startup and route spec create/read/grade-update through the `CommandExecutor`; **(c)** rename `.brunch/state.json` → `.brunch/workspace.json` and reshape to `{project:{name,slug}, current:{specId:int, sessionId}, posture:}`, dropping the dead `source` field; **(d)** collapse the `brunch.session_binding` entry to `{specId:int}`, dropping `sessionId`/`specTitle` and the `sessionId !== header.id` self-guard (fork-portable); **(e)** resolve spec names from the DB, not from JSONL or workspace state. +- **Why now / unlocks:** The rebuild branch regressed two capabilities the prior version had — DB-on-startup and specs-in-DB — because work optimized for green tests over working product runs (`createDb`/`new CommandExecutor` appear only in `:memory:` tests; the TUI shell builds extensions with no graph deps via `{ coordinator }`; no `.brunch/data.db` is ever created at runtime; mention-autocomplete reads `FIXTURE_GRAPH_MENTION_SOURCE`). This frontier repairs that and lands the spec row D45-L already assumes exists, unblocking the runtime portion of `agent-graph-integration` (the A14-L real-LLM proof needs the DB live at startup) and the deferred agent-runtime vocabulary work. Retires the spec-row persistence hole and the unscoped global graph namespace concern. +- **Acceptance:** + - `specs` table exists; spec create/get/grade-update route through `CommandExecutor`; `elicitation_posture` and `commitment_focus` are absent from the model. + - Startup creates `.brunch/data.db` if missing and opens it; spec creation writes a `specs` row with an integer id (no `spec-${uuid}` mint). + - `.brunch/workspace.json` (renamed from `state.json`) persists `{project:{name,slug}, current:{specId:int, sessionId}, posture}`; first run scaffolds `posture` with empty-string axes; no `source` field; `activation`/`chrome` are not persisted. + - `brunch.session_binding` is `{schemaVersion, specId:int}`; the `sessionId !== header.id` guard is gone; a forked session retains its spec linkage through the inherited binding. + - The spec picker/inventory resolves spec names from the DB by `specId`. + - Init/startup succeeds across the foreseeable condition matrix: no `.brunch/`; workspace.json + DB present; workspace.json present + DB missing; current spec/session missing or stale; multiple specs; new-spec; new-session-for-current-spec; cancel. +- **Verification:** Inner — verify gate plus spec-`CommandExecutor` unit tests, `workspace.json` read/write/scaffold tests, binding-schema tests. Middle — the startup-condition-matrix integration tests (the north-star oracle), coordinator↔DB integration, picker-name-from-DB, architectural no-bypass (spec writes only via `CommandExecutor`). Outer — a real `brunch` boot/`--mode print` run against a regenerated `.brunch/` proving a live `.brunch/data.db` is created and read. +- **Cross-cutting obligations:** All spec writes route through `CommandExecutor` (D16-L/D20-L no-bypass). Session identity stays Pi-owned — Brunch contributes only `{specId}`; keep runtime state linear-transcript-backed and fork-portable. Do **not** wire graph-agent tools here — this frontier owns the DB lifecycle; `agent-graph-integration` inherits it to register graph tools. **SPEC reconciliation required (with the build):** retire the `elicitation_posture` half of D45-L, gut/retire D46-L, trim requirement #22, simplify I31-L to grade-only, delete prompt-assembly layer 7, and flip the SPEC §Vocabulary-evolution `posture` note from spec-level → workspace-level (POC-stubbed). Update `src/README.md`, `src/db/README.md`, `src/session/README.md` migration notes. +- **Traceability:** R22, R28 / D16-L, D20-L, D40-L, D45-L (revise), D46-L (retire), D52-L / I31-L (revise) / A19-L, A20-L. Retires: spec-row persistence hole; DB-on-startup regression. +- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (posture deferral note, §Vocabulary evolution); this session's locked state-model decisions. +- **Current execution pointer:** card queue → `memory/CARDS.md` (slice 1: spec entity + DB-on-startup foundation; slice 2: coordinator startup on the corrected model). + ### sealed-pi-profile-runtime-state - **Name:** Sealed Pi profile, transcript-backed runtime state, and graph-model prep (M4 prep envelope) @@ -297,6 +319,7 @@ Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jso nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) + spec-persistence-and-startup [active] (persistence-model fix + startup regression repair) agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] authority-model [not-started] (M6) @@ -308,6 +331,8 @@ nodes: edges: sealed-pi-profile-runtime-state -[hard]-> graph-data-plane graph-data-plane -[hard]-> agent-graph-integration + graph-data-plane -[hard]-> spec-persistence-and-startup + spec-persistence-and-startup -[hard]-> agent-graph-integration agent-graph-integration -[hard]-> authority-model agent-graph-integration -[hard]-> turn-boundary-reconciliation agent-graph-integration -[optional]-> subagents-for-proposal-diversity diff --git a/src/.pi/extensions/snapshot-cwd.ts b/src/.pi/extensions/snapshot-cwd.ts index e69de29bb..8c60e55f1 100644 --- a/src/.pi/extensions/snapshot-cwd.ts +++ b/src/.pi/extensions/snapshot-cwd.ts @@ -0,0 +1,16 @@ +/** @file snapshot-cwd.ts + * + * CONCEPT of this and other snapshot-* extensions: + * + * ...for initial framing or situations we could have the assistant always run a deterministic tool that does a bit of a scan of the workspace. For example to see if there are any existing sessions. Let's assume the tool is going to create a.branch folder when it starts up, but it may not. Maybe the check is: does a.branch folder exist? Are there sessions in the sessions subfolder? + * + * Even should we do a preliminary scan of those sessions or at least evaluate initially how long they are? We could count the lines in those sessions and get a sense of, "Oh there are sessions here with significant content or not." That would already tell you something. + * + * Another thing might be a quick scan of things like README files. Again not for the content but for the length of the files. That could be done deterministically and could be injected as a very quick kind of initial context injection to say, "Okay in the project where I've been launched there is or isn't pre-existing branch work. There is or isn't pre-existing documentation work of some kind." + * + * Initial signals could be just any markdown files gathered out of the space fed to the LLM as a table with a quick scan of how many lines they are or maybe just how many bytes they are. Maybe we don't even count the lines. Something to give a heuristic for either there is substantial documentation or not and otherwise what other kinds of top-level heuristics? Count the number of files that should be a fairly quick run so basically it's like a kick-off heuristic snapshot tool. + * + * So, to run with that idea for the moment, I guess there's a series of tools for snapshotting. Snapshot CWD could be the one that is used for that heuristic, and I suppose it could be invocable at different times, discretionally, or it can be automatically invoked following certain deterministic heuristics like: + * Whether this is the first session in a new specification. In that case, this snapshot will be necessary. Otherwise, other snapshotting tools are going to be Snapshot Graph and Snapshot Nodes at least. The Snapshot Nodes thing is maybe kind of a flexible way to get a range of different snapshots from one single node with only its direct dependencies, or two or more nodes, and/or a variable number of hops to build up a neighborhood + * + */ From 210a31d8c6e6d9e1bfe536857779c36fea90674b Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 11:23:05 +0200 Subject: [PATCH 10/34] Add spec persistence command boundary --- memory/CARDS.md | 4 +- memory/SPEC.md | 40 ++++---- src/db/README.md | 12 ++- src/db/connection.test.ts | 30 ++++++ src/db/connection.ts | 7 ++ src/db/row-schemas.ts | 6 +- src/db/schema.ts | 14 +++ src/graph/architecture.test.ts | 15 +++ src/graph/command-executor.test.ts | 70 ++++++++++++- src/graph/command-executor.ts | 157 +++++++++++++++++++++++++++++ src/graph/index.ts | 9 ++ 11 files changed, 335 insertions(+), 29 deletions(-) create mode 100644 src/db/connection.test.ts diff --git a/memory/CARDS.md b/memory/CARDS.md index befaf59dd..b46c13596 100644 --- a/memory/CARDS.md +++ b/memory/CARDS.md @@ -31,7 +31,7 @@ binding # lives: Pi JSONL (brunch.session_binding) --- -## Card 1 — Spec entity + DB-on-startup foundation [status: next] +## Card 1 — Spec entity + DB-on-startup foundation [status: done] ### Target Behavior @@ -99,7 +99,7 @@ Scores on **uncertainty** (retires the spec-row persistence hole) and **proof of --- -## Card 2 — Coordinator startup on the corrected model [status: queued — after Card 1] +## Card 2 — Coordinator startup on the corrected model [status: next] ### Target Behavior diff --git a/memory/SPEC.md b/memory/SPEC.md index da873b9c2..182060a92 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -78,7 +78,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c 19. Brunch must enforce a workspace state hierarchy `workspace(cwd) → spec → session`, where the workspace is only the current working directory invocation root, the user explicitly picks or creates one spec within that workspace before any agent loop runs, and then picks or creates a session within that spec. Spec selection persists across `/new`, and each session binds to exactly one spec. 20. Brunch must support multiple elicitation lenses within the `elicitor` agent role, with the agent owning lens selection and offer through transcript-native establishment offers; lens metadata is carried on elicitor-emitted custom entries for downstream routing. 21. Brunch must distinguish single-exchange elicitation flows from batch-proposal/review-set flows by capture and commitment mechanism: single-exchange answers are captured synchronously by the elicitor at turn boundaries, while batch proposals carry structured entity-draft payloads and are committed only through review-set approval. -22. Brunch must maintain spec-owned readiness grade and elicitation posture as forward gates inside the `elicit` operational mode. Grounding establishes the frame required for main elicitation; later grades unlock commitment and planning/export/execute posture without forbidding earlier gathering or refinement. +22. Brunch must maintain a spec-owned readiness grade as a forward gate inside the `elicit` operational mode. Grounding establishes the frame required for main elicitation; later grades unlock commitment and planning/export/execute posture without forbidding earlier gathering or refinement. 23. Brunch must support a review-cycle acceptance pattern for batch proposals and commitment review sets — approve / request changes (triggering regeneration) / reject — with batch acceptance committed atomically as one CommandExecutor call; partial acceptance is not representable. 28. Brunch must support assistant-first elicitation session driving over the public JSON-RPC surface: after workspace/spec/session activation, a client can start or resume elicitation, observe the current pending system/assistant-originated structured exchange, submit a response through Brunch product methods, and let Brunch advance the transcript-backed loop without ambient user prompt injection. @@ -117,7 +117,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A19-L | Pi's current settings/resource lifecycle can be made product-safe through a sealed Brunch Pi Profile without forking Pi: ambient discovery remains disabled, Brunch-owned extension factories may inject explicit resources, and remaining settings/keybinding leakage can be eliminated through programmatic policy or a narrow upstream seam. | medium | open | D39-L | FE-744/profile audit: source-backed resource-loader/settings audit, tests proving no ambient `.pi/` skills/prompts/themes/extensions/context files affect Brunch, and product-owned resources still load when intentionally injected. | | A20-L | The chosen Drizzle line and row-schema derivation path can be settled during the prep envelope without forcing later M4 rework: Brunch can prove migrations, SQLite fidelity, monotonic counter allocation, change-log writes, and runtime-schema derivation on one representative persistence slice before CRUD proper starts. | high | **validated** | D16-L, D41-L | **Validated by A20-L spike (2026-06-01).** Stack: `drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` + `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14`. Proved: (1) `drizzle-typebox` derives valid TypeBox insert/select schemas from Drizzle tables; `Value.Check` validates/rejects correctly. (2) Batch `commitGraph`-shaped transaction (multi-node → intra-batch ref resolution → multi-edge → LSN allocation → change-log append) works atomically; full rollback on FK violation or domain-validation throw. (3) `update().returning()` works for atomic monotonic counter increment; `insert().returning()` gives auto-increment IDs for ref resolution; JSON detail column round-trips cleanly. (4) Pi tool parameters (`typebox` v1.x) and Drizzle row schemas (`@sinclair/typebox` v0.34 via `drizzle-typebox`) serve different roles and never cross — shared enum `const` arrays bridge both. | | A21-L | The POC can treat coherence as a bounded product verdict over structural legality plus explicitly detected contradictions, gaps, and unresolved reconciliation needs, without solving a general theory of “spec coherence.” | low | open | D8-L | M8 must sharpen the coherence rubric before implementation: known-bad adversarial briefs should show what counts as incoherent, what is merely immature/underspecified, and what should become a reconciliation need. | -| A22-L | The elicitor can perform synchronous post-exchange capture well enough for the POC: high-confidence extractive facts and readiness/posture updates can be committed immediately, while low-confidence implications can be kept out of graph truth and used as disambiguation material. | medium | open | D18-L, D26-L, D45-L, I30-L | M5 agent-graph-integration fixtures and review: compare elicitor-captured graph updates against transcript evidence; track over-capture, missed obvious facts, and whether preface-led disambiguation resolves low-confidence material without an async observer owning primary extraction. | +| A22-L | The elicitor can perform synchronous post-exchange capture well enough for the POC: high-confidence extractive facts and readiness-grade updates can be committed immediately, while low-confidence implications can be kept out of graph truth and used as disambiguation material. | medium | open | D18-L, D26-L, D45-L, I30-L | M5 agent-graph-integration fixtures and review: compare elicitor-captured graph updates against transcript evidence; track over-capture, missed obvious facts, and whether preface-led disambiguation resolves low-confidence material without an async observer owning primary extraction. | ### Active Decisions @@ -140,7 +140,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D54-L — Graph node shape is a common flat interface with `title`, `body`, `basis`, `source`, and a per-kind `detail` JSON column; canonical contract is [`docs/design/GRAPH_MODEL.md` §GraphNode](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#graphnode--the-single-shape).** All planes and kinds share one `nodes` table. `plane` determines which closed `kind` enum applies; `kind` is structurally validated. `basis ∈ explicit | accepted_review_set` (same semantics as edges). `source` is a free-form string for epistemic attribution (e.g. "stakeholder", "regulatory", "derived") — convention by prompt, not structural validation; it exists for context-snapshot enrichment and will be rendered back into sparse text, not used for policy or filtering. `detail` is an optional JSON column with per-kind validated sub-structures: `decision` requires `{ chosen_option, rejected, rationale }`, `term` requires `{ definition, aliases? }`; all other kinds must omit `detail`. `provenance` is retired from the node shape — `change_log` at `createdAtLsn` owns all audit trail. The intent kind rubric (modality of claim + source question per kind) is agent-facing prompting guidance in GRAPH_MODEL.md §"Prompting guidance for kind discrimination", not structural enforcement. Depends on: D4-L, D16-L, D52-L, D56-L. Supersedes: D7-L (`framing_as` modality), the deferred Phase 2 node placeholder in prior GRAPH_MODEL.md. - **D55-L — `provenance` retired from both edges and nodes; `change_log` owns all audit trail.** Transcript entry pointers (`sessionId`, `entryId`, `proposalEntryId`) are fragile under compaction and redundant with `change_log` + `basis`. `basis` tells you the authority path; `change_log[createdAtLsn]` tells you the durable audit context. Edges retain `basis` and `rationale`. Nodes have `basis` and `source` (epistemic attribution). Depends on: D16-L, D51-L, D54-L. Supersedes: `EdgeProvenance` from Phase 1 edge lock, the planned node-side `provenance` symmetry with edges. - **D56-L — Intent node kinds: 11 kinds in 3 derived categories (basic / structural / reasoning); canonical contract is [`docs/design/GRAPH_MODEL.md` §Per-plane node kinds](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#per-plane-node-kinds).** `basic` (goal, thesis, term, context) carries grounding material; `structural` (requirement, assumption, constraint, invariant) carries core specification; `reasoning` (decision, criterion, example) carries decisions and evidence. Category is a pure function of `kind` — not stored on the node. The `basic` category maps to spec-grade progression: grounding-gate readiness depends on satisficing threshold of basic-category nodes (D57-L). `thesis` carries "what/who/why/for whom" material (La Carte Blanche style). `term` carries canonical naming commitments (ubiquitous language). `invariant` is first-class (not a constraint subtype) because its operational role differs: invariants get `dependency` and `proof` edges, constraints get `boundary` edges. Each intent kind has a modality-of-claim and source-question rubric for agent prompting (GRAPH_MODEL.md §"Prompting guidance"). Oracle (check, validation_method, evidence, obligation), design (module, interface), and plan (milestone, frontier, slice) kinds are stable from worked examples. Depends on: D54-L. Supersedes: D7-L (`framing_as`), A7-L. -- **D57-L — Spec-grade grounding gate is LLM-judged satisficiency with a count floor on basic-category nodes.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) but cannot declare grounding complete with zero `basic`-category nodes. Grounding elicitation interweaves basic intent nodes with spec-level posture establishment. `posture` is a spec-level property set, not a graph node kind. Depends on: D45-L, D56-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. +- **D57-L — Spec-grade grounding gate is LLM-judged satisficiency with a count floor on basic-category nodes.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) but cannot declare grounding complete with zero `basic`-category nodes. Grounding elicitation may establish workspace posture, but posture is not a spec-row field or graph node kind in the POC. Depends on: D45-L, D56-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. - **D51-L — Graph edge model is a closed structural-category set with a separate ReconciliationNeed substrate; canonical contract is [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md).** Every accepted edge is one of eight closed categories (`dependency`, `proof`, `support`, `realization`, `boundary`, `composition`, `association`, `supersession`); `stance: for | against` is valid only on `proof` and `support`; `basis ∈ explicit | accepted_review_set` (no `inferred`). Accepted edges have no mutable `status` field — `proposed` lives in review-set drafts, `rejected` is absent + change-log audit, `stale` is represented by a `ReconciliationNeed`. Identity fields (`category`, `sourceId`, `targetId`, `stance`) are immutable on an accepted edge; a "category change" is delete + recreate. Only `dependency` cascades automatically; other categories surface advisory recon-needs rather than auto-blocking. Cross-plane edges are unrestricted at the POC stage; `realization` subtypes (implementation/establishment/assertion/etc.) may be derived from node-tuple lookup later rather than encoded on the edge. `ReconciliationNeed` is a separate substrate whose target is exactly `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` — it is not itself a graph edge. Depends on: D4-L, D8-L, D16-L, D27-L, A14-L. Supersedes: the named-relation catalogue in `docs/architecture/pi-seam-extensions.md` §"Edge types" (`validates`, `instance_of`, `produces`, `discharges`, `depends_on`, `derived_from`, `counterexample_for`, `witnesses`), the per-relation policy registry / lookup, the brainstormed expanded edge taxonomy in `archive/docs/design/GRAPH_EDGE_CATEGORIES.md`, and any `concerns`-edge wiring from reconciliation needs to graph nodes. #### Authority & mutation @@ -206,7 +206,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D6-L — JSONL-first transcript persistence in `.brunch/sessions/`; SQLite-backed graph persistence in `.brunch/`.** Two durability surfaces with distinct responsibilities. Transcript starts on pi `SessionManager` redirected to the project-local directory; graph plane is SQLite from M4. Brunch does not recreate canonical `chat` or `turn` tables while Pi JSONL remains viable for Brunch-supported linear sessions. Validated by M2. Supersedes: —. - **D15-L — Side tasks are a first-class Brunch subsystem delivered through the same transcript/event substrate.** Side tasks are main-agent-invoked, non-blocking work items: the main agent fires them and continues without awaiting a return value. A Brunch-owned `SideTaskRegistry` tracks status; the only path a side task influences the main agent is by appending a custom-message status update to the session log that arrives at the next-turn boundary through the existing `prepareNextTurn` path — never mid-turn. Side-task writes remain subject to the same command-layer authority as primary-agent writes. This is distinct from D44-L Subagent (main-agent-invoked **blocking** tool call whose result is returned directly as tool content). Depends on: A11-L, D4-L. Supersedes: —. - **D16-L — Graph persistence uses Drizzle over `better-sqlite3`, with one-LSN-per-commit and no bypass paths.** The command layer owns precondition checks, structural validation, entity writes, LSN allocation, change-log append, and any coherence updates inside one transaction. This rule applies equally to migrations and maintenance code; there is no privileged write path outside the command-executor protocol. Runtime row/insert/update schemas are derived from Drizzle table definitions through `drizzle-typebox` (`createInsertSchema`, `createSelectSchema`) rather than hand-authored alongside the table. **Settled by A20-L spike (2026-06-01):** `drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` + `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14`. Pi tool parameter schemas use `typebox` v1.x (Pi's package) separately; Drizzle-derived row schemas stay internal to `db/`→`graph/`; shared enum `const` arrays bridge both. Depends on: A3-L, A4-L, A20-L (validated). Refined by: D41-L. Supersedes: —. -- **D18-L — Post-exchange capture is synchronous elicitor work for the POC; observer/auditor queues are deferred backstops, not primary extraction authority.** After a user response closes an elicitation exchange, the elicitor may run a post-exchange capture step in the same turn-boundary flow: commit high-confidence extractive facts, concrete reconciliation needs, and spec readiness/posture updates through the `CommandExecutor`; fold low-confidence implications into later questions rather than graph truth. Brunch may still introduce durable observer/auditor jobs keyed by session id plus exchange entry ids for restartable audit, quality checks, or later backfill, but those jobs are not the load-bearing path for keeping the next turn's world fresh. Any async job writes still route through the command layer and remain operational queue state unless they surface semantic work as reconciliation needs. Depends on: A13-L, A22-L, D4-L, D13-L, D16-L. Supersedes: the old DB-backed `chat` / `turn` mental model and the earlier observer-owned primary extraction path. +- **D18-L — Post-exchange capture is synchronous elicitor work for the POC; observer/auditor queues are deferred backstops, not primary extraction authority.** After a user response closes an elicitation exchange, the elicitor may run a post-exchange capture step in the same turn-boundary flow: commit high-confidence extractive facts, concrete reconciliation needs, and justified spec-readiness updates through the `CommandExecutor`; fold low-confidence implications into later questions rather than graph truth. Brunch may still introduce durable observer/auditor jobs keyed by session id plus exchange entry ids for restartable audit, quality checks, or later backfill, but those jobs are not the load-bearing path for keeping the next turn's world fresh. Any async job writes still route through the command layer and remain operational queue state unless they surface semantic work as reconciliation needs. Depends on: A13-L, A22-L, D4-L, D13-L, D16-L. Supersedes: the old DB-backed `chat` / `turn` mental model and the earlier observer-owned primary extraction path. - **D28-L — Regenerated review-set proposals are appended as successor entries in the linear Pi JSONL session; projection helpers filter to the accepted set for context economy.** When the user requests changes, the agent appends a successor proposal entry that references its predecessor via `supersedes`; prior proposals are *not* deleted from JSONL but remain visible as raw transcript history. This stays within Brunch's linear transcript policy — no Pi branching is created. Pi JSONL is treated as a "capture everything" store for replay and audit. Projection helpers used to drive the agent (context injection, summarization) walk the `supersedes` chain and surface only the latest (or ultimately accepted) proposal — the agent does not re-process every superseded proposal as live context. The reviewer likewise sees only the accepted set, not the regeneration history. Depends on: D6-L, D12-L, D17-L, D24-L, D27-L. Supersedes: any "in-place edit" or "fork-on-regenerate" mental model. - **D29-L — Reviewer is an async advisory role with narrow write authority.** After a batch acceptance closes, Brunch may enqueue a reviewer job keyed by session id plus the batch-acceptance entry id; the job survives process restart and analyzes the accepted batch plus its graph neighborhood for coherence, completeness, and gaps. **Reviewer writes only `reconciliation_need` records via the `CommandExecutor`**; it never writes graph entities, edges, change-log entries directly, or any other record class. Findings reach the user through next-turn delivery as advisory items on the reconciliation-need surface — the batch acceptance remains the user's atomic commitment and the reviewer cannot amend it. (Suggestion-shaped findings may later route to candidate-artefacts when that substrate exists; the POC routes everything to reconciliation needs.) Depends on: A16-L, D4-L, D8-L, D15-L, D17-L, D18-L, D20-L, D27-L. Supersedes: any "reviewer may quietly amend the graph" mental model. - **D24-L — Brunch POC enforces a linear transcript policy over Pi JSONL.** Pi's session tree is a substrate capability, not a Brunch product surface. Until branch-aware continuity/coherence is explicitly designed, Brunch-controlled interactive/runtime flows block `/tree`, `/fork`, and `/clone` through the thinnest available Pi hooks; transcript readers reject non-linear session files instead of flattening, adapting, migrating, or selecting a branch. This is intentional fail-fast pre-release posture: avoid compatibility debt with Pi internals or earlier Brunch revisions, and keep wrapper/adapter layers minimal. Depends on: D6-L, D11-L, D13-L. Supersedes: treating active-branch projection as Brunch product semantics. @@ -231,8 +231,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. - **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** `brunch.establishment_offer` records the agent's current offer tree and recommended next move as durable transcript state. Ambient chrome or web affordances may render the latest offer, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. -- **D45-L — Spec readiness is stored as grade/posture fields, not as session-local phase or workflow location.** The spec row owns two semi-independent control fields: `readiness_grade = grounding_onboarding | elicitation_ready | commitments_ready | planning_ready` and `elicitation_posture = gathering | refining | pinning`. Grade is a forward gate: it unlocks later strategies, commitment review sets, and eventual export/plan/execute operational modes, but it never forbids returning to earlier gathering/refinement when new ambiguity appears. Posture is the current dominant stance inside `elicit`. An optional `commitment_focus = design | oracle` may be added only if active review-set state and missing-commitment analysis cannot make the focus obvious; it is not required as canonical state now. Grade/posture changes route through `CommandExecutor`, carry provenance/rationale in the change log (and/or spec row metadata when M4 schema lands), and use hybrid transition authority: elicitor may advance low-risk gates with evidence, validators enforce hard prerequisites where known, and user-visible confirmation is required before entering commitment pinning. Depends on: D18-L, D20-L, D30-L. Supersedes: treating “phase” as a user-facing location/stepper or hidden session memory. -- **D46-L — Commitment posture pins projected claims through cohesive review sets.** Design and oracle lenses may create accepted graph material before commitment posture, but pinning is a separate projection step. In `pinning` posture, design-oriented commitments default first: Brunch projects requirement/invariant-like intent claims from the current intent/design/oracle graph plus support/provenance edges. Oracle-oriented commitments default second: Brunch projects criterion/check-obligation/example-like verification claims plus support/provenance edges to the pinned commitments and oracle material. Review sets are focus-primary rather than globally homogeneous: a design commitment set primarily pins requirement/invariant-like claims with support edges; an oracle commitment set primarily pins criteria/check/example-like claims with support edges. Approval accepts the cohesive batch as a whole through `acceptReviewSet`; request-changes regenerates a successor set; partial approval and accept-with-edits remain unrepresentable. Depends on: D27-L, D28-L, D45-L. Supersedes: per-item requirement/criterion confirmation and treating design/oracle commitment phases as first permission to discuss design/oracle topics. +- **D45-L — Spec readiness is stored as a DB-row grade, not as session-local phase, workflow location, or elicitation posture.** The `specs` row owns `readiness_grade = grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`; it also owns identity fields `id`, `name`, and `slug`. Grade is a forward gate: it unlocks later strategies, commitment review sets, and eventual export/plan/execute operational modes, but it never forbids returning to earlier gathering/refinement when new ambiguity appears. `elicitation_posture` and `commitment_focus` are retired as spec fields; active review-set state, strategy/lens selection, and workspace posture should carry those concerns when they become concrete. Grade changes route through `CommandExecutor` and carry audit context in the change log. Depends on: D18-L, D20-L, D30-L. Supersedes: treating “phase” as a user-facing location/stepper or hidden session memory, storing `elicitation_posture`, or adding `commitment_focus` as canonical spec state. +- **D46-L — Retired: commitment posture as persisted spec state.** Design and oracle lenses may still create accepted graph material, and cohesive review sets still commit atomically through `acceptReviewSet` per D27-L, but Brunch no longer models `pinning` or `commitment_focus` as spec-row state. Future commitment projection should derive from readiness grade, active strategy/lens/review-set state, and graph evidence rather than a persisted posture enum. Depends on: D27-L, D28-L, D45-L. Supersedes: per-item requirement/criterion confirmation, treating design/oracle commitment phases as first permission to discuss design/oracle topics, and storing commitment posture/focus on the spec. - **D47-L — Structured-exchange `preface` is the near-term carrier for non-committed elicitor interpretation.** The structured-exchange payload's plain prose `preface` summarizes working context before the next question: exploratory file-reading/tool-use findings, implied graph candidates, low-confidence edges, and the rationale for what is being asked next. Preface text is transcript truth and user-visible orientation, but it is not graph truth, not candidate-artefact schema, and not a hidden side store. High-confidence facts still commit through `CommandExecutor`; low-confidence implications stay in preface/question material until clarified, accepted, or escalated to reconciliation needs. Future `capture_*` analysis entries provide a separate post-exchange/review evidence surface for candidate semantic changes; they do not replace preface as next-question orientation and do not become graph truth. Structured candidate metadata is deferred until fixtures/projections prove plain prose is insufficient. Depends on: D12-L, D18-L, D37-L, D50-L. Supersedes: inventing a candidate-artefact substrate merely to carry ordinary next-question disambiguation material. - **D50-L — `capture_*` tools persist transcript-native ANALYSIS, not graph mutations.** Brunch may add a third structured-exchange tool family such as `capture_analysis` alongside `present_*` and `request_*`. A `capture_*` tool returns a normal persisted Pi `toolResult` with Brunch details and markdown content describing likely graph/node/edge changes, grouped into high-confidence candidates that could be committed later and low-confidence candidates that should drive clarification. `capture_*` output is transcript-visible evidence for Markdown/ASCII review and later graph-mutation cross-checking, but it is not graph truth and never bypasses the `CommandExecutor`. Product UI should hide capture analysis entirely if Pi exposes a supported hide seam; otherwise `renderResult` should be maximally collapsed/minimal while preserving full persisted `toolResult.content`/`details` for transcript renderers. The current schema layer deliberately defines only minimum capture details (`schema`, `v`, `exchange_id`, `tool_meta`) and rejects graph payloads; richer analysis payloads and shared component subparts (`Preface`, prompt body, option list, answer summary, capture analysis) require a later `ln-design` pass before implementation. Depends on: D12-L, D17-L, D18-L, D37-L, D41-L, D47-L. Supersedes: using ad hoc hidden custom entries, probe-only side files, or graph writes as the first carrier for pre-graph analysis. - **D44-L — Subagents are main-agent-invoked, blocking Pi tool calls that gather data and propose variants for candidate-proposal generation.** Brunch may register a single `subagent` Pi tool whose parameters are `{ agent, task }` or `{ tasks: [] }` (parallel). Each invocation runs as an isolated `pi --mode json -p --no-session --no-skills --no-extensions` subprocess inheriting Brunch's sealed Pi Profile (D39-L); the subagent has no inherited conversation context so the task string must carry everything it needs. Agent definitions are declarative markdown files under `src/.pi/extensions/subagents/agents/*.md` with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. Concurrency cap lives in an externalized [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json) (default 4) so it can be reviewed and updated without SPEC churn. The subagent's result text is returned directly to the main agent as tool result content; subagents do not append custom messages to the session log on their own behalf, do not invoke the `CommandExecutor`, and do not gain access to the parent's Brunch RPC handlers. POC starter agents split into two families: @@ -262,7 +262,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I14-L | If Brunch introduces deferred observer/auditor jobs, they are keyed by session id plus elicitation-exchange entry-range ids and have durable status; replay/restart cannot enqueue duplicate jobs for the same exchange, and job writes never become the primary freshness path for the next elicitor turn. | deferred/planned only if observer-audit queue lands (M5+ restart/idempotence tests) | D18-L, D4-L | | I15-L | Every review-set acceptance routes through `CommandExecutor` as one atomic `acceptReviewSet` command producing one LSN, one change-log entry, and one transaction over the entire batch. Partial acceptance is not representable through any product API. | planned (M5+ batch-acceptance command tests; review-set fixture parity) | D20-L, D27-L; I1-L, I11-L | | I16-L | Reviewer-attributed writes target only the `reconciliation_need` substrate; no reviewer-attributed `CommandExecutor` call writes graph entities, edges, change-log entries directly, or any other record class. | planned (M5+ architectural test on reviewer command writers; reviewer-attributed command-result audit) | D29-L; I2-L, I11-L | -| I17-L | Every batch-proposal or commitment review-set entry (`brunch.review_set_proposal`) declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | planned (M5+ proposal-entry schema test; fixture asserts status under thin and rich grounding) | D30-L, D46-L; A14-L | +| I17-L | Every batch-proposal or review-set entry (`brunch.review_set_proposal`) declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | planned (M5+ proposal-entry schema test; fixture asserts status under thin and rich grounding) | D30-L, D46-L; A14-L | | I18-L | Every elicitor-emitted prompt or proposal custom entry (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) carries a `lens` field; capture, reviewer, and future observer/auditor routing filters on this field. | planned (M5+ capture/reviewer routing tests; transcript-shape contract test) | D25-L, D26-L, D29-L | | I19-L | Brunch-controlled flows do not create or navigate Pi session branches, and Brunch transcript readers fail fast on non-linear JSONL rather than flattening, migrating, or branch-selecting. | partially covered (M3 transcript loader requires exactly one Pi session header, rejects malformed non-header entry shapes, and rejects non-linear child graphs, `parentSession`, and `branch_summary`; product-facing exchange projection helper preserves the non-linear error discriminant and is used by RPC and fixture replay assertions; `session.elicitationExchanges` returns a product-shaped error for non-linear selected sessions over stdio and WebSocket JSON-RPC; Brunch TUI extension cancels `session_before_tree` and `session_before_fork`; Pi command-containment source/RPC evidence shows `session_before_fork` can also cancel clone/fork effects but exact interactive built-ins still need product-shell policy if visibility must be strict; dynamic chrome remains projection-only and does not add branch or mutation authority) | D24-L, D6-L, D11-L, D13-L, D34-L, D35-L | | I20-L | Every user-reviewable review-set proposal has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface as reviewable review sets. | planned (M5+ proposal-validation contract + differential tests) | D27-L; A14-L | @@ -275,8 +275,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | | I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | | I29-L | Subagent subprocesses inherit Brunch Pi Profile sealing: every `subagent` tool invocation spawns `pi --mode json -p --no-session --no-skills --no-extensions` with an explicit per-agent tool allowlist and per-agent model; subagents never load ambient user/project `.pi/` skills, prompts, themes, extensions, context files, or behavior-shaping settings; subagents never gain direct access to the parent's `CommandExecutor`, Brunch RPC handlers, or graph persistence; subagent results return to the main agent only as tool result content (no side-effect transcript writes). | planned (subagent subprocess argv tests; isolation audit asserting absent ambient-resource leakage; tool-allowlist conformance test per starter agent) | D2-L, D39-L, D44-L; I2-L, I11-L, I24-L | -| I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec grade/posture updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | planned (M5 capture fixtures comparing committed graph facts and preface-only interpretations against transcript evidence) | D18-L, D47-L; A22-L | -| I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade/posture mutations route through `CommandExecutor` and carry provenance. | planned (M4 schema/command tests for spec row updates; M5 prompt/tool-policy tests for grade-gated availability) | D20-L, D45-L | +| I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec readiness-grade updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | planned (M5 capture fixtures comparing committed graph facts and preface-only interpretations against transcript evidence) | D18-L, D47-L; A22-L | +| I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade mutations route through `CommandExecutor` and carry audit through the change log. | partial (Card 1: specs table plus `createSpec` / `getSpec` / `updateReadinessGrade` command tests; M5 prompt/tool-policy tests for grade-gated availability remain) | D20-L, D45-L | | I32-L | Public RPC structured-exchange driving never requires a client to speak raw Pi RPC: after Brunch method discovery and workspace/spec/session activation, each pending assistant-originated exchange is answered exactly once through `elicitation.respond`, and the deterministic permutation run produces linear Pi JSONL whose transcript display and elicitation-exchange projections preserve the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity (`rpc.discover` contract tests, pending/respond lifecycle tests, current public-RPC structured-exchange permutations, terminal non-answered status handling, option content/rationale parity, no repeated deterministic prompts, and transcript/projection oracle in `src/probes/public-rpc-parity-proof.ts`) | R11, R16, R17, R24, R27, R28; D5-L, D12-L, D37-L, D48-L, D49-L | | I33-L | `capture_*` analysis entries are transcript evidence only: they persist as Brunch structured-exchange `toolResult` rows, are included by Brunch-semantic transcript renderers, are hidden or collapsed in TUI display, and never mutate graph truth or bypass `CommandExecutor`. | partially covered (minimum capture details schemas parse/export and reject graph payload fields; future runtime capture-analysis schema/rendering tests plus transcript renderer fixtures still need to prove persisted result rendering and TUI hide/collapse behavior; later graph-capture fixtures compare analysis candidates against committed graph mutations) | D17-L, D18-L, D37-L, D47-L, D50-L; I2-L, I11-L, I23-L, I30-L | | I34-L | `commitGraph` batch validation is all-or-nothing: if any node or edge in the batch is structurally illegal, the entire batch is rejected and no partial state is persisted; the agent receives diagnostics sufficient for bounded self-correction retry. | covered (22 tests in `command-executor.test.ts` — edge failure rolls back nodes, mixed-batch rejection, diagnostic sufficiency) | D53-L; I1-L, I11-L | @@ -306,20 +306,20 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/.pi/context/` layout migrates to this structure. -- Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` and `elicitation_posture` live on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade/posture diverge. Before these fields drive hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade/posture. +- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/.pi/context/` layout migrates to this structure. +- Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. - Core role/lens prompting should usually be product prompt packs rather than Pi skills. Pi skills remain available as Brunch-owned explicit resources when progressive disclosure is the right mechanism, but they are not the primary authority for operational mode/tool policy. ### Coherence and readiness semantics - Coherence must remain bounded for the POC: a visible verdict tied to structural legality and actionable reconciliation needs, not a vague promise that the specification “makes sense.” M8 owns the sharper rubric and adversarial examples. -- Avoid phase/stage/maturity language for the elicit lifecycle except when referring to legacy docs. The canonical internal model is readiness grade plus elicitation posture. PLAN/frontier text should describe concrete grade/posture gates rather than imply a user-facing phase machine. +- Avoid phase/stage/maturity language for the elicit lifecycle except when referring to legacy docs. The canonical internal model is readiness grade plus runtime strategy/lens/review-set state. PLAN/frontier text should describe concrete readiness gates rather than imply a user-facing phase machine. ### Vocabulary evolution - Whether public graph commands eventually split from one `graph.*` umbrella into `intent.*` / `oracle.*` / `design.*` / `plan.*` namespaces is deferred; current posture is unified `graph.*` for the POC. - ~~Whether `framing_as` values graduate to first-class node kinds~~ — resolved: `framing_as` retired, absorbed by `thesis`, `term`, `constraint.subtype`, and `goal` (D54-L, D56-L). -- `posture` is a spec-level property set for now; whether it earns a graph node kind is deferred until product pressure shows that per-subsystem or per-phase posture declarations need graph-native representation. +- `posture` is a workspace-level POC-stubbed property set for now; whether it earns richer persistence or graph-native representation is deferred until product pressure shows concrete readers beyond startup/prompt context. ### Thin transport/read posture @@ -359,10 +359,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Strategy** | An interaction-shape tactic within the active runtime bundle: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges from existing graph via review-set). Strategies determine the commitment mechanism (D26-L). | | **Lens** | A topical-focus framing applied within a role/strategy: `intent`, `design`, `oracle` for elicitation mode; `plan`, `sync`, `scope` for future execute mode. Strategy+lens pairs map to the prior lens-catalogue names (D25-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | -| **Prompt pack** | A Brunch-owned prompt fragment selected by operational mode, role preset, strategy, lens, readiness grade, or elicitation posture. Prompt packs compose at turn boundaries; they are product control-plane state, not ambient Pi prompt templates. | -| **Readiness grade** | Spec-owned forward gate stored on the spec row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, commitment review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | -| **Elicitation posture** | Spec-owned current stance inside `elicit`: `gathering | refining | pinning`. Semi-independent from readiness grade; a high-grade spec may still return to gathering/refining when new ambiguity appears. | -| **Commitment focus** | Optional/deferred target for `pinning` posture (`design | oracle`) if active review-set state and missing-commitment analysis cannot make the focus obvious. Not required as canonical state now. | +| **Prompt pack** | A Brunch-owned prompt fragment selected by operational mode, role preset, strategy, lens, or readiness grade. Prompt packs compose at turn boundaries; they are product control-plane state, not ambient Pi prompt templates. | +| **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | +| **Elicitation posture** | Retired as persisted spec state. Use readiness grade plus active strategy/lens/review-set state to explain elicit behavior. | +| **Commitment focus** | Retired as persisted spec state. Future commitment projection should derive from active review-set state and graph evidence if needed. | | **Coherence** | Bounded product-visible verdict over whether the current spec graph is structurally legal and free of known unresolved contradictions/gaps at the current maturity. It is backed by reconciliation needs and remains intentionally narrower than a general judgment that the whole idea is good or complete. | | **Structural legality** | Synchronous schema/ontology validity of graph mutations: edge categories from the closed set in `docs/design/GRAPH_MODEL.md`, per-category stance/cardinality/acyclicity rules, immutable accepted-edge identity (`category`, `sourceId`, `targetId`, `stance`), per-plane closed node `kind` enums, required `detail` sub-schemas for `decision`/`term`, `constraint.subtype` enum, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | | **Print snapshot** | The M1 meaning of the print transport mode: boot the Brunch host, resolve workspace/spec/session state through the coordinator, render product-shaped state, and exit without running an agent turn. | @@ -436,7 +436,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Node detail** | Optional JSON column on `GraphNode.detail` with per-kind validated sub-structures. `decision` requires `{ chosen_option, rejected, rationale }`; `term` requires `{ definition, aliases? }`; `constraint` may carry `{ subtype }`. All other kinds omit `detail`. | | **Context (node kind)** | A first-class intent node kind (`kind: "context"`). A descriptive claim about the environment — observed facts that color interpretation without driving decisions directly. Last-resort basic bucket: before filing as context, check the promotion heuristic (must be true for success → requirement/invariant; limits solutions → constraint; may be false → assumption; chooses among alternatives → decision; bet about users/market → thesis). | | **Intent kind category** | Derived grouping of intent node kinds: `basic` (goal, thesis, term, context), `structural` (requirement, assumption, constraint, invariant), `reasoning` (decision, criterion, example). A pure function of `kind`, not stored. Maps to spec-grade progression — grounding gate requires satisficing threshold of basic-category nodes. | -| **Posture** | A spec-level property set declaring project epistemic/strategic stance (certainty, stakes, audience, horizon, migration, sourcing). Not a graph node kind in the POC; spec-level rather than graph-level. Grounding elicitation interweaves basic intent nodes with posture establishment. | +| **Posture** | A workspace-level POC-stubbed property set declaring project epistemic/strategic stance (certainty, stakes, audience, horizon, migration, sourcing). Not a graph node kind or spec-row field in the POC. Grounding elicitation may help establish it, but startup persists only the workspace stub. | | **Kernel** | A behavioural elicitation pattern from `docs/design/BEHAVIORAL_KERNELS.md` (state/lifecycle, containment, concurrency, etc.). | | **Probe run** | A scripted or executable check of a Brunch seam that drives the public product surface and persists reviewable artifacts under `.fixtures/runs///`. | | **Transcript artifact** | The durable transcript evidence for a probe run, usually `session.jsonl` plus a Brunch-semantic `transcript.md`; reports explain the oracle, but transcript artifacts remain the evidence. | @@ -517,7 +517,7 @@ Infrastructure is not yet fully laid (Phase 3 of POC bootstrapping). Commands fo | Middle | Round-trip tests | JSONL reload, linear transcript validation, elicitation exchange projection, compaction, graph export/import, command result serialization, `supersedes`-chain reconstruction across regeneration. | D6-L, D13-L, D24-L, D28-L; I3-L, I8-L, I10-L, I19-L. | | Middle | Property-based / model-based tests | LSN monotonicity, change-log replay, reconciliation-need invariants, mention staleness, interest-set recomputation, side-task delivery ordering, **batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible even under mid-batch validation failure)**, **`supersedes`-chain acyclicity and unique-leaf-per-thread**, **lens-routing correctness (generated elicitor entries route to the right consumer)**, **reviewer-finding turn-boundary delivery ordering**. | A4-L, A8-L, A9-L, A11-L; I1-L, I4-L, I5-L, I6-L, I9-L, I12-L, I15-L, I16-L, I18-L. | | Middle | Contract tests | Named RPC method families and transport adapters share handler semantics; `rpc.discover` describes public methods with usable schemas/examples; pending-exchange start/read/respond handlers preserve transcript truth; subscriptions deliver initial snapshot plus ordered updates; `CommandExecutor` hides policy/transaction details; `acceptReviewSet` returns expected structured discriminants; only prevalidated proposals become reviewable review sets. | D5-L, D19-L, D20-L, D27-L, D48-L, D49-L; R11, R12, R27, R28. | -| Middle | Architectural boundary tests | No direct ORM/SQLite mutation outside `CommandExecutor`; no canonical chat/turn store; TUI/RPC/fixture code does not write `brunch.session_binding`; spec/session picker UI returns decisions rather than opening/mutating sessions; RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code; Brunch wrappers do not expose Pi branch creation/navigation as product behavior; spec readiness grade/posture mutations route through commands rather than session-local memory; reviewer-attributed writes target only `reconciliation_need`; Brunch-launched Pi runtimes do not load ambient `.pi/` resources or behavior-shaping settings outside the Brunch Pi Profile; Brunch product extensions load through the explicit static shell list rather than filesystem discovery or a runtime extension-metadata protocol. | D4-L, D6-L, D18-L, D21-L, D24-L, D29-L, D36-L, D39-L, D45-L; I2-L, I10-L, I11-L, I16-L, I19-L, I22-L, I24-L, I31-L. | +| Middle | Architectural boundary tests | No direct ORM/SQLite mutation outside `CommandExecutor`; no canonical chat/turn store; TUI/RPC/fixture code does not write `brunch.session_binding`; spec/session picker UI returns decisions rather than opening/mutating sessions; RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code; Brunch wrappers do not expose Pi branch creation/navigation as product behavior; spec readiness-grade mutations route through commands rather than session-local memory; reviewer-attributed writes target only `reconciliation_need`; Brunch-launched Pi runtimes do not load ambient `.pi/` resources or behavior-shaping settings outside the Brunch Pi Profile; Brunch product extensions load through the explicit static shell list rather than filesystem discovery or a runtime extension-metadata protocol. | D4-L, D6-L, D18-L, D21-L, D24-L, D29-L, D36-L, D39-L, D45-L; I2-L, I10-L, I11-L, I16-L, I19-L, I22-L, I24-L, I31-L. | | Middle | **Differential testing** | Dry-run validation at proposal time matches real-run validation at acceptance time (no drift between modes); free-form-generation vs constrained-generation legality rates (informs whether fallback path is needed per A14-L). | D27-L; A14-L. | | Middle | Probe transcript replay and property assertions | Probe runs preserve transcript evidence that can be replayed, rendered, and compared against current Brunch projections. Future brief-driven sessions, if revived, must produce the same probe-run artifact shape. For batch proposals/review sets: **structural-legality rate of LLM proposals tracked per-run in probe metadata as POC-phase fitness, not a merge gate**; first-attempt vs retry-with-feedback rates surfaced for human review. | A5-L, A6-L, A7-L, A14-L; I7-L; R20, R21, R22, R23. | | Middle | Deterministic public-RPC parity proof | A scripted agent-as-user discovers Brunch methods, activates workspace/spec/session, drives the current structured-exchange permutations through Brunch JSON-RPC only, compares Pi JSONL plus `session.transcriptDisplay` / `session.elicitationExchanges` projections against TUI-shaped structured-exchange expectations, rejects repeated deterministic prompts, and can persist a `.fixtures/runs/public-rpc-parity//` review bundle containing source `session.jsonl`, Brunch-semantic `transcript.md`, and `report.json`. | A5-L; D5-L, D48-L, D49-L; I23-L, I32-L; R24, R27, R28. | @@ -569,7 +569,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I28-L | Inner — TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) shape; deterministic anchor-rendering unit tests (same branch + same config → same header bytes). Middle (M9) — compaction round-trip property tests across all configured anchors and selection rules; fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer (M9) — long-horizon adversarial fixture confirms session binding, latest runtime state, latest establishment offer, in-flight side-task results, and unresolved staleness hints remain agent-intelligible post-compaction. | | I29-L | Inner — argv-shape tests for the `subagent` tool prove every spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent `--tools`/`--extension`/`--models`/`--append-system-prompt` set; TypeBox schema validation of `src/.pi/extensions/subagents/agents/*.md` frontmatter and `src/.pi/extensions/subagents/config.json`. Middle — isolation audit (no ambient `.pi/` resources reachable inside the subprocess; tool-allowlist conformance per starter agent; parent `CommandExecutor`/Brunch RPC handlers absent from subprocess environment). Outer — probe-driven proposal-generation runs invoking scout/researcher/graph-reader confirm grounding inputs flow through subagent outputs into review-set proposals without bypassing primary authority. | | I30-L | M5 post-exchange capture fixtures: compare committed graph facts, reconciliation needs, and preface-only interpretations against transcript evidence; known ambiguous exchanges must not silently become graph truth. | -| I31-L | M4/M5 spec-row command tests for grade/posture updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement. | +| I31-L | Spec-row command tests for grade updates plus prompt/tool-policy tests proving grade gates unlock later actions without disabling gathering/refinement. Card 1 covers the CommandExecutor grade-write path; prompt/tool-policy tests remain with M5. | | I32-L | FE-744 public-RPC structured-exchange parity proof: `rpc.discover` contract tests, pending/respond lifecycle tests, deterministic permutation run over Brunch JSON-RPC only, no repeated deterministic prompts, and parity assertions over the resulting Pi JSONL, transcript display, and elicitation-exchange projections. | | I33-L | Current schema tests cover minimum no-graph `capture_*` details and reject graph payload fields. Future capture-analysis runtime tests must still cover persisted result rendering, no graph-write side effects, Brunch-semantic transcript inclusion, and hidden/collapsed TUI rendering fallback. | | I36-L | M4 per-plane kind enum validation tests in CommandExecutor; kind-to-category derivation unit tests proving pure function parity with GRAPH_MODEL.md table. | diff --git a/src/db/README.md b/src/db/README.md index b45565ce9..c8a908487 100644 --- a/src/db/README.md +++ b/src/db/README.md @@ -4,11 +4,13 @@ SPEC decisions: D16-L, D41-L, D52-L ## Owns -- **Drizzle table definitions** (`schema.ts`) — nodes, edges, change_log, - graph_clock, reconciliation_need. Canonical column-level source of truth - for persisted shapes. Exports shared enum `const` arrays (`INTENT_KINDS`, - `EDGE_CATEGORIES`, etc.) reused by `graph/` domain types and Pi tool - parameter schemas. +- **Drizzle table definitions** (`schema.ts`) — specs, nodes, edges, + change_log, graph_clock, reconciliation_need. Canonical column-level source + of truth for persisted shapes. `specs` stores `{id, name, slug, + readiness_grade}` only; `elicitation_posture` and `commitment_focus` are + retired. Exports shared enum `const` arrays (`INTENT_KINDS`, + `READINESS_GRADES`, `EDGE_CATEGORIES`, etc.) reused by `graph/` domain types + and Pi tool parameter schemas. - **Row schema derivation** (`row-schemas.ts`) — runtime insert/select schemas derived from Drizzle tables via `drizzle-typebox`. Do not diff --git a/src/db/connection.test.ts b/src/db/connection.test.ts new file mode 100644 index 000000000..ec690c598 --- /dev/null +++ b/src/db/connection.test.ts @@ -0,0 +1,30 @@ +import { mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { createDb } from './connection.js'; +import { graphClock, specs } from './schema.js'; + +describe('createDb', () => { + it('creates a missing database file and can reopen it idempotently', async () => { + const dir = await mkdtemp(join(tmpdir(), 'brunch-db-')); + const dbPath = join(dir, 'data.db'); + + try { + const db = createDb(dbPath); + db.insert(specs) + .values({ name: 'Spec A', slug: 'spec-a', readiness_grade: 'grounding_onboarding' }) + .run(); + + expect((await stat(dbPath)).isFile()).toBe(true); + + const reopened = createDb(dbPath); + expect(reopened.select().from(specs).all()).toHaveLength(1); + expect(reopened.select().from(graphClock).all()[0]!.lsn).toBe(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/db/connection.ts b/src/db/connection.ts index 6d0e83264..4d83e5700 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -39,6 +39,13 @@ export function createDb(path: string): BrunchDb { */ function initSchema(sqlite: Database.Database): void { sqlite.exec(` + CREATE TABLE IF NOT EXISTS specs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT NOT NULL, + readiness_grade TEXT NOT NULL DEFAULT 'grounding_onboarding' + ); + CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, plane TEXT NOT NULL, diff --git a/src/db/row-schemas.ts b/src/db/row-schemas.ts index b7f7b4dbf..ceba9673c 100644 --- a/src/db/row-schemas.ts +++ b/src/db/row-schemas.ts @@ -10,7 +10,11 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'; -import { changeLog, edges, graphClock, nodes, reconciliationNeed } from './schema.js'; +import { changeLog, edges, graphClock, nodes, reconciliationNeed, specs } from './schema.js'; + +// --- Spec schemas --- +export const insertSpecSchema = createInsertSchema(specs); +export const selectSpecSchema = createSelectSchema(specs); // --- Node schemas --- export const insertNodeSchema = createInsertSchema(nodes); diff --git a/src/db/schema.ts b/src/db/schema.ts index fe580e702..ce17cbdb3 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -51,10 +51,24 @@ export const EDGE_CATEGORIES = [ export const EDGE_STANCES = ['for', 'against'] as const; +export const READINESS_GRADES = [ + 'grounding_onboarding', + 'elicitation_ready', + 'commitments_ready', + 'planning_ready', +] as const; + // --------------------------------------------------------------------------- // Tables // --------------------------------------------------------------------------- +export const specs = sqliteTable('specs', { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + slug: text().notNull(), + readiness_grade: text({ enum: READINESS_GRADES }).notNull().default('grounding_onboarding'), +}); + export const nodes = sqliteTable('nodes', { id: integer().primaryKey({ autoIncrement: true }), plane: text({ enum: ['intent', 'oracle', 'design', 'plan'] }).notNull(), diff --git a/src/graph/architecture.test.ts b/src/graph/architecture.test.ts index 5db3c6458..a42ae632a 100644 --- a/src/graph/architecture.test.ts +++ b/src/graph/architecture.test.ts @@ -28,4 +28,19 @@ describe('I26-L architectural boundary', () => { expect(importingFiles).toEqual([]); }); + + it('spec writes live only in CommandExecutor', () => { + const result = execSync( + `rg --files-with-matches "\\.(insert|update|delete)\\(schema\\.specs\\)|\\.(insert|update|delete)\\(specs\\)" src/ --glob '*.ts' --glob '!*.test.*' || true`, + { cwd: process.cwd(), encoding: 'utf-8' }, + ); + + const writingFiles = result + .trim() + .split('\n') + .filter(Boolean) + .filter((f) => f !== 'src/graph/command-executor.ts'); + + expect(writingFiles).toEqual([]); + }); }); diff --git a/src/graph/command-executor.test.ts b/src/graph/command-executor.test.ts index 2ee3a4e43..8cf47b546 100644 --- a/src/graph/command-executor.test.ts +++ b/src/graph/command-executor.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it, beforeEach } from 'vitest'; import { createDb, type BrunchDb } from '../db/connection.js'; -import { graphClock, changeLog, edges, nodes } from '../db/schema.js'; +import { graphClock, changeLog, edges, nodes, specs } from '../db/schema.js'; import { CommandExecutor } from './command-executor.js'; import type { CommitGraphInput } from './command-executor.js'; @@ -334,6 +334,74 @@ describe('CommandExecutor', () => { expect(result.status).toBe('success'); }); + // ========================================================================== + // specs + // ========================================================================== + + describe('specs', () => { + it('creates a spec row and returns an integer id', () => { + const result = executor.createSpec({ + name: 'Brunch POC', + slug: 'brunch-poc', + readinessGrade: 'grounding_onboarding', + }); + + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.specId).toBeTypeOf('number'); + expect(result.lsn).toBe(1); + + const row = db.select().from(specs).all()[0]!; + expect(row.id).toBe(result.specId); + expect(row.name).toBe('Brunch POC'); + expect(row.slug).toBe('brunch-poc'); + expect(row.readiness_grade).toBe('grounding_onboarding'); + }); + + it('reads a spec row by integer id', () => { + const created = executor.createSpec({ name: 'Spec A', slug: 'spec-a' }); + if (created.status !== 'success') throw new Error('unreachable'); + + const spec = executor.getSpec(created.specId); + + expect(spec).toEqual({ + id: created.specId, + name: 'Spec A', + slug: 'spec-a', + readinessGrade: 'grounding_onboarding', + }); + }); + + it('updates readiness grade through the command boundary', () => { + const created = executor.createSpec({ name: 'Spec A', slug: 'spec-a' }); + if (created.status !== 'success') throw new Error('unreachable'); + + const result = executor.updateReadinessGrade({ + specId: created.specId, + readinessGrade: 'elicitation_ready', + }); + + expect(result.status).toBe('success'); + if (result.status !== 'success') throw new Error('unreachable'); + expect(result.lsn).toBe(2); + expect(executor.getSpec(created.specId)?.readinessGrade).toBe('elicitation_ready'); + }); + + it('rejects an invalid readiness grade without writing', () => { + const created = executor.createSpec({ name: 'Spec A', slug: 'spec-a' }); + if (created.status !== 'success') throw new Error('unreachable'); + + const result = executor.updateReadinessGrade({ + specId: created.specId, + readinessGrade: 'pinning' as never, + }); + + expect(result.status).toBe('structural_illegal'); + expect(executor.getSpec(created.specId)?.readinessGrade).toBe('grounding_onboarding'); + expect(db.select().from(changeLog).all()).toHaveLength(1); + }); + }); + // ========================================================================== // commitGraph // ========================================================================== diff --git a/src/graph/command-executor.ts b/src/graph/command-executor.ts index 6f407a8c0..db6667428 100644 --- a/src/graph/command-executor.ts +++ b/src/graph/command-executor.ts @@ -24,6 +24,8 @@ import * as schema from '../db/schema.js'; import type { EdgeCategory, EdgeStance } from './schema/edges.js'; import type { NodeBasis, NodePlane } from './schema/nodes.js'; +export type ReadinessGrade = (typeof schema.READINESS_GRADES)[number]; + // --------------------------------------------------------------------------- // Result types // --------------------------------------------------------------------------- @@ -83,12 +85,35 @@ export interface ReconNeedResolveSuccess { readonly lsn: number; } +/** Successful spec creation. */ +export interface CreateSpecSuccess { + readonly status: 'success'; + readonly specId: number; + readonly lsn: number; +} + +/** Successful spec readiness-grade update. */ +export interface UpdateReadinessGradeSuccess { + readonly status: 'success'; + readonly lsn: number; +} + +/** Spec row returned by CommandExecutor reads. */ +export interface SpecRecord { + readonly id: number; + readonly name: string; + readonly slug: string; + readonly readinessGrade: ReadinessGrade; +} + /** Union of all possible command results. */ export type CommandResult = | CommandSuccess | CommitGraphSuccess | ReconNeedSuccess | ReconNeedResolveSuccess + | CreateSpecSuccess + | UpdateReadinessGradeSuccess | StructuralIllegal | NeedsHuman | PolicyBlocked @@ -106,10 +131,29 @@ export type CreateReconNeedResult = ReconNeedSuccess | StructuralIllegal; /** Result of a resolveReconciliationNeed command. */ export type ResolveReconNeedResult = ReconNeedResolveSuccess | StructuralIllegal; +/** Result of a createSpec command. */ +export type CreateSpecResult = CreateSpecSuccess | StructuralIllegal; + +/** Result of an updateReadinessGrade command. */ +export type UpdateReadinessGradeResult = UpdateReadinessGradeSuccess | StructuralIllegal; + // --------------------------------------------------------------------------- // Input types // --------------------------------------------------------------------------- +/** Input for creating a spec row. */ +export interface CreateSpecInput { + readonly name: string; + readonly slug: string; + readonly readinessGrade?: ReadinessGrade | undefined; +} + +/** Input for updating a spec readiness grade. */ +export interface UpdateReadinessGradeInput { + readonly specId: number; + readonly readinessGrade: ReadinessGrade; +} + /** Input for creating a single graph node. */ export interface CreateNodeInput { readonly plane: NodePlane; @@ -195,6 +239,11 @@ const VALID_KINDS_BY_PLANE: Record = { }; const KINDS_REQUIRING_DETAIL = new Set(['decision', 'term']); +const VALID_READINESS_GRADES = schema.READINESS_GRADES as unknown as string[]; + +function isReadinessGrade(value: string): value is ReadinessGrade { + return VALID_READINESS_GRADES.includes(value); +} function validateCreateNode(input: CreateNodeInput): Diagnostic[] { const diagnostics: Diagnostic[] = []; @@ -458,6 +507,114 @@ class BatchValidationError extends Error { export class CommandExecutor { constructor(private readonly db: BrunchDb) {} + /** Create a spec row through the command boundary. */ + createSpec(input: CreateSpecInput): CreateSpecResult { + const diagnostics: Diagnostic[] = []; + const name = input.name.trim(); + const slug = input.slug.trim(); + const readinessGrade = input.readinessGrade ?? 'grounding_onboarding'; + + if (!name) diagnostics.push({ field: 'name', message: 'name must be non-empty' }); + if (!slug) diagnostics.push({ field: 'slug', message: 'slug must be non-empty' }); + if (!isReadinessGrade(readinessGrade)) { + diagnostics.push({ + field: 'readinessGrade', + message: `"${readinessGrade}" is not a valid readiness grade`, + }); + } + if (diagnostics.length > 0) return { status: 'structural_illegal', diagnostics }; + + return this.db.transaction((tx) => { + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get(); + const lsn = clock!.lsn; + + const row = tx + .insert(schema.specs) + .values({ name, slug, readiness_grade: readinessGrade }) + .returning() + .get(); + + tx.insert(schema.changeLog) + .values({ + lsn, + operation: 'create_spec', + payload: JSON.stringify({ specId: row!.id, name, slug, readinessGrade }), + }) + .run(); + + return { status: 'success' as const, specId: row!.id, lsn }; + }); + } + + /** Read a spec row by id. */ + getSpec(specId: number): SpecRecord | undefined { + const row = this.db.select().from(schema.specs).where(eq(schema.specs.id, specId)).get(); + if (!row) return undefined; + return { + id: row.id, + name: row.name, + slug: row.slug, + readinessGrade: row.readiness_grade, + }; + } + + /** Update a spec's readiness grade through the command boundary. */ + updateReadinessGrade(input: UpdateReadinessGradeInput): UpdateReadinessGradeResult { + if (!isReadinessGrade(input.readinessGrade)) { + return { + status: 'structural_illegal', + diagnostics: [ + { + field: 'readinessGrade', + message: `"${input.readinessGrade}" is not a valid readiness grade`, + }, + ], + }; + } + + return this.db.transaction((tx) => { + const existing = tx + .select({ id: schema.specs.id }) + .from(schema.specs) + .where(eq(schema.specs.id, input.specId)) + .get(); + if (!existing) { + return { + status: 'structural_illegal' as const, + diagnostics: [{ field: 'specId', message: `spec ${input.specId} does not exist` }], + }; + } + + const clock = tx + .update(schema.graphClock) + .set({ lsn: sql`${schema.graphClock.lsn} + 1` }) + .where(eq(schema.graphClock.id, 1)) + .returning() + .get(); + const lsn = clock!.lsn; + + tx.update(schema.specs) + .set({ readiness_grade: input.readinessGrade }) + .where(eq(schema.specs.id, input.specId)) + .run(); + + tx.insert(schema.changeLog) + .values({ + lsn, + operation: 'update_spec_readiness_grade', + payload: JSON.stringify({ specId: input.specId, readinessGrade: input.readinessGrade }), + }) + .run(); + + return { status: 'success' as const, lsn }; + }); + } + /** * Create a single graph node. * diff --git a/src/graph/index.ts b/src/graph/index.ts index 2d2488874..55d10e746 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -20,6 +20,7 @@ export { DESIGN_KINDS, PLAN_KINDS, NODE_BASES, + READINESS_GRADES, } from '../db/schema.js'; export type { EdgeBasis, EdgeCategory, EdgeStance, GraphEdge } from './schema/edges.js'; @@ -76,14 +77,22 @@ export type { CreateNodeInput, CreateNodeResult, CreateReconNeedInput, + CreateSpecInput, + CreateSpecResult, + CreateSpecSuccess, CreateReconNeedResult, Diagnostic, NeedsHuman, PolicyBlocked, + ReadinessGrade, ReconNeedResolveSuccess, ReconNeedSuccess, ReconNeedTarget, ResolveReconNeedResult, + SpecRecord, StructuralIllegal, + UpdateReadinessGradeInput, + UpdateReadinessGradeResult, + UpdateReadinessGradeSuccess, VersionConflict, } from './command-executor.js'; From b6285bb8aec03946125c165d5db6ec1725848d35 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 11:40:23 +0200 Subject: [PATCH 11/34] Boot workspaces from DB-backed specs --- memory/CARDS.md | 175 ------------ memory/PLAN.md | 11 +- memory/SPEC.md | 22 +- src/.pi/components/workspace-dialog/model.ts | 8 +- src/README.md | 3 +- src/brunch-session-envelope.ts | 11 +- src/brunch-tui.test.ts | 2 +- src/brunch.test.ts | 8 +- src/elicitation-exchange.test.ts | 4 +- src/graph/index.ts | 1 + src/graph/workspace-store.ts | 14 + src/print-snapshot.test.ts | 8 +- src/print-snapshot.ts | 2 +- src/probes/public-rpc-parity-proof.test.ts | 2 +- src/probes/public-rpc-parity-proof.ts | 2 +- src/rpc/handlers.test.ts | 63 ++--- src/rpc/handlers.ts | 17 +- src/session-binding.ts | 17 +- src/session-projection-reader.ts | 4 +- src/session-transcript.test.ts | 2 +- src/session/README.md | 5 +- src/web-client/app.test.tsx | 4 +- src/web-client/app.tsx | 2 +- src/workspace-session-coordinator.test.ts | 52 ++-- src/workspace-session-coordinator.ts | 276 +++++++++++++------ 25 files changed, 325 insertions(+), 390 deletions(-) delete mode 100644 memory/CARDS.md create mode 100644 src/graph/workspace-store.ts diff --git a/memory/CARDS.md b/memory/CARDS.md deleted file mode 100644 index b46c13596..000000000 --- a/memory/CARDS.md +++ /dev/null @@ -1,175 +0,0 @@ - - -# Cards — `spec-persistence-and-startup` - -Frontier: **spec-persistence-and-startup** (PLAN.md). Branch: to create. -Two cards: an additive persistence foundation, then the integration that makes -startup correct end-to-end. Card 2 consumes Card 1's spec API; the model is -fully locked, so Card 2's shape does not depend on Card 1's build findings. - -Locked state-model decisions this queue implements (from the survey session): - -```yml -specs[] # lives: .brunch/data.db - id: integer # autoincrement; replaces 'spec-${uuid}' - name: string - slug: string - readiness_grade: enum # rename optional; elicitation_posture + commitment_focus RETIRED - -workspace # lives: .brunch/workspace.json (renamed from state.json) - project: { name, slug } # 'source' DROPPED (dead) - current: { specId:int, sessionId } - posture: { certainty, stakes, audience, horizon, migration, sourcing } # POC stub, empty strings - # activation, chrome: coordinator-derived at startup, NOT persisted - -binding # lives: Pi JSONL (brunch.session_binding) - { schemaVersion, specId:int } # sessionId + specTitle DROPPED; self-id guard removed -``` - ---- - -## Card 1 — Spec entity + DB-on-startup foundation [status: done] - -### Target Behavior - -A Brunch process opening a workspace creates `.brunch/data.db` when absent and can create, read, and grade-update a `specs` row exclusively through the `CommandExecutor`. - -### Boundary Crossings - -``` -→ createDb(path) (creates file if missing, idempotent) -→ db/schema.ts + initSchema DDL (new `specs` table) -→ graph/ CommandExecutor (createSpec / getSpec / updateReadinessGrade) -→ specs row persisted; integer id returned -``` - -### Risks and Assumptions - -``` -- RISK: should spec writes consume a graph LSN + change_log entry, or are they a - separate control-plane write? - → MITIGATION: default — route through CommandExecutor for the no-bypass invariant - and append a change_log entry for audit; reuse the single graph_clock LSN unless - the build surfaces a concrete reason to separate the clocks. Settle in build. -- RISK: graph nodes remain unscoped to a spec (no nodes.spec_id this slice). - → MITIGATION: out of scope — spec-row only; node↔spec scoping is a separate - decision, deliberately not opened here. -- ASSUMPTION: Drizzle + better-sqlite3 line is settled. - → IMPACT IF FALSE: low — already validated. - → VALIDATE: existing M4 A20-L spike. [→ memory/SPEC.md §Assumptions A20-L] -``` - -### Tracer-bullet check - -Scores on **uncertainty** (retires the spec-row persistence hole) and **proof of life** (first real `specs` row written to a real `.brunch/data.db` outside `:memory:` tests). Build it as-is. - -### Acceptance Criteria - -``` -✓ schema/initSchema — `specs` table created (Drizzle table def + CREATE IF NOT EXISTS DDL), - columns {id INTEGER PK AUTOINCREMENT, name, slug, readiness_grade}; NO elicitation_posture, - NO commitment_focus -✓ createSpec — writes a row through CommandExecutor, returns integer id -✓ getSpec — returns the row by integer id -✓ updateReadinessGrade — mutates grade through the command boundary; invalid grade rejected -✓ createDb on a non-existent path — creates the file; reopening the same path is idempotent -✓ no-bypass — a direct ORM write to `specs` outside CommandExecutor is caught by the - architectural boundary test (I26-L extension) -``` - -### Verification Approach - -``` -- Inner: vitest unit — CommandExecutor spec ops + createDb file-creation/idempotence -- Middle: architectural no-bypass test extended to `specs` -- Outer: n/a (boot integration is Card 2) -``` - -### Cross-cutting obligations - -``` -- All spec writes route through CommandExecutor (D16-L / D20-L no-bypass). -- SPEC reconciliation lands with this card (the model omits the dead fields): - retire the elicitation_posture half of D45-L; gut/retire D46-L; trim requirement #22; - simplify I31-L to grade-only. -``` - ---- - -## Card 2 — Coordinator startup on the corrected model [status: next] - -### Target Behavior - -Brunch boots end-to-end against the DB-backed spec model: it reads the workspace pointer from `.brunch/workspace.json`, the spec from the DB, and the session→spec link from the collapsed binding, and reaches a correct activation state under every foreseeable startup condition. - -### Boundary Crossings - -``` -→ runBrunchCli → openDefaultWorkspace / activateWorkspace -→ .brunch/workspace.json (renamed, reshaped) read / write / first-run scaffold -→ .brunch/data.db (create-if-missing) + spec row create/read (Card 1 API) -→ brunch.session_binding {specId:int} append/read; spec name resolved from DB -→ WorkspaceSessionState returned; activation/chrome DERIVED, not stored -``` - -### Risks and Assumptions - -``` -- RISK: specId string→int flip touches workspace.json + binding + coordinator + many - test fixtures at once. - → MITIGATION: pre-release posture — regenerate fixtures; land as one coherent change - that keeps `npm run verify` green; no compat shim for old spec-${uuid} data. -- RISK: the spec picker/inventory now needs a DB read (new coupling; today it reads JSONL only). - → MITIGATION: thread the DB/CommandExecutor into inspectWorkspaceInventory; ensure the - DB is created before inventory runs. -- ASSUMPTION: Card 1's spec API shape is stable. - → IMPACT IF FALSE: rework limited to the coordinator call sites. -``` - -### Tracer-bullet check - -Scores on **proof of life** (lights up the real boot path: a live `.brunch/data.db` created and read by an actual `brunch` run) and **invariants** (stabilizes "init/startup correct under all conditions" — the frontier's north-star oracle). - -### Acceptance Criteria - -``` -startup condition matrix (each leaf = one assertion): -├── no .brunch/ → scaffolds workspace.json (empty posture) + data.db -├── workspace.json + data.db, valid → ready; spec read from DB by specId -├── workspace.json present, db missing → recreates data.db; reconciles current spec -├── current specId absent from DB → needs_human(reason) -├── current sessionId/file missing/stale → needs_human or new-session path -├── multiple specs → picker lists names resolved from DB -├── new-spec → writes specs row, ready -├── new-session-for-current-spec → appends binding {specId}, ready -├── cancel → cancelled -└── forked session (binding inherited) → still resolves spec linkage (no self-id reject) -shape assertions: -├── workspace.json has no `source`; persists {project, current:{specId:int,sessionId}, posture} -└── brunch.session_binding is {schemaVersion, specId:int} — no sessionId, no specTitle -``` - -### Verification Approach - -``` -- Inner: workspace.json read/write/scaffold unit tests; binding schema test -- Middle: startup-condition-matrix integration tests (NORTH STAR); coordinator↔DB; - picker-name-from-DB; no-bypass -- Outer: real `brunch --mode print` (and TUI boot) against a regenerated `.brunch/`, - proving a live `.brunch/data.db` is created and read -``` - -### Cross-cutting obligations - -``` -- Session identity stays Pi-owned; Brunch contributes only {specId}; binding is fork-portable. -- Rename .brunch/state.json → .brunch/workspace.json (constant + tests). -- SPEC reconciliation: delete prompt-assembly layer 7 (elicitation posture); flip the - SPEC §Vocabulary-evolution `posture` note spec-level → workspace-level (POC-stubbed). -- README updates: src/README.md (state.json → workspace.json), src/db/README.md (specs table), - src/session/README.md (binding collapse). -- Do NOT wire graph-agent tools here — this frontier owns DB lifecycle only. -``` diff --git a/memory/PLAN.md b/memory/PLAN.md index 60661ad48..b4686cd94 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -51,11 +51,11 @@ The POC should maximize assumption falsification rather than merely implement mi ### Active -1. `spec-persistence-and-startup` — restore DB-on-startup + specs-in-DB, reshape `.brunch/workspace.json`, collapse the session binding; init/startup correct under all foreseeable conditions. Precedes the runtime portion of `agent-graph-integration`. +1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, and reviewer advisory writes through pi extension seams; all writes via the shared command layer. ### Next -1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, and reviewer advisory writes through pi extension seams; all writes via the shared command layer. +1. `probes-and-transcripts-evolution` — keep probe/transcript artifacts honest as new seams need evidence. ### Parallel / Low-conflict @@ -81,7 +81,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Linear:** unassigned (create in FE / brunch when branch opens) - **Branch:** to create — `ln/-spec-persistence-startup` - **Kind:** structural (persistence-model correction + startup-path regression repair) -- **Status:** active +- **Status:** done - **Objective:** Restore the DB-on-startup path lost in the rebuild and correct the workspace/spec/session persistence model so Brunch initializes and boots correctly under all foreseeable conditions, reading each fact from its canonical home. Concretely: **(a)** model specs as DB rows (`specs{id:int, name, slug, readiness_grade}`), retiring `elicitation_posture` and `commitment_focus`; **(b)** create `.brunch/data.db` if absent at startup and route spec create/read/grade-update through the `CommandExecutor`; **(c)** rename `.brunch/state.json` → `.brunch/workspace.json` and reshape to `{project:{name,slug}, current:{specId:int, sessionId}, posture:}`, dropping the dead `source` field; **(d)** collapse the `brunch.session_binding` entry to `{specId:int}`, dropping `sessionId`/`specTitle` and the `sessionId !== header.id` self-guard (fork-portable); **(e)** resolve spec names from the DB, not from JSONL or workspace state. - **Why now / unlocks:** The rebuild branch regressed two capabilities the prior version had — DB-on-startup and specs-in-DB — because work optimized for green tests over working product runs (`createDb`/`new CommandExecutor` appear only in `:memory:` tests; the TUI shell builds extensions with no graph deps via `{ coordinator }`; no `.brunch/data.db` is ever created at runtime; mention-autocomplete reads `FIXTURE_GRAPH_MENTION_SOURCE`). This frontier repairs that and lands the spec row D45-L already assumes exists, unblocking the runtime portion of `agent-graph-integration` (the A14-L real-LLM proof needs the DB live at startup) and the deferred agent-runtime vocabulary work. Retires the spec-row persistence hole and the unscoped global graph namespace concern. - **Acceptance:** @@ -95,7 +95,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Cross-cutting obligations:** All spec writes route through `CommandExecutor` (D16-L/D20-L no-bypass). Session identity stays Pi-owned — Brunch contributes only `{specId}`; keep runtime state linear-transcript-backed and fork-portable. Do **not** wire graph-agent tools here — this frontier owns the DB lifecycle; `agent-graph-integration` inherits it to register graph tools. **SPEC reconciliation required (with the build):** retire the `elicitation_posture` half of D45-L, gut/retire D46-L, trim requirement #22, simplify I31-L to grade-only, delete prompt-assembly layer 7, and flip the SPEC §Vocabulary-evolution `posture` note from spec-level → workspace-level (POC-stubbed). Update `src/README.md`, `src/db/README.md`, `src/session/README.md` migration notes. - **Traceability:** R22, R28 / D16-L, D20-L, D40-L, D45-L (revise), D46-L (retire), D52-L / I31-L (revise) / A19-L, A20-L. Retires: spec-row persistence hole; DB-on-startup regression. - **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (posture deferral note, §Vocabulary evolution); this session's locked state-model decisions. -- **Current execution pointer:** card queue → `memory/CARDS.md` (slice 1: spec entity + DB-on-startup foundation; slice 2: coordinator startup on the corrected model). +- **Current execution pointer:** Done. Card queue exhausted and removed after Card 1 (spec table + CommandExecutor create/read/grade update) and Card 2 (DB-backed startup, `.brunch/workspace.json`, collapsed binding, picker names from DB). Verified with `npm run verify` plus real `brunch --mode print` against a fresh cwd. ### sealed-pi-profile-runtime-state @@ -307,6 +307,7 @@ The POC should maximize assumption falsification rather than merely implement mi ## Recently Completed +- 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. - 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with target validation + LSN invariants; oracle-plane stub acceptance met by existing node kinds. Verified: `npm run verify` after each slice. `agent-graph-integration` (M5) is now unblocked. - 2026-06-01 `sealed-pi-profile-runtime-state` (FE-776) — Done: prep envelope tied off. Both strands complete: **(a)** Pi harness sealing including sealed profile, runtime-state transcript projection, session display names via Pi `session_info`; **(b)** graph-model lock-and-materialize with Phase 1 (edges) + Phase 2 (nodes) locked in `docs/design/GRAPH_MODEL.md`, code stubs under `src/graph/`, and A20-L persistence spike validating `drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`. `graph-data-plane` (M4 CRUD) is now unblocked. Verified: `npm run verify` after each slice. - 2026-06-01 `pi-ui-extension-patterns` (FE-744) — Done. All Pi extension seam evidence for M5/M6/M7 landed. Detailed frontier definition archived to [docs/archive/PLAN_HISTORY.md §2026-06-01 Sync archive](file:///Users/lunelson/Code/hashintel/brunch-next/docs/archive/PLAN_HISTORY.md). @@ -319,7 +320,7 @@ Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jso nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) - spec-persistence-and-startup [active] (persistence-model fix + startup regression repair) + spec-persistence-and-startup [done] (persistence-model fix + startup regression repair) agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] authority-model [not-started] (M6) diff --git a/memory/SPEC.md b/memory/SPEC.md index 182060a92..62a1afbef 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -155,9 +155,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D5-L — Brunch JSON-RPC is the single public product protocol.** Brunch exposes one public product RPC surface over stdio, WebSocket, and in-process handlers. Product clients — web UI, CLI probes, TUI adapters, and future relays — call Brunch method families and should not coordinate raw Pi RPC plus Brunch product RPC themselves. Pi RPC may be used behind a Brunch adapter for agent-loop mechanics and Pi extension UI, but it is not a second public product API. HTTP exists only as a transport shim (static bundle, health, uploads, webhooks). The Brunch stdio surface is also the agent-as-user probe driver interface, even when that driver internally relays Pi RPC events. Depends on: A5-L. Supersedes: treating raw Pi RPC as the product API for Brunch data. - **D10-L — Web client is a native Brunch React app over one WebSocket RPC client.** TanStack Router + TanStack Query + Brunch-owned elicitation/transcript primitives (Vercel AI SDK UI or TanStack AI style). `pi-web-ui` is not reused. The browser is a thin remote head over Brunch RPC method families, not a second product runtime or REST-backed data client. Depends on: D5-L. Supersedes: —. - **D17-L — Brunch semantics ride one transcript/event substrate, not parallel channels.** Pi JSONL transcript entries — ordinary messages, assistant tool-call/toolResult exchanges, and custom messages/entries — plus `deliverAs: "nextTurn" | "followUp"` and `prepareNextTurn` are the load-bearing mechanism for structured elicitation prompts/responses, `worldUpdate`, mention-staleness hints, and side-task-result delivery. New product semantics should compose onto this substrate before inventing a second event plane or a parallel chat/turn store. Depends on: D5-L, D6-L, D12-L, D15-L. Supersedes: custom-message-only interpretations of structured elicitation. -- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes named method families such as `rpc.*`, `workspace.*`, `session.*`, `elicitation.*`, `graph.*`, `coherence.*`, and `command.*`; each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/state.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch command layer. Subscriptions are first-class and may provide initial state plus updates, and adapter-only agent/UI events may be relayed into product-shaped notifications, but Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model and any two-public-RPC-surface split. +- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes named method families such as `rpc.*`, `workspace.*`, `session.*`, `elicitation.*`, `graph.*`, `coherence.*`, and `command.*`; each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch command layer. Subscriptions are first-class and may provide initial state plus updates, and adapter-only agent/UI events may be relayed into product-shaped notifications, but Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model and any two-public-RPC-surface split. - **D23-L — Transport modes, operational modes, agent roles, strategies, and lenses are separate axes.** TUI, RPC, print, and web are transport modes: ways of driving or observing the same Brunch host through Pi/Brunch harness seams. Operational modes are top-level authority/tooling postures such as `elicit` and future `execute`. Agent roles are active workers within an operational mode (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor). Strategies are interaction plans; lenses are narrower interpretive/extraction/review framings. M1 print mode is therefore only a transport proof-of-life: it boots through the same host/coordinator, renders a snapshot of product-shaped state, and exits without running an agent turn. A future single-turn headless print run is deferred until runtime bundle selection/defaults are explicit. Depends on: D1-L, D5-L, D19-L, D21-L, D40-L. Supersedes: overloading “mode” to mean both transport and agent strategy, or using “agent mode” for role/preset/lens interchangeably. -- **D33-L — Transport connections are client attachments, not Brunch sessions.** A Brunch session is a durable linear Pi JSONL transcript bound to exactly one spec; WebSocket connections, stdio streams, TUI instances, and browser tabs are ephemeral presentation attachments to product resources. Session-specific RPC methods should name their target spec/session explicitly or operate through an explicit client attachment; they must not infer durable session identity merely from the transport connection. `.brunch/state.json` remains launch/default acceleration, not concurrency authority. During the POC, Brunch targets a one-writer/many-observer local model: one interactive driver (typically TUI/agent) may write while web clients attach read-only for visual projections. Depends on: D5-L, D10-L, D11-L, D19-L, D21-L, D24-L. Supersedes: treating `/rpc`, a WebSocket, or workspace default state as the active session itself. +- **D33-L — Transport connections are client attachments, not Brunch sessions.** A Brunch session is a durable linear Pi JSONL transcript bound to exactly one spec; WebSocket connections, stdio streams, TUI instances, and browser tabs are ephemeral presentation attachments to product resources. Session-specific RPC methods should name their target spec/session explicitly or operate through an explicit client attachment; they must not infer durable session identity merely from the transport connection. `.brunch/workspace.json` remains launch/default acceleration, not concurrency authority. During the POC, Brunch targets a one-writer/many-observer local model: one interactive driver (typically TUI/agent) may write while web clients attach read-only for visual projections. Depends on: D5-L, D10-L, D11-L, D19-L, D21-L, D24-L. Supersedes: treating `/rpc`, a WebSocket, or workspace default state as the active session itself. Product RPC / Pi relay model: @@ -218,8 +218,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c #### Interaction & UI shape -- **D11-L — Workspace state hierarchy `workspace(cwd) → spec → session`, with spec and session selection gated before any agent loop.** A Brunch workspace is the single cwd where the CLI is invoked; it is not a user-created container and there is only one per launch context. The cwd's human-readable label may be derived by `src/project-identity.ts` from shallow project manifests (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`) or directory basename, but that label is presentation metadata, not a second selectable container. The first durable choice is the spec: create a new spec, or resume an existing spec. Within an existing spec, the second durable choice is the session: create a new session or resume an existing session. Creating a new spec implicitly creates its first session. Spec selection is durable across `/new` and persisted in `.brunch/state.json`. Each Pi session is bound to exactly one spec by a `brunch.session_binding` custom entry at session start; switching specs selects or creates another session rather than mutating the spec of the current session. Depends on: D6-L. Supersedes: treating “workspace” as the user-created product object in the boot dialog. -- **D21-L — Workspace session coordination is the spec/session boot seam.** Brunch owns a narrow `WorkspaceSessionCoordinator` for boot, spec inventory, spec/session selection, selected-session reopening, and `/new` session creation. It is the only product module allowed to create or open Pi sessions for Brunch user flows and the only module allowed to write `brunch.session_binding`; callers inspect workspace inventory and activate a product decision rather than mutating a session's bound spec directly. The coordinator hides `SessionManager.create/open/continueRecent(cwd, ".brunch/sessions/")`, internal session-start binding for pi-created replacement sessions, `.brunch/state.json` current-spec and current-session-file acceleration, binding validation, and chrome-state derivation. Because pi defers appending session JSONL until an assistant message exists, the coordinator flushes Brunch's binding when it is created, refreshes it at `before_agent_start`, and performs the final pre-assistant flush from Brunch's internal assistant `message_start` hook after pi has persisted the user message but before assistant persistence; each flush reloads the session file so pi's next assistant append does not duplicate the already-written prefix. Depends on: D6-L, D11-L. Supersedes: the loose `SpecRegistry` + caller-orchestrated session-binding mental model, and treating `.brunch/state.json` as an implicit instruction to resume without user-visible Brunch flow. +- **D11-L — Workspace state hierarchy `workspace(cwd) → spec → session`, with spec and session selection gated before any agent loop.** A Brunch workspace is the single cwd where the CLI is invoked; it is not a user-created container and there is only one per launch context. The cwd's human-readable label may be derived by `src/project-identity.ts` from shallow project manifests (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`) or directory basename, but that label is presentation metadata, not a second selectable container. The first durable choice is the spec: create a new DB-backed spec, or resume an existing spec. Within an existing spec, the second durable choice is the session: create a new session or resume an existing session. Creating a new spec implicitly creates its first session. Spec selection is durable across `/new` and persisted as `.brunch/workspace.json` `{project,current:{specId,sessionId},posture}`. Each Pi session is bound to exactly one spec by a collapsed `brunch.session_binding` custom entry `{schemaVersion,specId}` at session start; switching specs selects or creates another session rather than mutating the spec of the current session. Depends on: D6-L. Supersedes: treating “workspace” as the user-created product object in the boot dialog, string `spec-*` ids, and session self-id guards in the binding. +- **D21-L — Workspace session coordination is the spec/session boot seam.** Brunch owns a narrow `WorkspaceSessionCoordinator` for boot, spec inventory, spec/session selection, selected-session reopening, and `/new` session creation. It is the only product module allowed to create or open Pi sessions for Brunch user flows and the only module allowed to write `brunch.session_binding`; callers inspect workspace inventory and activate a product decision rather than mutating a session's bound spec directly. The coordinator hides `SessionManager.create/open/continueRecent(cwd, ".brunch/sessions/")`, DB-backed spec lookup through `CommandExecutor`, internal session-start binding for pi-created replacement sessions, `.brunch/workspace.json` current spec/session acceleration, binding validation, and chrome-state derivation. Because pi defers appending session JSONL until an assistant message exists, the coordinator flushes Brunch's binding when it is created, refreshes it at `before_agent_start`, and performs the final pre-assistant flush from Brunch's internal assistant `message_start` hook after pi has persisted the user message but before assistant persistence; each flush reloads the session file so pi's next assistant append does not duplicate the already-written prefix. Depends on: D6-L, D11-L. Supersedes: the loose `SpecRegistry` + caller-orchestrated session-binding mental model, treating `.brunch/workspace.json` as an implicit instruction to resume without user-visible Brunch flow, or resolving spec names from JSONL. - **D22-L — TUI boot is Brunch-owned before Pi interactive runtime begins.** Brunch's TUI mode may use `@earendil-works/pi-tui` directly for a pre-Pi startup gate that selects or creates the active spec/session before `InteractiveMode.run()`. After activation, persistent chrome is mounted by an internal Brunch extension through Pi's public UI seams. Brunch does not fork pi, monkeypatch `InteractiveMode`, or expose generic pi extension configuration to users for product boot/chrome. Depends on: D2-L, D21-L, D36-L. Supersedes: private-header/monkeypatch approaches for M0 chrome and raw readline-only spec selection as the durable TUI product flow. - **D12-L — Elicitation-first interaction, transcript-native structured prompts.** Brunch treats system/assistant prompts and user responses as Pi transcript truth. Structured action/choice/freeform surfaces may be represented by Brunch custom entries when needed, but there is no DB-owned prompt/response entity; at idle, the session waits on a system/assistant-originated elicitation prompt. Depends on: D6-L, D11-L. Supersedes: —. - **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction may be represented through the thinnest Pi-supported transcript seam for its shape. The current preferred seam for Brunch structured exchanges is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details; runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof now drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Brunch custom messages/entries remain valid for establishment offers, review-set proposals, annotations, and future product-native displays, but they are not mandatory for every structured exchange. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. @@ -239,7 +239,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **Data gatherers** — read-only context fetchers whose output grounds proposals: **scout** (codebase recon: `read`, `grep`, `find`, `ls`), **researcher** (web research: `web_search`, `web_fetch`), and **graph-reader** (read-only Brunch graph projection tools). - **Variant proposer** — **proposer** (no tools): given a grounding bundle plus a batch-proposal lens frame, emits exactly one well-formed variant of a candidate proposal. The main agent achieves diversity by issuing parallel `tasks: []` invocations of `proposer` with intentionally distinct framings — the subagent realization of the "design it twice" pattern from `ln-design` and the parallel fan-out anticipated by `ln-oracles`. Each `proposer` invocation runs in its own isolated context so variants don't cross-contaminate; the main agent collects N outputs and composes the comparison via the D31-L meta-rubric (and/or project-specific axes) before writing a `brunch.review_set_proposal` entry through the elicitor flow. `proposer` is system-prompt-only by design: its grounding inputs come entirely through the task string the main agent assembles from preceding `scout` / `researcher` / `graph-reader` calls. This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. -- **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/state.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/state.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/state.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. +- **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. ### Critical Invariants @@ -372,9 +372,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Session display name** | Optional human-readable label for a session, stored as Pi `session_info` metadata and used by pickers/chrome to distinguish sessions. It may be user-set or Brunch-generated from transcript content; it is not canonical spec/session identity. | | **Session binding** | The first Brunch custom entry in a session that binds the Pi session id to exactly one spec id and schema version. Makes JSONL self-describing; registry/index state is an acceleration, not the canonical binding. | | **Client attachment** | An ephemeral TUI instance, browser tab, stdio stream, or WebSocket connection attached to one or more Brunch product resources for viewing or driving. Client attachment state may guide subscriptions and UI routing, but it is not durable spec/session truth. | -| **Workspace session coordinator** | The Brunch boot seam that returns `ready | select_spec | needs_human` workspace-session state for a cwd/mode, owns spec selection, selected-session reopening, and `/new`, creates/opens Pi sessions through `SessionManager`, writes `brunch.session_binding`, persists current spec/session acceleration in `.brunch/state.json`, and derives chrome state for callers. “Workspace” in this name refers to cwd scope, not a selectable product object. | +| **Workspace session coordinator** | The Brunch boot seam that returns `ready | select_spec | needs_human` workspace-session state for a cwd/mode, owns spec selection, selected-session reopening, and `/new`, creates/opens Pi sessions through `SessionManager`, writes `brunch.session_binding`, persists current spec/session acceleration in `.brunch/workspace.json`, and derives chrome state for callers. “Workspace” in this name refers to cwd scope, not a selectable product object. | | **Workspace state hierarchy** | `workspace(cwd) → spec → session`. Each level scopes the one below it; active spec/session activation is Brunch-owned before any agent loop runs, and spec selection persists across `/new`. | -| **Workspace default state** | Lightweight `.brunch/state.json` acceleration for reopening the last selected spec/session in a cwd. It is a launch/default convenience, not the canonical binding of a session, not an instruction to resume without product flow, and not a multi-client concurrency authority. | +| **Workspace default state** | Lightweight `.brunch/workspace.json` acceleration for reopening the last selected spec/session in a cwd. It is a launch/default convenience, not the canonical binding of a session, not an instruction to resume without product flow, and not a multi-client concurrency authority. | | **Spec/session selection model** | Brunch-owned hierarchy over cwd-scoped inventory. In TUI, it can render as a picker with a continue-last fast path, then a tree: create new spec → name it → implicit first session; resume existing spec → choose spec → create session or resume existing session → choose session. In RPC/headless modes, the same requirement is exposed as structured product state and activation methods, not a TUI dialog. The model returns a decision; the `WorkspaceSessionCoordinator` activates it and owns all Pi session and binding effects. | | **Intent graph** | The canonical specification-meaning plane. Authority over what the system is for. | | **Oracle graph** | Verification-strategy plane accountable to intent. Houses Checks, Validation Methods, Evidence, Obligations. | @@ -396,7 +396,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Subscription** | A long-lived RPC operation that delivers live updates, often with an initial snapshot, for views that must stay current with session, workspace, graph, or coherence state. | | **Transport adapter** | The stdio, WebSocket, HTTP-shim, Pi-RPC relay, or in-process wrapper around the same Brunch handlers. Transport adapters do not own product semantics. | | **Pi RPC adapter** | A private Brunch adapter that speaks Pi's RPC protocol for agent-loop mechanics and extension UI requests, translating Pi events/dialogs into Brunch product-shaped events or method results for public clients. | -| **Canonical store** | The persistence surface that owns a fact: Pi JSONL for session transcript truth, `.brunch/state.json` for lightweight workspace binding state, SQLite graph/change log for graph truth and coherence substrates. | +| **Canonical store** | The persistence surface that owns a fact: Pi JSONL for session transcript truth, `.brunch/workspace.json` for lightweight workspace binding state, SQLite graph/change log for graph truth and coherence substrates. | | **Elicitation prompt** | System- or assistant-originated transcript span that prompts/directs the user's next response. At idle, a Brunch-supported linear session ends with an unresolved elicitation prompt. | | **User response** | User-originated text and/or structured action selection responding to the current elicitation prompt. There is no ambient chat input in the POC model. | | **Elicitation exchange** | A derived projection over Brunch-supported linear Pi JSONL: prompt-side span (system/assistant/tool-side entries since the prior response, excluding terminal structured-exchange results) plus response-side span (the user's text, linked structured action entries, and/or terminal structured-exchange toolResult details). This is the default post-exchange capture unit. | @@ -483,7 +483,7 @@ The structural/behavioral split is the key discipline: never let a behavioral fi | Dimension | Score | Notes | Raised by | | --- | --- | --- | --- | -| Observability | partial, improving to high by M4/M5 | Text-native artifacts are planned (`.brunch/state.json`, Pi JSONL, command results, graph exports, coherence exports, fixture bundles). Generative-lens material adds further text-native surfaces: `brunch.review_set_proposal`, `brunch.establishment_offer`, `brunch.elicitor_intent_hint` entries plus reviewer-finding `reconciliation_need` records. *Structural* observability is high; *behavioral* observability (proposal quality, lens-recommendation appropriateness, reviewer precision) remains low and outer-loop only. M0 TUI chrome and M3 browser UX remain partly visual unless paired with artifact/query checks. | Probe oracles; projection handlers; graph/coherence exports; transcript projection of lens/establishment/proposal entries. | +| Observability | partial, improving to high by M4/M5 | Text-native artifacts are planned (`.brunch/workspace.json`, Pi JSONL, command results, graph exports, coherence exports, fixture bundles). Generative-lens material adds further text-native surfaces: `brunch.review_set_proposal`, `brunch.establishment_offer`, `brunch.elicitor_intent_hint` entries plus reviewer-finding `reconciliation_need` records. *Structural* observability is high; *behavioral* observability (proposal quality, lens-recommendation appropriateness, reviewer precision) remains low and outer-loop only. M0 TUI chrome and M3 browser UX remain partly visual unless paired with artifact/query checks. | Probe oracles; projection handlers; graph/coherence exports; transcript projection of lens/establishment/proposal entries. | | Reproducibility | partial | Fixture briefs and captured runs create a repeatable path. M1/M2 proved the agent-as-user harness and JSONL projection/reload discipline. LLM runs remain variable, so deterministic postcondition checks and property assertions are required; batch-proposal/review-set flows additionally need seeded multi-run probes to characterize structural-legality rate at all. Driver extension for review-cycle flows (approve / request-changes / reject) is conditional on cost being worth the controllability gain. | Deterministic probe checks; captured-run metadata; replay/property fixtures; (planned) review-cycle driver extension. | | Controllability | partial → high (conditional) | `npm run fix` / `npm run verify` are agent-controllable. The agent-as-user stdio RPC driver covers single-exchange flows end-to-end; extending it to drive review-cycle acceptance/regeneration would lift batch-proposal/review-set controllability to "high" but carries implementation cost. TUI/browser/manual flows for ambient affordances, in-flight reviewer signals, and chrome rendering remain probe-oracle territory. | Store/projection postcondition checkers; stdio/WebSocket drivers; (planned) review-cycle driver extension; probe oracles for chrome surfaces. | @@ -534,7 +534,7 @@ A **probe oracle** is the preferred bridge for seams that require human interact Probe postconditions should be boring and product-shaped: paths exist, JSON fields match, JSONL entries are present and unique, projections reconstruct the same state, command results carry expected discriminants. Store-only checks are acceptable before projection handlers exist; projection-including checks become the default once `workspace.*`, `session.*`, `graph.*`, or `coherence.*` handlers exist. -The first required probe is M0: after manual TUI interaction, a checker proves `.brunch/` creation, `.brunch/state.json` current spec acceleration, Pi session JSONL files, exactly one `brunch.session_binding` per session, same-spec `/new`, and workspace/session reconstruction when available. FE-744 extends this with a startup-switcher probe: launch Brunch against a workspace with an existing selected transcript, assert the pre-Pi switcher appears before transcript rendering, choose new-session vs resume paths explicitly, and pair the visual capture with store/projection checks for activated spec/session state. +The first required probe is M0: after manual TUI interaction, a checker proves `.brunch/` creation, `.brunch/workspace.json` current spec/session acceleration, Pi session JSONL files, exactly one `brunch.session_binding` per session, same-spec `/new`, and workspace/session reconstruction when available. FE-744 extends this with a startup-switcher probe: launch Brunch against a workspace with an existing selected transcript, assert the pre-Pi switcher appears before transcript rendering, choose new-session vs resume paths explicitly, and pair the visual capture with store/projection checks for activated spec/session state. ### Invariant Oracle Coverage @@ -580,7 +580,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` - **Deterministic before generative.** Probe runs should prefer deterministic or tightly scripted paths before relying on LLM persona variance. Generative/adversarial probes come after the transcript substrate is trusted. Retired M1 scripted captures proved the early transport/projection substrate on then-current terms, but tuple-shaped FE-744 public-RPC probes are the current evidence path. - **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, pending/respond lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. - **Capture analysis before graph persistence.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before graph persistence or before comparing later graph mutations against transcript evidence. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. -- **Projection handlers are oracles, not stores.** Read/subscription tests should prove handlers reconstruct truth from Brunch-supported linear Pi JSONL, `.brunch/state.json`, or SQLite graph/change log; they should not introduce a canonical view-store just for testing. +- **Projection handlers are oracles, not stores.** Read/subscription tests should prove handlers reconstruct truth from Brunch-supported linear Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log; they should not introduce a canonical view-store just for testing. - **Behavioral quality boundary.** Inner/middle loops prove structural validity, durable state, invariants, and expected graph/property coverage. “Good interview”, “good question”, and “coherent UX feel” remain outer-loop checklist/generative-fixture judgments until enough examples justify sharper metrics. - **Subscriptions are scoped for the POC.** Initial subscription oracles should prove initial snapshot plus ordered live updates by invalidating/refetching canonical projection handlers rather than introducing a view store. Reconnect/resume semantics are acknowledged but deferred unless a frontier explicitly depends on them. diff --git a/src/.pi/components/workspace-dialog/model.ts b/src/.pi/components/workspace-dialog/model.ts index a7ded7ea9..44813ff83 100644 --- a/src/.pi/components/workspace-dialog/model.ts +++ b/src/.pi/components/workspace-dialog/model.ts @@ -13,11 +13,11 @@ export type WorkspaceSelectionStage = | { stage: 'specList' } | { stage: 'specAction'; - specId: string; + specId: number; } | { stage: 'sessionList'; - specId: string; + specId: number; }; export interface WorkspaceSelectionOption { @@ -41,7 +41,7 @@ export interface WorkspaceSelectionView { stage: WorkspaceSelectionStage['stage']; title: string; options: WorkspaceSelectionOption[]; - specId?: string; + specId?: number; } export interface WorkspaceSelectionViewOptions { @@ -239,7 +239,7 @@ function findCurrentSession(inventory: WorkspaceLaunchInventory): WorkspaceLaunc function findSpec( inventory: WorkspaceLaunchInventory, - specId: string, + specId: number, ): WorkspaceLaunchInventory['specs'][number] | undefined { return inventory.specs.find((candidate) => candidate.spec.id === specId); } diff --git a/src/README.md b/src/README.md index 276653644..9a81d8815 100644 --- a/src/README.md +++ b/src/README.md @@ -56,7 +56,8 @@ Rules: Some files currently at `src/` root belong in `src/session/` per this layout (workspace-session-coordinator, session-binding, session-projection-reader, brunch-session-envelope, session-transcript, elicitation-exchange, -structured-exchange). Move incrementally as each file is touched. +structured-exchange). The active workspace file is `.brunch/workspace.json` +(`state.json` is retired). Move files incrementally as each file is touched. Prompt composition currently under `src/tui-client/.pi/context/` migrates to `src/agents/` per D52-L. The `.pi/context/` README describes the diff --git a/src/brunch-session-envelope.ts b/src/brunch-session-envelope.ts index bc606c608..be9c5d23a 100644 --- a/src/brunch-session-envelope.ts +++ b/src/brunch-session-envelope.ts @@ -38,21 +38,12 @@ export async function readBrunchSessionEnvelope(file: string): Promise header.id), - ...bindings.map((binding) => binding.sessionId), - ]), + observedSessionIds: uniqueStrings(headers.map((header) => header.id)), }; } const header = headers[0]!; const binding = bindings[0]!; - if (binding.sessionId !== header.id) { - return { - ok: false, - observedSessionIds: uniqueStrings([header.id, binding.sessionId]), - }; - } assertFileBackedTranscriptEntries(entries); return { ok: true, envelope: { header, binding, entries } }; diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index e556f6a8c..72817e94e 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -898,7 +898,7 @@ async function writeHostilePiSettings(cwd: string, agentDir: string): Promise { manager.appendCustomEntry( 'brunch.session_binding', createSessionBindingData({ - sessionId: manager.getSessionId(), - specId: 'spec-1', - specTitle: 'Spec', + specId: 1, }), ); manager.appendMessage(assistantMessage('Question')); diff --git a/src/elicitation-exchange.test.ts b/src/elicitation-exchange.test.ts index 0482bca9e..5f7f9a33a 100644 --- a/src/elicitation-exchange.test.ts +++ b/src/elicitation-exchange.test.ts @@ -192,9 +192,7 @@ function appendBinding(manager: SessionManager): void { manager.appendCustomEntry( 'brunch.session_binding', createSessionBindingData({ - sessionId: manager.getSessionId(), - specId: 'spec-1', - specTitle: 'Spec', + specId: 1, }), ); } diff --git a/src/graph/index.ts b/src/graph/index.ts index 55d10e746..65fbd9c1b 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -65,6 +65,7 @@ export type { } from './snapshot.js'; export { CommandExecutor } from './command-executor.js'; +export { openWorkspaceCommandExecutor } from './workspace-store.js'; export type { BatchEdgeInput, BatchEdgeRef, diff --git a/src/graph/workspace-store.ts b/src/graph/workspace-store.ts new file mode 100644 index 000000000..34b7ad0e4 --- /dev/null +++ b/src/graph/workspace-store.ts @@ -0,0 +1,14 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { createDb } from '../db/connection.js'; +import { CommandExecutor } from './command-executor.js'; + +const BRUNCH_DIR = '.brunch'; +const DATA_DB_FILE = 'data.db'; + +export async function openWorkspaceCommandExecutor(cwd: string): Promise { + const brunchDir = join(cwd, BRUNCH_DIR); + await mkdir(brunchDir, { recursive: true }); + return new CommandExecutor(createDb(join(brunchDir, DATA_DB_FILE))); +} diff --git a/src/print-snapshot.test.ts b/src/print-snapshot.test.ts index 095379d37..41cfaabe9 100644 --- a/src/print-snapshot.test.ts +++ b/src/print-snapshot.test.ts @@ -9,7 +9,7 @@ function readyState(): WorkspaceSessionState { return { status: 'ready', cwd, - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, session: { id: 'session-1', file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', @@ -17,7 +17,7 @@ function readyState(): WorkspaceSessionState { }, chrome: { cwd, - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, phase: 'elicitation', chatMode: 'responding-to-elicitation', }, @@ -31,7 +31,7 @@ describe('print snapshot', () => { expect(snapshot).toEqual({ status: 'ready', cwd, - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, session: { id: 'session-1', file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', @@ -42,7 +42,7 @@ describe('print snapshot', () => { }, }); expect(renderWorkspaceSnapshot(snapshot)).toContain('status: ready'); - expect(renderWorkspaceSnapshot(snapshot)).toContain('spec: Alpha spec (spec-1)'); + expect(renderWorkspaceSnapshot(snapshot)).toContain('spec: Alpha spec (1)'); expect(renderWorkspaceSnapshot(snapshot)).toContain('session: session-1'); }); diff --git a/src/print-snapshot.ts b/src/print-snapshot.ts index fc94fef46..36c1d9453 100644 --- a/src/print-snapshot.ts +++ b/src/print-snapshot.ts @@ -4,7 +4,7 @@ export interface WorkspaceSnapshot { status: WorkspaceSessionState['status']; cwd: string; spec: { - id: string; + id: number; title: string; } | null; session?: { diff --git a/src/probes/public-rpc-parity-proof.test.ts b/src/probes/public-rpc-parity-proof.test.ts index 42d99523a..ebed13080 100644 --- a/src/probes/public-rpc-parity-proof.test.ts +++ b/src/probes/public-rpc-parity-proof.test.ts @@ -20,7 +20,7 @@ describe('public Brunch RPC structured-exchange parity proof', () => { maxTurnBudget: 3, completedTurns: 3, friction: [], - specId: expect.any(String), + specId: expect.any(Number), sessionId: expect.any(String), }); expect(Date.parse(report.generatedAt)).not.toBeNaN(); diff --git a/src/probes/public-rpc-parity-proof.ts b/src/probes/public-rpc-parity-proof.ts index 85b52c32c..2a6770198 100644 --- a/src/probes/public-rpc-parity-proof.ts +++ b/src/probes/public-rpc-parity-proof.ts @@ -79,7 +79,7 @@ export interface PublicRpcParityProofReport { completedTurns: number; friction: string[]; cwd: string; - specId: string; + specId: number; sessionId: string; toolCoverage: string[]; exchangeIds: string[]; diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index f0bbf3439..299cd7fd7 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -42,17 +42,17 @@ function coordinator( function launchInventory(): WorkspaceLaunchInventory { return { cwd: '/tmp/brunch-project', - currentSpec: { id: 'spec-1', title: 'Alpha spec' }, + currentSpec: { id: 1, title: 'Alpha spec' }, currentSessionFile: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', needsNewSpec: false, specs: [ { - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, sessions: [ { id: 'session-1', file: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', - specId: 'spec-1', + specId: 1, specTitle: 'Alpha spec', available: true, }, @@ -75,7 +75,7 @@ function cancelledState(): WorkspaceActivationState { cwd: '/tmp/brunch-project', chrome: { cwd: '/tmp/brunch-project', - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, phase: 'elicitation', chatMode: 'responding-to-elicitation', }, @@ -86,7 +86,7 @@ function readyState(sessionFile: string): WorkspaceSessionReadyState { return { status: 'ready', cwd: '/tmp/brunch-project', - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, session: { id: 'session-1', file: sessionFile, @@ -94,7 +94,7 @@ function readyState(sessionFile: string): WorkspaceSessionReadyState { }, chrome: { cwd: '/tmp/brunch-project', - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, phase: 'elicitation', chatMode: 'responding-to-elicitation', }, @@ -148,9 +148,7 @@ function appendBinding(manager: SessionManager): void { manager.appendCustomEntry( 'brunch.session_binding', createSessionBindingData({ - sessionId: manager.getSessionId(), - specId: 'spec-1', - specTitle: 'Spec', + specId: 1, }), ); } @@ -208,16 +206,14 @@ function requestAnswerEntry(parentId = 'present-question-1') { }; } -function sessionBindingEntry(sessionId = 'session-1', specId = 'spec-1') { +function sessionBindingEntry(sessionId = 'session-1', specId = 1) { return { id: `binding-${sessionId}-${specId}`, type: 'custom', parentId: null, customType: 'brunch.session_binding', data: createSessionBindingData({ - sessionId, specId, - specTitle: 'Spec', }), }; } @@ -393,9 +389,9 @@ describe('JSON-RPC handlers', () => { status: 'select_spec', requiresSelection: true, cwd: '/tmp/brunch-project', - currentSpec: { id: 'spec-1', title: 'Alpha spec' }, + currentSpec: { id: 1, title: 'Alpha spec' }, currentSessionFile: '/tmp/brunch-project/.brunch/sessions/session-1.jsonl', - specs: [{ spec: { id: 'spec-1' }, sessions: [{ id: 'session-1' }] }], + specs: [{ spec: { id: 1 }, sessions: [{ id: 'session-1' }] }], unavailableSessions: [{ reason: 'missing_header' }], }, }); @@ -419,15 +415,15 @@ describe('JSON-RPC handlers', () => { const validDecisions: SpecSessionActivationDecision[] = [ { action: 'cancel' }, { action: 'newSpec', title: 'New spec' }, - { action: 'newSession', specId: 'spec-1' }, + { action: 'newSession', specId: 1 }, { action: 'continue', - specId: 'spec-1', + specId: 1, sessionFile: 'session-1.jsonl', }, { action: 'openSession', - specId: 'spec-1', + specId: 1, sessionFile: 'session-2.jsonl', }, ]; @@ -445,10 +441,10 @@ describe('JSON-RPC handlers', () => { id: 21 + index, result: decision.action === 'cancel' - ? { status: 'cancelled', spec: { id: 'spec-1' } } + ? { status: 'cancelled', spec: { id: 1 } } : { status: 'ready', - spec: { id: 'spec-1' }, + spec: { id: 1 }, session: { id: 'session-1' }, }, }); @@ -468,7 +464,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 22, method: 'workspace.activate', - params: { decision: { action: 'openSession', specId: 'spec-1' } }, + params: { decision: { action: 'openSession', specId: 1 } }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', @@ -502,7 +498,7 @@ describe('JSON-RPC handlers', () => { id: 1, result: { status: 'ready', - spec: { id: 'spec-1', title: 'Alpha spec' }, + spec: { id: 1, title: 'Alpha spec' }, session: { id: 'session-1' }, }, }); @@ -727,7 +723,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 149, method: 'session.pendingExchange', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', @@ -762,7 +758,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 150, method: 'session.elicitationExchanges', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', @@ -783,7 +779,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 151, method: 'session.transcriptDisplay', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', @@ -859,7 +855,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 153, method: 'session.pendingExchange', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }), ).resolves.toMatchObject({ result: { status: 'idle', exchange: null }, @@ -921,7 +917,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 154, method: 'session.pendingExchange', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }), ).resolves.toMatchObject({ result: { status: 'idle', exchange: null }, @@ -1476,7 +1472,7 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 10, method: 'session.elicitationExchanges', - params: { sessionId: workspace.session.id, specId: 'spec-other' }, + params: { sessionId: workspace.session.id, specId: 9999 }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', @@ -1563,13 +1559,13 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 19, error: { - code: -32005, - message: 'Brunch session self-description is invalid', + code: -32004, + message: 'Brunch session not found', }, }); }); - it('returns a product-shaped error when explicit binding and Pi header session ids disagree', async () => { + it('resolves explicit forked sessions by Pi header id with inherited binding', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-rpc-header-mismatch-')); await writeExplicitSessionFixture(cwd, [ { type: 'session', id: 'session-header', cwd }, @@ -1585,14 +1581,13 @@ describe('JSON-RPC handlers', () => { jsonrpc: '2.0', id: 18, method: 'session.elicitationExchanges', - params: { sessionId: 'session-binding' }, + params: { sessionId: 'session-header' }, }), ).resolves.toMatchObject({ jsonrpc: '2.0', id: 18, - error: { - code: -32005, - message: 'Brunch session self-description is invalid', + result: { + exchanges: [], }, }); }); diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index 6d8a43dd6..d0165e6d0 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -169,12 +169,13 @@ function workspaceActivationSnapshotFromState(state: WorkspaceActivationState): } const NonBlankStringSchema = Type.String({ minLength: 1, pattern: '\\S' }); +const PositiveIntegerSchema = Type.Integer({ minimum: 1 }); export const SpecSessionActivationDecisionSchema = Type.Union([ Type.Object( { action: Type.Literal('continue'), - specId: NonBlankStringSchema, + specId: PositiveIntegerSchema, sessionFile: NonBlankStringSchema, }, { additionalProperties: false }, @@ -182,7 +183,7 @@ export const SpecSessionActivationDecisionSchema = Type.Union([ Type.Object( { action: Type.Literal('openSession'), - specId: NonBlankStringSchema, + specId: PositiveIntegerSchema, sessionFile: NonBlankStringSchema, }, { additionalProperties: false }, @@ -190,7 +191,7 @@ export const SpecSessionActivationDecisionSchema = Type.Union([ Type.Object( { action: Type.Literal('newSession'), - specId: NonBlankStringSchema, + specId: PositiveIntegerSchema, }, { additionalProperties: false }, ), @@ -445,7 +446,7 @@ const PUBLIC_RPC_METHOD_DISCOVERY: RpcMethodDiscovery[] = [ params: { decision: { action: 'openSession', - specId: 'spec-1', + specId: 1, sessionFile: '.brunch/sessions/session-1.jsonl', }, }, @@ -463,7 +464,7 @@ const PUBLIC_RPC_METHOD_DISCOVERY: RpcMethodDiscovery[] = [ jsonrpc: '2.0', id: 6, method: 'session.elicitationExchanges', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }, ], }, @@ -478,7 +479,7 @@ const PUBLIC_RPC_METHOD_DISCOVERY: RpcMethodDiscovery[] = [ jsonrpc: '2.0', id: 7, method: 'session.transcriptDisplay', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }, ], }, @@ -502,7 +503,7 @@ const PUBLIC_RPC_METHOD_DISCOVERY: RpcMethodDiscovery[] = [ jsonrpc: '2.0', id: 10, method: 'session.pendingExchange', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }, ], }, @@ -1162,7 +1163,7 @@ function parseSessionProjectionParams(value: unknown): SessionProjectionParamsPa if ( typeof sessionId !== 'string' || sessionId.length === 0 || - (specId !== undefined && (typeof specId !== 'string' || specId.length === 0)) + (specId !== undefined && (typeof specId !== 'number' || !Number.isInteger(specId) || specId < 1)) ) { return { ok: false }; } diff --git a/src/session-binding.ts b/src/session-binding.ts index 24cc97831..3c40f1226 100644 --- a/src/session-binding.ts +++ b/src/session-binding.ts @@ -5,9 +5,7 @@ export const SESSION_BINDING_SCHEMA_VERSION = 1; export interface SessionBindingData { schemaVersion: typeof SESSION_BINDING_SCHEMA_VERSION; - sessionId: string; - specId: string; - specTitle: string; + specId: number; } export type SessionBindingEntry = CustomEntry & { @@ -15,16 +13,10 @@ export type SessionBindingEntry = CustomEntry & { data: SessionBindingData; }; -export function createSessionBindingData(options: { - sessionId: string; - specId: string; - specTitle: string; -}): SessionBindingData { +export function createSessionBindingData(options: { specId: number }): SessionBindingData { return { schemaVersion: SESSION_BINDING_SCHEMA_VERSION, - sessionId: options.sessionId, specId: options.specId, - specTitle: options.specTitle, }; } @@ -47,8 +39,7 @@ export function isSessionBindingData(value: unknown): value is SessionBindingDat typeof value === 'object' && value !== null && (value as { schemaVersion?: unknown }).schemaVersion === SESSION_BINDING_SCHEMA_VERSION && - typeof (value as { sessionId?: unknown }).sessionId === 'string' && - typeof (value as { specId?: unknown }).specId === 'string' && - typeof (value as { specTitle?: unknown }).specTitle === 'string' + typeof (value as { specId?: unknown }).specId === 'number' && + Number.isInteger((value as { specId: number }).specId) ); } diff --git a/src/session-projection-reader.ts b/src/session-projection-reader.ts index 4492d235a..cd6d1a12c 100644 --- a/src/session-projection-reader.ts +++ b/src/session-projection-reader.ts @@ -5,7 +5,7 @@ import { readBrunchSessionEnvelope, type BrunchSessionEnvelope } from './brunch- export interface ExplicitSessionProjectionParams { sessionId: string; - specId?: string; + specId?: number; } export type SessionProjectionTarget = @@ -53,7 +53,7 @@ export async function resolveExplicitSessionProjectionTarget( } function sessionIds(readResult: Awaited>): string[] { - return readResult.ok ? [readResult.envelope.binding.sessionId] : readResult.observedSessionIds; + return readResult.ok ? [readResult.envelope.header.id] : readResult.observedSessionIds; } function invalidSessionSelfDescription(): SessionProjectionTarget { diff --git a/src/session-transcript.test.ts b/src/session-transcript.test.ts index fbc2e2c3e..f20e385b3 100644 --- a/src/session-transcript.test.ts +++ b/src/session-transcript.test.ts @@ -14,7 +14,7 @@ describe('session transcript renderer', () => { id: 'binding-1', type: 'custom', customType: 'brunch.session_binding', - data: { specId: 'spec-1', specTitle: 'Demo spec' }, + data: { schemaVersion: 1, specId: 1 }, }), line({ id: 'generic-tool-1', diff --git a/src/session/README.md b/src/session/README.md index 55af3f8da..b01027d4a 100644 --- a/src/session/README.md +++ b/src/session/README.md @@ -14,9 +14,10 @@ plus the coordination logic for workspace/spec/session lifecycle. span + response-side span, per D13-L. - **Workspace coordination** — boot flow, spec/session selection, - `.brunch/state.json` management. The `WorkspaceSessionCoordinator` + `.brunch/workspace.json` management. The `WorkspaceSessionCoordinator` is the only module that creates/opens Pi sessions for Brunch user flows - and writes `brunch.session_binding`. + and writes collapsed `brunch.session_binding` entries (`{schemaVersion, + specId}`). - **Session binding** — session↔spec binding entries in JSONL. diff --git a/src/web-client/app.test.tsx b/src/web-client/app.test.tsx index a558d7420..dd50191fd 100644 --- a/src/web-client/app.test.tsx +++ b/src/web-client/app.test.tsx @@ -20,7 +20,7 @@ interface RpcCall { const readySnapshot: WorkspaceSnapshot = { status: 'ready', cwd: '/tmp/brunch-project', - spec: { id: 'spec-1', title: 'Web spec' }, + spec: { id: 1, title: 'Web spec' }, session: { id: 'session-1', file: '/tmp/session.jsonl' }, chrome: { phase: 'elicitation', @@ -116,7 +116,7 @@ describe('Brunch React web app', () => { expect(calls).toContainEqual({ method: 'workspace.snapshot' }); expect(calls).toContainEqual({ method: 'session.transcriptDisplay', - params: { sessionId: 'session-1', specId: 'spec-1' }, + params: { sessionId: 'session-1', specId: 1 }, }); }); diff --git a/src/web-client/app.tsx b/src/web-client/app.tsx index 908abf6ca..2ff466542 100644 --- a/src/web-client/app.tsx +++ b/src/web-client/app.tsx @@ -19,7 +19,7 @@ type RouterContext = { type SessionProjectionTarget = { sessionId: string; - specId: string; + specId: number; }; export interface BrunchWebRuntime { diff --git a/src/workspace-session-coordinator.test.ts b/src/workspace-session-coordinator.test.ts index d8a7f2531..ef3842f79 100644 --- a/src/workspace-session-coordinator.test.ts +++ b/src/workspace-session-coordinator.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -29,7 +29,7 @@ describe('WorkspaceSessionCoordinator', () => { expect(result.status).toBe('ready'); expect(result.chrome.cwd).toBe(cwd); - expect(result.chrome.spec?.id).toMatch(/^spec-/u); + expect(result.chrome.spec?.id).toBeTypeOf('number'); expect(result.chrome.spec?.title).toBe('Scratch spec'); expect(result.chrome.phase).toBe('elicitation'); expect(result.chrome.chatMode).toBe('responding-to-elicitation'); @@ -74,10 +74,10 @@ describe('WorkspaceSessionCoordinator', () => { .find((entry) => isCustomEntry(entry) && entry.customType === SESSION_BINDING_TYPE); expect(firstBinding).toMatchObject({ - data: { specId: first.spec.id, specTitle: 'Scratch spec' }, + data: { specId: first.spec.id }, }); expect(secondBinding).toMatchObject({ - data: { specId: first.spec.id, specTitle: 'Scratch spec' }, + data: { specId: first.spec.id }, }); const oracle = await verifyWorkspaceSessionStores({ @@ -109,9 +109,7 @@ describe('WorkspaceSessionCoordinator', () => { expect(bindings[0]).toMatchObject({ customType: SESSION_BINDING_TYPE, data: { - sessionId: result.session.id, specId: result.spec.id, - specTitle: result.spec.title, }, }); }); @@ -161,9 +159,7 @@ describe('WorkspaceSessionCoordinator', () => { expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ data: { - sessionId: result.session.id, specId: result.spec.id, - specTitle: 'Scratch spec', }, }); }); @@ -249,7 +245,7 @@ describe('WorkspaceSessionCoordinator', () => { specTitle: 'Beta', createNewSpec: true, }); - const beforeState = await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'); + const beforeState = await readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8'); const beforeFirst = await readFile(first.session.file, 'utf8'); const beforeSecond = await readFile(second.session.file, 'utf8'); @@ -279,7 +275,7 @@ describe('WorkspaceSessionCoordinator', () => { }), ]); expect(inventory.unavailableSessions).toEqual([]); - await expect(readFile(join(cwd, '.brunch', 'state.json'), 'utf8')).resolves.toBe(beforeState); + await expect(readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8')).resolves.toBe(beforeState); await expect(readFile(first.session.file, 'utf8')).resolves.toBe(beforeFirst); await expect(readFile(second.session.file, 'utf8')).resolves.toBe(beforeSecond); }); @@ -321,9 +317,7 @@ describe('WorkspaceSessionCoordinator', () => { customType: SESSION_BINDING_TYPE, data: { schemaVersion: 1, - sessionId: 'other-session', specId: ready.spec.id, - specTitle: ready.spec.title, }, })}\n`, 'utf8', @@ -334,12 +328,9 @@ describe('WorkspaceSessionCoordinator', () => { const inventory = await coordinator.inspectWorkspace(); expect(inventory.specs).toHaveLength(1); - expect(inventory.specs[0]?.sessions).toHaveLength(1); + expect(inventory.specs[0]?.sessions).toHaveLength(2); + expect(inventory.specs[0]?.sessions.map((session) => session.file)).toContain(mismatchedFile); expect(inventory.unavailableSessions).toEqual([ - expect.objectContaining({ - file: mismatchedFile, - reason: 'incompatible_binding', - }), expect.objectContaining({ file: unboundFile, reason: 'missing_binding' }), ]); await expect(readFile(unboundFile, 'utf8')).resolves.toBe(beforeUnbound); @@ -382,9 +373,8 @@ describe('WorkspaceSessionCoordinator', () => { } expect(continued.spec).toEqual(second.spec); expect(continued.session.id).toBe(second.session.id); - expect(JSON.parse(await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'))).toMatchObject({ - currentSpec: second.spec, - currentSessionFile: second.session.file, + expect(JSON.parse(await readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8'))).toMatchObject({ + current: { specId: second.spec.id, sessionId: second.session.id }, }); }); @@ -443,13 +433,13 @@ describe('WorkspaceSessionCoordinator', () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); const coordinator = createWorkspaceSessionCoordinator({ cwd }); const ready = await coordinator.createSetupSession({ specTitle: 'Alpha' }); - const beforeState = await readFile(join(cwd, '.brunch', 'state.json'), 'utf8'); + const beforeState = await readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8'); const beforeSession = await readFile(ready.session.file, 'utf8'); const result = await coordinator.activateWorkspace({ action: 'cancel' }); expect(result.status).toBe('cancelled'); - await expect(readFile(join(cwd, '.brunch', 'state.json'), 'utf8')).resolves.toBe(beforeState); + await expect(readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8')).resolves.toBe(beforeState); await expect(readFile(ready.session.file, 'utf8')).resolves.toBe(beforeSession); }); @@ -471,7 +461,7 @@ describe('WorkspaceSessionCoordinator', () => { }); const mismatched = await coordinator.activateWorkspace({ action: 'openSession', - specId: 'spec-missing', + specId: 9999, sessionFile: ready.session.file, }); @@ -479,9 +469,8 @@ describe('WorkspaceSessionCoordinator', () => { expect(mismatched.status).toBe('needs_human'); }); - it('asks for spec selection when no current spec exists and creation is not allowed', async () => { + it('scaffolds workspace.json and data.db when no current spec exists', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); - await mkdir(join(cwd, '.brunch'), { recursive: true }); const coordinator = createWorkspaceSessionCoordinator({ cwd }); const result = await coordinator.openDefaultWorkspace(); @@ -489,6 +478,19 @@ describe('WorkspaceSessionCoordinator', () => { expect(result.status).toBe('select_spec'); expect(result.chrome.cwd).toBe(cwd); expect(result.chrome.spec).toBeNull(); + await expect(stat(join(cwd, '.brunch', 'data.db'))).resolves.toMatchObject({}); + expect(JSON.parse(await readFile(join(cwd, '.brunch', 'workspace.json'), 'utf8'))).toMatchObject({ + project: expect.objectContaining({ name: expect.any(String), slug: expect.any(String) }), + current: null, + posture: { + certainty: '', + stakes: '', + audience: '', + horizon: '', + migration: '', + sourcing: '', + }, + }); }); it('generates a display name for new sessions and persists it as session_info', async () => { diff --git a/src/workspace-session-coordinator.ts b/src/workspace-session-coordinator.ts index 98b3692ee..b8569d7c9 100644 --- a/src/workspace-session-coordinator.ts +++ b/src/workspace-session-coordinator.ts @@ -1,9 +1,10 @@ -import { randomUUID } from 'node:crypto'; import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { SessionManager, type SessionHeader } from '@earendil-works/pi-coding-agent'; +import { openWorkspaceCommandExecutor, type SpecRecord } from './graph/index.js'; +import { discoverProjectIdentity } from './project-identity.js'; import { createSessionBindingData, isSessionBindingEntry, @@ -12,19 +13,39 @@ import { } from './session-binding.js'; const BRUNCH_DIR = '.brunch'; -const STATE_FILE = 'state.json'; +const STATE_FILE = 'workspace.json'; const SESSION_DIR = 'sessions'; const STATE_SCHEMA_VERSION = 1; export interface WorkspaceSpecState { - id: string; + id: number; title: string; } +interface WorkspaceProjectState { + name: string; + slug: string; +} + +interface WorkspacePostureState { + certainty: string; + stakes: string; + audience: string; + horizon: string; + migration: string; + sourcing: string; +} + +interface WorkspaceCurrentState { + specId: number; + sessionId: string; +} + interface WorkspaceStateFile { schemaVersion: 1; - currentSpec: WorkspaceSpecState; - currentSessionFile?: string; + project: WorkspaceProjectState; + current: WorkspaceCurrentState | null; + posture: WorkspacePostureState; } export interface WorkspaceSessionChromeState { @@ -73,19 +94,19 @@ export type WorkspaceSessionState = export interface WorkspaceContinueDecision { action: 'continue'; - specId: string; + specId: number; sessionFile: string; } export interface WorkspaceOpenSessionDecision { action: 'openSession'; - specId: string; + specId: number; sessionFile: string; } export interface WorkspaceNewSessionDecision { action: 'newSession'; - specId: string; + specId: number; } export interface WorkspaceNewSpecDecision { @@ -112,7 +133,7 @@ export type WorkspaceActivationState = export interface WorkspaceLaunchSession { id: string; file: string; - specId: string; + specId: number; specTitle: string; name?: string; available: true; @@ -192,10 +213,11 @@ class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { async activateWorkspace(decision: SpecSessionActivationDecision): Promise { if (decision.action === 'cancel') { const state = await readWorkspaceState(this.#cwd); + const spec = state ? await currentSpecFromState(this.#cwd, state) : null; return { status: 'cancelled', cwd: this.#cwd, - chrome: chromeState(this.#cwd, state?.currentSpec ?? null), + chrome: chromeState(this.#cwd, spec), }; } @@ -219,7 +241,7 @@ class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { if (decision.action === 'newSession') { const session = await createBoundSession(this.#cwd, spec.spec); - await writeCurrentWorkspaceState(this.#cwd, spec.spec, session.file); + await writeCurrentWorkspaceState(this.#cwd, spec.spec, session.id); return readyState(this.#cwd, spec.spec, session); } @@ -234,13 +256,14 @@ class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { const manager = SessionManager.open(session.file, sessionDir(this.#cwd), this.#cwd); const opened = bindSessionToSpec(manager, spec.spec); - await writeCurrentWorkspaceState(this.#cwd, spec.spec, opened.file); + await writeCurrentWorkspaceState(this.#cwd, spec.spec, opened.id); return readyState(this.#cwd, spec.spec, opened); } async openDefaultWorkspace(): Promise { - const state = await readWorkspaceState(this.#cwd); - if (!state) { + const state = await readOrCreateWorkspaceState(this.#cwd); + const current = state.current; + if (!current) { return { status: 'select_spec', cwd: this.#cwd, @@ -248,26 +271,36 @@ class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { }; } - const session = await openCurrentSession(this.#cwd, state.currentSpec, state.currentSessionFile); - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); - return readyState(this.#cwd, state.currentSpec, session); + const spec = await getSpecState(this.#cwd, current.specId); + if (!spec) { + return needsHumanState(this.#cwd, null, 'Current spec is missing from the workspace database.'); + } + + const session = await openCurrentSession(this.#cwd, spec, current.sessionId); + if (!session) { + return needsHumanState(this.#cwd, spec, 'Current session is missing or stale.'); + } + await writeCurrentWorkspaceState(this.#cwd, spec, session.id); + return readyState(this.#cwd, spec, session); } async createSetupSession(options?: { specTitle?: string; createNewSpec?: boolean; }): Promise { - await ensureWorkspaceDirs(this.#cwd); - const existing = await readWorkspaceState(this.#cwd); - const spec = existing && !options?.createNewSpec ? existing.currentSpec : createSpec(options?.specTitle); + const state = await readOrCreateWorkspaceState(this.#cwd); + const existing = + state.current && !options?.createNewSpec ? await getSpecState(this.#cwd, state.current.specId) : null; + const spec = existing ?? (await createSpec(this.#cwd, options?.specTitle)); const session = await createBoundSession(this.#cwd, spec); - await writeCurrentWorkspaceState(this.#cwd, spec, session.file); + await writeCurrentWorkspaceState(this.#cwd, spec, session.id); return readyState(this.#cwd, spec, session); } async createSetupSessionForCurrentSpec(): Promise { const state = await readWorkspaceState(this.#cwd); - if (!state) { + const spec = state ? await currentSpecFromState(this.#cwd, state) : null; + if (!spec) { return { status: 'needs_human', cwd: this.#cwd, @@ -276,30 +309,55 @@ class FileWorkspaceSessionCoordinator implements WorkspaceSessionCoordinator { }; } - const session = await createBoundSession(this.#cwd, state.currentSpec); - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); - return readyState(this.#cwd, state.currentSpec, session); + const session = await createBoundSession(this.#cwd, spec); + await writeCurrentWorkspaceState(this.#cwd, spec, session.id); + return readyState(this.#cwd, spec, session); } async bindCurrentSpecToReplacementSession(manager: SessionManager): Promise { const state = await readWorkspaceState(this.#cwd); - if (!state) { + const spec = state ? await currentSpecFromState(this.#cwd, state) : null; + if (!spec) { throw new Error('No current spec is selected for this workspace.'); } - const session = bindSessionToSpec(manager, state.currentSpec); - await writeCurrentWorkspaceState(this.#cwd, state.currentSpec, session.file); - return readyState(this.#cwd, state.currentSpec, session); + const session = bindSessionToSpec(manager, spec); + await writeCurrentWorkspaceState(this.#cwd, spec, session.id); + return readyState(this.#cwd, spec, session); } async deriveDefaultChromeState(): Promise { const state = await readWorkspaceState(this.#cwd); - return chromeState(this.#cwd, state?.currentSpec ?? null); + const spec = state ? await currentSpecFromState(this.#cwd, state) : null; + return chromeState(this.#cwd, spec); + } +} + +async function createSpec(cwd: string, title = 'Untitled spec'): Promise { + const executor = await openWorkspaceCommandExecutor(cwd); + const result = executor.createSpec({ name: title, slug: slugifySpecName(title) }); + if (result.status !== 'success') { + throw new Error(`Unable to create spec: ${result.diagnostics.map((d) => d.message).join(', ')}`); } + return { id: result.specId, title }; +} + +async function getSpecState(cwd: string, specId: number): Promise { + const executor = await openWorkspaceCommandExecutor(cwd); + const spec = executor.getSpec(specId); + return spec ? specStateFromRecord(spec) : null; +} + +function specStateFromRecord(spec: SpecRecord): WorkspaceSpecState { + return { id: spec.id, title: spec.name }; } -function createSpec(title = 'Untitled spec'): WorkspaceSpecState { - return { id: `spec-${randomUUID()}`, title }; +function slugifySpecName(name: string): string { + const slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + return slug.length > 0 ? slug : 'spec'; } async function createBoundSession( @@ -316,7 +374,7 @@ async function createBoundSession( return bindSessionToSpec(manager, spec, existingSessionCount + 1); } -async function countSessionsForSpec(cwd: string, specId: string): Promise { +async function countSessionsForSpec(cwd: string, specId: number): Promise { const files = await listSessionFiles(cwd); let count = 0; for (const file of files) { @@ -331,20 +389,18 @@ async function countSessionsForSpec(cwd: string, specId: string): Promise { + currentSessionId: string, +): Promise { await ensureWorkspaceDirs(cwd); const files = await listSessionFiles(cwd); - const manager = currentSessionFile - ? SessionManager.open(currentSessionFile, sessionDir(cwd), cwd) - : files.length === 0 - ? SessionManager.create(cwd, sessionDir(cwd)) - : SessionManager.continueRecent(cwd, sessionDir(cwd)); - const sessionFile = manager.getSessionFile(); - if (!sessionFile) { - throw new Error('Pi SessionManager did not open a persisted session file'); + for (const file of files) { + const inspected = await inspectSessionFile(file); + if (inspected.available && inspected.id === currentSessionId && inspected.specId === spec.id) { + const manager = SessionManager.open(file, sessionDir(cwd), cwd); + return bindSessionToSpec(manager, spec); + } } - return bindSessionToSpec(manager, spec); + return null; } function bindSessionToSpec( @@ -362,9 +418,7 @@ function bindSessionToSpec( manager.appendCustomEntry( SESSION_BINDING_TYPE, createSessionBindingData({ - sessionId: manager.getSessionId(), specId: spec.id, - specTitle: spec.title, }), ); // Generate and persist a display name for new sessions @@ -372,11 +426,7 @@ function bindSessionToSpec( const displayName = sessionDisplayName(spec.title, sessionOrdinal); manager.appendSessionInfo(displayName); } - } else if ( - existingBindings.length !== 1 || - existingBindings[0]?.data.sessionId !== manager.getSessionId() || - existingBindings[0].data.specId !== spec.id - ) { + } else if (existingBindings.length !== 1 || existingBindings[0]?.data.specId !== spec.id) { throw new Error('Session already has an incompatible Brunch session binding'); } @@ -427,8 +477,9 @@ async function readWorkspaceState(cwd: string): Promise; if ( parsed.schemaVersion === STATE_SCHEMA_VERSION && - typeof parsed.currentSpec?.id === 'string' && - typeof parsed.currentSpec.title === 'string' + isProjectState(parsed.project) && + (parsed.current === null || isCurrentState(parsed.current)) && + isPostureState(parsed.posture) ) { return parsed as WorkspaceStateFile; } @@ -441,15 +492,74 @@ async function readWorkspaceState(cwd: string): Promise { + const existing = await readWorkspaceState(cwd); + if (existing) return existing; + const identity = await discoverProjectIdentity(cwd); + const state: WorkspaceStateFile = { + schemaVersion: STATE_SCHEMA_VERSION, + project: { name: identity.name, slug: identity.slug }, + current: null, + posture: emptyWorkspacePosture(), + }; + await writeWorkspaceState(cwd, state); + await openWorkspaceCommandExecutor(cwd); + return state; +} + +async function currentSpecFromState( + cwd: string, + state: WorkspaceStateFile, +): Promise { + return state.current ? getSpecState(cwd, state.current.specId) : null; +} + +function isProjectState(value: unknown): value is WorkspaceProjectState { + return ( + typeof value === 'object' && + value !== null && + typeof (value as { name?: unknown }).name === 'string' && + typeof (value as { slug?: unknown }).slug === 'string' + ); +} + +function isCurrentState(value: unknown): value is WorkspaceCurrentState { + return ( + typeof value === 'object' && + value !== null && + typeof (value as { specId?: unknown }).specId === 'number' && + Number.isInteger((value as { specId: number }).specId) && + typeof (value as { sessionId?: unknown }).sessionId === 'string' + ); +} + +function isPostureState(value: unknown): value is WorkspacePostureState { + return ( + typeof value === 'object' && + value !== null && + typeof (value as { certainty?: unknown }).certainty === 'string' && + typeof (value as { stakes?: unknown }).stakes === 'string' && + typeof (value as { audience?: unknown }).audience === 'string' && + typeof (value as { horizon?: unknown }).horizon === 'string' && + typeof (value as { migration?: unknown }).migration === 'string' && + typeof (value as { sourcing?: unknown }).sourcing === 'string' + ); +} + +function emptyWorkspacePosture(): WorkspacePostureState { + return { certainty: '', stakes: '', audience: '', horizon: '', migration: '', sourcing: '' }; +} + async function inspectWorkspaceInventory(cwd: string): Promise { - const state = await readWorkspaceState(cwd); + const state = await readOrCreateWorkspaceState(cwd); const files = await listSessionFiles(cwd); - const specsById = new Map(); + const specsById = new Map(); const unavailableSessions: WorkspaceUnavailableSession[] = []; + const currentSpec = await currentSpecFromState(cwd, state); - if (state) { - specsById.set(state.currentSpec.id, { - spec: state.currentSpec, + if (currentSpec) { + specsById.set(currentSpec.id, { + spec: currentSpec, sessions: [], }); } @@ -457,11 +567,13 @@ async function inspectWorkspaceInventory(cwd: string): Promise left.spec.title.localeCompare(right.spec.title)); + const currentSessionFile = state.current + ? (specs.flatMap((spec) => spec.sessions).find((session) => session.id === state.current?.sessionId) + ?.file ?? null) + : null; + return { cwd, - currentSpec: state?.currentSpec ?? null, - currentSessionFile: state?.currentSessionFile ?? null, + currentSpec, + currentSessionFile, needsNewSpec: specs.length === 0, specs, unavailableSessions: unavailableSessions.sort((left, right) => left.file.localeCompare(right.file)), @@ -499,7 +616,7 @@ async function inspectSessionFile(file: string): Promise { } const binding = bindings[0]!; - if (bindings.length !== 1 || binding.data.sessionId !== header.id) { + if (bindings.length !== 1) { return { file, reason: 'incompatible_binding', available: false }; } @@ -514,14 +631,14 @@ async function inspectSessionFile(file: string): Promise { id: header.id, file, specId: binding.data.specId, - specTitle: binding.data.specTitle, + specTitle: '', ...(name != null ? { name } : {}), available: true, }; } function getOrCreateLaunchSpec( - specsById: Map, + specsById: Map, spec: WorkspaceSpecState, ): WorkspaceLaunchSpec { const existing = specsById.get(spec.id); @@ -541,12 +658,12 @@ async function writeWorkspaceState(cwd: string, state: WorkspaceStateFile): Prom async function writeCurrentWorkspaceState( cwd: string, spec: WorkspaceSpecState, - currentSessionFile: string, + currentSessionId: string, ): Promise { + const existing = await readOrCreateWorkspaceState(cwd); await writeWorkspaceState(cwd, { - schemaVersion: STATE_SCHEMA_VERSION, - currentSpec: spec, - currentSessionFile, + ...existing, + current: { specId: spec.id, sessionId: currentSessionId }, }); } @@ -593,7 +710,7 @@ export interface WorkspaceStoreOracleOptions { export interface WorkspaceStoreOracleSuccess { ok: true; - specId: string; + specId: number | null; sessions: Array<{ file: string; sessionId: string; @@ -616,7 +733,7 @@ export async function verifyWorkspaceSessionStores( const errors: string[] = []; const state = await readWorkspaceState(cwd); if (!state) { - return { ok: false, errors: ['Missing or invalid .brunch/state.json'] }; + return { ok: false, errors: ['Missing or invalid .brunch/workspace.json'] }; } const files = await listSessionFiles(cwd); @@ -639,11 +756,8 @@ export async function verifyWorkspaceSessionStores( continue; } const binding = bindings[0]!.data; - if (binding.specId !== state.currentSpec.id) { - errors.push(`${file} binding spec ${binding.specId} does not match state ${state.currentSpec.id}`); - } - if (binding.sessionId !== header.id) { - errors.push(`${file} binding session ${binding.sessionId} does not match header ${header.id}`); + if (state.current && binding.specId !== state.current.specId) { + errors.push(`${file} binding spec ${binding.specId} does not match state ${state.current.specId}`); } sessions.push({ file, @@ -653,7 +767,9 @@ export async function verifyWorkspaceSessionStores( }); } - return errors.length === 0 ? { ok: true, specId: state.currentSpec.id, sessions } : { ok: false, errors }; + return errors.length === 0 + ? { ok: true, specId: state.current?.specId ?? null, sessions } + : { ok: false, errors }; } async function listSessionFiles(cwd: string): Promise { From 945d59656fe6d4208922da0fe2ff79d9467575b2 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 12:01:36 +0200 Subject: [PATCH 12/34] Move src/ to D52-L topology; reconcile spec-persistence - Move session-domain files to src/session/ (coordinator, binding, envelope, projection-reader, transcript, elicitation/structured-exchange, project-identity) - Move web-host to src/rpc/, brunch-pi-profile to src/.pi/, web-client/ to src/web/, test-helpers to src/probes/; delete empty snapshot-{graph,nodes} stubs - Fix imports (tsc-driven), web entry path, source-path test reads, and web-host dist-web resolution for the new locations - Reconcile src/ and session/ READMEs; PLAN branch note; trim specTitle placeholder --- index.html | 2 +- memory/PLAN.md | 4 ++-- src/{ => .pi}/brunch-pi-profile.ts | 0 .../components/workspace-dialog/component.ts | 2 +- src/.pi/components/workspace-dialog/model.ts | 2 +- .../components/workspace-dialog/preflight.ts | 2 +- src/.pi/extensions/chrome.ts | 2 +- src/.pi/extensions/snapshot-graph.ts | 0 src/.pi/extensions/snapshot-nodes.ts | 0 .../extensions/structured-exchange/index.ts | 2 +- .../shared/editor-fallback.ts | 2 +- src/.pi/extensions/workspace-dialog.ts | 2 +- src/README.md | 18 ++++++++++-------- src/brunch-tui.test.ts | 10 +++++----- src/brunch-tui.ts | 10 +++++----- src/brunch.test.ts | 6 +++--- src/brunch.ts | 4 ++-- src/print-snapshot.test.ts | 2 +- src/print-snapshot.ts | 2 +- src/probes/check-workspace-session-stores.ts | 2 +- src/probes/public-rpc-parity-proof.ts | 4 ++-- src/{ => probes}/test-helpers.ts | 0 src/rpc/handlers.test.ts | 8 ++++---- src/rpc/handlers.ts | 10 +++++----- src/{ => rpc}/web-host.test.ts | 6 +++--- src/{ => rpc}/web-host.ts | 8 ++++---- src/session/README.md | 10 ++++------ src/{ => session}/brunch-session-envelope.ts | 0 src/{ => session}/elicitation-exchange.test.ts | 2 +- src/{ => session}/elicitation-exchange.ts | 4 ++-- .../jsonl-session-viability.test.ts | 2 +- src/{ => session}/project-identity.test.ts | 0 src/{ => session}/project-identity.ts | 0 src/{ => session}/session-binding.ts | 0 src/{ => session}/session-projection-reader.ts | 0 src/{ => session}/session-transcript.test.ts | 0 src/{ => session}/session-transcript.ts | 2 +- src/{ => session}/structured-exchange.ts | 0 .../workspace-session-coordinator.test.ts | 2 +- .../workspace-session-coordinator.ts | 5 ++--- src/{web-client => web}/app.test.tsx | 2 +- src/{web-client => web}/app.tsx | 2 +- src/{web-client => web}/main.tsx | 0 src/{web-client => web}/rpc-client.test.ts | 0 src/{web-client => web}/rpc-client.ts | 0 45 files changed, 70 insertions(+), 71 deletions(-) rename src/{ => .pi}/brunch-pi-profile.ts (100%) delete mode 100644 src/.pi/extensions/snapshot-graph.ts delete mode 100644 src/.pi/extensions/snapshot-nodes.ts rename src/{ => probes}/test-helpers.ts (100%) rename src/{ => rpc}/web-host.test.ts (99%) rename src/{ => rpc}/web-host.ts (95%) rename src/{ => session}/brunch-session-envelope.ts (100%) rename src/{ => session}/elicitation-exchange.test.ts (99%) rename src/{ => session}/elicitation-exchange.ts (98%) rename src/{ => session}/jsonl-session-viability.test.ts (99%) rename src/{ => session}/project-identity.test.ts (100%) rename src/{ => session}/project-identity.ts (100%) rename src/{ => session}/session-binding.ts (100%) rename src/{ => session}/session-projection-reader.ts (100%) rename src/{ => session}/session-transcript.test.ts (100%) rename src/{ => session}/session-transcript.ts (98%) rename src/{ => session}/structured-exchange.ts (100%) rename src/{ => session}/workspace-session-coordinator.test.ts (99%) rename src/{ => session}/workspace-session-coordinator.ts (99%) rename src/{web-client => web}/app.test.tsx (98%) rename src/{web-client => web}/app.tsx (98%) rename src/{web-client => web}/main.tsx (100%) rename src/{web-client => web}/rpc-client.test.ts (100%) rename src/{web-client => web}/rpc-client.ts (100%) diff --git a/index.html b/index.html index 50a09ed34..a465c6cc0 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,6 @@
- + diff --git a/memory/PLAN.md b/memory/PLAN.md index b4686cd94..39104acf8 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -78,8 +78,8 @@ The POC should maximize assumption falsification rather than merely implement mi ### spec-persistence-and-startup - **Name:** Spec persistence and startup integrity -- **Linear:** unassigned (create in FE / brunch when branch opens) -- **Branch:** to create — `ln/-spec-persistence-startup` +- **Linear:** none — folded into FE-785 (`agent-graph-integration`); no separate issue created. +- **Branch:** landed on `ln/fe-785-agent-graph-integration` (commits `c1053963`, `5e5cf1a1`); not branched separately, contrary to the one-branch-per-frontier default. - **Kind:** structural (persistence-model correction + startup-path regression repair) - **Status:** done - **Objective:** Restore the DB-on-startup path lost in the rebuild and correct the workspace/spec/session persistence model so Brunch initializes and boots correctly under all foreseeable conditions, reading each fact from its canonical home. Concretely: **(a)** model specs as DB rows (`specs{id:int, name, slug, readiness_grade}`), retiring `elicitation_posture` and `commitment_focus`; **(b)** create `.brunch/data.db` if absent at startup and route spec create/read/grade-update through the `CommandExecutor`; **(c)** rename `.brunch/state.json` → `.brunch/workspace.json` and reshape to `{project:{name,slug}, current:{specId:int, sessionId}, posture:}`, dropping the dead `source` field; **(d)** collapse the `brunch.session_binding` entry to `{specId:int}`, dropping `sessionId`/`specTitle` and the `sessionId !== header.id` self-guard (fork-portable); **(e)** resolve spec names from the DB, not from JSONL or workspace state. diff --git a/src/brunch-pi-profile.ts b/src/.pi/brunch-pi-profile.ts similarity index 100% rename from src/brunch-pi-profile.ts rename to src/.pi/brunch-pi-profile.ts diff --git a/src/.pi/components/workspace-dialog/component.ts b/src/.pi/components/workspace-dialog/component.ts index a205302cd..9cb6d70e2 100644 --- a/src/.pi/components/workspace-dialog/component.ts +++ b/src/.pi/components/workspace-dialog/component.ts @@ -8,7 +8,7 @@ import { Key, matchesKey, truncateToWidth, visibleWidth, type Component } from ' import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from '../../../workspace-session-coordinator.js'; +} from '../../../session/workspace-session-coordinator.js'; import { formatBrunchProductIdentity, readBrunchAnsiLogo, diff --git a/src/.pi/components/workspace-dialog/model.ts b/src/.pi/components/workspace-dialog/model.ts index 44813ff83..982d3bf6c 100644 --- a/src/.pi/components/workspace-dialog/model.ts +++ b/src/.pi/components/workspace-dialog/model.ts @@ -2,7 +2,7 @@ import type { WorkspaceLaunchInventory, WorkspaceLaunchSession, SpecSessionActivationDecision, -} from '../../../workspace-session-coordinator.js'; +} from '../../../session/workspace-session-coordinator.js'; export type WorkspaceSelectionStage = | { stage: 'home' } diff --git a/src/.pi/components/workspace-dialog/preflight.ts b/src/.pi/components/workspace-dialog/preflight.ts index 8f1cb2ba1..bccd1432a 100644 --- a/src/.pi/components/workspace-dialog/preflight.ts +++ b/src/.pi/components/workspace-dialog/preflight.ts @@ -4,7 +4,7 @@ import { ProcessTerminal, TUI, type Terminal } from '@earendil-works/pi-tui'; import type { WorkspaceLaunchInventory, SpecSessionActivationDecision, -} from '../../../workspace-session-coordinator.js'; +} from '../../../session/workspace-session-coordinator.js'; import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, diff --git a/src/.pi/extensions/chrome.ts b/src/.pi/extensions/chrome.ts index 58ac41bb5..6f5de6f7b 100644 --- a/src/.pi/extensions/chrome.ts +++ b/src/.pi/extensions/chrome.ts @@ -4,7 +4,7 @@ import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; import type { WorkspaceSessionChromeState, WorkspaceSessionReadyState, -} from '../../workspace-session-coordinator.js'; +} from '../../session/workspace-session-coordinator.js'; import { BRUNCH_COMPACT_WORDMARK } from '../components/brunch-identity.js'; export type BrunchChromeStage = 'idle' | 'streaming' | 'observer-review'; diff --git a/src/.pi/extensions/snapshot-graph.ts b/src/.pi/extensions/snapshot-graph.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/.pi/extensions/snapshot-nodes.ts b/src/.pi/extensions/snapshot-nodes.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/.pi/extensions/structured-exchange/index.ts b/src/.pi/extensions/structured-exchange/index.ts index 96b639d53..a0ed6b61b 100644 --- a/src/.pi/extensions/structured-exchange/index.ts +++ b/src/.pi/extensions/structured-exchange/index.ts @@ -9,7 +9,7 @@ import { REQUEST_CHOICE_TOOL, requestChoiceTool } from './request-choice.js'; import { REQUEST_CHOICES_TOOL, requestChoicesTool } from './request-choices.js'; import { REQUEST_REVIEW_TOOL, requestReviewTool } from './request-review.js'; -export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from '../../../structured-exchange.js'; +export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from '../../../session/structured-exchange.js'; export { buildStructuredExchangeEditorPrefill, diff --git a/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts index 6a19dfcbd..c847d93bf 100644 --- a/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts +++ b/src/.pi/extensions/structured-exchange/shared/editor-fallback.ts @@ -3,7 +3,7 @@ import { type StructuredExchangeAnswer, type StructuredExchangeMode, type StructuredExchangeOption, -} from '../../../../structured-exchange.js'; +} from '../../../../session/structured-exchange.js'; import { isRecord } from './model.js'; export interface StructuredExchangeEditorPrefillParams { diff --git a/src/.pi/extensions/workspace-dialog.ts b/src/.pi/extensions/workspace-dialog.ts index c57eef9f9..2cf01b006 100644 --- a/src/.pi/extensions/workspace-dialog.ts +++ b/src/.pi/extensions/workspace-dialog.ts @@ -4,7 +4,7 @@ import { type WorkspaceSessionReadyState, type SpecSessionActivationCoordinator, type SpecSessionActivationDecision, -} from '../../workspace-session-coordinator.js'; +} from '../../session/workspace-session-coordinator.js'; import { WORKSPACE_DIALOG_WIDTH, createWorkspaceDialogComponent, diff --git a/src/README.md b/src/README.md index 9a81d8815..e7b9b5faa 100644 --- a/src/README.md +++ b/src/README.md @@ -53,12 +53,14 @@ Rules: ## Migration notes -Some files currently at `src/` root belong in `src/session/` per this layout -(workspace-session-coordinator, session-binding, session-projection-reader, -brunch-session-envelope, session-transcript, elicitation-exchange, -structured-exchange). The active workspace file is `.brunch/workspace.json` -(`state.json` is retired). Move files incrementally as each file is touched. +The session-domain files (workspace-session-coordinator, session-binding, +session-projection-reader, brunch-session-envelope, session-transcript, +elicitation-exchange, structured-exchange, project-identity) now live in +`src/session/`; `brunch-pi-profile.ts` in `src/.pi/`; `web-host` in `src/rpc/`; +the React client in `src/web/` (formerly `web-client/`); shared test helpers +in `src/probes/`. The active workspace file is `.brunch/workspace.json` +(`state.json` is retired). -Prompt composition currently under `src/tui-client/.pi/context/` migrates -to `src/agents/` per D52-L. The `.pi/context/` README describes the -current interim layout. +Still pending: prompt composition under `src/.pi/context/` migrates to +`src/agents/` per D52-L — deferred until the agent-runtime vocabulary work, +since that move reconciles the strategy/lens model rather than just relocating it. diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index 72817e94e..d02d00853 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -11,6 +11,7 @@ import { } from '@earendil-works/pi-coding-agent'; import { describe, expect, it } from 'vitest'; +import { createBrunchPiProfile } from './.pi/brunch-pi-profile.js'; import { BRUNCH_WORKSPACE_COMMAND, BRUNCH_WORKSPACE_SHORTCUT, @@ -21,7 +22,6 @@ import { runBrunchWorkspaceCommand, runBrunchWorkspaceAction, } from './.pi/pi-extension-shell.js'; -import { createBrunchPiProfile } from './brunch-pi-profile.js'; import { BRUNCH_SETTINGS_AUDITED_GETTERS, BRUNCH_SETTINGS_POLICY, @@ -30,13 +30,13 @@ import { createBrunchSettingsManager, runBrunchTui, } from './brunch-tui.js'; -import { userMessage } from './test-helpers.js'; +import { userMessage } from './probes/test-helpers.js'; import { createWorkspaceSessionCoordinator, verifyWorkspaceSessionStores, type WorkspaceLaunchInventory, type WorkspaceSessionReadyState, -} from './workspace-session-coordinator.js'; +} from './session/workspace-session-coordinator.js'; describe('Brunch TUI boot', () => { it('gates spec selection through the coordinator before launching interactive mode', async () => { @@ -790,7 +790,7 @@ describe('Brunch TUI boot', () => { it('keeps Pi settings/resource policy out of the TUI launcher', async () => { const launcherSource = await readFile(join(import.meta.dirname, 'brunch-tui.ts'), 'utf8'); - const profileSource = await readFile(join(import.meta.dirname, 'brunch-pi-profile.ts'), 'utf8'); + const profileSource = await readFile(join(import.meta.dirname, '.pi', 'brunch-pi-profile.ts'), 'utf8'); expect(launcherSource).toContain('createBrunchPiProfile'); expect(launcherSource).not.toContain('SettingsManager.create'); @@ -801,7 +801,7 @@ describe('Brunch TUI boot', () => { it('keeps the Brunch settings override and audit list in the profile boundary', async () => { const launcherSource = await readFile(join(import.meta.dirname, 'brunch-tui.ts'), 'utf8'); - const profileSource = await readFile(join(import.meta.dirname, 'brunch-pi-profile.ts'), 'utf8'); + const profileSource = await readFile(join(import.meta.dirname, '.pi', 'brunch-pi-profile.ts'), 'utf8'); const settingsManagerTypes = await readFile( join( import.meta.dirname, diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index 749c7ebe7..f62c0e465 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -9,14 +9,14 @@ import { type CreateAgentSessionRuntimeFactory, } from '@earendil-works/pi-coding-agent'; -import { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; -import { chromeStateForWorkspace, createBrunchPiExtensionShell } from './.pi/pi-extension-shell.js'; import { applyBrunchOfflineDefault, brunchResourceLoaderOptions, createBrunchPiProfile, createBrunchSettingsManager, -} from './brunch-pi-profile.js'; +} from './.pi/brunch-pi-profile.js'; +import { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; +import { chromeStateForWorkspace, createBrunchPiExtensionShell } from './.pi/pi-extension-shell.js'; import { createWorkspaceSessionCoordinator, type WorkspaceLaunchInventory, @@ -24,7 +24,7 @@ import { type WorkspaceSessionReadyState, type SpecSessionActivationCoordinator, type SpecSessionActivationDecision, -} from './workspace-session-coordinator.js'; +} from './session/workspace-session-coordinator.js'; export { BRUNCH_SETTINGS_AUDITED_GETTERS, BRUNCH_SETTINGS_POLICY, @@ -32,7 +32,7 @@ export { brunchResourceLoaderOptions, createBrunchPiProfile, createBrunchSettingsManager, -} from './brunch-pi-profile.js'; +} from './.pi/brunch-pi-profile.js'; export { BRUNCH_BRANCH_FLOW_BLOCKED_MESSAGE, chromeStateForWorkspace, diff --git a/src/brunch.test.ts b/src/brunch.test.ts index 6e1ef6859..51faf7870 100644 --- a/src/brunch.test.ts +++ b/src/brunch.test.ts @@ -7,12 +7,12 @@ import { SessionManager } from '@earendil-works/pi-coding-agent'; import { describe, expect, it } from 'vitest'; import { runBrunchCli, type WebHostRunnerOptions } from './brunch.js'; -import { createSessionBindingData } from './session-binding.js'; -import { assistantMessage, userMessage } from './test-helpers.js'; +import { assistantMessage, userMessage } from './probes/test-helpers.js'; +import { createSessionBindingData } from './session/session-binding.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from './workspace-session-coordinator.js'; +} from './session/workspace-session-coordinator.js'; function coordinator(sessionFile?: string): WorkspaceSessionCoordinator { return { diff --git a/src/brunch.ts b/src/brunch.ts index 8a4a40de0..daf9ceb40 100644 --- a/src/brunch.ts +++ b/src/brunch.ts @@ -5,11 +5,11 @@ import { fileURLToPath } from 'node:url'; import { runBrunchTui } from './brunch-tui.js'; import { renderWorkspaceSnapshot, workspaceSnapshotFromState } from './print-snapshot.js'; import { createRpcHandlers, runJsonRpcLineServer } from './rpc/handlers.js'; -import { startWebHost } from './web-host.js'; +import { startWebHost } from './rpc/web-host.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from './workspace-session-coordinator.js'; +} from './session/workspace-session-coordinator.js'; export interface WebHostRunnerOptions { cwd: string; diff --git a/src/print-snapshot.test.ts b/src/print-snapshot.test.ts index 41cfaabe9..dd4c8e307 100644 --- a/src/print-snapshot.test.ts +++ b/src/print-snapshot.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { renderWorkspaceSnapshot, workspaceSnapshotFromState } from './print-snapshot.js'; -import type { WorkspaceSessionState } from './workspace-session-coordinator.js'; +import type { WorkspaceSessionState } from './session/workspace-session-coordinator.js'; const cwd = '/tmp/brunch-project'; diff --git a/src/print-snapshot.ts b/src/print-snapshot.ts index 36c1d9453..0356a3453 100644 --- a/src/print-snapshot.ts +++ b/src/print-snapshot.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from './workspace-session-coordinator.js'; +import type { WorkspaceSessionState } from './session/workspace-session-coordinator.js'; export interface WorkspaceSnapshot { status: WorkspaceSessionState['status']; diff --git a/src/probes/check-workspace-session-stores.ts b/src/probes/check-workspace-session-stores.ts index f0835c8b3..2023a510d 100644 --- a/src/probes/check-workspace-session-stores.ts +++ b/src/probes/check-workspace-session-stores.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import process from 'node:process'; -import { verifyWorkspaceSessionStores } from '../workspace-session-coordinator.js'; +import { verifyWorkspaceSessionStores } from '../session/workspace-session-coordinator.js'; const cwd = process.argv[2]; const expectedSessionCount = process.argv[3] ? Number(process.argv[3]) : undefined; diff --git a/src/probes/public-rpc-parity-proof.ts b/src/probes/public-rpc-parity-proof.ts index 2a6770198..3df83afbd 100644 --- a/src/probes/public-rpc-parity-proof.ts +++ b/src/probes/public-rpc-parity-proof.ts @@ -3,8 +3,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createRpcHandlers } from '../rpc/handlers.js'; -import { renderSessionTranscript } from '../session-transcript.js'; -import { createWorkspaceSessionCoordinator } from '../workspace-session-coordinator.js'; +import { renderSessionTranscript } from '../session/session-transcript.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; const PUBLIC_RPC_PARITY_PERMUTATION_COUNT = 3; diff --git a/src/test-helpers.ts b/src/probes/test-helpers.ts similarity index 100% rename from src/test-helpers.ts rename to src/probes/test-helpers.ts diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index 299cd7fd7..10127ce39 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -7,9 +7,9 @@ import { SessionManager } from '@earendil-works/pi-coding-agent'; import { Value } from 'typebox/value'; import { describe, expect, it } from 'vitest'; -import { createSessionBindingData } from '../session-binding.js'; -import { assistantMessage, userMessage } from '../test-helpers.js'; -import { createWorkspaceSessionCoordinator } from '../workspace-session-coordinator.js'; +import { assistantMessage, userMessage } from '../probes/test-helpers.js'; +import { createSessionBindingData } from '../session/session-binding.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; import type { DefaultWorkspaceCoordinator, WorkspaceActivationState, @@ -18,7 +18,7 @@ import type { WorkspaceSessionState, SpecSessionActivationCoordinator, SpecSessionActivationDecision, -} from '../workspace-session-coordinator.js'; +} from '../session/workspace-session-coordinator.js'; import { createRpcHandlers, runJsonRpcLineServer } from './handlers.js'; function coordinator( diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index d0165e6d0..36591f70c 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -6,21 +6,21 @@ import { Value } from 'typebox/value'; import type { StructuredExchangePresentDetails } from '../.pi/extensions/structured-exchange/shared/model.js'; import { isStructuredExchangePresentDetails } from '../.pi/extensions/structured-exchange/shared/recovery.js'; +import { workspaceSnapshotFromState } from '../print-snapshot.js'; import { readBrunchSessionEnvelope, NonLinearTranscriptError, type BrunchSessionEnvelope, -} from '../brunch-session-envelope.js'; +} from '../session/brunch-session-envelope.js'; import { projectLinearElicitationExchangeProjection, projectLinearTranscriptDisplayProjection, -} from '../elicitation-exchange.js'; -import { workspaceSnapshotFromState } from '../print-snapshot.js'; +} from '../session/elicitation-exchange.js'; import { resolveExplicitSessionProjectionTarget, type ExplicitSessionProjectionParams, type SessionProjectionTarget, -} from '../session-projection-reader.js'; +} from '../session/session-projection-reader.js'; import type { DefaultWorkspaceCoordinator, WorkspaceActivationState, @@ -28,7 +28,7 @@ import type { WorkspaceSessionState, SpecSessionActivationCoordinator, SpecSessionActivationDecision, -} from '../workspace-session-coordinator.js'; +} from '../session/workspace-session-coordinator.js'; import { createJsonRpcFailure, createJsonRpcSuccess, diff --git a/src/web-host.test.ts b/src/rpc/web-host.test.ts similarity index 99% rename from src/web-host.test.ts rename to src/rpc/web-host.test.ts index 85c56f030..31a0c1c38 100644 --- a/src/web-host.test.ts +++ b/src/rpc/web-host.test.ts @@ -6,12 +6,12 @@ import { join } from 'node:path'; import { SessionManager } from '@earendil-works/pi-coding-agent'; import { describe, expect, it } from 'vitest'; -import { assistantMessage, userMessage } from './test-helpers.js'; -import { startWebHost } from './web-host.js'; +import { assistantMessage, userMessage } from '../probes/test-helpers.js'; import { createWorkspaceSessionCoordinator, type WorkspaceSessionCoordinator, -} from './workspace-session-coordinator.js'; +} from '../session/workspace-session-coordinator.js'; +import { startWebHost } from './web-host.js'; function text(response: Response): Promise { return response.text(); diff --git a/src/web-host.ts b/src/rpc/web-host.ts similarity index 95% rename from src/web-host.ts rename to src/rpc/web-host.ts index b03134c0a..b74c52909 100644 --- a/src/web-host.ts +++ b/src/rpc/web-host.ts @@ -3,9 +3,9 @@ import { createServer, type Server } from 'node:http'; import { dirname, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { createRpcHandlers } from './rpc/handlers.js'; -import { attachWebRpcTransport } from './rpc/websocket.js'; -import type { WorkspaceSessionCoordinator } from './workspace-session-coordinator.js'; +import type { WorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; +import { createRpcHandlers } from './handlers.js'; +import { attachWebRpcTransport } from './websocket.js'; export interface WebHostOptions { cwd: string; @@ -107,7 +107,7 @@ export async function startWebHost(options: WebHostOptions): Promise | WorkspaceUnavailableSession; async function inspectSessionFile(file: string): Promise { const entries = await readJsonl(file); @@ -631,7 +631,6 @@ async function inspectSessionFile(file: string): Promise { id: header.id, file, specId: binding.data.specId, - specTitle: '', ...(name != null ? { name } : {}), available: true, }; diff --git a/src/web-client/app.test.tsx b/src/web/app.test.tsx similarity index 98% rename from src/web-client/app.test.tsx rename to src/web/app.test.tsx index dd50191fd..1b6d38e77 100644 --- a/src/web-client/app.test.tsx +++ b/src/web/app.test.tsx @@ -3,8 +3,8 @@ import { cleanup, render, screen, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { TranscriptDisplayProjection } from '../elicitation-exchange.js'; import type { WorkspaceSnapshot } from '../print-snapshot.js'; +import type { TranscriptDisplayProjection } from '../session/elicitation-exchange.js'; import { BrunchWebApp, createBrunchWebRuntime } from './app.js'; import type { WebSocketRpcClient, diff --git a/src/web-client/app.tsx b/src/web/app.tsx similarity index 98% rename from src/web-client/app.tsx rename to src/web/app.tsx index 2ff466542..8dd3c72f4 100644 --- a/src/web-client/app.tsx +++ b/src/web/app.tsx @@ -8,8 +8,8 @@ import { import { RouterProvider, createRootRouteWithContext, createRouter } from '@tanstack/react-router'; import { Suspense, useEffect } from 'react'; -import type { TranscriptDisplayProjection } from '../elicitation-exchange.js'; import type { WorkspaceSnapshot } from '../print-snapshot.js'; +import type { TranscriptDisplayProjection } from '../session/elicitation-exchange.js'; import type { WebSocketRpcClient } from './rpc-client.js'; type RouterContext = { diff --git a/src/web-client/main.tsx b/src/web/main.tsx similarity index 100% rename from src/web-client/main.tsx rename to src/web/main.tsx diff --git a/src/web-client/rpc-client.test.ts b/src/web/rpc-client.test.ts similarity index 100% rename from src/web-client/rpc-client.test.ts rename to src/web/rpc-client.test.ts diff --git a/src/web-client/rpc-client.ts b/src/web/rpc-client.ts similarity index 100% rename from src/web-client/rpc-client.ts rename to src/web/rpc-client.ts From dcd4dbf36e620860ba5d7c12cdcd32c1e3d7b02a Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 13:17:57 +0200 Subject: [PATCH 13/34] Reconcile agent-composition model into SPEC; replan frontiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC: revise D25-L (orthogonal strategy/lens axes), D40-L (session-agent record, role derived from op_mode); add D58-L (3-layer composition), D59-L (goal axis), D60-L (snapshot pull/render/surface); rewrite §309/§311 to the 3-layer model; update I25-L; add Lexicon terms (Session agent, Goal, AUTO, Capability/skill, Snapshot, Agent definition); retire legacy (lens-catalogue, pinning posture, runtime bundle, flat composition prose). PLAN: register agent-runtime-vocabulary (active) and agents-composition-layer (next) frontiers with dependencies; sequence ahead of the M5 A14-L proof. --- memory/PLAN.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++-- memory/SPEC.md | 44 +++++++++++++++++++++++----------------- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 39104acf8..6458709a2 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -51,11 +51,13 @@ The POC should maximize assumption falsification rather than merely implement mi ### Active -1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, and reviewer advisory writes through pi extension seams; all writes via the shared command layer. +1. `agent-runtime-vocabulary` — fix the session-agent record to the reconciled vocabulary (un-collapse lens, add `propose-graph`/`project-graph`/`goal`, derive role from `op_mode`). Foundational; gates the A14-L proof and the composition layer. +2. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, reviewer advisory writes; all writes via the command layer. Its A14-L real-LLM proof (the `propose-graph → commitGraph` path) waits on `agent-runtime-vocabulary`. ### Next -1. `probes-and-transcripts-evolution` — keep probe/transcript artifacts honest as new seams need evidence. +1. `agents-composition-layer` — the `agents/` 3-layer prompt composition (D58-L), goal/strategy/lens packs + AUTO selector, snapshot pull/render/surface (D60-L), and the `.pi/context → agents/` migration. Depends on `agent-runtime-vocabulary`. +2. `probes-and-transcripts-evolution` — keep probe/transcript artifacts honest as new seams need evidence. ### Parallel / Low-conflict @@ -97,6 +99,49 @@ The POC should maximize assumption falsification rather than merely implement mi - **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (posture deferral note, §Vocabulary evolution); this session's locked state-model decisions. - **Current execution pointer:** Done. Card queue exhausted and removed after Card 1 (spec table + CommandExecutor create/read/grade update) and Card 2 (DB-backed startup, `.brunch/workspace.json`, collapsed binding, picker names from DB). Verified with `npm run verify` plus real `brunch --mode print` against a fresh cwd. +### agent-runtime-vocabulary + +- **Name:** Session-agent runtime vocabulary fix +- **Linear:** unassigned (create in FE / brunch when branch opens) +- **Branch:** to create — `ln/-agent-runtime-vocabulary` +- **Kind:** structural (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) +- **Status:** active +- **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the `goal` axis (`grounding-advance | elicit-I | elicit-II | commitment-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. +- **Why now / unlocks:** The code is the last place still carrying the pre-reconciliation model (collapsed lens, missing `propose-graph`/`project-graph`, role-as-state). `propose-graph` must exist before `agent-graph-integration` can run the A14-L real-LLM proof (the `propose-graph → commitGraph` path is the named proof target), and the orthogonal axes are the precondition for the `agents/` 3-layer composition. Foundational and small. +- **Acceptance:** + - `AgentLensId` is independent of `AgentStrategyId`; lens ∈ `intent | design | oracle`. + - strategy ∈ `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; old names gone everywhere. + - `goal` axis present with grade-derived default and `auto` accepted; `op_mode` derives the foreground role; no stored `agentRole`. + - append/project/switch helpers round-trip the new record; malformed/illegal tuples rejected; I25-L tests updated and green. + - all fixtures/tests regenerated to the new vocabulary; `npm run verify` green. +- **Verification:** Inner — runtime-state append/project/switch unit tests over the new axes (I25-L); legal-tuple acceptance / illegal-tuple rejection. Middle — projection round-trip from real JSONL reload. No new outer-loop here; the A14-L behavioral proof rides on `agent-graph-integration`. +- **Cross-cutting obligations:** Keep state linear-transcript-backed (D40-L); foreground role derived, not stored. Do **not** author prompt packs or build `compose()` here — that is `agents-composition-layer`. If `agents/state.ts` is introduced for the shared enums, keep it to type/enum + legal-combination homes only. +- **Traceability:** D25-L (revised), D40-L (revised), D59-L, I25-L (revised) / A14-L (sharpens the proof). Retires: collapsed lens, stale strategy vocabulary, role-as-session-state. +- **Design docs:** `memory/SPEC.md` §Active Decisions (D25-L, D40-L, D58-L, D59-L), Lexicon. +- **Current execution pointer:** Not started. Likely cards: (1) enums + `BrunchAgentState` shape + `DEFAULT` + projection/switch helpers; (2) fixture/test regeneration + I25-L update. + +### agents-composition-layer + +- **Name:** agents/ 3-layer prompt composition, snapshot tiers, and .pi/context migration +- **Linear:** unassigned +- **Branch:** to create — `ln/-agents-composition-layer` +- **Kind:** structural (new composition seam + held Class-B migration; spans `agents/`, `graph/`, `session/`, `.pi/extensions/`) +- **Status:** next +- **Objective:** Build the `agents/` layer per D58-L/D59-L/D60-L and migrate `.pi/context/` into it. **(1)** `agents/state.ts` — the legal `(op_mode × goal × strategy × lens)` combination table + shared axis enums. **(2)** `agents/compose.ts` — the per-agent 3-layer selector: Layer-1 agent definition (identity + model/thinking + mode-gated tool authority + allow-lists), Layer-2 objective-config packs (`goal`/`strategy`/`lens`; AUTO ⇒ selection-guidance injection over the allowed set), Layer-3 capabilities as gated Pi skills; replaces the fixed `PROMPT_PACK_ORDER` concatenation. **(3)** `agents/{definitions,strategies,lenses,goals}/` packs — author the (currently absent) strategy/lens/goal packs; rehome `brunch-base`/`elicit`/`elicitor` as Layer-1, convert `structured-exchange`/`capture-analysis`/`candidate-proposals` to Layer-3 skills. **(4)** `agents/contexts/` — snapshot RENDER (D60-L): LLM-string/JSON from typed pulls, scaled by lens-plane + grade-depth; wire `snapshot-*` Pi tool wrappers (SURFACE) over the renderer; PULL stays read-only in `graph/snapshot.ts` (+ a `session/` cwd-overview pull). **(5)** Migrate `src/.pi/context/* → src/agents/` (the held Class-B move) and delete the old composer. +- **Why now / unlocks:** Turns the locked composition model into a real selector so mode/strategy/lens/goal actually drive the prompt body (today they only change a header sentence), and finally injects the M4 graph snapshots into context. Unblocks grade-gated, goal-driven elicitation behavior. +- **Acceptance:** + - `compose(agentId, sessionState, spec, workspace, snapshot)` selects packs by axes and gates by mode/grade/allow-list; output varies by axis, not just a header line. + - AUTO on `goal`/`strategy`/`lens` injects selection guidance listing exactly the agent's allowed set. + - `agents/state.ts` accepts every legal tuple, rejects illegal; a pack exists for each `strategies`/`lenses`/`goals` enum value. + - Layer-3 capabilities resolve as Pi skills gated by allow-list ∩ `op_mode` ∩ grade. + - snapshot RENDER scales by lens-plane + grade-depth; `snapshot-{cwd,graph,nodes}` tools wrap the renderer (markdown in `content`, typed JSON in `details`); PULL stays read-only in `graph/`/`session/`. + - `src/.pi/context/` removed; composition lives in `src/agents/`; `npm run verify` green. +- **Verification:** Inner — `compose()` output assertions (pack set/order/gating per axes; AUTO-guidance injection); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip. Middle — migration differential (old vs new composer equivalence for fixed states, throwaway cutover gate); AUTO-choice legality as a probe fitness metric (not a gate). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). **Oracle pass pending** — see SPEC §Verification Design once `ln-oracles` completes. +- **Cross-cutting obligations:** Layers 1–2 = prompt packs; Layer 3 = Pi skills (D58-L/§311). PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). +- **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location. +- **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture, Lexicon; this session's locked composition model. +- **Current execution pointer:** Not started. Oracle diagnostic + claims drafted this session (machinery = structural inner-loop; AUTO-choice = middle-loop fitness; agent behavior = outer A14-L) but `ln-oracles` was not completed — finish it before scoping. Likely first slice: `agents/state.ts` + `compose.ts` skeleton over the fixed vocabulary, then pack authoring, then snapshots, then migration. + ### sealed-pi-profile-runtime-state - **Name:** Sealed Pi profile, transcript-backed runtime state, and graph-model prep (M4 prep envelope) @@ -321,6 +366,8 @@ nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) spec-persistence-and-startup [done] (persistence-model fix + startup regression repair) + agent-runtime-vocabulary [active] (session-agent record vocabulary fix) + agents-composition-layer [next] (agents/ 3-layer composition + snapshots + .pi/context migration) agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] authority-model [not-started] (M6) @@ -334,6 +381,9 @@ edges: graph-data-plane -[hard]-> agent-graph-integration graph-data-plane -[hard]-> spec-persistence-and-startup spec-persistence-and-startup -[hard]-> agent-graph-integration + spec-persistence-and-startup -[hard]-> agent-runtime-vocabulary + agent-runtime-vocabulary -[hard]-> agents-composition-layer + agent-runtime-vocabulary -[hard]-> agent-graph-integration agent-graph-integration -[hard]-> authority-model agent-graph-integration -[hard]-> turn-boundary-reconciliation agent-graph-integration -[optional]-> subagents-for-proposal-diversity diff --git a/memory/SPEC.md b/memory/SPEC.md index 62a1afbef..8805dff04 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -89,7 +89,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c #### Runtime profile & prompting 25. Brunch must run the embedded Pi harness through a sealed Brunch Pi Profile: programmatic settings, resource-loader, extension-factory, keybinding, tool, and prompt policy must determine product behavior; ambient user/project `.pi/` resources must not influence Brunch sessions unless Brunch deliberately imports them. -26. Brunch must distinguish transport modes from operational modes and agent roles: operational modes such as `elicit` and future `execute` gate tool authority and prompt posture, while role presets/bundles select the active top-level role, model/thinking posture, prompt packs, allowed strategies/lenses, and tool policy. +26. Brunch must distinguish transport modes from operational modes and agent roles: operational modes such as `elicit` and future `execute` gate tool authority and prompt posture; the foreground session agent (`elicitor` now, future `executor`) is derived from the operational mode, and each agent definition carries its own model/thinking preset, prompt packs, applicability allow-lists, and tool policy. ## Live Architecture Register @@ -126,7 +126,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D1-L — Depend on `pi-coding-agent`, not only `pi-agent-core`.** The POC reuses the coding-agent service bundle, TUI/print adapters, RPC machinery, session logging, and tool plumbing. Dropping down to `pi-agent-core` is a fallback if Brunch proves too different. Depends on: A1-L. Supersedes: —. - **D2-L — Brunch is an opinionated product, not a pi platform shell.** The POC hardcodes its toolset, system prompt, and policy doctrine; scopes state to `.brunch/`; and hides pi's generic extension surface from end users. Depends on: A1-L. Supersedes: —. - **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture routes TUI launch policy through `src/brunch-pi-profile.ts`, creates an in-memory Brunch-owned `SettingsManager` policy instead of reading ambient global/project `.pi/settings.json`, disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell, and defaults Brunch-launched Pi to offline mode; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/.pi/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/.pi/extensions/*`, and reusable Pi TUI components live under `src/.pi/components/*`, so they can also be iterated by launching Pi from `src/` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; extension/component tests live under `src/.pi/__tests__/`. The profile boundary now owns the audited behavior-shaping settings list in code (`BRUNCH_SETTINGS_POLICY` / `BRUNCH_SETTINGS_AUDITED_GETTERS`), with hostile ambient settings and reload-resilience tests covering shell path/prefix, npm command, ambient resources, skill commands, double-escape behavior, compaction/retry, image/terminal/UI, transport/theme/changelog, and telemetry settings. Remaining profile work is runtime-state/prompt/tool posture, not ambient settings file leakage. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. -- **D40-L — Runtime posture is a transcript-backed Brunch state machine, not hidden extension memory.** Brunch distinguishes operational modes (`elicit`, future `execute`) from agent roles (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor roles) and from strategies/lenses. The active top-level role is selected through a role preset/runtime bundle that derives model, thinking level, prompt packs, allowed strategies/lenses, and tool policy rather than storing each knob independently. Brunch runtime helpers append full selected-state product custom entries under `brunch.agent_runtime_state` with `reason: "init" | "switch"`; turn preparation projects the latest valid linear transcript snapshot into prompt and tool posture. Brunch product prompt packs are private code-composed assets under `src/.pi/context/prompt-packs/*`, composed only through `src/.pi/context/compose-brunch-prompt.ts` and appended by the explicit `src/.pi/extensions/prompting.ts` product extension; they are not Pi prompt templates, skills, context-file discovery, or user-invoked slash-command resources. The current `elicit` tool policy is a denylist over side-effecting tools (`bash`, `edit`, `write`) plus user-shell interception, so new safe Brunch extension tools are not hidden by a stale allowlist. The Pi extension module that owns tool policy is `src/.pi/extensions/operational-mode.ts`, while product prompting is owned separately by `src/.pi/extensions/prompting.ts`; neither should duplicate the other's control-plane responsibility. Depends on: D17-L, D23-L, D25-L, D39-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority for agent behavior, modeling read-only posture as a volatile allowlist of every safe tool, or exposing Brunch prompt packs through Pi resource discovery. +- **D40-L — Runtime state is a transcript-backed Brunch session-agent record, not hidden extension memory.** Brunch projects one session-agent record from linear `brunch.agent_runtime_state` entries (`reason: "init" | "switch"`), last-writer-wins at turn preparation. Its axes are `op_mode` (`elicit`, future `execute`) plus the optional, AUTO-able objective axes `strategy`, `lens`, and `goal` (D25-L, D59-L); a deferred `world_view` watermark group (last-seen LSN, graph mentions, optional git head) is reserved for M7. The **foreground session agent** (`elicitor` now, future `executor`) is *derived* from `op_mode`, not stored; the other agent roles (`reviewer`, `reconciler`, future `scout`/`researcher`) are async sub-agent/side-chain workers (D29-L, D44-L) invoked out-of-band, never part of the session state machine. `op_mode` gates tool authority, owned by `src/.pi/extensions/operational-mode.ts` (current `elicit` policy denies side-effecting `bash`/`edit`/`write` plus user-shell interception). Prompt composition is a separate concern (D58-L). Depends on: D17-L, D23-L, D25-L, D39-L, D58-L, D59-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority, storing the foreground role as independent session state, the "runtime bundle / role preset" as one knob deriving model/thinking/packs, and binding prompt-pack location to `src/.pi/context/`. - **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. - **D35-L — Dynamic TUI chrome is a Brunch projection wrapper over Pi UI primitives.** Downstream TUI affordances should call a Brunch-owned renderer (`renderBrunchChrome` or its successor) with one activated product-state snapshot rather than scattering raw `ctx.ui.setHeader`, `setFooter`, `setWidget`, title, or working-indicator calls. The wrapper is stateless projection over canonical workspace/session/graph facts, including the real activated session id, while its TUI footer compositor may read Pi footer telemetry (`getGitBranch`, foreign `getExtensionStatuses`) at render time. Brunch chrome does not publish a `brunch.chrome` status key; `ctx.ui.setStatus(key, text)` remains a lateral contribution channel for other extensions and future dynamic Brunch state. RPC clients should rely only on surfaces Pi actually emits for the wrapper (currently diagnostic widget/title, plus any future explicit status adapter) because header/footer/working-indicator are TUI-only in current Pi RPC mode. Session display names are likewise product projections over Pi session metadata: Brunch may append Pi `session_info` entries, but generated names must characterize the selected spec/session transcript rather than replace spec identity or graph truth. Depends on: D2-L, D21-L, D34-L, A18-L. Supersedes: treating Pi UI methods as direct downstream affordance APIs, rendering placeholder session state such as `unbound` after a session is activated, or consuming the status-key namespace for chrome's own static summary. - **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by axis (`modes/`, `strategies/`, `lenses/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, and the state definitions that drive mode/role/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. @@ -226,8 +226,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D38-L — JSON-over-editor is the Pi-RPC compatibility seam for complex extension UI, not a second product API.** Pi RPC supports `ctx.ui.select`, `confirm`, `input`, and `editor`, but not `ctx.ui.custom()`. When a structured-exchange tool needs a complex shape (multi-select, review-style response, or a deferred multi-question/questionnaire shape) over raw Pi RPC, the tool may call `ctx.ui.editor()` with schema-tagged JSON prefill and validate the returned JSON before producing normal `toolResult.content` plus self-contained `toolResult.details`. A Brunch-aware adapter may render that JSON as a native product form and translate the user response back into Pi's documented `extension_ui_response`; public clients still speak Brunch RPC methods/events, not ad hoc raw Pi RPC extensions. Depends on: D5-L, D19-L, D33-L, D37-L. Supersedes: inventing unsupported Pi RPC command types for Brunch interactions or exposing raw editor JSON as the product UX. - **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text, linked structured response entries, and/or terminal structured-exchange toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. - **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. -- **D25-L — Elicitation strategies are *lenses* within the `elicitor` agent role, not separate roles or operational modes.** Lens is metadata on elicitor-emitted custom transcript entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`, etc.); roles (`elicitor`, `reviewer`, `reconciler`, and any deferred observer/auditor roles) remain orthogonal. The catalogue is organized along two axes: *strategies* describe the interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`) while *lenses* describe the topical focus (`intent`, `design`, `oracle`, and future execute-mode lenses `plan`, `sync`, `scope`). The prior lens-catalogue names map to strategy+lens combinations: `propose-scenarios-with-tradeoffs` = propose-graph strategy under intent lens; `propose-design-shapes` = propose-graph under design lens; `propose-oracle-ensembles` = propose-graph under oracle lens; `project-requirements-from-upstream` = project-graph under intent lens; `step-by-step` and `disambiguate-via-examples` = extractive strategies applicable across lenses. The catalogue is expected to grow. Capture, review, and future audit routing may filter on lens. Depends on: D12-L, D17-L, D23-L. Supersedes: collapsing strategy and agent role into one vocabulary axis. -- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation before any commitment posture; later commitment posture changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. +- **D25-L — Strategy and lens are two orthogonal session-agent axes within the `elicitor` role, not separate roles or operational modes.** *Strategies* describe interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`); *lenses* describe topical focus (`intent`, `design`, `oracle`; future execute-mode `plan`, `sync`, `scope`). Both are optional, AUTO-able fields of the projected session-agent record (D40-L) and are stamped onto elicitor-emitted custom entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) as provenance (I18-L); capture/reviewer/audit routing may filter on lens. Strategy determines the commitment mechanism (D26-L); the catalogue is expected to grow. Depends on: D12-L, D17-L, D23-L, D40-L. Supersedes: collapsing strategy and agent role into one vocabulary axis; treating strategies as a sub-kind of lenses; and the prior free-text lens-catalogue names (`propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`, `step-by-step`, `disambiguate-via-examples`), now retired in favor of the strategy×lens axes. +- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation; commitment (the `commitment-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. - **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** `brunch.establishment_offer` records the agent's current offer tree and recommended next move as durable transcript state. Ambient chrome or web affordances may render the latest offer, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. @@ -241,6 +241,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. +- **D58-L — Brunch prompt composition is a 3-layer model composed per agent.** `agents/compose(agentId, sessionState, spec, workspace, snapshot)` assembles, in order: **(1) agent definition** — the keyed agent's identity/system prompt, model/thinking preset, mode-gated tool authority, and applicability allow-lists (`strategies`/`lenses`/`skills`); **(2) objective config** — the optional, AUTO-able `goal` (D59-L), `strategy`, and `lens` packs, with the legal `(goal × strategy × lens)` tuple constrained by `agents/state.ts`, gated by `spec.readiness_grade` and conditioned by `workspace.posture`; **(3) capabilities** — Layer-3 tool-usage skills via Pi progressive disclosure, gated by allow-list ∩ `op_mode` ∩ grade. A dynamic context layer appends agent-context snapshots (D60-L). `AUTO` on any objective axis means the axis is unpinned and the composer injects selection guidance over the agent's allowed set so the agent self-selects. `agents/` is a keyed multi-agent registry (`definitions/` plus `strategies/`, `lenses/`, `goals/`, `contexts/`); the foreground session agent is resolved from `op_mode`, side/sub-agents addressed by explicit `agentId`. Depends on: D23-L, D25-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; and "role preset / runtime bundle" as the composition unit. +- **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-I` / `elicit-II` (main elicitation levels), `commitment-converge` (reduce / lock down), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO` for agent self-selection. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. +- **D60-L — "Snapshot" splits into pull / render / surface, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview), `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization). **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L. Supersedes: pre-rendering snapshots to strings in the pull layer, and scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs. ### Critical Invariants @@ -270,7 +273,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | | I23-L | Every structured elicitation interaction that owns the response surface persists durable semantic display only through Pi `toolResult` rows rendered by `renderResult`; `renderCall` and live `ctx.ui.*` surfaces are transient. A structured-exchange tuple has a recoverable `present_*` result and, when required, exactly one matching terminal `request_*` result before the next agent turn consumes it. The target details model is checked by `schema` + `v`, `exchange_id`, and `tool_meta`; request outcomes are an exactly-one property-presence union; user-authored text is `comment` and runtime-authored text is `message`; present-side status/kind/expected-request aliases and capture graph payloads are invalid in the Zod-authored schema layer. `toolResult.content` is rich markdown suitable for both TUI transcript display and model context; `toolResult.details` carries structured projection/recovery data. | covered for current FE-744 structured-exchange tools (registered sequential `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices`; tests cover non-semantic `renderCall`, markdown `renderResult`, present/request details, unmatched-present recovery, active-vs-stub registry, JSON-editor fallback for multi-choice, terminal `answered`/`cancelled`/`unavailable` projection closure, option content/rationale parity, and same-assistant-message `present_options → request_choice` ordering over a real Pi RPC run. The Zod-authored schema layer is covered by JSON Schema export and drift-rejection tests for present/request/capture details; runtime tools still need a deliberate migration to those exports. `present_review_set`, `present_candidates`, and `request_review` remain named stubs and intentionally unregistered.) | D12-L, D13-L, D17-L, D37-L, D38-L, D41-L | | I24-L | A Brunch-launched Pi runtime does not load ambient user/project Pi context files, extensions, skills, prompt templates, themes, or behavior-shaping settings unless the Brunch Pi Profile explicitly allows them; Brunch-owned extension-discovered resources are identified as intentional product resources. | covered for TUI-launch profile boundary by contract tests: ambient resource flags and explicit extension factories are preserved; hostile ambient global/project settings are ignored by the in-memory Brunch settings policy before and after reload; audited Pi settings getters are tracked in `src/brunch-pi-profile.ts`. Subagent subprocess inheritance remains future coverage under I29-L. | D2-L, D39-L | -| I25-L | The active operational mode, role preset/runtime bundle, strategy, and lens are reconstructable from linear transcript entries at turn start; tool gating follows the reconstructed operational mode so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted by the bundle. | covered for the current POC runtime bundle: runtime-state append/project/switch helpers write full `brunch.agent_runtime_state` snapshots to Pi custom entries, init is idempotent, switches carry previous state, malformed/impossible mode-role-strategy-lens entries are ignored or rejected, real SessionManager JSONL reload projects the latest valid state, and session-start/before-agent-start prompt/tool policy derives from the transcript-projected state without extension-local memory. | D17-L, D23-L, D40-L | +| I25-L | The active `op_mode`, `strategy`, `lens`, and `goal` are reconstructable from linear `brunch.agent_runtime_state` entries at turn start; the foreground session-agent role is derived from `op_mode`, not separately stored; tool gating follows the reconstructed `op_mode` so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted. | covered for the current runtime-state machine (append/project/switch helpers write and project `brunch.agent_runtime_state` snapshots from Pi custom entries, init idempotent, switches carry previous state, malformed entries rejected, latest valid state drives prompt/tool policy without extension-local memory); the axis vocabulary fix (un-collapse lens from strategy, add `propose-graph`/`project-graph`/`goal`, derive role from `op_mode`) is pending the agent-runtime frontier. | D17-L, D23-L, D40-L, D58-L, D59-L | | I27-L | Session-name generation is best-effort presentation metadata only: lifecycle hooks may append Pi `session_info` entries, but naming failures never block shutdown/session replacement and generated names never mutate spec identity, session binding, or graph truth. | planned (session-lifecycle naming tests with empty transcript/auth failure/success paths; picker projection tests read session names when present) | D6-L, D21-L, D35-L, D42-L | | I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | | I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | @@ -306,14 +309,14 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is explicit and layered: base Brunch product prompt + operational-mode prompt pack + top-level role preset + strategy prompt pack + lens prompt pack + spec readiness grade + current graph/coherence/world state + pending structured-interaction rules. The target topology per D52-L is `src/agents/` organized by axis: `modes/` (operational mode prompts and rules), `strategies/` (interaction-shape prompts per strategy), `lenses/` (topical-focus prompts per lens), and `contexts/` (snapshot orchestration functions that import from `graph/` and `session/`). Prompt composition lives in `agents/compose.ts`; state definitions (valid mode/role/strategy/lens combinations) in `agents/state.ts`. The current `src/.pi/context/` layout migrates to this structure. +- Brunch prompt composition is a **3-layer model**, composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshot)` (D58-L): **(1) agent definition** — identity/system prompt + model/thinking + mode-gated tool authority + applicability allow-lists; **(2) objective config** — the optional/AUTO-able `goal` (grade-derived, D59-L), `strategy`, and `lens` packs, gated by `spec.readiness_grade` and conditioned by `workspace.posture`; **(3) capabilities** — tool-usage skills via Pi progressive disclosure, gated by allow-list ∩ `op_mode` ∩ grade. A dynamic **context** layer appends agent-context snapshots built in `agents/contexts/` from `graph/`+`session/` reads, scaled by lens (plane) and grade (depth) (D60-L). Target topology per D52-L is `src/agents/{definitions, strategies, lenses, goals, contexts}` with `agents/compose.ts` and `agents/state.ts` (axes + legal-combination table); the current `src/.pi/context/` layout migrates here. - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. -- Core role/lens prompting should usually be product prompt packs rather than Pi skills. Pi skills remain available as Brunch-owned explicit resources when progressive disclosure is the right mechanism, but they are not the primary authority for operational mode/tool policy. +- Layers 1–2 (agent definition, objective config) are **product prompt packs**; Layer-3 capabilities are **Pi skills** delivered by progressive disclosure (how to run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps), gated by the agent's allow-list ∩ active `op_mode` ∩ `spec.readiness_grade`. Skills are not the authority for operational-mode/tool policy. ### Coherence and readiness semantics - Coherence must remain bounded for the POC: a visible verdict tied to structural legality and actionable reconciliation needs, not a vague promise that the specification “makes sense.” M8 owns the sharper rubric and adversarial examples. -- Avoid phase/stage/maturity language for the elicit lifecycle except when referring to legacy docs. The canonical internal model is readiness grade plus runtime strategy/lens/review-set state. PLAN/frontier text should describe concrete readiness gates rather than imply a user-facing phase machine. +- Avoid phase/stage/maturity language for the elicit lifecycle except when referring to legacy docs. The canonical internal model is readiness grade plus the session-agent `goal` / `strategy` / `lens` axes and active review-set state. PLAN/frontier text should describe concrete readiness gates rather than imply a user-facing phase machine. ### Vocabulary evolution @@ -354,12 +357,17 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Brunch host** | The local process-level authority. Owns `.brunch/` resolution, agent session lifecycle, mode dispatch, and event fanout. | | **Transport mode** | One of TUI, web, RPC, print. All four drive the same host; they are presentation/protocol surfaces, not separate products or agent strategies. | | **Operational mode** | A top-level Brunch authority/tooling posture such as `elicit` or future `execute`. It determines what kind of work is allowed and which tools/prompt posture are available. Distinct from Pi's transport mode concept. | -| **Agent role** | A worker identity within an operational mode. Top-level roles drive the main turn (`elicitor`, future `executor/orchestrator`); side roles run async/advisory or delegated work (`reviewer`, `reconciler`, deferred observer/auditor, future `scout` / `researcher`). | -| **Runtime bundle / role preset** | The transcript-backed Brunch selection that derives active operational mode, top-level role, model, thinking level, prompt packs, allowed strategies/lenses, and tool policy. Commands switch bundles instead of mutating hidden extension memory. | -| **Strategy** | An interaction-shape tactic within the active runtime bundle: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges from existing graph via review-set). Strategies determine the commitment mechanism (D26-L). | -| **Lens** | A topical-focus framing applied within a role/strategy: `intent`, `design`, `oracle` for elicitation mode; `plan`, `sync`, `scope` for future execute mode. Strategy+lens pairs map to the prior lens-catalogue names (D25-L). | +| **Agent role** | A worker identity. The **foreground session-agent role** (`elicitor` now, future `executor`) drives the main turn and is *derived* from `op_mode`, not stored as session state. **Side/sub-agent roles** (`reviewer`, `reconciler`, future `scout`/`researcher`) run async/advisory or delegated work out-of-band and are never part of the session state machine. | +| **Agent definition** | Layer-1 composition unit (D58-L): a keyed agent's identity/system prompt + model/thinking preset + mode-gated tool authority + applicability allow-lists (`strategies`/`lenses`/`skills`). A keyed registry covers the foreground session agent plus side/sub-agents. Replaces the prior "runtime bundle / role preset" framing. | +| **Session agent** | The main-thread agent that drives the session forward — `elicitor` now, future `executor` — resolved from `op_mode`. It is the only agent represented in session state (D40-L); side/sub-agents are out-of-band. | +| **Strategy** | An optional, AUTO-able session-agent axis (D25-L) describing interaction shape: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges via review-set). Strategy determines the commitment mechanism (D26-L). | +| **Lens** | An optional, AUTO-able session-agent axis (D25-L) describing topical focus: `intent`, `design`, `oracle` for elicit mode; future execute-mode `plan`, `sync`, `scope`. Orthogonal to strategy; stamped onto elicitor-emitted entries as provenance (I18-L). | +| **Goal** | An optional, AUTO-able session-agent objective axis (D59-L): what the agent currently pursues, derived/gated by `spec.readiness_grade` — `grounding-advance`, `elicit-I`, `elicit-II`, `commitment-converge`, plus always-on `capture-posture`. Distinct from strategy (the *how*) and lens (the topical focus). | +| **AUTO** | The unpinned state of an objective axis (`goal` / `strategy` / `lens`): instead of a fixed pack, composition injects selection guidance over the agent's allowed set and the agent self-selects (D58-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | -| **Prompt pack** | A Brunch-owned prompt fragment selected by operational mode, role preset, strategy, lens, or readiness grade. Prompt packs compose at turn boundaries; they are product control-plane state, not ambient Pi prompt templates. | +| **Prompt pack** | A Brunch-owned prompt fragment composed at turn boundaries (D58-L): Layer-1 agent-definition packs and Layer-2 `goal`/`strategy`/`lens` packs. Layer-3 capabilities are Pi skills, not packs. Packs are product control-plane state, not ambient Pi prompt templates. | +| **Capability / skill** | A Layer-3 tool-usage competence delivered as a Pi skill via progressive disclosure (run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps), gated by the agent's allow-list ∩ `op_mode` ∩ `spec.readiness_grade` (D58-L). Not a prompt pack. | +| **Snapshot** | An *agent-context* content view the agent reasons over — `cwd`, `graph`, or `node` (D60-L): pulled (typed, read-only) from `graph/`/`session/`, rendered to LLM-string (in `agents/contexts/`) or JSON, surfaced pushed (compose) or pulled (`snapshot-*` tools). Distinct from the **workspace projection** (`workspace.snapshot`), which is product/UI state, not agent content. | | **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | | **Elicitation posture** | Retired as persisted spec state. Use readiness grade plus active strategy/lens/review-set state to explain elicit behavior. | | **Commitment focus** | Retired as persisted spec state. Future commitment projection should derive from active review-set state and graph evidence if needed. | @@ -422,7 +430,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Proposer subagent** | The system-prompt-only starter subagent that emits exactly one well-formed candidate-proposal variant per invocation given a grounding bundle plus a batch-proposal lens frame. Diversity arises from parallel `tasks: []` invocations with intentionally distinct framings; the main agent assembles outputs into a `brunch.review_set_proposal` via the D31-L meta-rubric. Realizes the "design it twice" / parallel-fan-out pattern from `ln-design` and `ln-oracles` skills in subagent form. | | **Subagent registry** | The set of registered subagent definitions loaded from `src/.pi/extensions/subagents/agents/*.md` at extension activation. Brunch-owned only for the POC; cross-extension agent registration is deferred. | | **Subagent agent definition** | A markdown file with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. The frontmatter is the registry contract; the body is the subagent's standing instructions. | -| **Auto-compaction extension** | The Brunch-owned `session_before_compact` extension (`src/.pi/extensions/auto-compaction.ts`) that renders the preserved anchor set as a deterministic markdown header and prepends it to an LLM-generated narrative summary. Resolves its summarization model through the active runtime bundle; falls through to Pi default compaction on auth/empty-output/unexpected errors. | +| **Auto-compaction extension** | The Brunch-owned `session_before_compact` extension (`src/.pi/extensions/auto-compaction.ts`) that renders the preserved anchor set as a deterministic markdown header and prepends it to an LLM-generated narrative summary. Resolves its summarization model through the active agent definition's model preference; falls through to Pi default compaction on auth/empty-output/unexpected errors. | | **Preserved anchor set** | The configured list of transcript entry kinds and selection rules that must survive compaction byte-stable. Canonical source is [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json); each rule is `{ kind, select, rationale }` where `select ∈ first | latest | active-leaves | all-unresolved`. Externalized so it can be reviewed and updated for correctness without SPEC churn. | | **Anchor contract** | The data inside the preserved-anchor JSON config — distinct from the rendering policy (which lives in code) and the LLM summarization (which is bundle-resolved). | | **World update** | `worldUpdate` custom message synthesised in `prepareNextTurn` summarising relevant graph changes since the session's `lastSeenLsn`. | @@ -441,15 +449,15 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Probe run** | A scripted or executable check of a Brunch seam that drives the public product surface and persists reviewable artifacts under `.fixtures/runs///`. | | **Transcript artifact** | The durable transcript evidence for a probe run, usually `session.jsonl` plus a Brunch-semantic `transcript.md`; reports explain the oracle, but transcript artifacts remain the evidence. | | **Probe brief** | Optional future input text for an agent-as-user probe. A brief is not a canonical artifact family by itself; if brief-based golden fixtures return, they produce normal probe runs and transcript artifacts. | -| **Elicitation lens** | A narrower interpretive strategy applied within the `elicitor` agent role — e.g. `step-by-step`, `disambiguate-via-examples`, `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`. Lens is metadata on elicitor-emitted custom transcript entries. Agent roles (`elicitor` / `reviewer` / `reconciler` / deferred observer-auditor roles) remain orthogonal. | +| **Elicitation lens** | Retired term. The interaction-shape axis is now **Strategy** (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`) and the topical-focus axis is **Lens** (`intent`, `design`, `oracle`) — two orthogonal session-agent axes (D25-L). The prior free-text catalogue (`step-by-step`, `disambiguate-via-examples`, `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`) is superseded. | | **Single-exchange elicitation flow** | A prompt/answer exchange such as step-by-step questioning or contrastive disambiguation. The elicitor captures high-confidence extractive content synchronously post-exchange; low-confidence implications stay in preface/question material. | | **Batch-proposal flow** | A proposal/review flow with structured entity-draft payloads in `brunch.review_set_proposal` entries. Durable graph changes land only through review-set approval. | | **Grounding bundle** | The minimum set of anchors required to establish the frame for main elicitation: a *domain anchor*, a *protagonist anchor*, a *pain/pull anchor*, and a *constraint anchor*. Captured technical constraints land in the constraint anchor and bound subsequent technical-design fan-outs. | | **Grounding anchor** | One sentence-scale fact captured during early elicitation that contributes to the grounding bundle. | | **Establishment offer** | A `brunch.establishment_offer` custom transcript entry summarising the elicitor's perceived gaps, the available lens strategies for the next move, the recommended lens, and the agent's confidence. Source of ambient affordances rendered in the chrome region; inspectable post-hoc and fixture-able. Orientation artifact, not a default exhaustive strategy menu. | | **Elicitor intent hint** | A `brunch.elicitor_intent_hint` custom transcript entry emitted alongside a prompt or proposal, declaring `lens` and semantic targets (e.g. expected ontological sub-type) for downstream capture/reviewer/future-auditor routing and extraction guidance. | -| **Review set** | A cohesive batch proposal presented to the user for review-cycle acceptance (approve / request changes / reject), modeled on the GitHub PR-review-cycle. Used for batch-proposal flows and for design/oracle commitment pinning. | -| **Commitment review set** | A focus-primary review set in `pinning` posture: design-oriented sets primarily pin requirement/invariant-like intent claims; oracle-oriented sets primarily pin criterion/check/example-like verification claims. Support/provenance edges are part of the accepted batch. | +| **Review set** | A cohesive batch proposal presented to the user for review-cycle acceptance (approve / request changes / reject), modeled on the GitHub PR-review-cycle. Used for batch-proposal flows and for design/oracle commitment review sets (the `commitment-converge` goal, D59-L). | +| **Commitment review set** | A focus-primary review set: design-oriented sets primarily commit requirement/invariant-like intent claims; oracle-oriented sets primarily commit criterion/check/example-like verification claims. Support/provenance edges are part of the accepted batch. Driven by the `commitment-converge` goal (D59-L), not a persisted posture. | | **Batch acceptance** | The single `CommandExecutor` call (`acceptReviewSet`) that commits an entire review set atomically as one LSN and one change-log entry, attributed to the user. Partial acceptance and accept-with-edits are not product operations. | | **Reviewer** | An agent role that runs async after batch acceptance, scoped to the accepted batch plus graph neighborhood, analyzing for coherence / completeness / gaps. Authority is narrow: writes only `reconciliation_need` records via `CommandExecutor`. | | **Anchor scenario** | A particular vignette embedded inside one alternative pitch to ground its framing. Transcript-rendered; not persisted as a graph entity. | @@ -564,7 +572,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I22-L | FE-744 coordinator inventory/activation tests plus pty/ANSI-stripped TUI probe assertions: no stale transcript before explicit resume, new-spec path creates an implicit first session, new-session path yields binding-only JSONL, resume path renders the chosen transcript, chrome includes activated session id, and RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code. | | I23-L | FE-744 structured-exchange tests: `present_*` results persist rich markdown display through `toolResult.content`/`renderResult`; `request_*` tools mount an input-replacing TUI response surface when available; single-choice, multi-choice, freeform, and freeform-plus-choice answers persist as self-contained request result details or linked custom entries; RPC/fixture paths submit the same semantic response through JSON-editor fallback or Brunch product handlers; recovery helpers detect unmatched required presents; elicitation-exchange projection pairs the prompt-side present/custom entry with the terminal request result. Structured-exchange schema tests cover the landed target details model: checked `schema`/`v`, `tool_meta`, candidate rubric/graph-ref drift rejection, exactly-one request outcomes, comment/message placement, minimal capture details, and JSON Schema export. | | I24-L | Sealed-profile tests: resource-loader options disable ambient discovery; inline Brunch extension resources still load intentionally through `resources_discover`; settings/keybinding/tool/prompt policy audit proves no ambient user/project `.pi/` setting changes Brunch product behavior. | -| I25-L | Runtime-state tests: append init/switch custom entries, reload the linear transcript, reconstruct the active operational mode/role preset/strategy/lens, and verify before-agent-start/tool-call policy suppresses disallowed tools for `elicit`. | +| I25-L | Runtime-state tests: append init/switch custom entries, reload the linear transcript, reconstruct the active `op_mode` / `strategy` / `lens` / `goal` (foreground role derived from `op_mode`), and verify before-agent-start/tool-call policy suppresses disallowed tools for `elicit`. | | I26-L | Structured-exchange schema tests prove the acknowledged Zod seam parses and exports JSON Schema; future M4 architectural tests should grep/import-audit schema libraries and Drizzle row-schema derivation boundaries. | | I28-L | Inner — TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) shape; deterministic anchor-rendering unit tests (same branch + same config → same header bytes). Middle (M9) — compaction round-trip property tests across all configured anchors and selection rules; fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer (M9) — long-horizon adversarial fixture confirms session binding, latest runtime state, latest establishment offer, in-flight side-task results, and unresolved staleness hints remain agent-intelligible post-compaction. | | I29-L | Inner — argv-shape tests for the `subagent` tool prove every spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent `--tools`/`--extension`/`--models`/`--append-system-prompt` set; TypeBox schema validation of `src/.pi/extensions/subagents/agents/*.md` frontmatter and `src/.pi/extensions/subagents/config.json`. Middle — isolation audit (no ambient `.pi/` resources reachable inside the subprocess; tool-allowlist conformance per starter agent; parent `CommandExecutor`/Brunch RPC handlers absent from subprocess environment). Outer — probe-driven proposal-generation runs invoking scout/researcher/graph-reader confirm grounding inputs flow through subagent outputs into review-set proposals without bypassing primary authority. | From 79581825db54d5f0187035a2fa05b8ee6decb09d Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:10:16 +0200 Subject: [PATCH 14/34] remove legacy brunch memory files --- archive/memory/PLAN.md | 439 ------------------------ archive/memory/SERVER_REFACTOR_NOTES.md | 60 ---- archive/memory/SPEC.md | 427 ----------------------- 3 files changed, 926 deletions(-) delete mode 100644 archive/memory/PLAN.md delete mode 100644 archive/memory/SERVER_REFACTOR_NOTES.md delete mode 100644 archive/memory/SPEC.md diff --git a/archive/memory/PLAN.md b/archive/memory/PLAN.md deleted file mode 100644 index 93ab5d29a..000000000 --- a/archive/memory/PLAN.md +++ /dev/null @@ -1,439 +0,0 @@ - - -# Plan - -## Context - -The interaction model is mature: four-phase interview, interviewer-autonomous question format, phase-agnostic preface cards with workspace exploration, structured review with per-item commenting, observer knowledge extraction, workflow ownership extraction, distribution hardening, graph view's structured-list peer route, the first relation-first observer capture seam, the multi-chat substrate, side-chat V3.0 hard-impact cascade, and side-chat V3.1 agent-grouped reconciliation resolution all ship as working product. - -The next product arc is the **Conversational Workspace Runtime** umbrella (`docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md`) plus a stronger semantic/generative substrate. The umbrella synthesizes MULTI_CHAT, SIDE_CHAT, PATCH_LEDGER, and CONTINUOUS_WORKSPACE_HYBRID into five sub-tracks: workspace shell (Track 1, shipped as `continuous-workspace` / FE-709), inline secondary-chat runtime over the existing chat/turn substrate (`chat-runtime-secondary-chats`), reconciliation runtime absorption (`reconciliation-runtime`), changeset ledger (`changeset-ledger`), and transcript-first chat context provision (`chat-context-provision`). The shell is now the stable host; schema-level `thread` is deferred until chat/turn proves insufficient. Secondary chats are the near-term runtime primitive for side, reconciliation, qa, and strategy conversations. The chat runtime is the critical unblocker for reconciliation absorption; chat context provision can proceed against chat/turn with explicit transcript snapshots and graph-item handles. The changeset ledger runs in parallel. The umbrella supersedes the independent side-chat V4a persistence horizon — persistent side-chat history becomes inline secondary chats in the workspace. The FE-705 branch contributes an integration substrate — a local agent capability CLI and external LLM-as-user probe harness — that should be reconciled into main before graph-review and scenario-options work depends on generated completed-spec fixtures. After that, the highest-coordination work is intent-graph semantics and the semantic changeset ledger; FE-701 should follow soon after the FE-705 reconciliation because the current schema already carries transitional multi-chat / reconciliation placeholders that only become coherent once `changeset` / `change` owns semantic mutation history. Lower-coordination provider, gitignore, and web-research work can proceed in parallel. - -The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agent-mutation design notes are reconciled into one direction. `docs/design/MULTI_CHAT.md` is the substrate document. `docs/design/SIDE_CHAT.md` describes side-chat V1 / V2 / V3.0 / V3.1 / V4 phasing on top of that substrate. `docs/design/PATCH_LEDGER.md` remains historical deeper design pressure for semantic mutation history, but canonical future-facing vocabulary is `changeset` / `change`. The product-layer ontology trajectory is split out as `docs/design/INTENT_GRAPH_SEMANTICS.md` and `docs/design/BEHAVIORAL_KERNELS.md`; broader synthesis lives in `docs/archive/design/INTENT_SPEC_EVOLUTION.md`. FE-705's branch-local strategy/proposal notes add scenario options, graph-review oracle, chat-local strategies, and concern/dependency mapping; those notes should become a canonical design doc when the branch is integrated. Coordination uses a substrate-strangler posture: keep existing frontend REST/SSE contracts stable while route adapters and capability adapters converge on shared server-owned handlers, then cut over UI flows only after parity and changeset-backed authority exist. The dev-layer self-tooling trajectory lives in `docs/design/ln-skills/EVOLUTION.md`. - -## Sequencing - -### Active - -1. `agent-fixture-substrate` — branch-complete off main, reconciling — FE-705 integration substrate for JSONL agent capability CLI and LLM-as-user probes. -2. `intent-graph-semantics` — FE-700 relation-policy foundation ready for PR review; remaining ontology expansion candidates need fresh scoping before more build work. - -### Next - -1. `chat-runtime-secondary-chats` — Track 2 of the runtime umbrella; immediate successor to continuous-workspace. Implement inline/collapsible secondary chats over existing chat/turn; explicitly defer a `thread` table. -2. `changeset-ledger` — Track 4 of the runtime umbrella; parallel with Track 2; semantic history spine needed before canonical proposal acceptance, direct-edit atomicity, and productized scenario options. -3. `chat-context-provision` — Track 5 of the runtime umbrella recast as transcript-first context; can proceed against chat/turn once secondary-chat entry/anchor shape is settled. -4. `reconciliation-runtime` — Track 3 of the runtime umbrella; after Track 2 + Track 4 provide the secondary-chat surface and durable attribution. -5. `graph-review-scenario-options` — artifact-only critique/probe lane; can advance in parallel with FE-700 if it does not commit canonical graph truth. -6. `productized-scenario-options` — user-facing acceleration surface after FE-700 semantics, FE-701 changesets, and graph-review probes. - -### Parallel / Low-conflict - -- `first-run-provider-setup` — provider/key UX and runtime seam can progress independently of semantic-stack work. -- `workspace-gitignore-assist` — small workspace hygiene surface with low overlap. -- `productized-web-research` — waits on prompt/context scenario substrate for probe quality, but can remain separate from semantic schema work. - -### Horizon - -- `relation-first-observer-enrichment` -- `architect-generator-loop` -- `server-mini-library-compartmentalization` -- `side-chat-v4b-item-versioning` (depends on `changeset-ledger`) -- `dashboard-summaries` -- `spatial-graph-layout` -- `graph-view-active-path-filter` -- `mcp-adapter` -- `file-based-persistence` -- `typed-fixture-builder-convergence` -- `structured-development-spec-registry` -- `portability-boundaries` - -## Frontier Definitions - -### continuous-workspace - -- **Name:** Continuous workspace / phase-addressable interview surface (Conversational Workspace Runtime — Track 1) -- **Linear:** FE-709 -- **Kind:** structural -- **Status:** done -- **Objective:** Replace per-phase rendering boundaries with a cumulative center pane, realized phase sections, one chat runtime per specification, sidebar section navigation, scroll/focus behavior, and preservation of the single actionable frontier at the current reachable phase. -- **Why now / unlocks:** Workflow read/write ownership is extracted, the multi-chat substrate ships chat containers below the specification, and side-chat V3.0/V3.1 closed the cascade surface. This gives future side-chat persistence, strategy chats, and graph/workspace routes a stable host without introducing a second durable workflow model. -- **Acceptance:** Realized phase sections remain legible, future sections stay unreachable until valid, navigation is focus/scroll state only, and the current phase retains exactly one actionable frontier/recovery/handoff/completion affordance. -- **Verification:** Manual workspace walkthroughs across kickoff-ready, active, review-active, recovery, close-to-next-phase, resume/reload, and future-phase deep-link states; regression tests around route/workflow state where available. -- **Traceability:** A58; D86, D87, D110, D113, D114; I24, I102. -- **Design docs:** `docs/design/CONTINUOUS_WORKSPACE_HYBRID.md`; umbrella synthesis in `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` (Track 1). - -### chat-runtime-secondary-chats - -- **Name:** Chat runtime — inline secondary chats (Conversational Workspace Runtime — Track 2) -- **Linear:** FE-710 if retitled; otherwise unassigned in this plan snapshot -- **Kind:** structural -- **Status:** not-started / replanned -- **Objective:** Render side, reconciliation, qa, and strategy chats inline as collapsible secondary chats in the workspace using the existing chat/turn substrate. Defer schema-level `thread`; do not add `thread` / `turn.thread_id` unless a later RFC proves chat/turn insufficient. Retire the SideChatPopover as a UI surface only after parity exists over durable secondary chats. -- **Why now / unlocks:** Track 1 (workspace shell) ships, providing the stable host. Inline secondary chats are the critical unblocker for reconciliation absorption (Track 3) and give chat-context provision (Track 5) stable initiating anchors without creating a competing strategy/context substrate. Supersedes the prior side-chat V4a persistence horizon — persistent side-chat history becomes durable secondary chats rendered inline. -- **Acceptance:** Secondary chat kinds (`side`, `reconciliation`, `qa`, `strategy`) are representable with chat/turn; each active/resumable chat preserves one open assistant/system-first frontier turn; secondary chats render inline/collapsible in the unified workspace; SideChatPopover retires as cutover; transient staged-patches strip does not become a new source of semantic truth; turn-zero (`turn_kind='kickoff'`) seeds secondary chats with explicit context snapshots. -- **Verification:** Chat/turn persistence and reload tests, inline secondary-chat rendering tests, one-open-frontier-per-chat tests, manual walkthroughs for side/qa/strategy chat creation/display/collapse, and regression on existing interview flow. -- **Traceability:** Requirement 45; A49, A94; D86, D87, D110, D114, D138, D153; I111, I116, I120. -- **Design docs:** `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` §3.2 + §5 Track 2; `docs/design/MULTI_CHAT.md`; `docs/design/SIDE_CHAT.md`; `docs/design/SPEC_EVOLUTION_STRATEGIES.md`. - -### reconciliation-runtime - -- **Name:** Reconciliation runtime — async-by-default with in-stream secondary chat (Conversational Workspace Runtime — Track 3) -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** not-started -- **Objective:** Absorb reconciliation into the unified chat surface as a target-grouped secondary chat with async-by-default classifier scheduling and a "Reconcile Now" user trigger. Retire the standalone PendingReviewSection. Auto-confirmed rows resolve invisibly; only `auto-edit` (one-click apply) and `substantive` (judgment affordances) reach the user. -- **Why now / unlocks:** Tracks 2 (chat runtime) and 4 (changeset ledger) provide the secondary-chat surface and durable attribution. The reconciliation chat replaces the V3.1 Pending review section and the side-chat popover's reconciliation surface with a conversational target-grouped chat inside the workspace. -- **Acceptance:** Reconciliation chat renders target-grouped (topologically sorted upstream-first per PATCH_LEDGER target ordering); async classifier runs in background; auto-confirmed never surfaces; auto-edit has one-click apply; substantive has judgment affordances; "Reconcile Now" trigger in workspace shell; standalone PendingReviewSection retired as cutover. -- **Verification:** Reconciliation chat rendering tests, classifier scheduling tests, target-ordering tests, manual walkthroughs for async classification + Reconcile Now trigger, regression on existing reconciliation flow. -- **Traceability:** Requirement 45; A49, A88, A96; D135, D137, D138, D146, D153; I111, I113, I114, I120. -- **Design docs:** `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` §3.3 + §5 Track 3; `docs/design/MULTI_CHAT.md` §5; `docs/design/PATCH_LEDGER.md` §Target Ordering, §Reconciliation Flow. - -### chat-context-provision - -- **Name:** Chat context provision — transcript-first snapshots, handles, `#` mention, turn-zero (Conversational Workspace Runtime — Track 5) -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** not-started -- **Objective:** Implement transcript-first context provision for chats: turn-zero inserts explicit context snapshots stored on turns and derived from chat kind/strategy/anchors; `#` mention resolves to item ids, an inserted context snapshot, and an active chat handle; before new assistant turns, stale handles detect newer graph item versions/fingerprints and insert fresh snapshots only for changed subjects. Do not persist a hidden context-spec table by default. TOON or another compact graph serializer may format inserted snapshots/context packs. -- **Why now / unlocks:** Secondary chats and strategy chats need stable, replayable prompt context that survives multi-chat edits without ambient graph rehydration. Transcript-first snapshots let prompt/context engineering remain the authority while preserving replay and audit. -- **Acceptance:** Chat prompts use transcript context first; initial anchors and mentions insert visible/replayable context snapshot artifacts on turns; active chat handles store referenced item ids plus last-snapshotted version/fingerprint and refresh changed graph subjects by inserting new snapshots before the next assistant turn; unchanged handles do not duplicate snapshots; snapshots preserve old versions rather than mutating; context builders can render one-or-more item snapshots, item-neighborhood snapshots, and economic whole-graph snapshots via typed context packs; neighborhood modes cover immediate adjacency, dependencies, dependents/impact, evidence, and reconciliation, with changeset-historical neighborhoods added once the ledger can identify original-capture and last-update surroundings; handles are revocable or expire by explicit transcript event/policy. -- **Verification:** Context snapshot artifact tests; changeset-backed stale-handle refresh tests across changes from another chat; no-refresh tests for unchanged item versions; `#` mention resolution/disambiguation tests; structured JSON assertions plus selected golden renderings for item-list, neighborhood-mode, and economic-graph context builders; historical-neighborhood tests once changesets can identify original capture / last update context; turn-zero prompt assembly tests per chat kind/strategy; and manual walkthroughs for side/qa/strategy chat context. Handle freshness waits on real item versions from `changeset-ledger` rather than temporary fingerprints. -- **Traceability:** Requirement 45; A80, A81, A84, A85, A95; D136, D137, D139, D140, D154; I112, I120. -- **Design docs:** `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` §3.5 + §5 Track 5; `docs/design/SPEC_EVOLUTION_STRATEGIES.md`; prompt/context pack docs. - -### agent-fixture-substrate - -- **Name:** FE-705 integration — agent capability CLI + LLM-as-user fixture probe -- **Linear:** FE-705 -- **Kind:** structural -- **Status:** branch-complete / reconciling -- **Objective:** Integrate the branch-complete local `brunch agent` JSONL capability adapter and external probe runner so agents can drive the real Brunch interview flow through Brunch-owned contracts rather than privileged ORM access. -- **Why now / unlocks:** Prompt/context and graph-review probes need realistic graph/transcript fixtures, but hand-authoring those fixtures is chicken-and-egg. A JSONL capability adapter lets an external LLM-as-user drive the real lifecycle through the same mutation authority future agents must use, pressure-testing tool-call vocabulary, chat readiness, resource identity, fixture curation, and import-boundary discipline. Pi comparison remains FE-635 after this seam has a real Brunch use case to compare against. -- **Acceptance:** Server-owned capability contracts and JSONL protocol/session code are integrated; the probe runner uses only the JSONL client/process boundary; fixture-candidate artifacts preserve scenario briefs, model policy, generated transcripts, and workspace-state inspection without becoming Brunch authority. -- **Verification:** Contract/dispatcher tests, JSONL protocol/session tests, import-boundary tests, fake process tests, opt-in real-provider smoke, and fixture-candidate structure/readiness checks. -- **Traceability:** Requirement 43; A89; D143, D147; I115. Also protects Requirements 40, 41, 42 by making prompt/context and mutation-surface probes executable through a real adapter. -- **Design docs:** `docs/design/AGENT_MUTATION_SURFACE.md`; `docs/design/SUBSTRATE_STRANGLER_COORDINATION.md`; `docs/archive/design/INTENT_SPEC_EVOLUTION.md`; FE-705 branch artifacts until rebased. - -### intent-graph-semantics - -- **Name:** Intent graph semantics + relation-policy directionality foundation -- **Linear:** FE-700 -- **Kind:** structural -- **Status:** ready for PR review — relation-policy registry, read-only intent neighborhood snapshots, policy-driven cascade impact, and side-chat/apply impact parity are done; invariant/example ontology expansion candidates are deferred until freshly scoped. -- **Objective:** Refine the ontology and relation policy so the graph can represent invariants, examples/counterexamples, constraint subtypes, narrowed decisions, witness strength, checkability gaps, and operational edge behavior as source/destination material for future generative features. -- **Why now / unlocks:** Candidate generation, behavioral kernels, graph review, scenario-options acceleration, architect proposals, direct-edit cascade, and downstream verification-aware decomposition all need a sharper semantic target than the current exploration/review ontology. This semantic-layer lane is most likely to collide with parallel work, so it should land before broad observer enrichment or canonical candidate-bundle acceptance. -- **Acceptance:** `invariant` and `example` are first-class durable kinds; examples are subtyped; `decision` is narrowed; `constraint`, `criterion`, and `invariant` semantics are enriched; `checkability` and witness strength are represented; relation families, negative relations, edge epistemic metadata, relation-policy directionality, and endpoint-relative display labels for dependency/dependent context snapshots are explicit. -- **Verification:** Corpus/fixture observer probes comparing old vs refined ontology; relation-policy unit tests for mixed-direction relations and endpoint-relative labels; graph-review manual assessment for precision/noise; context-pack probe outputs show authority, witness, relation support, dependency/dependent grouping, and directionality labels. -- **Traceability:** Requirement 38; A77, A78, A80, A81, A84; D134, D136, D137, D139, D140. -- **Design docs:** `docs/design/INTENT_GRAPH_SEMANTICS.md`; `docs/archive/design/INTENT_SPEC_EVOLUTION.md`; FE-705 strategy/proposal notes for relation directionality. - -### changeset-ledger - -- **Name:** Semantic changeset ledger + proposal-turn staleness -- **Linear:** FE-701 -- **Kind:** structural -- **Status:** not-started -- **Objective:** Introduce the semantic history spine that separates graph mutation history from conversational turn ancestry. -- **Why now / unlocks:** Scenario bundle acceptance, direct-edit atomicity, accepted-with-issues flows, stale proposal detection, graph-review repairs, side-chat V4b item versioning, and future architect/reconciliation agents all need a durable semantic mutation boundary. Without it, productized scenario-options can stay probe-only but cannot safely commit candidate bundles. The current DB substrate is already halfway there: `chat` and `reconciliation_need` exist, `specification.active_turn_id` / `chat.active_turn_id` are deliberately duplicated during the multi-chat transition, and `reconciliation_need.caused_by_patch_id` is a historical placeholder that should become changeset-backed provenance rather than be deleted as ordinary cruft. -- **Current schema observations:** Legacy dedicated knowledge tables (`decision`, `assumption`, `requirement`, `criterion`, and old join/parent tables) are retired in migration `0010`; current semantic truth is `knowledge_item` + `knowledge_edge` + `turn_knowledge_item`. `annotation` and `reconciliation_need` are active process/read-model tables even when empty in local DBs. `turn.turn_kind` / `turn.is_resolution` remain transitional structural-artifact markers until continuous workspace and multi-chat proposal semantics replace that projection. `docs/schema.dbml` is stale relative to `src/server/schema.ts` and should be regenerated or deleted when FE-701 touches schema docs. -- **Migration watch:** Live local `.brunch/brunch.db` was observed with only 18 applied migrations, stopping at `0017_reconciliation_need`; it lacked `0018` source snapshot columns and `0019` reconciliation-agent columns even though `src/server/schema.ts` defines them. There is no explicit `npm run migrate`; app/server `createDb()` runs Drizzle migrations automatically. Before FE-701 schema work, verify the target DB by inspecting `__drizzle_migrations` and `PRAGMA table_info(reconciliation_need)` so drift is not misread as product intent. -- **Acceptance:** Schema and operation vocabulary use `changeset` / `change`; specifications track latest semantic changeset; proposal turns carry base/opened changeset identity; `reconciliation_need.caused_by_changeset_id` replaces/connects the historical patch placeholder; non-accept proposal actions cannot mutate graph truth; a changeset is the smallest atomic unit preserving semantic coherence. -- **Verification:** DB atomicity tests for changeset + changes + reconciliation_need writes, staleness tests for open proposal turns across multi-chat changes, migration/drift checks against an actual SQLite DB, and capability/transition tests proving non-accept actions cannot mutate graph truth. -- **Traceability:** Requirements 39, 42, 44; A71, A79; D135, D138, D143. -- **Design docs:** `docs/design/PATCH_LEDGER.md` (historical filename; future vocabulary is changeset/change); `docs/design/SUBSTRATE_STRANGLER_COORDINATION.md`; FE-705 strategy/proposal notes for semantic history and proposal turns. - -### graph-review-scenario-options - -- **Name:** Graph-review oracle + scenario-options probes -- **Linear:** FE-702 for graph-review / scenario probes; FE-649 and FE-640 remain productization children under FE-698 where relevant -- **Kind:** structural -- **Status:** not-started -- **Objective:** Build the internal critique path and artifact-only candidate bundle probes before product UI. -- **Why now / unlocks:** Product wants first-turn strategy choice and mid-interview acceleration, but engineering needs graph-review critique to make generated candidate bundles credible. This lane can advance in parallel with FE-700 if it stays artifact-only and does not commit canonical graph truth. -- **Acceptance:** Candidate graph bundle and graph-review finding artifacts exist; graph-review prompt/context pack and rubric cover coherence, fixed-premise respect, coverage, tradeoff honesty, checkability, granularity, scenario fidelity, epistemic labels, provenance, and downstream usefulness; candidate readiness is classified as `draft` / `reviewing` / `reviewed_clean` / `reviewed_with_issues` / `blocked`; broader graph-review issues remain turn-owned unless querying/filtering needs prove otherwise. -- **Verification:** Scenario-runner fixtures, FE-705 JSONL-generated completed-spec fixtures, raw output review, structured parse validation, qualitative scorecards, and comparison against drilldown-produced graphs. Middle/outer-loop oracle design should decide when fixture candidates become golden. -- **Traceability:** Requirements 20, 21, 31, 32, 40, 41, 43, 44; A67, A68, A80, A85, A87, A89; D126, D127, D139, D141, D147. -- **Design docs:** `docs/design/BEHAVIORAL_KERNELS.md`; `docs/design/INTENT_GRAPH_SEMANTICS.md`; `docs/design/AGENT_MUTATION_SURFACE.md`; FE-705 strategy/proposal notes. - -### productized-scenario-options - -- **Name:** Productized scenario-options / candidate-spec completion assist -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** blocked -- **Objective:** Replace skip-only remainder handling with first-turn strategy choice and a mid-interview `speed this up` path that generates reviewed candidate graph bundles with tradeoffs, completing the current direction by default. -- **Why now / unlocks:** This is the likely first user-visible alternative to long drilldown, but product UI waits on graph-review probes, FE-700 semantics, and FE-701 changesets. Until then, scenario-options remain artifact/proposal-only. -- **Acceptance:** Users can choose or request acceleration via scenario options; generated bundles preserve accepted graph truth as fixed premise, present tradeoff profiles, and become canonical only through coherent accepted changesets with known issues represented as follow-on review/process debt. -- **Verification:** Probe comparison against direct drilldown, graph-review scorecards, accepted-with-issues flow tests once changesets exist, and manual user-flow review for trust/comprehension. -- **Traceability:** Requirements 31, 40, 44; A67, A77, A78, A85, A90, A91; D126, D134, D136, D139, D151, D152. -- **Design docs:** FE-705 strategy/proposal notes until canonicalized; `docs/design/BEHAVIORAL_KERNELS.md`; `docs/design/INTENT_GRAPH_SEMANTICS.md`. - -### first-run-provider-setup - -- **Name:** First-run provider setup -- **Linear:** FE-633 covers the OpenRouter/default-provider part; dashboard credential UX + XDG key storage may need a sibling issue if split from provider proving -- **Kind:** bounded feature -- **Status:** not-started -- **Objective:** Make missing LLM credentials visible on the dashboard, add a shared AI runtime provider seam for interviewer/observer model construction, support UI-entered keys through XDG-compliant user auth state, and evaluate whether OpenRouter should become the preferred onboarding provider while preserving Anthropic-specific capabilities or explicit degradation. -- **Why now / unlocks:** Can proceed independently and reduces first-run friction for real users and probe workflows. -- **Acceptance:** Dashboard surfaces provider credential status before specification creation; setup flow stores UI-entered keys outside the project workspace; interviewer/observer construction routes through a shared provider seam. -- **Verification:** Unit tests for provider precedence/storage paths, manual first-run walkthroughs, and provider capability spike for model naming, structured output, tool use, and reasoning/thinking support. -- **Traceability:** Requirements 34, 35, 36; A74, A75; D130, D131, D132; I106. -- **Design docs:** none yet beyond SPEC/PLAN entries. - -### workspace-gitignore-assist - -- **Name:** Workspace hygiene / `.brunch/` gitignore assist -- **Linear:** FE-648 -- **Kind:** bounded feature -- **Status:** not-started -- **Objective:** Detect whether generated local state is already ignored and, with explicit confirmation, add an idempotent `.gitignore` entry or create `.gitignore` when absent. -- **Why now / unlocks:** Low-conflict guardrail that reduces accidental commits of local Brunch state. -- **Acceptance:** The app detects absent, present, and already-covering ignore states; previews repository mutation; mutates `.gitignore` only after explicit confirmation; append/create behavior is idempotent and content-preserving. -- **Verification:** Unit tests for ignore detection/append behavior and manual dashboard walkthrough with absent, present, and already-covering `.gitignore` states. -- **Traceability:** Requirement 37; A76; D133; I107. -- **Design docs:** none yet beyond SPEC/PLAN entries. - -### productized-web-research - -- **Name:** Productized web research capability -- **Linear:** FE-649 -- **Kind:** structural -- **Status:** not-started -- **Objective:** Add web search and page-fetch tools as interviewer-invoked context gathering, surfaced as preface cards after the scenario substrate proves query framing, tool ergonomics, and provisional-context handling. -- **Why now / unlocks:** Extends the same phase-agnostic preface-card model to external research, but should wait for prompt/context scenario substrate proof so web research does not become an ad hoc tool surface. -- **Acceptance:** Research tools are invoked through interviewer context gathering, outputs render as provisional preface cards paired with questions, and observer capture treats the validated full turn as atomic. -- **Verification:** Prompt/context scenario probes for query framing and tool-output summarization, plus manual review of provisional-context handling. -- **Traceability:** Requirements 20, 21, 40, 41; D125, D139, D140, D142. -- **Design docs:** FE-698 prompt/context scenario substrate references; future productized research notes if needed. - -### relation-first-observer-enrichment - -- **Name:** Relation-first observer capture enrichment -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** horizon -- **Objective:** Broaden observer output across the refined ontology without flooding the graph. -- **Why now / unlocks:** First cut is shipped; enrichment waits for FE-700 relation policy so observer output can become semantically richer while preserving prompt-budgeted compact anchors and user trust. -- **Acceptance:** Observer extraction captures richer relation families and operational metadata with abstention under weak support. -- **Verification:** Observer corpus probes, graph/export review for precision/noise, and context-pack output review. -- **Traceability:** Requirements 30, 38, 40; A66, A81, A84; D125, D136, D137, D139, D140; I109. -- **Design docs:** `docs/design/INTENT_GRAPH_SEMANTICS.md`. - -### architect-generator-loop - -- **Name:** Architect / generator loop -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** horizon -- **Objective:** Explore an autonomous agent that iterates over the intent graph and proposes semantic changes for HITL review through the same future changeset/reconciliation pathway as user-driven edits. -- **Why now / unlocks:** Related to scenario-options but broader; keep productized architect proposals behind multi-chat, reconciliation, and semantic changesets. Use the scenario substrate for shadow/proposal-only probes first. -- **Acceptance:** Shadow/proposal-only architect outputs can be compared against user-driven edits without mutating canonical graph truth. -- **Verification:** Scenario substrate probes and human comparison against accepted user edits. -- **Traceability:** A73, A85, A87; D139, D141. -- **Design docs:** `docs/design/BEHAVIORAL_KERNELS.md`; future design doc if promoted. - -### server-mini-library-compartmentalization - -- **Name:** Server mini-library compartmentalization -- **Linear:** unassigned in this plan snapshot -- **Kind:** refactor -- **Status:** in-progress opportunistically on FE-705 lane; `db.ts` persistence facade extraction complete, broader server roots remain horizon. -- **Objective:** Refactor growing server seams into plural public roots with same-named private subtrees where FE-698 / FE-705 pressure has made boundaries too implicit. -- **Why now / unlocks:** Near-term refactor candidate after FE-705 integration, not product roadmap work. The persistence facade now proves the pattern: `db.ts` owns connection setup and curated public exports while private `src/server/db/*-store.ts` modules own cohesive persistence implementation. -- **Acceptance:** Candidate seams such as `db.ts`, `fixtures.ts`, `context-packs.ts`, `prompts.ts`, `scenario-runner.ts`, `entity-apis.ts`, and `agent-apis.ts` hide private implementation subtrees behind stable public roots where real pressure exists. -- **Verification:** Existing test suite plus import-boundary review; for the completed `db.ts` slice, focused store/route/workflow tests, `npm run check`, and `npm run build` pass. -- **Traceability:** code organization convention in `AGENTS.md`. -- **Design docs:** none. - -### side-chat-v4b-item-versioning - -- **Name:** Side-chat V4b — item versioning + branched exploration -- **Linear:** FE-675 umbrella, V4b half -- **Kind:** structural -- **Status:** horizon -- **Objective:** Add item versioning and branched exploration once the changeset ledger lands. -- **Why now / unlocks:** Item versioning unblocks dangling-annotation repair and soft-edit audit; branched exploration lets drill-downs, past-turn edits, and revisits coexist with the original chain. -- **Acceptance:** Prior item versions are queryable for diff/comparison/audit while active-path projection always reflects latest semantic truth. -- **Verification:** Changeset-backed versioning tests, revisit cascade tests, and annotation repair walkthroughs. -- **Traceability:** A72, A73, A85; D139, D141. -- **Design docs:** `docs/design/MULTI_CHAT.md`; `docs/design/PATCH_LEDGER.md`. - -### dashboard-summaries - -- **Name:** Dashboard result summaries and completeness metrics -- **Linear:** unassigned in this plan snapshot -- **Kind:** bounded feature -- **Status:** horizon -- **Objective:** Improve progress visibility across specifications. -- **Why now / unlocks:** Lower-priority product surface after core workspace and semantic substrate stabilize. -- **Acceptance:** Dashboard communicates spec progress/completeness without implying false closure. -- **Verification:** Manual dashboard walkthroughs. -- **Traceability:** Requirements 8, 13, 15. -- **Design docs:** none. - -### spatial-graph-layout - -- **Name:** Spatial canvas layout for graph view -- **Linear:** unassigned in this plan snapshot -- **Kind:** bounded feature -- **Status:** horizon -- **Objective:** Add the spatial DAG layout as a second layout choice inside graph mode, alongside the structured-list route. -- **Why now / unlocks:** Graph view already ships as a structured-list peer route; spatial layout follows once relation density and graph interaction needs justify it. -- **Acceptance:** Users can switch between structured-list and spatial canvas layouts without changing projection semantics or action contracts. -- **Verification:** Manual graph-view walkthroughs at low/high edge density plus visual regression if available. -- **Traceability:** Requirement 33; A69, A70; D128. -- **Design docs:** graph-view sections in SPEC; future graph-view design notes if promoted. - -### graph-view-active-path-filter - -- **Name:** Graph view active-path render filter + scope toggle -- **Linear:** unassigned in this plan snapshot -- **Kind:** bounded feature -- **Status:** horizon -- **Objective:** Render only active-path items by default in graph view, with a `Show all` toggle. -- **Why now / unlocks:** Lower-priority graph legibility improvement after core graph semantics and projection surfaces stabilize. -- **Acceptance:** Active-path filtering is default, user can inspect all items, and edge rendering remains honest under both scopes. -- **Verification:** Graph-view fixtures for active-path/all toggles. -- **Traceability:** D128 and graph-view requirements. -- **Design docs:** none. - -### mcp-adapter - -- **Name:** MCP server adapter for core operations -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** horizon -- **Objective:** Expose future adapter over capability contracts, not direct ORM/route wrappers. -- **Why now / unlocks:** Deferred until capability contracts stabilize through FE-705 and real agent/probe use. -- **Acceptance:** MCP tools wrap Brunch-owned capability contracts and preserve resource identity, authority metadata, and mutation semantics. -- **Verification:** Contract adapter tests and import-boundary tests. -- **Traceability:** Requirements 42, 43; D143, D147. -- **Design docs:** `docs/design/AGENT_MUTATION_SURFACE.md`. - -### file-based-persistence - -- **Name:** Git-friendly file-based persistence representation for diffable exported specs -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** horizon -- **Objective:** Explore a diffable file representation for exported/durable spec truth. -- **Why now / unlocks:** Deferred until product ontology and changeset semantics are clearer. -- **Acceptance:** File representation preserves intent graph meaning and review/export boundaries without becoming a second source of truth. -- **Verification:** Round-trip and diff-fixture tests if promoted. -- **Traceability:** Product direction from planning specs toward intent specs; D134, D135. -- **Design docs:** future design needed if promoted. - -### typed-fixture-builder-convergence - -- **Name:** Typed fixture-builder convergence for happy-path tests -- **Linear:** unassigned in this plan snapshot -- **Kind:** hardening -- **Status:** horizon -- **Objective:** Converge test fixtures around typed builders that represent current product semantics. -- **Why now / unlocks:** Useful after semantic schema work stabilizes so tests do not fossilize obsolete ontology names. -- **Acceptance:** Happy-path tests can create coherent specs/chats/turns/intent graph state through typed builders with minimal duplication. -- **Verification:** Existing test suite, fixture API review, and migration of representative tests. -- **Traceability:** I48, I109, I111, I112. -- **Design docs:** none. - -### structured-development-spec-registry - -- **Name:** Structured development spec registry -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural / process -- **Status:** horizon -- **Objective:** Prototype file-backed canonical spec records, deterministic checks, generated markdown views, and task-local slices for Brunch's own development workflow. -- **Why now / unlocks:** Self-tooling experiment, not product functionality. It would make `memory/SPEC.md` / `memory/PLAN.md` generated views over structured records to reduce drift and merge conflicts. -- **Acceptance:** Generated views preserve current planning ergonomics while reducing merge churn and cross-reference drift. -- **Verification:** Deterministic generation checks and branch-conflict dry runs. -- **Traceability:** dev-layer trajectory only; not product-layer ontology. -- **Design docs:** `docs/design/ln-skills/EVOLUTION.md`. - -### portability-boundaries - -- **Name:** Portability boundaries -- **Linear:** unassigned in this plan snapshot -- **Kind:** structural -- **Status:** horizon -- **Objective:** Split durable store/read-model, interview session runtime, and workspace capability provider if Brunch targets hosted, remote, embedded, or sandbox-backed operation. -- **Why now / unlocks:** Future architecture boundary map for non-local deployments or adapter-backed execution. Deferred until hosted/remote/sandbox operation becomes a product goal. -- **Acceptance:** Boundary map supports hosted/remote/sandbox decisions without prematurely abstracting the local-first product. -- **Verification:** Architecture review and spike if product direction changes. -- **Traceability:** portability assumptions in design docs; current local-first constraint in SPEC. -- **Design docs:** `docs/design/PORTABILITY_BOUNDARIES.md`. - -## Recently Completed - -- [2026-05-13] `continuous-workspace` — Done: FE-709 / PR #134. Replaced per-phase InterviewView with ContinuousWorkspaceView (cumulative center pane), extracted `useContinuousWorkspaceController`, added sidebar scroll-spy via WorkspaceFocusContext, extracted shared controller helpers to core, retired route-first test assumptions. Verified: `npm run verify` 1213 / 1214 pass (1 pre-existing flake). Watch: Step 5 route-collapse decision deferred — hybrid works as intended. -- [2026-05-11] `side-chat-v3-1-agent-grouped-reconciliation` — Done: FE-674 / PR #124 + downstack closed the V3.x arc end-to-end with spec-level classifier route, per-row reset route, agent classification lifecycle, chips, per-class actions, and bulk Confirm-all / Apply-all-suggested. Verified: `npm run verify` 1178 / 1179 pass with one unrelated `side-chat-route` flake. Watch: A88 outer-loop walkthrough on a dense spec remains open to assess legibility vs V3.0's flat list. -- [2026-05-11] `fe-698-reconciliation-context-pack` — Done: added proposal-only reconciliation prompt/context scenario rendering open reconciliation needs with source/target anchors, reason/status, prompt/context fingerprints, and read-only capability metadata. Verified: `npm run verify`. Watch: next FE-698 work can broaden read-only/proposal-only probes and Pi adapter spike without treating this pack as a resolution agent. -Older history: `docs/archive/PLAN_HISTORY.md` - -## Dependencies - -```text -TRACK A — Conversational Workspace Runtime umbrella -continuous-workspace (Track 1, done — FE-709) - └──→ chat-runtime-secondary-chats (Track 2; no schema-level thread) - ├──→ reconciliation-runtime (Track 3, also needs Track 4) - └──→ chat-context-provision (Track 5; transcript-first snapshots/handles) -changeset-ledger (Track 4, parallel with Track 2) - ├──→ richer attribution in reconciliation-runtime (Track 3) - ├──→ real item versions for chat-context-provision handle freshness (Track 5) - ├──→ original-capture / last-update historical neighborhoods for context snapshots (Track 5) - └──→ unlocks architect-generator-loop and side-chat-v4b-item-versioning - -TRACK B — Agent fixture substrate / strangler handler seam -prompt/context scenario substrate foundation (completed) - └──→ agent-fixture-substrate - ├──→ shared route/capability handler seam without frontend DTO cutover - ├──→ generated completed-spec fixture candidates - ├──→ graph-review-scenario-options - └──→ Pi harness comparison (future, FE-635) - -TRACK C — Semantic substrate (highest coordination) -multi-chat-substrate + reconciliation-needs (completed) - ├──→ intent-graph-semantics - │ ├──→ relation-first-observer-enrichment - │ ├──→ robust direct-edit / reconciliation cascade policy - │ └──→ graph-review-scenario-options becomes semantically meaningful - └──→ changeset-ledger - ├──→ canonical scenario bundle acceptance - ├──→ direct-edit atomicity with caused_by_changeset_id - ├──→ stale open proposal detection - └──→ architect-generator-loop / verifier/import mutation provenance - -TRACK D — Strategy probes, frontend artifacts, and product acceleration -agent-fixture-substrate + intent-graph-semantics - └──→ graph-review-scenario-options - ├──→ fixture-backed candidate / graph-review UI artifacts can proceed without canonical mutation - └──→ productized-scenario-options - ├──→ absorbs / reshapes two-axis interview framing - └──→ absorbs / reshapes progressive detail / recursive deflation - -TRACK E — Low-conflict parallel work -first-run-provider-setup -workspace-gitignore-assist -productized-web-research - -LOWER-PRIORITY / DEFERRED -side-chat-v4b-item-versioning (depends on changeset-ledger) -spatial-graph-layout + graph-view-active-path-filter -dashboard-summaries -mcp-adapter / file-based-persistence / typed-fixture-builder-convergence -structured-development-spec-registry -portability-boundaries - -RETIRED -side-chat-persistence-v4a — superseded by chat-runtime-secondary-chats (Track 2) -``` diff --git a/archive/memory/SERVER_REFACTOR_NOTES.md b/archive/memory/SERVER_REFACTOR_NOTES.md deleted file mode 100644 index 951bc4188..000000000 --- a/archive/memory/SERVER_REFACTOR_NOTES.md +++ /dev/null @@ -1,60 +0,0 @@ -# Temporary Review Notes — Remaining `src/server/` Structuring Findings - -Captured from the `src/server/` structure review after the `db.ts` extraction was completed. This is a temporary working note, not canonical architecture truth; reconcile durable decisions back into `memory/SPEC.md` / `memory/PLAN.md` if selected for implementation. - -## Already addressed in this thread - -- `db.ts` now acts as a public persistence facade over private `src/server/db/*-store.ts` modules: - - `annotation-store.ts` - - `edit-impact-store.ts` - - `entity-projection-store.ts` - - `intent-graph-store.ts` - - `reconciliation-store.ts` - - `review-materialization-store.ts` - - `specification-store.ts` - - `workflow-store.ts` - -## Remaining findings - -1. **`app.ts` is both route registry and workflow orchestrator** — category: depth / seam — impact: high - - **Files:** `src/server/app.ts`, `*-route.ts`, `chat-route-transition.ts`, `turn-response-transition.ts`, `phase-intent-runtime.ts` - - **Problem:** `app.ts` still wires Express routes, parses params, maps errors, manages observer capture concurrency, validates AI SDK messages, runs chat transitions, streams interviewer output, persists artifacts, and registers unrelated route families. Some route families have their own `*-route.ts` handlers while specification/chat routes remain inline. - - **Possible direction:** Keep `createApp` as the public composition root, but move route families and shared request/error helpers behind `src/server/app/*` private modules. The chat streaming route likely deserves its own route module because it is already a substantial imperative shell. - - **Benefit:** Clearer HTTP ownership, smaller composition root, easier addition of provider/setup/gitignore routes, and less risk that route-local concerns become global `app.ts` state. - -2. **The capability adapter is not yet a clean adapter over product operations** — category: seam / coupling — impact: medium-high - - **Files:** `src/server/capabilities.ts`, `agent-jsonl.ts`, `capability-registry.ts`, `chat-route-transition.ts`, `turn-response-transition.ts`, `core.ts`, `schema.ts` - - **Problem:** SPEC/PLAN describe the agent capability CLI as an adapter over Brunch-owned capability contracts, but `capabilities.ts` still imports workflow transitions, core functions, DB helpers, and schema directly. That makes the agent path a parallel orchestration surface rather than a thin transport adapter. - - **Possible direction:** Split `capabilities.ts` into a public root plus `capabilities/*` private handlers. Route selected capability implementations through named application operations that are also usable by HTTP routes, with JSONL remaining protocol glue. - - **Benefit:** Prevents agent paths from bypassing server-owned mutation semantics and makes future MCP / external harness adapters safer. - -3. **Provider/model construction is scattered across server shells** — category: seam / model — impact: medium - - **Files:** `src/server/side-chat-route.ts`, `src/server/reconciliation-agent.ts`, likely interviewer / observer construction paths. - - **Problem:** Direct `@ai-sdk/anthropic` imports and env reads remain in route/agent modules. SPEC names an **AI runtime provider** seam and says interviewer/observer model creation should not encode direct provider imports or environment-variable reads as product truth. - - **Possible direction:** When `first-run-provider-setup` starts, make provider/model resolution a real server subsystem rather than patching each caller independently. - - **Benefit:** One credential/model precedence story, less duplicated provider knowledge, and a cleaner path to OpenRouter/provider-neutral routing. - -4. **Lexicon drift still makes server ownership harder to read** — category: naming — impact: medium - - **Canonical terms:** `specification`, `intent graph`, `intent item`, `intent edge`, `changeset/change`. - - **Remaining deviations:** - - Many tests and some helpers still use `project` variables for specification rows, e.g. `const project = createSpecification(...)`, `project.id`. - - `createLegacyKickoffTurnForTesting(db, projectId)` takes a specification id and should prefer `specificationId` if retained. - - API/path/function names still expose `knowledge-items` / `knowledge-edges`; persistence compatibility is acknowledged, but new server structure should prefer intent terminology at module boundaries. - - CLI/help/test vocabulary still contains historical `patch` wording; SPEC says `changeset/change` supersedes patch vocabulary. - - **Possible direction:** Run a targeted naming refactor once active semantic-schema work decides how aggressively to rename public API paths vs internal implementation. - -5. **Legacy test scaffolding preserves old workflow rows** — category: model / naming — impact: medium - - **Files:** `src/server/test-support/legacy-control-rows.ts`, usages in `app.test.ts`, `core.test.ts` - - **Problem:** The product is pre-release and SPEC says projection-only control cards are current truth, but tests still keep `createLegacyKickoffTurnForTesting` to fabricate older durable kickoff rows. It may be useful as regression coverage, but currently reads like supported compatibility. - - **Possible direction:** Either delete the legacy-path tests if no longer product-relevant, or quarantine them under an explicit legacy regression area with a narrow purpose. - -6. **Test files still mirror some of the flat spread** — category: testability / coupling — impact: medium - - **Files:** especially `src/server/app.test.ts`, `src/server/db.test.ts`, plus many top-level `*.test.ts`. - - **Problem:** The persistence implementation is now split, but the largest tests still exercise many seams through broad files. This makes it harder to tell which subsystem owns an invariant. - - **Possible direction:** As further modules are deepened, move or split tests around the public seams: route-family tests near route-family modules, store tests by semantic store, capability adapter tests around the adapter seam. - -## Suggested next refactor candidates - -1. `app.ts` private route modules — highest remaining structural payoff. -2. `capabilities.ts` public root + private handlers — important for FE-705 / future adapter safety. -3. Provider/model resolver seam — best handled under `first-run-provider-setup` rather than as a pure structure cleanup. diff --git a/archive/memory/SPEC.md b/archive/memory/SPEC.md deleted file mode 100644 index 61f1d88f6..000000000 --- a/archive/memory/SPEC.md +++ /dev/null @@ -1,427 +0,0 @@ - - -# Brunch v2 — Spec Elicitation Tool - -## Product Contract - -### Concept - -Brunch is an AI-guided spec elicitation tool that turns natural-language goals into structured specifications through a four-phase interview: - -1. **grounding** — goals, terms, context, constraints -2. **design** — commitments and tradeoffs -3. **requirements** — capability review and gap-finding -4. **criteria** — verification coverage - -An interviewer agent conducts the conversation. A separate observer agent extracts typed intent items from answered turns and links them into an intent graph. The interviewer may invoke context-gathering capabilities when it lacks orientation; visible outputs appear as provisional preface cards paired with question cards inside the same turn. - -Brunch's output is a calibrated handoff, not fake closure. The product direction is from **planning specs** toward **intent specs**: the durable source artifact should preserve meaning first — commitments, correctness properties, examples and counterexamples, assumptions, accepted evidence, and unresolved ambiguity. Planning and downstream sequencing remain useful projections from that source truth. - -Brunch operates inside a **workspace**: the cwd-backed software context whose local `.brunch/` directory stores one or more specifications. Grounding supports **elicitation-first** greenfield work and **analysis-first** brownfield work. The interview must also support whole-product work and partial-scope / incremental feature elicitation. - -### Constraints & Non-goals - -- Anthropic direct is the current runtime implementation; provider work may add OpenRouter or provider-neutral routing, but Brunch remains user-supplied-key / no hosted inference account for now. -- No collaborative editing. -- No explicit document-ingestion UX in V1. -- No hard turn-tree branching UX in V1; refinement and revisit operate through graph edit mode, multi-chat, and reconciliation surfaces. -- No automatic cascade deletion; downstream effects are surfaced and re-resolved explicitly. -- No task-planning or downstream execution-management surface in V1; Brunch elicits specs and stops at the handoff/export boundary. -- No general-purpose inline document editor in review phases; requirements and criteria review stay recommendation-led with lightweight comments. -- No offline-first or multi-tab sync layer; the current system stays server-authoritative and local-first. - -### Capability Requirements - -#### Runtime & persistence - -1. `npx brunch` in a project directory with configured supported LLM provider credentials opens a working app in the browser with state in local `.brunch/`. -14. Closing and reopening the browser resumes the specification from persisted state. -15. The dashboard shows multiple specifications / elicitation runs within one `.brunch/` directory. -34. First-run setup detects missing expected LLM provider credentials before the user starts a specification, makes the missing-key state visible on the dashboard, and offers a guided setup path. -35. If Brunch accepts an API key through the UI, it stores credentials outside the project workspace in XDG-compliant user auth/config state; project `.env` files and `.brunch/` never become the default secret-storage target. -36. LLM provider configuration is owned by a shared AI runtime provider seam, so interviewer and observer model creation do not encode direct provider imports or environment-variable reads as product truth. -37. Workspace hygiene detects whether local `.brunch/` is git-ignored and, with explicit user confirmation, can add an idempotent `.gitignore` entry, creating `.gitignore` when absent. - -#### Interview workflow - -2. Starting a new specification asks only for the specification name before entering the workspace; greenfield / brownfield grounding strategy is chosen through grounding entry states inside the specification workspace. -3. Brownfield grounding can use read-only workspace analysis to ground the opening flow and first substantive question. -4. Structured responses support turn-appropriate option selections or explicit action submissions, an explicit `none of the above` path where relevant, and one attached response note. One turn may carry multiple assistant-part artifacts rendered as stacked cards with one unified response submission. -5. Users can see thinking, tool usage, and streaming progress in real time; replay keeps concise durable activity metadata for live-only artifacts instead of dropping them. -8. Each workflow mode has deterministic closeability plus a separate readiness signal. -9. Phase close records summary text and closure basis. -16. Partial-scope elicitation works for a feature or bounded sub-area, not just whole-workspace greenfield specs. -17. Each phase exposes an explicit kickoff, frontier, recovery, handoff, or completion affordance; the UI must not strand the user with a bare generic composer as the only visible action. -18. Open interview phases default to a projected kickoff card, current frontier turn, visible generation state, or projected recovery affordance; closed phases terminate in a projected handoff or completion artifact. -19. The first phase is grounding in both product language and canonical workflow identifiers. -20. The interviewer may invoke context-gathering capabilities such as workspace analysis in any phase when the workspace directory is available; outputs appear as visible preface cards paired with question cards. -21. Preface cards are provisional context rendered as turn-internal artifacts, so observer capture uses the whole validated unit: preface context + question + user response. -24. Each phase section opens with a projected header that states phase purpose and captured knowledge kinds. -25. Review revisions stack in turn lineage but visually render only the current revision live with a version badge; prior revisions collapse to compact answered-turn summaries. -27. Grounding prompts use hint-guided, priority-ordered topics with example question shapes rather than generating every question from scratch. -28. Observer capture treats the full turn — including preface/revision artifacts, offer, and user response — as one atomic validated unit. -29. Grounding captures both workspace novelty (`greenfield` / `brownfield`) and delivery posture (`end-to-end build` / `incremental feature`). -31. Users can request a turn-owned candidate-spec set during grounding or design; accepting a direction may steer the next interview move and materialize intent items, but does not itself close the phase. -32. Interview detail can proceed as a progressive broad-pass-to-detail flow with explicit `next level of detail` actions. -44. Specifications can evolve through multiple chat-local strategies rather than one global interviewer mode. Each active/resumable chat has at most one open assistant/system-first frontier turn waiting for user completion. Proposal turns use normalized completion semantics; only proposal acceptance may apply semantic changes. -45. The workspace runtime can host secondary chats (`side`, `reconciliation`, `qa`, `strategy`) inline with the primary interview chat while keeping transcript replay, explicit turn-level context snapshots, graph-item handle refresh, and semantic mutations server-authoritative. Schema-level `thread` and hidden persisted context-spec tables are deferred. - -#### Knowledge / intent graph - -6. The observer extracts typed intent items and intent edges from answered turns. -7. The accumulated knowledge layer and readiness state stay visible during the interview. -10. Users can revisit knowledge through edit mode, cascade preview, and reconciliation / secondary-chat surfaces. -22. Grounding and elicitation persist only the durable exploration ontology (`goal`, `term`, `context`, `constraint`, `decision`, `assumption`); `non-goal` is represented as a `constraint` subtype, and requirements / criteria become durable only through accepted review outputs. -23. The knowledge/intent ontology is defined once and projected consistently through schema, shared registries, observer prompts, API types, fixtures, and UI copy. -30. Observer extraction treats typed relationships as first-class across the ontology and records them when reasonably supported while abstaining when support is weak. -38. The product ontology should expand beyond current exploration + review kinds to support `invariant` and `example` as first-class durable knowledge kinds. -39. Specifications can own multiple durable chat containers below the specification; turns belong to chats while legacy spec-scoped pointers remain transitional. Reconciliation needs remain process debt, separate from semantic intent edges. - -#### Review & export - -11. Requirements review synthesizes a candidate requirement set from the knowledge layer, presents stable item reference codes, supports per-item comments, and resolves through explicit `accept review` / `request changes` submission. -12. Criteria review synthesizes a candidate verification set from accepted requirements plus the knowledge layer, presents stable item reference codes, and supports the same per-item commenting and full-set review seam. -13. Export is available only when workflow closure, accepted review outputs, and staleness rules are satisfied. - -#### Workspace / graph UI - -26. The homepage surfaces workspace (CWD) binding so the user understands listed specifications and the new-spec affordance are scoped to the current project directory. -33. Graph view is a first-class alternative to chat view, accessed as a peer route, and projects the intent graph as a navigable workspace with visible relationship topology and graph-launched refinement. The first ship is a structured-list layout; a spatial canvas follows as a layout switch inside graph mode. - -#### Provider / agent substrate - -40. Prompt and context engineering are first-class server subsystems: prompts and reusable policy doctrines live as inspectable markdown assets, while typed context-pack builders derive scenario-specific intent-graph renderings. -41. Agent-heavy future capabilities can be tested before product UI exists through a lightweight scenario substrate that runs prompt/context packs against seeded graphs or transcript fixtures, captures outputs, and supports harness comparison. -42. Agent-originated mutations of Brunch data use one typed server-owned mutation surface regardless of caller; agents and harnesses may not mutate durable Brunch state by calling the ORM directly. -43. A local agent capability CLI can expose Brunch-owned capability contracts over long-lived JSONL stdin/stdout so an external probe runner or harness can drive the real specification flow without privileged ORM access. - -## Live Architecture Register - -### Open Assumptions - - - -| # | Assumption | Confidence | Status | Depends on | Validation approach | -| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| A15 | LLM readiness and closure recommendations can be useful, but closure authority must remain explainable and user-legible rather than model-owned. | medium | open | D65, D66 | Manual comparison of model recommendations vs user judgment across varied projects. | -| A20 | Users experience observer capture as responsive when every eligible answered turn enters one turn-owned background capture backlog instead of blocking chat stream completion. | medium | open | D22, D113, I108 | Measure stream completion timing, backlog draining, and replay clarity. | -| A48 | Intent graph edges are sufficient to drive accurate cascade preview for revisit work. | medium | open | D50, D137, D146 | Structural cascade tests plus manual judgment about scope. | -| A49 | A chat-shaped secondary surface can resolve revisit implications without forcing a full interview restart. | medium | open | D80, D138 | Manual revisit walkthrough once the secondary-chat lifecycle lands. | -| A57 | A specification-scoped lifecycle seam can own duplicate-safe automatic phase entry/continue, late-event suppression, and route-independent in-flight operation identity without introducing a second durable workflow model. | medium | open | D113 | Prototype lifecycle edges; revisit if restart or duplicate-submit truth remains ambiguous. | -| A58 | A cumulative workspace can preserve phase legibility if realized sections stay visible, future sections stay unreachable, and section focus remains navigation-only state. | medium | open | D86, D110, D113, D114 | Prototype continuous workspace deep links, scroll/focus transitions, close-to-next-phase motion, and resume/reload. | -| A64 | Query-owned invalidation boundaries can eliminate scroll-jank cascades without stale-data bugs; the near-term boundary may be one specification bundle plus one entities domain. | medium | open | D87, I110 | Prototype bundle/entities decomposition and measure scroll stability plus data freshness during observer updates. | -| A65 | The interviewer can adapt usefully to the full `greenfield <> brownfield` by `end-to-end build <> incremental feature` matrix without making kickoff feel bureaucratic. | medium | open | D124 | Manual walkthroughs across all four corners of the matrix. | -| A66 | Relation-first observer capture will improve revisit, export grounding, and graph-view utility without flooding the graph with speculative or low-value edges. | medium | open | D50, D125 | Observer corpus probes plus manual graph/export review focused on edge precision, coverage, and usefulness. | -| A67 | Users who are tired, rushed, or under-informed will converge faster by reacting to synthesized candidate directions than by continuing a long direct interview or force-closing early. | medium | open | D126, D127 | Manual comparison between direct questioning, skip-close, and candidate-spec reaction flows. | -| A68 | Broad-pass interviewing followed by explicit deepen-detail actions will preserve coherence better than a single depth-first drill-down while still producing export-worthy specifications. | medium | open | D127 | Prototype broad-pass-first flows and compare knowledge completeness and user comprehension. | -| A69 | A graph-centric refinement surface can launch side-chats without splitting durable specification truth. | medium | open | D128, D114 | Prototype graph-launched refinement with reload/resume checks. | -| A70 | Structured-list graph view remains valuable even when edge density is low, provided relation footers gracefully collapse. | medium | open | A66, D128, D129 | Manual walkthroughs at low and high edge density. | -| A71 | Semantic mutations will eventually need a changeset-ledger history distinct from conversational turn ancestry, but the first implementation should prove chat containers and reconciliation needs first. | medium | open | D135 | Revisit after chat containers plus reconciliation needs stabilize. | -| A72 | Intent items can carry version history without breaking the active-path durable-truth contract. | low | future | A71, D135 | Prototype item versioning behind the changeset ledger. | -| A73 | Autonomous architect/generator loops can propose useful graph mutations only after human-driven multi-chat and reconciliation surfaces prove the shared mutation pipeline. | low | future | A71, D135 | Run architect proposals in shadow mode after multi-chat/reconciliation seams stabilize. | -| A74 | OpenRouter may reduce first-run friction, but capability parity and AI SDK support need proof before making it the default provider path. | medium | open | D130, D131 | Spike provider configuration against interviewer/observer calls. | -| A75 | XDG-compliant user-scoped auth/config storage is acceptable for UI-entered API keys and safer than writing secrets to project workspace. | medium | open | D130, D132 | Prototype key save/load/delete precedence and inspect OS/XDG paths. | -| A76 | Users will accept Brunch editing `.gitignore` when the action is explicit, previewable, and idempotent. | high | open | D133 | Unit-test ignore detection/append behavior and manual dashboard walkthroughs. | -| A77 | Progressive checkability will improve generated specs more than a binary formal/not-formal framing. | medium | open | D134 | Prototype intent-item-to-witness review on a small corpus. | -| A78 | Adding `invariant` and `example` as product ontology candidates will make intent drift easier to detect without overwhelming early interviews. | medium | open | D134 | Run transcript probes for examples, counterexamples, not-relevant cases, and state/transition rules. | -| A79 | Once semantic truth can change through graph edits, side-chats, reconciliation, verifier feedback, or implementation feedback, turn ancestry alone will be insufficient as the semantic history spine. | medium | open | D135 | Revisit after chat containers and reconciliation needs. | -| A80 | Behavioral kernels can generate higher-yield disambiguating questions than generic elicitation prompts if they emit checkable artifacts rather than user-visible formalism. | low | open | D134 | Try state/lifecycle and containment/topology prototypes first. | -| A81 | Knowledge/intent edges can carry semantics without becoming noisy only if relation policy distinguishes semantic relations from reconciliation needs and operational participation. | medium | open | D137 | Design relation-policy semantics before broad observer edge expansion. | -| A84 | Scenario-specific graph context packs can replace transcript-as-default prompt context without losing conversational nuance. | medium | open | D139, D140 | Build prompt/context probes over seeded graphs and compare outputs against transcript-heavy baselines. | -| A85 | A lightweight prompt scenario substrate will validate LLM-heavy directions faster than UI-first development if it captures rendered prompts, context packs, model settings, raw outputs, parses, and review notes. | medium | open | D139 | Run multi-scenario prompt probes before productizing UI. | -| A86 | Pi can serve as a useful pre-UI agent harness or tool-spike backend without forcing Brunch to adopt Pi as production runtime. | medium | open | D142 | `docs/next/architecture/jsonl-session-viability-note.md` raises confidence for a transcript-first POC: native JSONL plus `custom` / `custom_message` sidecars looks workable, but pi still has no supported `SessionStore` seam. | -| A87 | Verification-aware post-spec decomposition can be explored as agent scenarios before it is a Brunch product surface. | low | future | D141 | Prototype decomposition and oracle-design probes. | -| A88 | Deterministic enumeration over existing intent edges incident on a changed item can produce a useful cascade preview without requiring the reconciliation agent. | medium | open | D135, D137, D138, D146 | Manual hard-edit walkthroughs across side-chat V3.0 fixture matrix. | -| A89 | A long-lived local JSONL agent capability CLI can drive the real Brunch interview flow well enough for external LLM-as-user probes to produce credible completed-spec fixtures. | medium | open | D143, D147, Requirement 43 | Prototype the minimal JSONL loop and run LLM-as-user scenarios end-to-end. | -| A90 | Users who ask to speed up a long interview will prefer a side-chat that generates 2–3 reviewed scenario options completing the current direction. | medium | open | D126, D148, D151, Requirement 44 | Probe scenario-options against drilldown fixtures and run manual flow review. | -| A91 | Graph-review critique can make scenario-generated candidate bundles safe enough for product use if readiness states and follow-on review work are explicit. | medium | open | D151, D152, Requirement 44 | Run candidate bundle probes with graph-review scoring and human review. | -| A92 | A conservative global staleness rule for open proposal turns is acceptable before neighborhood-level staleness calculation exists. | medium | open | D149, I117 | Exercise multi-chat proposal flows where another chat applies a changeset while a proposal remains open. | -| A93 | Relation-policy directionality lookup is safer than forcing all useful intent-edge verbs into one dependency direction. | medium | open | D137, D150 | Define canonical/inverse sentences and source/target change behavior for each relation. | -| A94 | Durable secondary chats can replace independent side-chat persistence while preserving reloadable side, reconciliation, qa, and strategy conversations inside one workspace surface without introducing a `thread` table yet. | medium | open | D138, D153, Requirement 45 | In-stream secondary-chat rendering/reload walkthroughs over the existing chat/turn substrate. | -| A95 | Transcript-first context with explicit context snapshots on turn rows plus active graph-item handles on chats can keep secondary chats useful across multi-chat item changes without a persisted context-spec table. Handles only need re-snapshotting when the referenced item's version/fingerprint advances. | medium | open | D139, D140, D154, Requirement 45 | Context-provision tests for snapshot insertion, item-list/neighborhood/economic-graph snapshot builders, stale-handle refresh, and prompt/context-pack rendering. | -| A96 | Async-by-default reconciliation can move Pending review into an in-stream target-grouped reconciliation chat without hiding judgment work or surfacing auto-confirmed noise. | medium | open | D135, D137, D138, D146, D153 | Track 3 classifier scheduling, target-ordering tests, and dense reconciliation walkthroughs. | - -### Active Decisions - - - -#### Workflow runtime and workspace projection - -22. **Observer-result sync is turn-owned and background by default** — eligible answered turns enter one turn-owned observer capture backlog after durable turn finalization, and chat stream completion must not wait on extraction. -65. **Phase outcomes are explicit durable records** — workflow status, closeability, readiness, and closure provenance project from durable phase outcomes on the active path. -66. **Interviewer-recommended and user-forced closes share one transcript-friendly seam** — one phase-close transport handles both paths, with explicit closure basis. -86. **The client is organized by phase-addressable routing and three concentric layout shells** — AppLayout, SpecificationWorkspaceLayout, and ViewLayout own route structure; phases remain router-addressable for links, gating, and sibling composition. -87. **Layout-level data ownership partitions invalidation** — the specification bundle and entity collections subscribe through separately owned query domains / route surfaces instead of one monolithic refresh boundary. -110. **The workspace stream is a merged read model, not identical to the turn tree** — active-path durable turns are the lineage spine; anchored workflow facts and projected control/activity/phase elements derive from workflow state plus nearby anchors. -111. **The app is seed-first and migration-light until the data model settles** — prefer one truthful read-model contract and current seeded scenarios over compatibility for unstable local rows. -113. **Phase lifecycle side effects are specification-scoped, not route-scoped** — durable workflow truth stays server/read-model authoritative; an ephemeral lifecycle seam owns auto-entry, continuation, duplicate-submit suppression, stale-event rejection, and capture-backlog reseeding. -114. **Continuous workspace rendering and phase addressability are separate concerns** — the center pane may render one cumulative realized-section stream while the router preserves focus addresses, gating, and sibling routes. -124. **Interview framing is two-axis, not novelty-only** — interviewer orientation uses workspace novelty and delivery posture. - -#### Intent graph, semantic mutation, and review - -50. **Knowledge relationships live behind one typed graph seam** — persisted graph edges are first-class and drive dependency, derivation, and revisit behavior. -80. **Intent-graph revisit replaces hard turn-tree branching for V1** — revisit starts from graph edit/refinement surfaces and resolves cascades through reconciliation-oriented flows rather than generic turn-tree branching. -125. **Observer capture is a prompt-budgeted graph-delta seam** — observer output includes per-kind item collections plus relationship candidates resolved and validated by the server through relation policy. -126. **Recognition-first assists synthesize proposals through turn-owned candidate direction sets** — grounding/design/future wizard modes can present candidate direction artifacts for structured user reaction. -127. **Interview detail flows through turn-owned breadth skeletons and detail-focus reactions** — broad-pass maps and next-detail affordances steer ordinary successor turns without creating a second topic-tree store. -128. **Graph view becomes an actionable workspace mode through a projection-first, intent-emitting seam** — graph mode projects shared entity truth, owns only ephemeral graph-local interaction, and emits intents into the workspace lifecycle. -129. **Graph view fetch scope and render scope are decoupled** — graph view fetches whole-spec entities while the intended default render scope trends to active-path items with a later `Show all` toggle. -134. **Brunch specs evolve toward recognition-first intent graphs with progressive checkability** — the product direction is typed intent items, semantic edges, examples/counterexamples, witnesses, unresolved ambiguity, and validation status rather than prose inventory alone. -135. **Semantic mutation history should split from conversational turn history when graph editing becomes first-class** — turns remain conversational provenance, the intent graph remains current truth, future changesets record semantic mutation history, and reconciliation needs record semantic debt. -136. **Observer ontology should classify intent items by modality, not answer shape** — goal/context/constraint/assumption/decision/requirement/invariant/criterion/example semantics should be distinct, with decisions narrowed to durable choices among alternatives. -137. **Intent edges are semantic relations, while reconciliation needs are process debt** — relation policy decides which edges participate in display, cascade, export, staleness, reconciliation, criteria help, or weak suggestions. -138. **Multi-chat substrate is the first concrete persistence slice before the full changeset ledger** — chat containers and minimal reconciliation needs precede canonical changeset history while legacy spec-scoped pointers remain transitional. -144. **Intent graph vocabulary supersedes knowledge graph vocabulary** — planning, product language, capability contracts, and context packs should prefer intent item/edge vocabulary; knowledge item/edge remains implementation language during transition. -145. **Changeset/change supersedes patch/patch_change** — new semantic mutation history vocabulary is changeset/change; patch vocabulary is historical. -146. **Hard-impact edit cascade reads from the `reconciliation_need` queue, not from REVISIT walk state** — direct hard edits enumerate incident relations, open reconciliation needs, and resolve through the current reconciliation review surface until Track 3 absorbs that work into an in-stream reconciliation chat. -148. **Spec evolution strategies are chat-local, turn-mediated process state** — strategy belongs to chats/turns, not specification-level semantic truth; broad acceleration should branch into strategy chats rather than mutate the primary interview chat in place. -149. **Changesets are the atomic semantic mutation boundary, while proposal turns are not mutations until accepted** — proposal actions other than accept create successor/process state rather than graph truth. -150. **Relation policy owns operational and display directionality for intent edges** — cascade/reconciliation behavior and compact context labels are declared per relation, not inferred from raw source/target edge direction. Each relation needs canonical source→target wording plus endpoint-relative labels so snapshots can summarize an item's dependencies and dependents without reversing semantics ad hoc. -151. **Scenario-options acceleration is product-facing, but graph review is its safety oracle** — generated candidate bundles may become the user-facing alternative to long drilldown only with fixed-premise, tradeoff, checkability, provenance, and graph-review safeguards. -152. **Graph review and reconciliation are separate graph operations** — reconciliation repairs known disturbance debt; graph review critiques graph quality and starts as turn-owned structured artifacts unless independent lifecycle needs emerge. -153. **Conversational Workspace Runtime supersedes independent side-chat persistence without adding schema-level threads now** — continuous workspace is the host; side, reconciliation, qa, and strategy work should converge into inline secondary chats over the existing chat/turn substrate, while `changeset` / `change` remains the semantic mutation spine. A future `thread` table is deferred until chat/turn proves insufficient. -154. **Chat context is transcript-first with turn-level snapshots and chat-level handles** — a chat primarily uses its own transcript as prompt context. Additional graph/workspace context enters through explicit context snapshot artifacts stored on `turn` rows, so replay shows what the assistant saw at snapshot time. Active chat handles reference mentioned or anchored intent item ids and record the last snapshotted item version/fingerprint; they trigger fresh snapshots only when the referenced subject advances, including changes made by other chats. Do not introduce a persisted context-spec table by default; derive snapshots from intent graph truth via reusable context builders for item, neighborhood, economic whole-graph, and eventually changeset-historical neighborhoods. - -#### Provider, prompt/context, and agent substrate - -130. **First-run setup becomes a product surface, not README-only configuration** — dashboard/provider setup replaces project `.env` docs as the only user-facing path. -131. **Provider access moves behind one AI runtime provider seam** — interviewer and observer construction consume a shared provider/model resolver. -132. **UI-entered credentials are user-scoped auth state, not workspace state** — UI-entered keys go to XDG-compliant user auth/config, not `.brunch/` or project `.env` by default. -133. **`.brunch/` gitignore support is confirm-gated deterministic workspace mutation** — repository mutation is previewable, idempotent, and user-confirmed. -139. **Prompt/context scenario substrate is a first-class foundation** — prompts/doctrines are markdown assets, context packs are typed server builders, and prompt scenarios produce repeatable probe artifacts. -140. **Intent graph context packs are scenario-specific semantic briefings** — packs render bounded graph truth, workflow state, provenance, unresolvedness, relation neighborhoods, and authority labels for one agent task. -141. **Post-spec decomposition remains a probe frontier, not a committed Brunch UI** — decomposition/oracle probes should run through the scenario substrate before product commitment. -142. **Pi is a candidate harness adapter, not current product runtime truth** — Pi may be evaluated for probes/tools, but Brunch owns workflow, replay, mutation authority, reconciliation, and credential UX. -143. **Brunch owns the agent mutation surface; harnesses adapt it as tools** — agent-originated writes route through Brunch-owned mutation handlers; adapters translate transport/tool shape only. -147. **The local agent CLI is a long-lived JSONL adapter over Brunch capability contracts** — `brunch agent` exposes capability contracts with explicit resource ids; the LLM-as-user probe runner remains an external client. - -### Critical Invariants - - - -Each invariant is a formalization candidate: the property is stated in human language, protected today by tests/manual oracles, and may later graduate to stronger runtime/model oracles. - -| # | Invariant | Protected by | Proves | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| I4 | Vite proxy routing and the runtime backend-port seam stay aligned through one explicit configuration path. | `runtime-config.test.ts` | Requirement 1 | -| I17 | Data Part schema validation remains confined to true LLM / HTTP boundaries rather than mirrored internal seams. | `parts.test.ts` | Requirement 4 | -| I24 | The routed interview surface preserves hydration, stream projection, lifecycle orchestration, mutation transport, phase projection, successor-frontier continuity, activity summaries, projected controls, preface/revision artifacts, and trailing observer attachment. | `InterviewView.test.tsx`, workspace stream / controller / app tests | D86, D87, D110, D113, D114 | -| I44 | Structured turn responses round-trip through persistence, hydration, projection, and UI affordance state without collapsing back to scalar semantics. | `turn-response.test.ts`, `context.test.ts`, `InterviewView.test.tsx` | Requirement 4 | -| I48 | Canonical intent/knowledge kinds persist with provenance and project through typed entity collections, stable reference codes, turn-linked capture projection, and graph edges without ontology drift. | `db.test.ts`, `core.test.ts`, `knowledge.test.ts`, sidebar/graph tests | D50, Requirements 22, 23 | -| I54 | Phase-aware capture preserves the ontology boundary: grounding/design persist exploration knowledge, accepted review outputs materialize requirements/criteria, and both seams survive persistence and replay. | `observer.test.ts`, `context.test.ts`, `app.test.ts`, `InterviewView.test.tsx` | Requirements 22, 23 | -| I72 | Explicit phase outcomes project shared workflow status, closeability, readiness, closure basis, and closed-phase markers through one durable seam. | `phase-close.test.ts`, `db.test.ts`, `app.test.ts` | D65, D66, D110 | -| I87 | Requirements and criteria review persist interviewer-owned review metadata on the review turn, project stable reference codes, submit semantic review replies, and carry accepted outputs downstream without dead frontier states. | `interview.test.ts`, `db.test.ts`, `app.test.ts`, `InterviewView.test.tsx` | Requirements 11, 12; D110 | -| I100 | Local-first distribution keeps `.brunch/` workspace resolution, package-bin startup, built-client serving, bound URL reporting, runtime ownership, and JSON request limits correct. | `project.test.ts`, `launcher.test.ts`, `cli.test.ts`, `runtime-config.test.ts`, `app.test.ts` | Requirement 1 | -| I102 | File-route generation, directory-based nesting, three-shell route architecture, and phase addressability remain the runtime routing source of truth; graph view stays code-split. | `router.test.tsx`, `build-boundary.test.ts`, `GraphView.test.tsx` | D86, Requirement 33 | -| I106 | Provider credential discovery, precedence, dashboard status, and model-provider resolution stay explicit without exposing raw secret values through `/api/config`, logs, persisted specification state, or client-visible payloads. | planned: config/app/dashboard tests | Requirements 34, 35, 36; D130, D131, D132 | -| I107 | `.brunch/` gitignore hygiene is idempotent and confirmation-gated. | planned: project-gitignore/app/dashboard tests | Requirement 37; D133 | -| I108 | Observer capture does not block chat stream completion for eligible answered turns; backlog state is re-derived from durable turns and persists results to the originating turn. | planned: app/controller tests | D22, D113 | -| I109 | Observer prompts remain compact as relation extraction widens; candidates resolve only through validated existing ids or same-turn provisional references, and accepted reviews reuse relation policy. | `context.test.ts`, `observer.test.ts`, `db.test.ts`, `app.test.ts` | Requirement 30; D50, D125 | -| I110 | Workflow read truth and write truth stay behind named seams instead of transport handlers owning workflow semantics. | workflow projector / transition / phase-close tests | D110, D113 | -| I111 | Multi-chat substrate preserves primary-chat active-head equivalence during transition, same-spec/chat ancestry on turn writes, and reconciliation-need dedupe without conflating process debt with semantic edges. Turns remain chat-owned; schema-level threads are not part of the current persistence model. | `chat-substrate.test.ts`, `reconciliation-need.test.ts`, `db.test.ts` | Requirements 39, 45; D137, D138, D153 | -| I112 | Prompt/context scenarios render from packaged markdown prompts and typed context-pack / context-snapshot builders, with deterministic fingerprints, relation-policy-selected neighborhoods, and reviewable golden coverage. | prompt loader/build/golden, context-pack, context-snapshot, scenario-runner tests | Requirements 40, 41; D139, D140, D154 | -| I113 | Hard-impact direct edit opens reconciliation needs for affected relation-policy endpoints, records provenance, deduplicates idempotently, and no longer returns deferred placeholder responses. | `cascade-producer.test.ts`, `edit-route.test.ts`; planned: reconciliation/overlay app tests | Requirement 10; A88; D146, D150 | -| I114 | The reconciliation classifier lifecycle is explicit and recoverable; labels are constrained, failures persist parser/thrown errors, and proposals are never auto-applied. | reconciliation-agent tests | Requirement 10; A88; D139 | -| I115 | The agent capability CLI remains an adapter over Brunch capability contracts: calls validate explicit resource ids/schemas, mutating calls dispatch through server-owned handlers, and probes exercise only the JSONL boundary. | planned: capabilities, agent-jsonl, probe-runner tests | Requirements 42, 43; A89; D143, D147 | -| I116 | Each active/resumable chat has at most one open assistant/system-first frontier turn; user responses complete it through normalized semantics, and strategy is chat-local process state. | planned: chat/transition/capability tests | Requirement 44; D138, D148 | -| I117 | Open proposal turns are stamped with the latest applied changeset id at creation and conservatively stale when the specification's latest changeset advances before completion. | planned: changeset/transition/app tests | A92; D149 | -| I118 | Reconciliation/direct-edit cascade never infers affected endpoints from raw edge direction alone; it consults relation policy source-change / target-change behavior. | `knowledge-relationship-policy.test.ts`, `cascade-producer.test.ts`, `edit-route.test.ts`, `side-chat-route.test.ts`, `reconciliation-need.test.ts` | A93; D137, D150 | -| I119 | Scenario-option candidate bundles can become canonical only by accepting a coherent bundle changeset; accepted-with-issues candidates also create durable follow-on review/process debt. | planned: scenario-runner, turn-artifacts, changeset tests | A90, A91; D151, D152 | -| I120 | Secondary chats remain conversational process containers, not workflow or semantic truth: inline rendering, collapse/reload state, turn-level context snapshot replay, and item-version-gated stale-handle refresh may organize discussion, but accepted mutations still flow through Brunch-owned handlers and changesets. | planned: chat-runtime, context-provision, changeset/app tests | Requirement 45; A94, A95; D143, D149, D153, D154 | - -## Future Direction Register - -### Conversational workspace runtime - -- The Conversational Workspace Runtime is the next architectural umbrella after continuous workspace Track 1: the shipped shell hosts unified chat, inline secondary chats, reconciliation, changesets, and transcript-first context provision. See `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` and PLAN items `chat-runtime-secondary-chats`, `reconciliation-runtime`, `changeset-ledger`, and `chat-context-provision`. -- Persistent side-chat history is no longer an independent V4a frontier. It is absorbed by secondary chats over the existing chat/turn substrate; schema-level `thread` is explicitly deferred until chat/turn proves insufficient. -- Context provision should be transcript-first: context builders may derive initial snapshots from chat kind/strategy/anchors, explicit item lists, item neighborhoods, or an economic whole-graph view, but durable prompt context is represented by context snapshot artifacts stored on turns and refreshed graph-item handles on chats rather than a hidden persisted context-spec table. Once changeset history exists, neighborhood builders may also derive historical neighborhoods around the changeset that originally captured or last updated an item. -- Reconciliation absorption and context provision stay coordinated with the chat runtime; changeset/change history can proceed in parallel and remains the semantic mutation authority. - -### Semantic / generative substrate - -- Intent graph semantics, relation policy, examples/invariants, checkability, and witness strength are the next semantic substrate focus. See `docs/design/INTENT_GRAPH_SEMANTICS.md` and PLAN item `intent-graph-semantics`. -- Changeset/change history is the future semantic mutation spine. See `docs/design/PATCH_LEDGER.md` and PLAN item `changeset-ledger`. -- Graph review and scenario-options acceleration remain probe-first until graph semantics and changeset acceptance are safe. See PLAN items `graph-review-scenario-options` and `productized-scenario-options`. - -### Agent capability substrate - -- Prompt/context packs and scenario runners are the pre-UI harness for LLM-heavy features. See `docs/design/AGENT_MUTATION_SURFACE.md` and PLAN item `agent-fixture-substrate`. -- Pi and other harnesses are adapters over Brunch-owned capability contracts, not product authority. - -### Provider / workspace hardening - -- Provider setup, XDG key storage, AI runtime provider resolution, and `.gitignore` assist are independent near-horizon hardening frontiers. - -## Interaction Stream Model - -The center column is a **merged stream projection** over multiple artifact families. Only conversational turns participate in branch-bearing lineage; other artifacts anchor to that lineage or project from workflow state. - -| Artifact family | Durable | Branch-bearing | Examples | Rule | -| ------------------------- | ------- | -------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------- | -| Conversational turn cards | yes | yes | questions, review proposals, closure proposals, answered-turn replay | Ordered by active-path turn chain. | -| Anchored workflow facts | yes | no | phase outcome | Anchored to turn ids; hidden/superseded if anchor leaves active path. | -| Projected control cards | no | no | kickoff, recovery, proceed/handoff | Derived from workflow state plus nearby anchors. | -| Activity cards | mixed | no | generation state, activity summary, trailing observer state | Adjacent to a turn/control boundary; not branch nodes. | -| Phase markers | no | no | phase start/closed | Projected from workflow position and phase outcomes. | -| Phase section headers | no | no | grounding purpose + captured knowledge kinds | Projected at the top of each realized phase section. | - -## Layout Architecture - -Brunch uses three route/layout shells: AppLayout, SpecificationWorkspaceLayout, and ViewLayout. The top bar identifies Brunch, version, tagline, and working directory. The specification workspace presents: - -- **Left pane:** back-to-workspace link, read-only specification name, phase stepper/section navigator, status/readiness/turn-count metadata, and conditional output access. -- **Center pane:** a continuous or phase-focused workspace stream with a sticky header, realized phase sections, phase headers, one actionable bottom artifact for the current reachable phase, activity cards, durable turn cards, and projected recovery/handoff/completion controls. -- **Right pane:** intent graph sidebar grouped by kind, showing item counts and relationship cues. -- **Graph view:** a peer route inside the specification shell, initially a structured list over the intent graph; spatial canvas is deferred. - -Detailed card styling, typography tokens, and legacy layout minutiae are implementation/design-system truth, not SPEC authority. - -## Lexicon - -| Term | Definition | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **workspace** | The cwd-backed software context whose local `.brunch/` directory stores specifications and runtime state. | -| **specification** | One elicitation run within a workspace. Canonical product term for what older code may still call a project. | -| **project** | Deprecated older name for a specification record; remove rather than preserving as long-term compatibility language. | -| **intent spec** | A specification optimized for preserving and validating meaning rather than sequencing downstream work. | -| **planning spec** | A specification optimized for downstream work sequencing; in Brunch it should be a projection from intent truth, not the source artifact. | -| **intent graph** | Canonical semantic substrate: typed intent items, intent edges, examples/counterexamples, validation status, and semantic mutation state. | -| **intent item** | One durable typed semantic unit in the intent graph. Current implementation may still persist `knowledge_item`. | -| **intent edge** | One durable typed semantic relation between intent items. Current implementation may still persist `knowledge_edge`. | -| **reconciliation need** | Durable process debt saying existing intent truth may require renewed judgment because related truth changed. Not an intent edge. | -| **changeset** | Future canonical term for one submitted semantic mutation bundle against the intent graph. Supersedes patch. | -| **change** | One atomic semantic mutation inside a changeset. Supersedes patch_change. | -| **chat** | A conversation container inside one specification; primary interview, side, reconciliation, qa, and strategy chats may own turns without owning semantic truth directly. Current persistence uses chat + turn; schema-level thread is deferred. | -| **secondary chat** | Non-primary chat rendered inline or as a collapsible surface inside the workspace. It is a product/runtime use of `chat`, not a separate `thread` table. | -| **context snapshot** | Turn artifact that records graph/workspace context explicitly inserted into a chat transcript at a point in time, including subject id(s), version/fingerprint(s), snapshot content, reason, and provenance. Snapshots are derived from intent graph truth at snapshot time and do not mutate when source truth changes. | -| **neighborhood snapshot** | Context snapshot centered on one or more intent items and their relation-policy-selected surroundings. Modes may include immediate adjacency, dependencies, dependents/impact, evidence, reconciliation, and later changeset-historical neighborhoods around original capture or last update. | -| **context handle** | Lightweight chat-level or transcript-derived reference saying a chat is tracking one or more intent item ids across turns. Before new assistant turns, stale handles cause fresh context snapshots only when the subject's current version/fingerprint has advanced since the last snapshot. | -| **turn-zero** | Assistant-authored kickoff turn that seeds a new secondary chat with kind-appropriate explicit context snapshots and options before the user responds. | -| **turn** | One persisted authored conversational interaction with typed offer/reply parts and parent linkage. | -| **frontier turn** | The single actionable durable conversational turn currently awaiting user completion. | -| **proposal turn** | An assistant/system-first frontier turn offering a candidate bundle, graph-review finding, reconciliation suggestion, or other proposed action. It is not semantic truth until accepted. | -| **workspace stream** | The merged center-column read model composed from active-path turns, anchored workflow facts, projected controls, phase markers, and activity cards. | -| **projected control card** | A workflow affordance derived from durable state rather than authored conversational content. | -| **preface card** | A turn-internal artifact presenting provisional context from context gathering, paired with a question card and captured only as part of the validated whole turn. | -| **review set** | A synthesized candidate requirements or criteria list with stable reference codes and per-item/full-set review actions. | -| **phase outcome** | Durable closure artifact for a phase, including summary and closure basis. | -| **closeability** | Deterministic minimum bar for whether the user may close a phase now. | -| **readiness band** | Coarse descriptive signal separate from closeability. | -| **exploration knowledge** | Durable grounding/design knowledge: `goal`, `term`, `context`, `constraint`, `decision`, and `assumption`. | -| **constraint** | A durable boundary on acceptable scope or solution space; `non-goal` is a subtype. | -| **decision** | A chosen direction among plausible alternatives, with durable consequences. | -| **assumption** | A material belief supporting a direction or decision that could later prove false. | -| **invariant** | Planned ontology kind for a property that must remain true across relevant states, transitions, executions, versions, or semantic revisions. | -| **example** | Planned ontology kind for concrete positive, negative/counterexample, edge-case, or not-relevant cases that disambiguate intent. | -| **progressive checkability** | Represent each intent item at the weakest useful witness level today while preserving paths toward stronger witnesses. | -| **checkability** | A typed field describing the strongest oracle currently witnessing an intent item. | -| **witness strength** | The breadth/confidence of an item's oracle coverage, distinct from the oracle kind. | -| **relation policy** | Per-relation registry deciding display, cascade, export, staleness, reconciliation, criteria-help, weak-suggestion participation, support/status semantics, operational directionality, and endpoint-relative labels for compact dependency/dependent snapshots. | -| **context pack** | A scenario-specific semantic briefing derived from intent graph truth, workflow state, provenance, unresolvedness, relation neighborhoods, and authority labels. | -| **prompt/context scenario substrate** | Foundation for markdown prompts, reusable doctrines, typed context packs, and repeatable prompt probes before UI commitment. | -| **agent mutation surface** | Brunch-owned typed handler layer for any durable data mutation initiated by an agent. | -| **agent capability contract** | Stable, typed read or mutation contract exposed to agents/harnesses with authority and replay metadata. | -| **agent capability CLI** | Local JSONL adapter exposing Brunch capability contracts without defining its own product API or mutation authority. | -| **AI runtime provider** | Shared server seam resolving configured LLM provider, model names, API-key source, and provider-specific options. | -| **XDG auth state** | User-scoped credential/config storage outside the project workspace. | -| **graph-review finding** | A turn-owned structured critique artifact; not itself semantic truth or reconciliation debt. | -| **candidate graph bundle** | Coherent scenario-options commit/review unit with tradeoffs, generated intent items/edges, provenance, risks, and preconditions. | -| **greenfield / brownfield** | Grounding strategies for new concepts vs existing-codebase work. | -| **end-to-end build / incremental feature** | Delivery postures for whole-system shaping vs bounded changes. | -| **output view** | Terminal route available when phases are closed; not a workflow phase. | - -## Verification Design - -### Verification Commands - -| Step | Check | Command | -| ---- | ----------------- | ------------------- | -| 1 | Formatting | `npm run fmt:check` | -| 2 | Lint + type check | `npm run lint` | -| 3 | Unit tests | `npm run test` | -| 4 | Build | `npm run build` | -| all | Full gate | `npm run verify` | - -### Verification Policy - -Every meaningful code change should pass `npm run fix` in the inner loop and `npm run verify` before commit. Slices that touch user-facing boundaries should also stay manually walkthrough-able via the local app. - -### Verification Stance - -- Verification is first-class work; current product work is manual-heavy by deliberate choice, not accident. -- **Inner loop** proves structural validity, boundary safety, and non-destructive behavior. -- **Middle loop** proves replay, refresh-boundary ownership, explicit state projection, and corpus/golden stability where cheap automated checks remove bad degrees of freedom. -- **Outer loop** is the authority for brownfield grounding quality, transcript legibility, waiting-state clarity, graph/workspace staging, and qualitative generation trust. -- LLM-heavy features need layered oracles: schema/contract tests inside, fixture/golden/corpus probes in the middle, and human review outside. - -### Oracle Strategy by Loop Tier - -| Tier | Oracle families | What they prove | Main targets | -| ------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| Inner | Schema validation, type-aware linting, focused unit/integration tests, negative-space regressions | Boundaries remain type-safe; persistence/transport seams do not collapse. | I4, I17, I24, I44, I48, I54, I72, I87, I100–I119 | -| Middle | Round-trip/replay oracles for seeded projects, hydration, export, and resume | Seeded or persisted state can be loaded, projected, re-rendered, and exported without semantic loss. | Requirements 13–15; I24, I44, I100 | -| Middle | Route/query ownership and state-model oracles | Mutations refresh owned surfaces only; major in-flight modes are named and projectable. | Requirements 5, 7, 14; A20, A64; I24, I108, I110 | -| Middle | Prompt/context golden and classifier corpora | Prompt/context output remains inspectable and regressable as prompts evolve. | Requirements 40, 41; A84, A88; I112, I114 | -| Middle | Context-snapshot replay and handle-refresh oracles | Turn-level snapshots replay unchanged after graph edits; active handles re-snapshot only when changeset-backed item versions advance. | Requirement 45; A95; D154; I120 | -| Middle | Structured context-builder assertions plus selected golden renderings | Item-list, neighborhood, and economic whole-graph snapshots contain required ids, sections, relation/provenance signals, and stable rendering boundaries without overfitting prose. | Requirements 40, 45; A84, A95; I112, I120 | -| Outer | Fixture-backed manual walkthroughs | Phase transitions, export, resume, graph view, and waiting states feel legible. | Requirements 5, 13–15, 33 | -| Outer | Brownfield and scenario-quality review | Generated questions/bundles are useful, grounded, honest about tradeoffs, and not overconfident. | Requirements 3, 16, 20; A67, A68, A90, A91 | -| Outer | Dense cascade/reconciliation walkthroughs | Users can understand and resolve downstream graph impact without skipping necessary judgment. | A48, A88, I113, I114 | - -### Acknowledged Blind Spots - -| Blind spot | Current mitigation | Revisit trigger | -| ------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| Qualitative interviewer and kickoff quality across many repo shapes | Manual brownfield walkthroughs on representative repos. | Brownfield regressions recur or kickoff strategy debates cannot resolve qualitatively. | -| Transcript trust after hydration | Legible placeholders/summaries plus manual transcript review. | Users cannot understand what happened after replay. | -| UI lock/wait causality | Explicit visible in-flight states and manual browser inspection. | Manual inspection cannot explain repeated lock/disappearance bugs. | -| Story quality and phase differentiation | Story variants reviewed against seeded walkthroughs. | Story/app drift grows or design disagreement blocks implementation. | -| Observer latency and layout refresh freshness | Runtime observation during manual sessions. | A20 shows recurring latency or coarse refresh pain. | -| Revisit/reconciliation UX adequacy | Structural coverage on graph/persistence seams plus manual cascade walkthroughs. | Revisit work moves active or users skip unresolved needs. | -| Real browser scroll/hover/touch behavior | Outer-loop manual graph-view walkthroughs. | Users report chip navigation/preview failures. | -| Performance under large intent graphs | Defer explicit budget until dense specs are common. | Render lag visible on representative walkthroughs. | -| Visual regression infrastructure | Manual-heavy stance accepted. | Three or more visual regressions are caught only after merge. | -| LLM classifier correctness and determinism | Proposals never auto-apply; re-run exists; corpora/goldens grow from failures. | Substantive items are mislabeled as auto-confirm or repeated runs diverge materially. | -| Economic whole-graph snapshot quality | Structured assertions plus one selected golden rendering fixture; human review for whether compact context is useful. | Secondary-chat answers show missing authority/provenance/relation context or snapshots become too large for routine prompts. | -| Context-handle refresh before real item versions | Defer handle freshness semantics until `changeset-ledger` supplies real item versions rather than blessing a temporary content fingerprint. | `chat-context-provision` is pulled before changeset-backed item versions exist. | - -### Design Notes - -- Context-handle freshness should wait for real item versions from `changeset-ledger`; do not bless a temporary content/edge fingerprint as the durable refresh oracle. -- Economic whole-graph snapshot verification should pair structured JSON assertions for required sections/counts/ids with a small number of golden renderings and human review, rather than treating exact prose as the primary oracle. - -### Acceptance Criteria - -1. `npx brunch` can start from a workspace directory with local-first persistence in `.brunch/`. -2. Greenfield and brownfield grounding both work, with brownfield able to start from workspace analysis and converge into the same grounding phase purpose. -3. Structured turns support rich responses without losing semantic fidelity. -4. The intent layer stays visible, typed, and linked through graph relationships. -5. Phase closeability, readiness, and closure provenance stay legible to the user. -6. Requirements and criteria review remain explicit, lightweight, durable at the turn level, and export-relevant. -7. Revisit can invalidate intent, surface cascade through the `reconciliation_need` queue, and re-resolve without a separate modal-only substrate. -8. The routed UI stays stable across dashboard, phase views, sidebar intent graph, and graph view. -9. Resume works from persisted state. -10. The verification gate passes. -11. Structural kickoff / recovery / handoff / completion affordances project without a bare generic composer. -12. Hydrated transcripts preserve interviewer-side structure plus stable durable activity summaries for live-only artifacts. -13. Open phases bottom-load one visible next action; completed turns replay as answered-turn records; closed phases bottom-load handoff/completion artifacts. -14. Preface cards render as turn-internal artifacts paired with question cards, so observer capture uses the whole validated turn. -15. Grounding and elicitation persist only the durable exploration ontology, with `non-goal` represented as a `constraint` subtype. -16. Observer prompt, shared kind registry, schema/API types, fixtures, and UI copy describe the same ontology. -17. The interview can orient anywhere in the two-axis workspace novelty × delivery posture matrix. -18. Observer capture records useful intent edges while abstaining under weak support. -19. Users can request candidate directions with explained tradeoffs and refine by reacting to them. -20. The interview can stop at a broad pass and deepen selected areas incrementally. -21. Graph view renders the intent graph as a navigable workspace with visible edges and node-launched refinement flows. -22. First-run setup makes missing provider credentials visible and recoverable without hand-editing project `.env` files. -23. Brunch can help users keep `.brunch/` out of version control through explicit, idempotent `.gitignore` confirmation. From 50d4194b26cd5487621057ef923f18eff402659d Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:20:42 +0200 Subject: [PATCH 15/34] agent context composition re-spec I --- memory/SPEC.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/memory/SPEC.md b/memory/SPEC.md index 8805dff04..4b1ab6604 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -89,7 +89,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c #### Runtime profile & prompting 25. Brunch must run the embedded Pi harness through a sealed Brunch Pi Profile: programmatic settings, resource-loader, extension-factory, keybinding, tool, and prompt policy must determine product behavior; ambient user/project `.pi/` resources must not influence Brunch sessions unless Brunch deliberately imports them. -26. Brunch must distinguish transport modes from operational modes and agent roles: operational modes such as `elicit` and future `execute` gate tool authority and prompt posture; the foreground session agent (`elicitor` now, future `executor`) is derived from the operational mode, and each agent definition carries its own model/thinking preset, prompt packs, applicability allow-lists, and tool policy. +26. Brunch must distinguish transport modes from operational modes and agent roles: operational modes such as `elicit` and future `execute` gate tool authority and prompt posture; the foreground session agent (`elicitor` now, future `executor`) is derived from the operational mode, and each agent definition carries its own model/thinking preset, prompt-resource manifest allow-lists (including goal/strategy/lens/method resources), and tool policy. ## Live Architecture Register @@ -126,10 +126,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D1-L — Depend on `pi-coding-agent`, not only `pi-agent-core`.** The POC reuses the coding-agent service bundle, TUI/print adapters, RPC machinery, session logging, and tool plumbing. Dropping down to `pi-agent-core` is a fallback if Brunch proves too different. Depends on: A1-L. Supersedes: —. - **D2-L — Brunch is an opinionated product, not a pi platform shell.** The POC hardcodes its toolset, system prompt, and policy doctrine; scopes state to `.brunch/`; and hides pi's generic extension surface from end users. Depends on: A1-L. Supersedes: —. - **D39-L — Brunch owns a sealed Pi Profile around the embedded harness.** Product behavior must come from Brunch-owned programmatic policy, not ambient Pi discovery. The profile includes settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. Current known posture routes TUI launch policy through `src/brunch-pi-profile.ts`, creates an in-memory Brunch-owned `SettingsManager` policy instead of reading ambient global/project `.pi/settings.json`, disables ambient context files, extensions, prompt templates, skills, and themes while loading Brunch's inline extension shell, and defaults Brunch-launched Pi to offline mode; Pi source confirms extension `resources_discover` can still inject explicit Brunch-owned skill/prompt/theme paths even when `noSkills`/`noPromptTemplates`/`noThemes` disable ambient discovery. Brunch-owned Pi extensions are loaded by an explicit product shell (`src/.pi/pi-extension-shell.ts`) rather than ambient discovery; *explicit* means the shell statically imports its product extensions and registers them from a fixed ordered list — it must not filesystem-discover or dynamically `import()` extension modules at runtime, because a Brunch-internal discovery layer is itself the discovery this decision rejects. Each product extension exposes one registrar taking explicit dependencies, and the shell wires those dependencies at the call site; the `default` exports under `src/.pi/extensions/*` exist only for dev `/reload` iteration, not as a product load path. Product extension modules live under `src/.pi/extensions/*`, and reusable Pi TUI components live under `src/.pi/components/*`, so they can also be iterated by launching Pi from `src/` and using `/reload`; the root project-local `.pi/` probe runtime files are retired and must not be treated as product configuration. Test files must not live directly under auto-discovered `.pi/extensions` or `.pi/components` resource directories; extension/component tests live under `src/.pi/__tests__/`. The profile boundary now owns the audited behavior-shaping settings list in code (`BRUNCH_SETTINGS_POLICY` / `BRUNCH_SETTINGS_AUDITED_GETTERS`), with hostile ambient settings and reload-resilience tests covering shell path/prefix, npm command, ambient resources, skill commands, double-escape behavior, compaction/retry, image/terminal/UI, transport/theme/changelog, and telemetry settings. Remaining profile work is runtime-state/prompt/tool posture, not ambient settings file leakage. Depends on: D1-L, D2-L, A19-L. Supersedes: treating `noSkills: true` as full profile isolation, relying on user/project `.pi/` defaults to be harmless, nesting Brunch's product extension modules under `src/.pi/extensions/brunch/`, or replacing the explicit static shell list with a Brunch-internal filesystem-discovery / `brunchExtensionMeta` / `loadOrder` mechanism as the product runtime load path. -- **D40-L — Runtime state is a transcript-backed Brunch session-agent record, not hidden extension memory.** Brunch projects one session-agent record from linear `brunch.agent_runtime_state` entries (`reason: "init" | "switch"`), last-writer-wins at turn preparation. Its axes are `op_mode` (`elicit`, future `execute`) plus the optional, AUTO-able objective axes `strategy`, `lens`, and `goal` (D25-L, D59-L); a deferred `world_view` watermark group (last-seen LSN, graph mentions, optional git head) is reserved for M7. The **foreground session agent** (`elicitor` now, future `executor`) is *derived* from `op_mode`, not stored; the other agent roles (`reviewer`, `reconciler`, future `scout`/`researcher`) are async sub-agent/side-chain workers (D29-L, D44-L) invoked out-of-band, never part of the session state machine. `op_mode` gates tool authority, owned by `src/.pi/extensions/operational-mode.ts` (current `elicit` policy denies side-effecting `bash`/`edit`/`write` plus user-shell interception). Prompt composition is a separate concern (D58-L). Depends on: D17-L, D23-L, D25-L, D39-L, D58-L, D59-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority, storing the foreground role as independent session state, the "runtime bundle / role preset" as one knob deriving model/thinking/packs, and binding prompt-pack location to `src/.pi/context/`. +- **D40-L — Runtime state is a transcript-backed Brunch session-agent record, not hidden extension memory.** Brunch projects one session-agent record from linear `brunch.agent_runtime_state` entries (`reason: "init" | "switch"`), last-writer-wins at turn preparation. Its axes are `op_mode` (`elicit`, future `execute`) plus the optional, AUTO-able objective axes `strategy`, `lens`, and `goal` (D25-L, D59-L); a deferred `world_view` watermark group (last-seen LSN, graph mentions, optional git head) is reserved for M7. The **foreground session agent** (`elicitor` now, future `executor`) is *derived* from `op_mode`, not stored; the other agent roles (`reviewer`, `reconciler`, future `scout`/`researcher`) are async sub-agent/side-chain workers (D29-L, D44-L) invoked out-of-band, never part of the session state machine. `op_mode` gates tool authority, owned by `src/.pi/extensions/operational-mode.ts` (current `elicit` policy denies side-effecting `bash`/`edit`/`write` plus user-shell interception). Prompt composition is a separate concern (D58-L). Depends on: D17-L, D23-L, D25-L, D39-L, D58-L, D59-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority, storing the foreground role as independent session state, the "runtime bundle / role preset" as one knob deriving model/thinking/resources, and binding prompt-resource location to `src/.pi/context/`. - **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. - **D35-L — Dynamic TUI chrome is a Brunch projection wrapper over Pi UI primitives.** Downstream TUI affordances should call a Brunch-owned renderer (`renderBrunchChrome` or its successor) with one activated product-state snapshot rather than scattering raw `ctx.ui.setHeader`, `setFooter`, `setWidget`, title, or working-indicator calls. The wrapper is stateless projection over canonical workspace/session/graph facts, including the real activated session id, while its TUI footer compositor may read Pi footer telemetry (`getGitBranch`, foreign `getExtensionStatuses`) at render time. Brunch chrome does not publish a `brunch.chrome` status key; `ctx.ui.setStatus(key, text)` remains a lateral contribution channel for other extensions and future dynamic Brunch state. RPC clients should rely only on surfaces Pi actually emits for the wrapper (currently diagnostic widget/title, plus any future explicit status adapter) because header/footer/working-indicator are TUI-only in current Pi RPC mode. Session display names are likewise product projections over Pi session metadata: Brunch may append Pi `session_info` entries, but generated names must characterize the selected spec/session transcript rather than replace spec identity or graph truth. Depends on: D2-L, D21-L, D34-L, A18-L. Supersedes: treating Pi UI methods as direct downstream affordance APIs, rendering placeholder session state such as `unbound` after a session is activated, or consuming the status-key namespace for chrome's own static summary. -- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by axis (`modes/`, `strategies/`, `lenses/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, and the state definitions that drive mode/role/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. +- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by registry/resource family (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods`/`capabilities`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, prompt resources, and the state definitions that drive `op_mode`/goal/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. #### Data model & vocabulary @@ -241,8 +241,8 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. -- **D58-L — Brunch prompt composition is a 3-layer model composed per agent.** `agents/compose(agentId, sessionState, spec, workspace, snapshot)` assembles, in order: **(1) agent definition** — the keyed agent's identity/system prompt, model/thinking preset, mode-gated tool authority, and applicability allow-lists (`strategies`/`lenses`/`skills`); **(2) objective config** — the optional, AUTO-able `goal` (D59-L), `strategy`, and `lens` packs, with the legal `(goal × strategy × lens)` tuple constrained by `agents/state.ts`, gated by `spec.readiness_grade` and conditioned by `workspace.posture`; **(3) capabilities** — Layer-3 tool-usage skills via Pi progressive disclosure, gated by allow-list ∩ `op_mode` ∩ grade. A dynamic context layer appends agent-context snapshots (D60-L). `AUTO` on any objective axis means the axis is unpinned and the composer injects selection guidance over the agent's allowed set so the agent self-selects. `agents/` is a keyed multi-agent registry (`definitions/` plus `strategies/`, `lenses/`, `goals/`, `contexts/`); the foreground session agent is resolved from `op_mode`, side/sub-agents addressed by explicit `agentId`. Depends on: D23-L, D25-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; and "role preset / runtime bundle" as the composition unit. -- **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-I` / `elicit-II` (main elicitation levels), `commitment-converge` (reduce / lock down), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO` for agent self-selection. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. +- **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and ``/`` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods`/`capabilities`, `contexts/`); composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; and direct Layer-2 eager prompt-pack injection as the default mechanism. +- **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-I` / `elicit-II` (main elicitation levels), `commitment-converge` (reduce / lock down), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO`; in either case D58-L manifests advertise the legal resource(s) rather than injecting the whole goal body. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. - **D60-L — "Snapshot" splits into pull / render / surface, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview), `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization). **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L. Supersedes: pre-rendering snapshots to strings in the pull layer, and scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs. ### Critical Invariants @@ -283,9 +283,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I32-L | Public RPC structured-exchange driving never requires a client to speak raw Pi RPC: after Brunch method discovery and workspace/spec/session activation, each pending assistant-originated exchange is answered exactly once through `elicitation.respond`, and the deterministic permutation run produces linear Pi JSONL whose transcript display and elicitation-exchange projections preserve the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity (`rpc.discover` contract tests, pending/respond lifecycle tests, current public-RPC structured-exchange permutations, terminal non-answered status handling, option content/rationale parity, no repeated deterministic prompts, and transcript/projection oracle in `src/probes/public-rpc-parity-proof.ts`) | R11, R16, R17, R24, R27, R28; D5-L, D12-L, D37-L, D48-L, D49-L | | I33-L | `capture_*` analysis entries are transcript evidence only: they persist as Brunch structured-exchange `toolResult` rows, are included by Brunch-semantic transcript renderers, are hidden or collapsed in TUI display, and never mutate graph truth or bypass `CommandExecutor`. | partially covered (minimum capture details schemas parse/export and reject graph payload fields; future runtime capture-analysis schema/rendering tests plus transcript renderer fixtures still need to prove persisted result rendering and TUI hide/collapse behavior; later graph-capture fixtures compare analysis candidates against committed graph mutations) | D17-L, D18-L, D37-L, D47-L, D50-L; I2-L, I11-L, I23-L, I30-L | | I34-L | `commitGraph` batch validation is all-or-nothing: if any node or edge in the batch is structurally illegal, the entire batch is rejected and no partial state is persisted; the agent receives diagnostics sufficient for bounded self-correction retry. | covered (22 tests in `command-executor.test.ts` — edge failure rolls back nodes, mixed-batch rejection, diagnostic sufficiency) | D53-L; I1-L, I11-L | -| I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject based on mode/role/strategy/lens/grade. | partially covered (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; context-builder integration deferred to M5) | D52-L, D53-L | +| I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject or advertise based on mode/goal/strategy/lens/grade. | partially covered (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; context-builder integration deferred to M5) | D52-L, D53-L, D58-L | | I36-L | Node `kind` is drawn from a per-plane closed enum structurally validated by the `CommandExecutor`; the intent kind category (basic / structural / reasoning) is a pure function of `kind` and is never stored on the node. | covered (CommandExecutor rejects invalid kind-for-plane; `intentKindCategory` is pure derivation with exhaustive switch; tests in `command-executor.test.ts`) | D54-L, D56-L | | I37-L | `detail` is per-kind validated by the `CommandExecutor`: `decision` and `term` nodes REQUIRE `detail` with their respective sub-schemas; all other kinds must omit `detail`; unknown fields in `detail` are rejected. | covered (detail-required/prohibited/shape tests in `command-executor.test.ts`) | D54-L | +| I38-L | Every Brunch prompt-resource manifest injected for an agent turn is generated from projected runtime state and spec/workspace gates: listed resources are Brunch-owned, readable under the active tool policy, legal for the current `(op_mode × goal × strategy × lens)` / grade / agent allow-list, and off-list resources are not advertised as available. AUTO axes never list illegal choices; pinned axes point to the pinned resource. | planned (`agents-composition-layer` compose tests for manifest filtering, read locations, tuple/grade/allow-list gates, and ambient-resource exclusion; probe fitness may track whether the agent reads selected resources before use) | D39-L, D40-L, D58-L, D59-L | ## Future Direction Register @@ -309,9 +310,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is a **3-layer model**, composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshot)` (D58-L): **(1) agent definition** — identity/system prompt + model/thinking + mode-gated tool authority + applicability allow-lists; **(2) objective config** — the optional/AUTO-able `goal` (grade-derived, D59-L), `strategy`, and `lens` packs, gated by `spec.readiness_grade` and conditioned by `workspace.posture`; **(3) capabilities** — tool-usage skills via Pi progressive disclosure, gated by allow-list ∩ `op_mode` ∩ grade. A dynamic **context** layer appends agent-context snapshots built in `agents/contexts/` from `graph/`+`session/` reads, scaled by lens (plane) and grade (depth) (D60-L). Target topology per D52-L is `src/agents/{definitions, strategies, lenses, goals, contexts}` with `agents/compose.ts` and `agents/state.ts` (axes + legal-combination table); the current `src/.pi/context/` layout migrates here. +- Brunch prompt composition is a **runtime-header + gated prompt-resource manifest** composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshots)` (D58-L). The direct injection is intentionally small: agent control summary, runtime state, legal resource manifests (``, ``, ``, ``/`` with `name`, `description`, `location`), router rules for pinned/AUTO axes, and compact context handles. Detailed objective and method bodies are Brunch-owned markdown resources that the agent loads with `read` when needed, matching Pi's skill-loading pattern while allowing Brunch to filter availability per turn. Target topology per D52-L is `src/agents/{definitions, goals, strategies, lenses, methods|capabilities, contexts}` with `agents/compose.ts` and `agents/state.ts` (axes + legal-combination table); the current `src/.pi/context/` layout migrates here. - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. -- Layers 1–2 (agent definition, objective config) are **product prompt packs**; Layer-3 capabilities are **Pi skills** delivered by progressive disclosure (how to run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps), gated by the agent's allow-list ∩ active `op_mode` ∩ `spec.readiness_grade`. Skills are not the authority for operational-mode/tool policy. +- Prompt resources and Pi skills are both progressive-disclosure mechanisms, but they are not authority. Brunch code owns runtime-state projection, legal tuple filtering, grade/allow-list gating, tool activation, and tool-call blocking. Pi-native skills may be used for startup-scoped capabilities; runtime-state-specific objective/method availability is advertised through Brunch's per-turn manifest so ambient user/project resources cannot leak into product behavior. ### Coherence and readiness semantics @@ -358,15 +359,16 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Transport mode** | One of TUI, web, RPC, print. All four drive the same host; they are presentation/protocol surfaces, not separate products or agent strategies. | | **Operational mode** | A top-level Brunch authority/tooling posture such as `elicit` or future `execute`. It determines what kind of work is allowed and which tools/prompt posture are available. Distinct from Pi's transport mode concept. | | **Agent role** | A worker identity. The **foreground session-agent role** (`elicitor` now, future `executor`) drives the main turn and is *derived* from `op_mode`, not stored as session state. **Side/sub-agent roles** (`reviewer`, `reconciler`, future `scout`/`researcher`) run async/advisory or delegated work out-of-band and are never part of the session state machine. | -| **Agent definition** | Layer-1 composition unit (D58-L): a keyed agent's identity/system prompt + model/thinking preset + mode-gated tool authority + applicability allow-lists (`strategies`/`lenses`/`skills`). A keyed registry covers the foreground session agent plus side/sub-agents. Replaces the prior "runtime bundle / role preset" framing. | +| **Agent definition** | Composition control unit (D58-L): a keyed agent's identity/system prompt, model/thinking preset, mode-gated tool authority summary, and applicability allow-lists (`goals`/`strategies`/`lenses`/`methods`/`capabilities`). A keyed registry covers the foreground session agent plus side/sub-agents. Replaces the prior "runtime bundle / role preset" framing. | | **Session agent** | The main-thread agent that drives the session forward — `elicitor` now, future `executor` — resolved from `op_mode`. It is the only agent represented in session state (D40-L); side/sub-agents are out-of-band. | -| **Strategy** | An optional, AUTO-able session-agent axis (D25-L) describing interaction shape: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges via review-set). Strategy determines the commitment mechanism (D26-L). | -| **Lens** | An optional, AUTO-able session-agent axis (D25-L) describing topical focus: `intent`, `design`, `oracle` for elicit mode; future execute-mode `plan`, `sync`, `scope`. Orthogonal to strategy; stamped onto elicitor-emitted entries as provenance (I18-L). | +| **Strategy** | An optional, AUTO-able session-agent axis (D25-L) describing interaction shape: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges via review-set). Strategy determines the commitment mechanism (D26-L). Detailed strategy behavior lives in a Brunch prompt resource advertised through D58-L manifests. | +| **Lens** | An optional, AUTO-able session-agent axis (D25-L) describing topical focus: `intent`, `design`, `oracle` for elicit mode; future execute-mode `plan`, `sync`, `scope`. Orthogonal to strategy; stamped onto elicitor-emitted entries as provenance (I18-L). Detailed lens behavior lives in a Brunch prompt resource advertised through D58-L manifests. | | **Goal** | An optional, AUTO-able session-agent objective axis (D59-L): what the agent currently pursues, derived/gated by `spec.readiness_grade` — `grounding-advance`, `elicit-I`, `elicit-II`, `commitment-converge`, plus always-on `capture-posture`. Distinct from strategy (the *how*) and lens (the topical focus). | -| **AUTO** | The unpinned state of an objective axis (`goal` / `strategy` / `lens`): instead of a fixed pack, composition injects selection guidance over the agent's allowed set and the agent self-selects (D58-L). | +| **AUTO** | The unpinned state of an objective axis (`goal` / `strategy` / `lens`): composition advertises the legal choices in the current prompt-resource manifest and instructs the agent to self-select from that manifest only, reading the selected resource when detail matters (D58-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | -| **Prompt pack** | A Brunch-owned prompt fragment composed at turn boundaries (D58-L): Layer-1 agent-definition packs and Layer-2 `goal`/`strategy`/`lens` packs. Layer-3 capabilities are Pi skills, not packs. Packs are product control-plane state, not ambient Pi prompt templates. | -| **Capability / skill** | A Layer-3 tool-usage competence delivered as a Pi skill via progressive disclosure (run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps), gated by the agent's allow-list ∩ `op_mode` ∩ `spec.readiness_grade` (D58-L). Not a prompt pack. | +| **Prompt resource** | A Brunch-owned markdown file under `src/agents/` containing detailed objective, lens, strategy, method, capability, or agent guidance. Prompt resources are loaded by the agent with `read` when needed; they are product control-plane assets, not ambient Pi prompt templates. | +| **Prompt-resource manifest** | The small per-turn D58-L manifest injected into the system prompt, listing only runtime-legal Brunch resources with `name`, `description`, and `location`. It mirrors Pi's skill-list pattern but is filtered by Brunch runtime state, grade, and allow-lists. | +| **Method / capability** | A tool-usage or workflow competence (run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps). It may be advertised as a Brunch prompt resource or as a Pi-native skill, but actual tool authority remains code-owned through `op_mode` policy and active-tool gating. | | **Snapshot** | An *agent-context* content view the agent reasons over — `cwd`, `graph`, or `node` (D60-L): pulled (typed, read-only) from `graph/`/`session/`, rendered to LLM-string (in `agents/contexts/`) or JSON, surfaced pushed (compose) or pulled (`snapshot-*` tools). Distinct from the **workspace projection** (`workspace.snapshot`), which is product/UI state, not agent content. | | **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | | **Elicitation posture** | Retired as persisted spec state. Use readiness grade plus active strategy/lens/review-set state to explain elicit behavior. | @@ -520,7 +522,7 @@ Infrastructure is not yet fully laid (Phase 3 of POC bootstrapping). Commands fo | Loop | Oracle family | Proves | Primary claims | | --- | --- | --- | --- | | Inner | Type-aware lint, type checks, fast unit tests | Local module correctness, typed command/result shapes (including `acceptReviewSet` and reviewer-writable record-class types), projection helper behavior (including `supersedes`-chain filtering). | D12-L, D13-L, D20-L, D21-L, D27-L, D28-L, D29-L. | -| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, `brunch.review_set_proposal` / `brunch.establishment_offer` / `brunch.elicitor_intent_hint` custom-entry payloads (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L. | +| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, runtime-gated prompt-resource manifests, `brunch.review_set_proposal` / `brunch.establishment_offer` / `brunch.elicitor_intent_hint` custom-entry payloads (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L, I38-L. | | Middle | **Probe oracles**: prose manual actions plus executable postcondition checkers | Interactive seams leave correct durable state. Early M0 checkers may inspect stores only; once handlers exist, prefer projection-including checks. Extends to workspace-dialog startup behavior, in-flight reviewer-signal chrome behavior, and ambient-affordance rendering from latest establishment-offer entry. | D11-L, D21-L, D22-L, D25-L, D29-L, D36-L; I8-L, I13-L, I22-L. | | Middle | Round-trip tests | JSONL reload, linear transcript validation, elicitation exchange projection, compaction, graph export/import, command result serialization, `supersedes`-chain reconstruction across regeneration. | D6-L, D13-L, D24-L, D28-L; I3-L, I8-L, I10-L, I19-L. | | Middle | Property-based / model-based tests | LSN monotonicity, change-log replay, reconciliation-need invariants, mention staleness, interest-set recomputation, side-task delivery ordering, **batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible even under mid-batch validation failure)**, **`supersedes`-chain acyclicity and unique-leaf-per-thread**, **lens-routing correctness (generated elicitor entries route to the right consumer)**, **reviewer-finding turn-boundary delivery ordering**. | A4-L, A8-L, A9-L, A11-L; I1-L, I4-L, I5-L, I6-L, I9-L, I12-L, I15-L, I16-L, I18-L. | @@ -582,9 +584,11 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I33-L | Current schema tests cover minimum no-graph `capture_*` details and reject graph payload fields. Future capture-analysis runtime tests must still cover persisted result rendering, no graph-write side effects, Brunch-semantic transcript inclusion, and hidden/collapsed TUI rendering fallback. | | I36-L | M4 per-plane kind enum validation tests in CommandExecutor; kind-to-category derivation unit tests proving pure function parity with GRAPH_MODEL.md table. | | I37-L | M4 node-creation tests: decision/term rejected without detail; constraint accepted with or without detail; other kinds rejected with detail; unknown detail fields rejected. | +| I38-L | `agents-composition-layer` inner tests: given projected runtime states and spec grades, compose emits manifests whose goal/strategy/lens/method resources are legal, Brunch-owned, readable, and filtered by the agent allow-list; AUTO axes list only legal choices and pinned axes point to their selected resource. Middle/outer probes may track whether the model actually reads the selected resource before applying it as fitness, not as an inner-loop gate. | ### Design Notes +- **Prompt-resource manifests before eager prompt injection.** For goal, strategy, lens, and method/capability guidance, prefer a deterministic per-turn manifest plus agent-driven `read` loading over a Brunch state machine that selects and concatenates large semantic prompt bodies. Inner-loop tests prove manifest legality and filtering; behavioral probes judge whether the agent loads and applies the right resource. - **Deterministic before generative.** Probe runs should prefer deterministic or tightly scripted paths before relying on LLM persona variance. Generative/adversarial probes come after the transcript substrate is trusted. Retired M1 scripted captures proved the early transport/projection substrate on then-current terms, but tuple-shaped FE-744 public-RPC probes are the current evidence path. - **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, pending/respond lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. - **Capture analysis before graph persistence.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before graph persistence or before comparing later graph mutations against transcript evidence. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. @@ -602,6 +606,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | Performance and scale | Local POC graph/session sizes are small; premature budgets may distort design. | Keep exports/checkers text-native and simple; add budgets when slow tests appear. | `npm run verify` or fixture runs exceed acceptable local iteration time. | | Cross-platform terminal rendering | TUI chrome visuals may differ by terminal. | Test state derivation and keep manual smoke on primary dev environment. | Distribution target broadens or terminal rendering bugs recur. | | Lens-recommendation appropriateness | No deterministic ground truth for "did the agent offer the right strategy at the right time" given temperament + grounding density inputs. | Probe-driven outer-loop walkthrough; small targeted scenarios where recommended lens is judged by reviewer; tracked as fitness, not gated. | Repeated user complaints that the offered strategies feel wrong, or fixture review reveals systematic mis-offers. | +| Prompt-resource discretionary loading | The Pi-like `read` loading mechanism is model behavior: Brunch can advertise the legal goal/strategy/lens/method resources, but the model may skip reading, read the wrong listed resource, or act from stale memory. | Inner gate proves manifest legality/filtering (I38-L); middle/outer probes track selected-resource read rate and application quality as fitness, not merge gates. | The agent repeatedly applies AUTO choices without loading needed resources, or loads off-list/stale resources despite manifest instructions. | | Framing/proposal quality at thin grounding | Generative-lens proposals may be syntactically legal but semantically weak when grounding is thin; `epistemic_status` honesty may not be enforceable without human judgment. | A14-L proposal-legality rate tracked as fitness; outer-loop walkthrough of proposals under thin vs rich grounding; `epistemic_status` distribution surfaced per run. | Acceptance-without-rework rates drop, or reviewers consistently mark proposals as `inferred`/`asserted` despite asserted grounding. | | Reviewer finding precision (false positives/negatives) | Advisory-only reviewer can spam reconciliation needs (false positives) or miss real coherence gaps (false negatives); both erode trust. | Targeted adversarial briefs with known-bad coherence problems; precision/recall surfaced per run as fitness; user can dismiss reviewer findings without consequence. | Users systematically ignore reviewer findings, or coherence gaps slip past reviewer in known-bad fixtures. | | In-flight reviewer-signal UX | Chrome rendering of "reviewer running / has findings" before next-turn delivery is not yet designed; cost may exceed value in POC. | Probe oracle on chrome state after batch-accept; defer in-flight progress affordances unless a frontier explicitly demands them. | Users report confusion about whether reviewer ran or completed; or async job latency makes silence feel like failure. | From c36101d2b643dc06d9121a45d5cae8d355338487 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:21:00 +0200 Subject: [PATCH 16/34] agent context composition re-spec/-plan II --- memory/PLAN.md | 22 +++++++++++----------- memory/SPEC.md | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 6458709a2..e75dd20b2 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -127,20 +127,20 @@ The POC should maximize assumption falsification rather than merely implement mi - **Branch:** to create — `ln/-agents-composition-layer` - **Kind:** structural (new composition seam + held Class-B migration; spans `agents/`, `graph/`, `session/`, `.pi/extensions/`) - **Status:** next -- **Objective:** Build the `agents/` layer per D58-L/D59-L/D60-L and migrate `.pi/context/` into it. **(1)** `agents/state.ts` — the legal `(op_mode × goal × strategy × lens)` combination table + shared axis enums. **(2)** `agents/compose.ts` — the per-agent 3-layer selector: Layer-1 agent definition (identity + model/thinking + mode-gated tool authority + allow-lists), Layer-2 objective-config packs (`goal`/`strategy`/`lens`; AUTO ⇒ selection-guidance injection over the allowed set), Layer-3 capabilities as gated Pi skills; replaces the fixed `PROMPT_PACK_ORDER` concatenation. **(3)** `agents/{definitions,strategies,lenses,goals}/` packs — author the (currently absent) strategy/lens/goal packs; rehome `brunch-base`/`elicit`/`elicitor` as Layer-1, convert `structured-exchange`/`capture-analysis`/`candidate-proposals` to Layer-3 skills. **(4)** `agents/contexts/` — snapshot RENDER (D60-L): LLM-string/JSON from typed pulls, scaled by lens-plane + grade-depth; wire `snapshot-*` Pi tool wrappers (SURFACE) over the renderer; PULL stays read-only in `graph/snapshot.ts` (+ a `session/` cwd-overview pull). **(5)** Migrate `src/.pi/context/* → src/agents/` (the held Class-B move) and delete the old composer. -- **Why now / unlocks:** Turns the locked composition model into a real selector so mode/strategy/lens/goal actually drive the prompt body (today they only change a header sentence), and finally injects the M4 graph snapshots into context. Unblocks grade-gated, goal-driven elicitation behavior. +- **Objective:** Build the `agents/` layer per D58-L/D59-L/D60-L and migrate `.pi/context/` into it. **(1)** `agents/state.ts` — the legal `(op_mode × goal × strategy × lens)` combination table, shared axis enums, and the code-owned `{name, description, location}` manifest entry for every goal/strategy/lens/method resource. **(2)** `agents/compose.ts` — the per-agent projection that emits the runtime header (agent control + runtime-state) plus the gated prompt-resource manifest filtered by tuple/grade/`op_mode`/allow-list, with router rules for pinned/AUTO axes; it is projection, not a selector that concatenates semantic bodies, and it replaces the fixed `PROMPT_PACK_ORDER`. **(3)** `agents/{definitions,goals,strategies,lenses,methods}/*.md` — author the (currently absent) goal/strategy/lens/method resources as `read`-on-demand markdown; rehome `brunch-base`/`elicit`/`elicitor` as `definitions/*.md` (frontmatter = model/thinking + tool authority + allow-lists; body = system prompt); convert `structured-exchange`/`capture-analysis`/`candidate-proposals` guidance into `methods/*.md` resources. **(4)** `agents/contexts/*.ts` — snapshot RENDER (D60-L): LLM-string/JSON from typed pulls, scaled by lens-plane + grade-depth; wire `snapshot-*` Pi tool wrappers (SURFACE) over the renderer; PULL stays read-only in `graph/snapshot.ts` (+ a `session/` cwd-overview pull). `contexts/` is render-only and carries no manifest family. **(5)** Migrate `src/.pi/context/* → src/agents/` (the held Class-B move) and delete the old composer. +- **Why now / unlocks:** Turns the locked composition model into a real composer so mode/strategy/lens/goal actually drive the prompt body (today they only change a header sentence), and finally injects the M4 graph snapshots into context. Unblocks grade-gated, goal-driven elicitation behavior. - **Acceptance:** - - `compose(agentId, sessionState, spec, workspace, snapshot)` selects packs by axes and gates by mode/grade/allow-list; output varies by axis, not just a header line. - - AUTO on `goal`/`strategy`/`lens` injects selection guidance listing exactly the agent's allowed set. - - `agents/state.ts` accepts every legal tuple, rejects illegal; a pack exists for each `strategies`/`lenses`/`goals` enum value. - - Layer-3 capabilities resolve as Pi skills gated by allow-list ∩ `op_mode` ∩ grade. + - `compose(agentId, sessionState, spec, workspace, snapshots)` emits the runtime header + a gated prompt-resource manifest; the manifest set varies by `(op_mode × goal × strategy × lens)`/grade/allow-list, and detailed bodies are `read`-on-demand `.md`, not concatenated into the prompt. + - AUTO on `goal`/`strategy`/`lens` lists exactly the agent's legal set in the manifest, with a router rule instructing the agent to choose only from the current manifest. + - `agents/state.ts` accepts every legal tuple, rejects illegal; a `.md` resource and a `state.ts` manifest entry (`{name, description, location}`) exist for each `goals`/`strategies`/`lenses`/`methods` value. + - `` resolves from `agents/methods/*.md`, gated by allow-list ∩ `op_mode` ∩ grade; methods are `read`-on-demand resources, not eager injection (a method may still be backed by a Pi-native skill). - snapshot RENDER scales by lens-plane + grade-depth; `snapshot-{cwd,graph,nodes}` tools wrap the renderer (markdown in `content`, typed JSON in `details`); PULL stays read-only in `graph/`/`session/`. - `src/.pi/context/` removed; composition lives in `src/agents/`; `npm run verify` green. -- **Verification:** Inner — `compose()` output assertions (pack set/order/gating per axes; AUTO-guidance injection); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip. Middle — migration differential (old vs new composer equivalence for fixed states, throwaway cutover gate); AUTO-choice legality as a probe fitness metric (not a gate). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). **Oracle pass pending** — see SPEC §Verification Design once `ln-oracles` completes. -- **Cross-cutting obligations:** Layers 1–2 = prompt packs; Layer 3 = Pi skills (D58-L/§311). PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). -- **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location. -- **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture, Lexicon; this session's locked composition model. -- **Current execution pointer:** Not started. Oracle diagnostic + claims drafted this session (machinery = structural inner-loop; AUTO-choice = middle-loop fitness; agent behavior = outer A14-L) but `ln-oracles` was not completed — finish it before scoping. Likely first slice: `agents/state.ts` + `compose.ts` skeleton over the fixed vocabulary, then pack authoring, then snapshots, then migration. +- **Verification:** Inner — `compose()` output assertions (manifest set/filtering/gating per axes; pinned-vs-AUTO router rules; code-owned `{name, description, location}` from `state.ts`); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip (I38-L). Middle — migration differential (old vs new composer equivalence for fixed states, throwaway cutover gate); AUTO-choice legality as a probe fitness metric (not a gate). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). **Oracle pass pending** — see SPEC §Verification Design once `ln-oracles` completes; resumable state captured in the execution pointer below. +- **Cross-cutting obligations:** All of goals/strategies/lenses/methods are `read`-on-demand `.md` resources advertised through the per-turn manifest; manifest `{name, description, location}` metadata is code-owned in `agents/state.ts`, never filesystem-discovered (D39-L sealing). `contexts/` is the D60-L render layer only — no `` family. PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Workspace posture has no persisted home (deferred, SPEC §313). Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). +- **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L, I38-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location; the "Layer-2 eager packs / Layer-3 Pi-skill" framing; `capability` as a synonym for `method`. +- **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture (concrete `agents/` topology), Lexicon. +- **Current execution pointer:** Not started. The prompt-injection-automation question is **resolved**: composition always owns a deterministic envelope (`op_mode`/grade/allow-list/legal-tuple) and fills it three ways — *push* (header + pinned manifest entries), *delegate* (AUTO axis lists the legal set, agent self-selects), *pull* (agent `read`s the `.md` body, or `snapshot-*` tools) — which is the locked D58-L manifest model. **Oracle pass still owed** (`ln-oracles` unfinished): the O/R/C diagnostic splits the subject into composition machinery (HIGH observability/reproducibility/controllability → structural inner-loop) vs agent-behavior-under-composition (PARTIAL → behavioral middle/outer). Three open grill questions to resolve before filing SPEC §Verification Design: (1) **AUTO boundary** — inner loop proves only that the manifest lists exactly the legal set (the injection), with the agent's *choice quality* a middle-loop fitness metric, not a merge gate? (2) **migration differential** — build a golden-master old-vs-new composer equivalence for the `.pi/context → agents/` cutover, or skip given pre-release regenerate-don't-preserve posture? (3) **`agents/state.ts` model-based sweep** — is the legal-combination set small/enumerable enough for a generated tuple sweep? Likely first slice (after oracles): `agents/state.ts` + `compose.ts` skeleton over the landed vocabulary, then resource authoring, then snapshots, then migration. Depends on `agent-runtime-vocabulary` landing the enums first. ### sealed-pi-profile-runtime-state diff --git a/memory/SPEC.md b/memory/SPEC.md index 4b1ab6604..42495ee14 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -129,7 +129,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D40-L — Runtime state is a transcript-backed Brunch session-agent record, not hidden extension memory.** Brunch projects one session-agent record from linear `brunch.agent_runtime_state` entries (`reason: "init" | "switch"`), last-writer-wins at turn preparation. Its axes are `op_mode` (`elicit`, future `execute`) plus the optional, AUTO-able objective axes `strategy`, `lens`, and `goal` (D25-L, D59-L); a deferred `world_view` watermark group (last-seen LSN, graph mentions, optional git head) is reserved for M7. The **foreground session agent** (`elicitor` now, future `executor`) is *derived* from `op_mode`, not stored; the other agent roles (`reviewer`, `reconciler`, future `scout`/`researcher`) are async sub-agent/side-chain workers (D29-L, D44-L) invoked out-of-band, never part of the session state machine. `op_mode` gates tool authority, owned by `src/.pi/extensions/operational-mode.ts` (current `elicit` policy denies side-effecting `bash`/`edit`/`write` plus user-shell interception). Prompt composition is a separate concern (D58-L). Depends on: D17-L, D23-L, D25-L, D39-L, D58-L, D59-L. Supersedes: mode-only vocabulary, extension-local mutable state as authority, storing the foreground role as independent session state, the "runtime bundle / role preset" as one knob deriving model/thinking/resources, and binding prompt-resource location to `src/.pi/context/`. - **D34-L — Command containment separates visibility suppression from effect blocking.** Current Pi extension seams can hide unsupported slash suggestions with autocomplete wrapping and can cancel branch/session effects through lifecycle hooks, but they cannot strictly suppress exact interactive built-in commands before `InteractiveMode` dispatches them. Brunch-owned commands must use product-specific names and route writes through Brunch handlers/`CommandExecutor`; extension command collisions are not an override mechanism. Strict built-in command/keybinding policy is a Pi upstream/API ask, while POC safety relies on hiding generic affordances, blocking dangerous effects (`/fork`, `/clone`, `/tree`, raw session replacement), and failing fast on branched transcripts. Brunch's command-policy code should live in `src/.pi/extensions/command-policy.ts`, merging branch/session-effect blocking with any product command allow/deny behavior instead of preserving a branch-only module. Depends on: D2-L, D24-L, A18-L. Supersedes: treating extension `input` handlers or command-name collisions as built-in command allowlisting. - **D35-L — Dynamic TUI chrome is a Brunch projection wrapper over Pi UI primitives.** Downstream TUI affordances should call a Brunch-owned renderer (`renderBrunchChrome` or its successor) with one activated product-state snapshot rather than scattering raw `ctx.ui.setHeader`, `setFooter`, `setWidget`, title, or working-indicator calls. The wrapper is stateless projection over canonical workspace/session/graph facts, including the real activated session id, while its TUI footer compositor may read Pi footer telemetry (`getGitBranch`, foreign `getExtensionStatuses`) at render time. Brunch chrome does not publish a `brunch.chrome` status key; `ctx.ui.setStatus(key, text)` remains a lateral contribution channel for other extensions and future dynamic Brunch state. RPC clients should rely only on surfaces Pi actually emits for the wrapper (currently diagnostic widget/title, plus any future explicit status adapter) because header/footer/working-indicator are TUI-only in current Pi RPC mode. Session display names are likewise product projections over Pi session metadata: Brunch may append Pi `session_info` entries, but generated names must characterize the selected spec/session transcript rather than replace spec identity or graph truth. Depends on: D2-L, D21-L, D34-L, A18-L. Supersedes: treating Pi UI methods as direct downstream affordance APIs, rendering placeholder session state such as `unbound` after a session is activated, or consuming the status-key namespace for chrome's own static summary. -- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by registry/resource family (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods`/`capabilities`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, prompt resources, and the state definitions that drive `op_mode`/goal/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. +- **D52-L — Source topology is `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies.** `graph/` is the domain layer: CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, reconciliation-need substrate; it imports from `db/` (Drizzle schema, migrations, connection lifecycle) and no other layer imports `db/` directly. `session/` owns transcript projection, exchange extraction, workspace coordination, session binding, and LSN staleness tracking over Pi JSONL. `agents/` is organized by registry/resource family (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods/`, `contexts/`) and imports snapshot functions from `graph/` and `session/`; it owns prompt composition, context building, prompt resources, and the state definitions that drive `op_mode`/goal/strategy/lens selection. `.pi/extensions/` houses Pi adapter registrars (agent tools, TUI commands, TUI enhancements); `.pi/components/` houses reusable TUI components. `rpc/` owns Brunch JSON-RPC handlers. `web/` owns the React client. Dependency direction: `.pi/extensions/` and `rpc/` may import from `graph/`, `session/`, and `agents/`; `agents/` imports from `graph/` and `session/`; `graph/` imports from `db/`; `web/` is a standalone build target. Depends on: D2-L, D4-L, D39-L, D40-L. Supersedes: scattering session domain files at `src/` root; nesting prompt composition exclusively under `src/.pi/context/`. #### Data model & vocabulary @@ -241,7 +241,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. -- **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and ``/`` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods`/`capabilities`, `contexts/`); composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; and direct Layer-2 eager prompt-pack injection as the default mechanism. +- **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and `` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; the `{name, description, location}` triples are code-owned in `agents/state.ts`, not filesystem-discovered, honoring D39-L sealing; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods/`, `contexts/`); `agents/contexts/` is the D60-L snapshot render layer (code), not a manifest resource family; composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; direct Layer-2 eager prompt-pack injection as the default mechanism; and `capability` as a parallel name for `method` / ``. - **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-I` / `elicit-II` (main elicitation levels), `commitment-converge` (reduce / lock down), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO`; in either case D58-L manifests advertise the legal resource(s) rather than injecting the whole goal body. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. - **D60-L — "Snapshot" splits into pull / render / surface, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview), `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization). **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L. Supersedes: pre-rendering snapshots to strings in the pull layer, and scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs. @@ -310,7 +310,28 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ### Prompt/runtime profile architecture -- Brunch prompt composition is a **runtime-header + gated prompt-resource manifest** composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshots)` (D58-L). The direct injection is intentionally small: agent control summary, runtime state, legal resource manifests (``, ``, ``, ``/`` with `name`, `description`, `location`), router rules for pinned/AUTO axes, and compact context handles. Detailed objective and method bodies are Brunch-owned markdown resources that the agent loads with `read` when needed, matching Pi's skill-loading pattern while allowing Brunch to filter availability per turn. Target topology per D52-L is `src/agents/{definitions, goals, strategies, lenses, methods|capabilities, contexts}` with `agents/compose.ts` and `agents/state.ts` (axes + legal-combination table); the current `src/.pi/context/` layout migrates here. +- Brunch prompt composition is a **runtime-header + gated prompt-resource manifest** composed per agent by `agents/compose(agentId, sessionState, spec, workspace, snapshots)` (D58-L). The direct injection is intentionally small: agent control summary, runtime state, legal resource manifests (``, ``, ``, `` with `name`, `description`, `location`), router rules for pinned/AUTO axes, and compact context handles. Detailed goal/strategy/lens/method bodies are Brunch-owned markdown resources the agent loads with `read` when needed, matching Pi's skill-loading pattern while letting Brunch filter availability per turn. The current `src/.pi/context/` layout migrates into `src/agents/`. +- Concrete `agents/` topology (D52-L). The markdown/code boundary falls exactly on the control-plane/behavior split: enforcement and projection are TypeScript; semantic prompting material is markdown. + +```text +src/agents/ + state.ts [ts] axis enums + legal (op_mode × goal × strategy × lens) tuple table; + also owns each resource's {name, description, location} manifest entry + compose.ts [ts] projection -> runtime header + gated manifest (not a state machine) + index.ts [ts] public entry / resource registry + definitions/*.md [md+] keyed agents (elicitor foreground; reviewer side). frontmatter = + model/thinking + tool authority + allow-lists; body = system prompt + goals/*.md [md] grounding-advance, elicit-I, elicit-II, commitment-converge, capture-posture + strategies/*.md [md] step-wise-decision-tree, step-wise-disambiguate, propose-graph, project-graph + lenses/*.md [md] intent, design, oracle (future execute: plan, sync, scope) + methods/*.md [md] run-structured-exchange, infer-and-capture, generate-proposal, + read-snapshot, commit-graph, review-for-gaps + contexts/*.ts [ts] D60-L snapshot RENDER layer (cwd, graph, node) -- NOT a manifest resource +``` + +- Manifest metadata is code-owned, not filesystem-discovered: `agents/state.ts` binds each legal axis value to its `{name, description, location}`, and `compose()` emits that binding; the agent `read`s the `.md` body at the listed `location` only when detail matters. This keeps the legal set and its labels in one tested place and honors D39-L sealing (no runtime resource discovery). Frontmatter-sourced manifest metadata is a deferred ergonomics option, not the POC mechanism. +- `agents/contexts/` is the D60-L snapshot render layer (TypeScript), surfaced as the header's compact pushed context or via the `snapshot-*` tools; it is not part of the `read`-on-demand resource manifest and carries no `` family. +- Workspace **posture** has no persisted home in the POC: D57-L keeps it off the spec row and the graph. The runtime header surfaces posture when known and the always-on `capture-posture` goal (D59-L) confirms it conversationally; a durable store (a `.brunch/` workspace setting versus an optional `agent_runtime_state` field) is deferred until cross-session posture stability is needed. [open decision] - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. - Prompt resources and Pi skills are both progressive-disclosure mechanisms, but they are not authority. Brunch code owns runtime-state projection, legal tuple filtering, grade/allow-list gating, tool activation, and tool-call blocking. Pi-native skills may be used for startup-scoped capabilities; runtime-state-specific objective/method availability is advertised through Brunch's per-turn manifest so ambient user/project resources cannot leak into product behavior. @@ -359,16 +380,16 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | **Transport mode** | One of TUI, web, RPC, print. All four drive the same host; they are presentation/protocol surfaces, not separate products or agent strategies. | | **Operational mode** | A top-level Brunch authority/tooling posture such as `elicit` or future `execute`. It determines what kind of work is allowed and which tools/prompt posture are available. Distinct from Pi's transport mode concept. | | **Agent role** | A worker identity. The **foreground session-agent role** (`elicitor` now, future `executor`) drives the main turn and is *derived* from `op_mode`, not stored as session state. **Side/sub-agent roles** (`reviewer`, `reconciler`, future `scout`/`researcher`) run async/advisory or delegated work out-of-band and are never part of the session state machine. | -| **Agent definition** | Composition control unit (D58-L): a keyed agent's identity/system prompt, model/thinking preset, mode-gated tool authority summary, and applicability allow-lists (`goals`/`strategies`/`lenses`/`methods`/`capabilities`). A keyed registry covers the foreground session agent plus side/sub-agents. Replaces the prior "runtime bundle / role preset" framing. | +| **Agent definition** | Composition control unit (D58-L): a keyed agent's identity/system prompt, model/thinking preset, mode-gated tool authority summary, and applicability allow-lists (`goals`/`strategies`/`lenses`/`methods`). A keyed registry covers the foreground session agent plus side/sub-agents. Replaces the prior "runtime bundle / role preset" framing. | | **Session agent** | The main-thread agent that drives the session forward — `elicitor` now, future `executor` — resolved from `op_mode`. It is the only agent represented in session state (D40-L); side/sub-agents are out-of-band. | | **Strategy** | An optional, AUTO-able session-agent axis (D25-L) describing interaction shape: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges via review-set). Strategy determines the commitment mechanism (D26-L). Detailed strategy behavior lives in a Brunch prompt resource advertised through D58-L manifests. | | **Lens** | An optional, AUTO-able session-agent axis (D25-L) describing topical focus: `intent`, `design`, `oracle` for elicit mode; future execute-mode `plan`, `sync`, `scope`. Orthogonal to strategy; stamped onto elicitor-emitted entries as provenance (I18-L). Detailed lens behavior lives in a Brunch prompt resource advertised through D58-L manifests. | | **Goal** | An optional, AUTO-able session-agent objective axis (D59-L): what the agent currently pursues, derived/gated by `spec.readiness_grade` — `grounding-advance`, `elicit-I`, `elicit-II`, `commitment-converge`, plus always-on `capture-posture`. Distinct from strategy (the *how*) and lens (the topical focus). | | **AUTO** | The unpinned state of an objective axis (`goal` / `strategy` / `lens`): composition advertises the legal choices in the current prompt-resource manifest and instructs the agent to self-select from that manifest only, reading the selected resource when detail matters (D58-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | -| **Prompt resource** | A Brunch-owned markdown file under `src/agents/` containing detailed objective, lens, strategy, method, capability, or agent guidance. Prompt resources are loaded by the agent with `read` when needed; they are product control-plane assets, not ambient Pi prompt templates. | -| **Prompt-resource manifest** | The small per-turn D58-L manifest injected into the system prompt, listing only runtime-legal Brunch resources with `name`, `description`, and `location`. It mirrors Pi's skill-list pattern but is filtered by Brunch runtime state, grade, and allow-lists. | -| **Method / capability** | A tool-usage or workflow competence (run structured exchanges, infer-and-capture per D50-L, generate proposals/projections, read snapshots, mutate the graph, review for gaps). It may be advertised as a Brunch prompt resource or as a Pi-native skill, but actual tool authority remains code-owned through `op_mode` policy and active-tool gating. | +| **Prompt resource** | A Brunch-owned markdown file under `src/agents/` containing detailed goal, strategy, lens, method, or agent-definition guidance. Prompt resources are loaded by the agent with `read` when needed; they are product control-plane assets, not ambient Pi prompt templates. | +| **Prompt-resource manifest** | The small per-turn D58-L manifest injected into the system prompt, listing only runtime-legal Brunch resources with `name`, `description`, and `location`. The `name`/`description`/`location` for each entry are code-owned in `agents/state.ts` (not filesystem-discovered), honoring D39-L sealing; `agents/contexts/` snapshot renderers are not manifest resources. It mirrors Pi's skill-list pattern but is filtered by Brunch runtime state, grade, and allow-lists. | +| **Method** | A tool-usage or workflow competence advertised as a Brunch prompt resource (`agents/methods/*.md`): run structured exchanges, infer-and-capture (D50-L), generate proposals/projections, read snapshots, mutate the graph, review for gaps. A method may also be backed by a Pi-native skill, but actual tool authority remains code-owned through `op_mode` policy and active-tool gating. `capability` is retired as a synonym — use `method` and ``. | | **Snapshot** | An *agent-context* content view the agent reasons over — `cwd`, `graph`, or `node` (D60-L): pulled (typed, read-only) from `graph/`/`session/`, rendered to LLM-string (in `agents/contexts/`) or JSON, surfaced pushed (compose) or pulled (`snapshot-*` tools). Distinct from the **workspace projection** (`workspace.snapshot`), which is product/UI state, not agent content. | | **Readiness grade** | Spec-owned forward gate stored on the `specs` row: `grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`. It unlocks later strategies, review sets, and eventual export/plan/execute posture, but never forbids earlier gathering or refinement. | | **Elicitation posture** | Retired as persisted spec state. Use readiness grade plus active strategy/lens/review-set state to explain elicit behavior. | @@ -588,7 +609,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` ### Design Notes -- **Prompt-resource manifests before eager prompt injection.** For goal, strategy, lens, and method/capability guidance, prefer a deterministic per-turn manifest plus agent-driven `read` loading over a Brunch state machine that selects and concatenates large semantic prompt bodies. Inner-loop tests prove manifest legality and filtering; behavioral probes judge whether the agent loads and applies the right resource. +- **Prompt-resource manifests before eager prompt injection.** For goal, strategy, lens, and method guidance, prefer a deterministic per-turn manifest plus agent-driven `read` loading over a Brunch state machine that selects and concatenates large semantic prompt bodies. Inner-loop tests prove manifest legality and filtering; behavioral probes judge whether the agent loads and applies the right resource. - **Deterministic before generative.** Probe runs should prefer deterministic or tightly scripted paths before relying on LLM persona variance. Generative/adversarial probes come after the transcript substrate is trusted. Retired M1 scripted captures proved the early transport/projection substrate on then-current terms, but tuple-shaped FE-744 public-RPC probes are the current evidence path. - **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, pending/respond lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. - **Capture analysis before graph persistence.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before graph persistence or before comparing later graph mutations against transcript evidence. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. From 2fe8bcc34fa8bf1d3820eeb09dc4a7d54e60dd73 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:22:22 +0200 Subject: [PATCH 17/34] Collapse duplicate session entry predicate --- src/session/brunch-session-envelope.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/session/brunch-session-envelope.ts b/src/session/brunch-session-envelope.ts index be9c5d23a..ca6e814f3 100644 --- a/src/session/brunch-session-envelope.ts +++ b/src/session/brunch-session-envelope.ts @@ -80,7 +80,7 @@ function assertFileBackedTranscriptEntries(entries: readonly unknown[]): asserts continue; } - if (!hasRequiredSessionEntryShape(entry)) { + if (!isSessionEntry(entry)) { throw new Error( 'Invalid Pi JSONL transcript: every non-header entry must have a string id, string-or-null parentId, and string type', ); @@ -132,10 +132,6 @@ function isSessionEntry(value: unknown): value is SessionEntry { return isTranscriptEntry(value) && hasStringOrNullParentId(value); } -function hasRequiredSessionEntryShape(value: unknown): value is SessionEntry { - return isTranscriptEntry(value) && hasStringOrNullParentId(value); -} - function isTranscriptEntry(value: unknown): value is SessionEntry { return ( typeof value === 'object' && From 677a20b6d4bfd8d14903b34564dcfee3506448ac Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:22:42 +0200 Subject: [PATCH 18/34] Share transcript entry predicates --- src/session/brunch-session-envelope.ts | 6 +++--- src/session/elicitation-exchange.ts | 23 ++--------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/session/brunch-session-envelope.ts b/src/session/brunch-session-envelope.ts index ca6e814f3..26bff1741 100644 --- a/src/session/brunch-session-envelope.ts +++ b/src/session/brunch-session-envelope.ts @@ -128,11 +128,11 @@ function isPiSessionHeader(value: unknown): value is PiSessionHeader { ); } -function isSessionEntry(value: unknown): value is SessionEntry { +export function isSessionEntry(value: unknown): value is SessionEntry { return isTranscriptEntry(value) && hasStringOrNullParentId(value); } -function isTranscriptEntry(value: unknown): value is SessionEntry { +export function isTranscriptEntry(value: unknown): value is SessionEntry { return ( typeof value === 'object' && value !== null && @@ -142,7 +142,7 @@ function isTranscriptEntry(value: unknown): value is SessionEntry { ); } -function hasStringOrNullParentId(value: unknown): boolean { +export function hasStringOrNullParentId(value: unknown): boolean { return ( (value as { parentId?: unknown }).parentId === null || typeof (value as { parentId?: unknown }).parentId === 'string' diff --git a/src/session/elicitation-exchange.ts b/src/session/elicitation-exchange.ts index 2ca0d156a..5dac99b55 100644 --- a/src/session/elicitation-exchange.ts +++ b/src/session/elicitation-exchange.ts @@ -16,6 +16,8 @@ import { import { assertLinearBrunchSessionEnvelope, loadJsonlTranscriptEntries, + isSessionEntry, + isTranscriptEntry, NonLinearTranscriptError, readBrunchSessionEnvelope, type BrunchSessionEnvelope, @@ -230,27 +232,6 @@ function rangeFor(ids: string[]): EntryRange { return { start: ids[0]!, end: ids[ids.length - 1]! }; } -function isTranscriptEntry(value: unknown): value is SessionEntry { - return ( - typeof value === 'object' && - value !== null && - (value as { type?: unknown }).type !== 'session' && - typeof (value as { id?: unknown }).id === 'string' && - typeof (value as { type?: unknown }).type === 'string' - ); -} - -function isSessionEntry(value: unknown): value is SessionEntry { - return isTranscriptEntry(value) && hasStringOrNullParentId(value); -} - -function hasStringOrNullParentId(value: unknown): boolean { - return ( - (value as { parentId?: unknown }).parentId === null || - typeof (value as { parentId?: unknown }).parentId === 'string' - ); -} - function requestClosesPresent( request: StructuredExchangeRequestDetails, present: StructuredExchangePresentDetails, From 1206588aee736fe1fc0f890b1aa24938fead26ce Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:24:32 +0200 Subject: [PATCH 19/34] Handle unreadable session files --- .../workspace-session-coordinator.test.ts | 23 +++++++++++++ src/session/workspace-session-coordinator.ts | 33 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/session/workspace-session-coordinator.test.ts b/src/session/workspace-session-coordinator.test.ts index 83de8ff74..dd75b453a 100644 --- a/src/session/workspace-session-coordinator.test.ts +++ b/src/session/workspace-session-coordinator.test.ts @@ -337,6 +337,29 @@ describe('WorkspaceSessionCoordinator', () => { await expect(readFile(mismatchedFile, 'utf8')).resolves.toBe(beforeMismatched); }); + it('reports malformed session files without aborting inventory or store verification', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + await coordinator.createSetupSession({ specTitle: 'Alpha' }); + const corruptedFile = join(cwd, '.brunch', 'sessions', 'corrupted.jsonl'); + await writeFile( + corruptedFile, + `${JSON.stringify({ type: 'session', id: 'corrupted-session', cwd })}\n{not json}\n`, + 'utf8', + ); + + const inventory = await coordinator.inspectWorkspace(); + const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 2 }); + + expect(inventory.unavailableSessions).toEqual([ + expect.objectContaining({ file: corruptedFile, reason: 'unreadable' }), + ]); + expect(oracle.ok).toBe(false); + if (!oracle.ok) { + expect(oracle.errors).toEqual([expect.stringContaining(`${corruptedFile} is unreadable`)]); + } + }); + it('activates explicit open and continue decisions as the current workspace', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-ws-')); const coordinator = createWorkspaceSessionCoordinator({ cwd }); diff --git a/src/session/workspace-session-coordinator.ts b/src/session/workspace-session-coordinator.ts index 144b3a4a2..7faf720ca 100644 --- a/src/session/workspace-session-coordinator.ts +++ b/src/session/workspace-session-coordinator.ts @@ -144,7 +144,11 @@ export interface WorkspaceLaunchSpec { sessions: WorkspaceLaunchSession[]; } -export type WorkspaceUnavailableSessionReason = 'missing_header' | 'missing_binding' | 'incompatible_binding'; +export type WorkspaceUnavailableSessionReason = + | 'missing_header' + | 'missing_binding' + | 'incompatible_binding' + | 'unreadable'; export interface WorkspaceUnavailableSession { file: string; @@ -604,7 +608,16 @@ async function inspectWorkspaceInventory(cwd: string): Promise | WorkspaceUnavailableSession; async function inspectSessionFile(file: string): Promise { - const entries = await readJsonl(file); + let entries: unknown[]; + try { + entries = await readJsonl(file); + } catch (error) { + if (isJsonParseError(error)) { + return { file, reason: 'unreadable', available: false }; + } + throw error; + } + const header = entries.find(isSessionHeader); if (!header) { return { file, reason: 'missing_header', available: false }; @@ -743,7 +756,17 @@ export async function verifyWorkspaceSessionStores( const sessions: WorkspaceStoreOracleSuccess['sessions'] = []; for (const file of files) { - const entries = await readJsonl(file); + let entries: unknown[]; + try { + entries = await readJsonl(file); + } catch (error) { + if (isJsonParseError(error)) { + errors.push(`${file} is unreadable`); + continue; + } + throw error; + } + const header = entries.find(isSessionHeader); const bindings = entries.filter(isSessionBindingEntry); if (!header) { @@ -794,6 +817,10 @@ async function readJsonl(file: string): Promise { .map((line) => JSON.parse(line) as unknown); } +function isJsonParseError(error: unknown): error is SyntaxError { + return error instanceof SyntaxError; +} + function isSessionHeader(value: unknown): value is SessionHeader { return ( typeof value === 'object' && From 6333203811c360294e9bc29a4204b13f98b88348 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:35:07 +0200 Subject: [PATCH 20/34] Lock agent-composition manifest model; reconcile SPEC/PLAN - D58-L: thin runtime header + gated prompt-resource manifest; methods (capability synonym retired); manifest {name,description,location} metadata code-owned in agents/state.ts, not filesystem-discovered. - D52-L/lexicon: agents/ topology = methods/; contexts/ is the D60-L render layer, not a manifest resource family. - SPEC: concrete agents/ topology (md/ts boundary); posture deferred. - PLAN: rewrite agents-composition-layer to the manifest model; oracle design reconciled (already filed as I38-L), not owed. --- memory/PLAN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index e75dd20b2..23a00a465 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -136,11 +136,11 @@ The POC should maximize assumption falsification rather than merely implement mi - `` resolves from `agents/methods/*.md`, gated by allow-list ∩ `op_mode` ∩ grade; methods are `read`-on-demand resources, not eager injection (a method may still be backed by a Pi-native skill). - snapshot RENDER scales by lens-plane + grade-depth; `snapshot-{cwd,graph,nodes}` tools wrap the renderer (markdown in `content`, typed JSON in `details`); PULL stays read-only in `graph/`/`session/`. - `src/.pi/context/` removed; composition lives in `src/agents/`; `npm run verify` green. -- **Verification:** Inner — `compose()` output assertions (manifest set/filtering/gating per axes; pinned-vs-AUTO router rules; code-owned `{name, description, location}` from `state.ts`); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip (I38-L). Middle — migration differential (old vs new composer equivalence for fixed states, throwaway cutover gate); AUTO-choice legality as a probe fitness metric (not a gate). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). **Oracle pass pending** — see SPEC §Verification Design once `ln-oracles` completes; resumable state captured in the execution pointer below. +- **Verification:** Inner — `compose()` output assertions (manifest set/filtering/gating per axes; pinned-vs-AUTO router rules; code-owned `{name, description, location}` from `state.ts`); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip. Filed as I38-L in SPEC §Verification Design. Middle — `compose()` legality across projected runtime states/grades; AUTO-choice sensibility and whether the agent reads the selected resource before applying it are tracked as **probe fitness, not gates** (SPEC §Verification Design structural/behavioral split). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). No old-vs-new composer equivalence differential: the cutover is a clean delete-and-replace (the thin manifest is intentionally not equivalent to the old eager concatenation; pre-release regenerate-don't-preserve posture). - **Cross-cutting obligations:** All of goals/strategies/lenses/methods are `read`-on-demand `.md` resources advertised through the per-turn manifest; manifest `{name, description, location}` metadata is code-owned in `agents/state.ts`, never filesystem-discovered (D39-L sealing). `contexts/` is the D60-L render layer only — no `` family. PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Workspace posture has no persisted home (deferred, SPEC §313). Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). - **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L, I38-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location; the "Layer-2 eager packs / Layer-3 Pi-skill" framing; `capability` as a synonym for `method`. - **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture (concrete `agents/` topology), Lexicon. -- **Current execution pointer:** Not started. The prompt-injection-automation question is **resolved**: composition always owns a deterministic envelope (`op_mode`/grade/allow-list/legal-tuple) and fills it three ways — *push* (header + pinned manifest entries), *delegate* (AUTO axis lists the legal set, agent self-selects), *pull* (agent `read`s the `.md` body, or `snapshot-*` tools) — which is the locked D58-L manifest model. **Oracle pass still owed** (`ln-oracles` unfinished): the O/R/C diagnostic splits the subject into composition machinery (HIGH observability/reproducibility/controllability → structural inner-loop) vs agent-behavior-under-composition (PARTIAL → behavioral middle/outer). Three open grill questions to resolve before filing SPEC §Verification Design: (1) **AUTO boundary** — inner loop proves only that the manifest lists exactly the legal set (the injection), with the agent's *choice quality* a middle-loop fitness metric, not a merge gate? (2) **migration differential** — build a golden-master old-vs-new composer equivalence for the `.pi/context → agents/` cutover, or skip given pre-release regenerate-don't-preserve posture? (3) **`agents/state.ts` model-based sweep** — is the legal-combination set small/enumerable enough for a generated tuple sweep? Likely first slice (after oracles): `agents/state.ts` + `compose.ts` skeleton over the landed vocabulary, then resource authoring, then snapshots, then migration. Depends on `agent-runtime-vocabulary` landing the enums first. +- **Current execution pointer:** Not started; **oracle design reconciled, not owed.** The premise change (eager packs → thin manifest, D58-L) already propagated into SPEC §Verification Design: I38-L is the manifest-legality inner gate, the structural/behavioral split (§Verification Stance) makes AUTO-choice and read-compliance fitness metrics rather than gates, and the "manifests before eager injection" design note is filed. Of the prior three grill questions, two are answered by that design (AUTO boundary = manifest-lists-the-legal-set is the inner gate, choice/read quality is fitness; tuple legality = model-based inner test) and the migration differential is resolved as **skip** (clean cutover, pre-release posture). The prompt-injection-automation question is resolved via the push/delegate/pull cut. Likely first slice (after `agent-runtime-vocabulary` lands the enums): `agents/state.ts` + `compose.ts` skeleton over the landed vocabulary, then resource authoring, then snapshots, then migration. ### sealed-pi-profile-runtime-state From 18c330081816731d01b2af4bf967ab304e33064f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:46:50 +0200 Subject: [PATCH 21/34] Align agents/ READMEs to locked composition model D58-L manifest model (thin header + read-on-demand resources, not eager packs), D40-L/D59-L state axes (op_mode + goal/strategy/lens, role derived), D60-L contexts-as-render, and the concrete agents/ topology. Fixes stale session.mode/agent vocab, modes/ dir, readiness-context, and the .pi/context migration table. --- src/agents/README.md | 157 ++++++++++++++++++-------------- src/agents/lenses/README.md | 4 +- src/agents/strategies/README.md | 10 +- 3 files changed, 97 insertions(+), 74 deletions(-) diff --git a/src/agents/README.md b/src/agents/README.md index dc9d64123..68cfb2e2b 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -1,97 +1,120 @@ # agents/ — Agent intelligence layer -SPEC decisions: D25-L, D40-L, D52-L +SPEC decisions: D25-L, D40-L, D52-L, D58-L, D59-L, D60-L ## Owns -Everything that shapes what the LLM sees and does: state definitions, -prompt composition, strategy/lens content, and context snapshot orchestration. +Everything that shapes what the LLM sees and does: the session-agent state +definitions and legal-combination table, per-turn prompt composition, the +Brunch-owned prompt resources (markdown the agent reads on demand), and the +snapshot render layer. -### Agent state hierarchy +## Session-agent state (D40-L, D59-L) + +Projected from linear `brunch.agent_runtime_state` entries at turn start +(last-writer-wins). One WHO field, three optional objective axes: ``` -spec.grade - grounding → elicitation I,II → commitment → export - -session.mode = elicitation | execution (future) | reconciliation (deferred) -session.agent = elicitor | planner (future) | reconciler (deferred) -session.strategy = per-agent interaction shape -session.lens = per-mode topical focus -session.sub-agents = research, explore, design, oracle, review, reconcile +op_mode = elicit | execute (future) ← the only stored WHO field + foreground role (elicitor) is DERIVED from op_mode, never stored +goal = grounding-advance | elicit-I | elicit-II | commitment-converge + | capture-posture [pinned | AUTO] grade-derived (D59-L) +strategy = step-wise-decision-tree | step-wise-disambiguate + | propose-graph | project-graph [pinned | AUTO] (D25-L) +lens = intent | design | oracle [pinned | AUTO] (future: plan/sync/scope) ``` -### Strategy × lens (D25-L) +Gates that condition composition but are not session-agent axes: + +``` +spec.readiness_grade grounding_onboarding → elicitation_ready + → commitments_ready → planning_ready (forward gate, D45-L) +workspace posture no persisted home in the POC (D57-L: off the spec row and graph); + surfaced when known, confirmed via the capture-posture goal +agent allow-list per-definition: which goals/strategies/lenses/methods are legal +``` -Strategies describe the interaction shape. Lenses describe topical focus. -The combination maps to the prior "lens catalogue" names: +The legal `(op_mode × goal × strategy × lens)` tuple table lives in `state.ts`. -| Strategy | Commitment path | Example lens combinations | -|--------------------------|--------------------|------------------------------------| -| `step-wise-decision-tree`| single-exchange | any lens | -| `step-wise-disambiguate` | single-exchange | any lens | -| `propose-graph` | direct commit | intent, design, oracle | -| `project-graph` | review-set | intent | +## Composition model (D58-L) — thin header + gated manifest, not eager packs -### Context building +`compose(agentId, sessionState, spec, workspace, snapshots)` is **projection, +not a state machine**. It runs before Pi provider requests and emits: -Snapshot functions live in `contexts/`. They orchestrate *which* snapshots -to inject based on mode/role/strategy/lens/grade, by calling into: +1. **agent control header** — identity, model/thinking, role derived from `op_mode`, tool authority. +2. **runtime-state header** — current pinned/AUTO `goal`/`strategy`/`lens`, `readiness_grade`, posture. +3. **resource manifests** — ``, ``, ``, + ``: each entry `{name, description, location}`, filtered by tuple/grade/`op_mode`/allow-list. +4. **compact pushed context** — minimal snapshot summary/handles (detail governed by D60-L). -``` -agents/contexts/ - │ - ├──▶ graph/ → snapshotGraph(detail), snapshotNode(id, hops) - │ - └──▶ session/ → workspace/spec envelope -``` +Detailed goal/strategy/lens/method bodies are **markdown the agent loads with +`read`** when detail matters — the same mechanism Pi uses for skills. The +composer never concatenates large semantic bodies on the agent's behalf. -Graph snapshots support multiple detail levels (I35-L): -- **Cursory** — compact full-graph overview for orientation -- **Neighborhood** — detailed node + N-hop expansion for focused work +- **AUTO** axis → the manifest lists exactly the legal set; a router rule tells the agent to + choose only from that manifest. **Pinned** axis → the manifest points at the pinned resource. +- Manifest `{name, description, location}` metadata is **code-owned in `state.ts`**, never + filesystem-discovered (honors the D39-L profile seal). ## Directory layout ``` agents/ ├── README.md -├── state.ts mode/role/strategy/lens type defs + valid combos -├── compose.ts prompt orchestrator: reads state, picks packs, calls snapshots -├── modes/ -│ └── elicit.md elicitation mode rules, tool authority -├── strategies/ -│ ├── step-wise-decision-tree.md -│ ├── step-wise-disambiguate.md -│ ├── propose-graph.md ← graph vocabulary, category rubric, batch format -│ └── project-graph.md -├── lenses/ -│ ├── intent.md -│ ├── design.md -│ └── oracle.md -└── contexts/ - ├── graph-context.ts calls graph/ snapshot fns, formats for prompt - └── readiness-context.ts +├── state.ts axis enums + legal (op_mode × goal × strategy × lens) tuple table; +│ also owns each resource's {name, description, location} manifest entry +├── compose.ts projection → runtime header + gated manifest +├── index.ts public entry / resource registry +├── definitions/ keyed agents; frontmatter = model/thinking + tool authority + allow-lists, +│ ├── elicitor.md body = system prompt +│ └── reviewer.md +├── goals/ grounding-advance, elicit-I, elicit-II, commitment-converge, capture-posture +├── strategies/ step-wise-decision-tree, step-wise-disambiguate, propose-graph, project-graph +├── lenses/ intent, design, oracle +├── methods/ run-structured-exchange, infer-and-capture, generate-proposal, +│ read-snapshot, commit-graph, review-for-gaps +└── contexts/ snapshot RENDER (D60-L) — TypeScript, NOT a manifest resource family + ├── cwd.ts + ├── graph.ts + └── node.ts ``` -## Does NOT own - -- Pi extension registration, tool definitions — those live in `.pi/extensions/`. -- Graph domain logic, CommandExecutor — those live in `graph/`. -- Session projection, transcript reading — those live in `session/`. +## Snapshots (D60-L) — pull / render / surface -## Imported by +- **PULL** — typed, read-only; owned by the data layer (`graph/snapshot.ts` for graph/node, + `session/` for cwd). The typed value *is* the JSON form. `agents/` never re-implements pulls. +- **RENDER** — `agents/contexts/*.ts` turn a typed snapshot into an LLM string, scaled by + lens-plane and grade-depth (I35-L). This is the only place LLM-string rendering lives. +- **SURFACE** — *pushed* (compose injects the compact summary) or *pulled* (`snapshot-{cwd,graph,nodes}` + Pi tools wrap the renderer: markdown in `toolResult.content`, typed JSON in `toolResult.details`). -- `.pi/extensions/prompting.ts` — calls compose.ts at turn boundaries -- `.pi/extensions/operational-mode.ts` — reads state definitions +`contexts/` is render-only and carries no `` manifest family. Reserve +"snapshot" for this agent-context family; `workspace.snapshot` is product/UI state (D60-L). -## Migration from .pi/context/ +## Does NOT own -The current `src/tui-client/.pi/context/` layout migrates here: +- Pi extension registration, tool definitions, `snapshot-*` tool wrappers — `.pi/extensions/`. +- Graph domain logic, CommandExecutor, snapshot PULL — `graph/`. +- Session projection, transcript reading, cwd PULL — `session/`. -| Current location | Target | -|-------------------------------------------|-------------------------------| -| `.pi/context/compose-brunch-prompt.ts` | `agents/compose.ts` | -| `.pi/context/prompt-packs/*.md` | `agents/modes/`, `strategies/`, `lenses/` | -| `.pi/context/builders/graph-context.ts` | `agents/contexts/graph-context.ts` | -| `.pi/context/builders/readiness-context.ts`| `agents/contexts/readiness-context.ts` | +## Imported by -Move incrementally as prompt composition is refactored. +- `.pi/extensions/` prompt registrar — calls `compose()` at turn boundaries. +- `.pi/extensions/operational-mode.ts` — reads the state enums from `state.ts`. + +## Migration from .pi/context/ (owned by frontier work, not yet done) + +`state.ts` enums land with **agent-runtime-vocabulary**; everything else (compose, +resources, contexts, the migration itself) lands with **agents-composition-layer**. + +| Current (.pi/context/) | Target | Kind | +|-------------------------------------------------|-------------------------------------|----------| +| `compose-brunch-prompt.ts` | `agents/compose.ts` | rewrite | +| `prompt-packs/{brunch-base,elicit,elicitor}.md` | `agents/definitions/elicitor.md` | fold | +| `prompt-packs/structured-exchange.md` | `agents/methods/run-structured-exchange.md` | fold | +| `prompt-packs/capture-analysis.md` | `agents/methods/infer-and-capture.md` | rehome | +| `prompt-packs/candidate-proposals.md` | `agents/methods/generate-proposal.md` | rehome | +| `builders/graph-context.ts` | `agents/contexts/graph.ts` | rewrite | +| `builders/readiness-context.ts` | (folded into compose runtime header)| retire | +| `builders/structured-exchange-context.ts` | `methods/run-structured-exchange.md`| retire/fold | +| — | `state.ts`, `index.ts`, `goals/*`, `methods/{read-snapshot,commit-graph,review-for-gaps}.md`, `definitions/reviewer.md`, `contexts/{cwd,node}.ts` | new | diff --git a/src/agents/lenses/README.md b/src/agents/lenses/README.md index a00b86ca2..9c0561813 100644 --- a/src/agents/lenses/README.md +++ b/src/agents/lenses/README.md @@ -1,4 +1,4 @@ -# lenses/ — Topical-focus prompt packs +# lenses/ — Topical-focus prompt resources SPEC decisions: D25-L, D56-L @@ -17,7 +17,7 @@ Future execute-mode lenses (`plan`, `sync`, `scope`) are deferred. ## Topology-driven question ranking (M5 input) -When `agent-graph-integration` lands prompt packs, each lens +When `agents-composition-layer` authors the lens resources, each lens should include topology-driven heuristics for what to ask next. These heuristics read graph shape, not templates: diff --git a/src/agents/strategies/README.md b/src/agents/strategies/README.md index 1859e8ca6..5c9aef900 100644 --- a/src/agents/strategies/README.md +++ b/src/agents/strategies/README.md @@ -1,4 +1,4 @@ -# strategies/ — Interaction-shape prompt packs +# strategies/ — Interaction-shape prompt resources SPEC decisions: D25-L, D26-L, D53-L @@ -15,10 +15,10 @@ the user experiences. | `propose-graph` | direct commit | concept → user accepts → commitGraph | | `project-graph` | review-set | derive from existing graph | -## Prompt pack contents +## Prompt resource contents -Each `.md` file in this directory is a prompt pack injected when -the strategy is active. It should contain: +Each `.md` file in this directory is a prompt resource the agent reads +(advertised via the D58-L `` manifest) when the strategy is active. It should contain: - What the agent is doing in this strategy - How to structure the turn @@ -28,7 +28,7 @@ the strategy is active. It should contain: ## Observer classification guide (M5 input) -When `agent-graph-integration` lands prompt packs, seed each +When `agents-composition-layer` authors the strategy resources, seed each strategy's prompt with the observer classification rules from the earlier `INTENT_GRAPH_SEMANTICS.md` translation table: From 82239bed68f6ed9741e2a739174062805a2266b2 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 14:56:00 +0200 Subject: [PATCH 22/34] Extract workspace boot session store classifier --- src/session/README.md | 4 +- src/session/workspace-session-coordinator.ts | 169 ++--------------- .../boot-session-store.ts | 177 ++++++++++++++++++ 3 files changed, 200 insertions(+), 150 deletions(-) create mode 100644 src/session/workspace-session-coordinator/boot-session-store.ts diff --git a/src/session/README.md b/src/session/README.md index 131670c47..609ba4a17 100644 --- a/src/session/README.md +++ b/src/session/README.md @@ -17,7 +17,9 @@ plus the coordination logic for workspace/spec/session lifecycle. `.brunch/workspace.json` management. The `WorkspaceSessionCoordinator` is the only module that creates/opens Pi sessions for Brunch user flows and writes collapsed `brunch.session_binding` entries (`{schemaVersion, - specId}`). + specId}`). Its private `workspace-session-coordinator/` subtree owns + coordinator-shaped boot/probe helpers such as canonical session-file + classification; external callers import only the public root module. - **Session binding** — session↔spec binding entries in JSONL. diff --git a/src/session/workspace-session-coordinator.ts b/src/session/workspace-session-coordinator.ts index 7faf720ca..c34d7c44f 100644 --- a/src/session/workspace-session-coordinator.ts +++ b/src/session/workspace-session-coordinator.ts @@ -1,7 +1,7 @@ -import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; -import { SessionManager, type SessionHeader } from '@earendil-works/pi-coding-agent'; +import { SessionManager } from '@earendil-works/pi-coding-agent'; import { openWorkspaceCommandExecutor, type SpecRecord } from '../graph/index.js'; import { discoverProjectIdentity } from './project-identity.js'; @@ -11,6 +11,10 @@ import { SESSION_BINDING_TYPE, type SessionBindingData, } from './session-binding.js'; +import { + inspectCanonicalSessionFiles, + verifyCanonicalSessionStore, +} from './workspace-session-coordinator/boot-session-store.js'; const BRUNCH_DIR = '.brunch'; const STATE_FILE = 'workspace.json'; @@ -379,15 +383,8 @@ async function createBoundSession( } async function countSessionsForSpec(cwd: string, specId: number): Promise { - const files = await listSessionFiles(cwd); - let count = 0; - for (const file of files) { - const session = await inspectSessionFile(file); - if (session.available && session.specId === specId) { - count++; - } - } - return count; + const sessions = await inspectCanonicalSessionFiles(cwd); + return sessions.filter((session) => session.available && session.specId === specId).length; } async function openCurrentSession( @@ -396,11 +393,10 @@ async function openCurrentSession( currentSessionId: string, ): Promise { await ensureWorkspaceDirs(cwd); - const files = await listSessionFiles(cwd); - for (const file of files) { - const inspected = await inspectSessionFile(file); - if (inspected.available && inspected.id === currentSessionId && inspected.specId === spec.id) { - const manager = SessionManager.open(file, sessionDir(cwd), cwd); + const sessions = await inspectCanonicalSessionFiles(cwd); + for (const session of sessions) { + if (session.available && session.id === currentSessionId && session.specId === spec.id) { + const manager = SessionManager.open(session.file, sessionDir(cwd), cwd); return bindSessionToSpec(manager, spec); } } @@ -556,7 +552,7 @@ function emptyWorkspacePosture(): WorkspacePostureState { async function inspectWorkspaceInventory(cwd: string): Promise { const state = await readOrCreateWorkspaceState(cwd); - const files = await listSessionFiles(cwd); + const sessions = await inspectCanonicalSessionFiles(cwd); const specsById = new Map(); const unavailableSessions: WorkspaceUnavailableSession[] = []; const currentSpec = await currentSpecFromState(cwd, state); @@ -568,12 +564,11 @@ async function inspectWorkspaceInventory(cwd: string): Promise | WorkspaceUnavailableSession; - -async function inspectSessionFile(file: string): Promise { - let entries: unknown[]; - try { - entries = await readJsonl(file); - } catch (error) { - if (isJsonParseError(error)) { - return { file, reason: 'unreadable', available: false }; - } - throw error; - } - - const header = entries.find(isSessionHeader); - if (!header) { - return { file, reason: 'missing_header', available: false }; - } - - const bindings = entries.filter(isSessionBindingEntry); - if (bindings.length === 0) { - return { file, reason: 'missing_binding', available: false }; - } - - const binding = bindings[0]!; - if (bindings.length !== 1) { - return { file, reason: 'incompatible_binding', available: false }; - } - - const sessionInfoEntries = entries.filter(isSessionInfoEntry); - const lastInfo = - sessionInfoEntries.length > 0 - ? (sessionInfoEntries[sessionInfoEntries.length - 1] as { name?: string }) - : undefined; - const name = lastInfo?.name; - - return { - id: header.id, - file, - specId: binding.data.specId, - ...(name != null ? { name } : {}), - available: true, - }; -} - function getOrCreateLaunchSpec( specsById: Map, spec: WorkspaceSpecState, @@ -742,94 +693,14 @@ export async function verifyWorkspaceSessionStores( options: WorkspaceStoreOracleOptions, ): Promise { const cwd = resolve(options.cwd); - const errors: string[] = []; const state = await readWorkspaceState(cwd); if (!state) { return { ok: false, errors: ['Missing or invalid .brunch/workspace.json'] }; } - const files = await listSessionFiles(cwd); - if (options.expectedSessionCount !== undefined && files.length !== options.expectedSessionCount) { - errors.push(`Expected ${options.expectedSessionCount} session file(s), found ${files.length}`); - } - - const sessions: WorkspaceStoreOracleSuccess['sessions'] = []; - - for (const file of files) { - let entries: unknown[]; - try { - entries = await readJsonl(file); - } catch (error) { - if (isJsonParseError(error)) { - errors.push(`${file} is unreadable`); - continue; - } - throw error; - } - - const header = entries.find(isSessionHeader); - const bindings = entries.filter(isSessionBindingEntry); - if (!header) { - errors.push(`${file} has no session header`); - continue; - } - if (bindings.length !== 1) { - errors.push(`${file} has ${bindings.length} ${SESSION_BINDING_TYPE} entries`); - continue; - } - const binding = bindings[0]!.data; - if (state.current && binding.specId !== state.current.specId) { - errors.push(`${file} binding spec ${binding.specId} does not match state ${state.current.specId}`); - } - sessions.push({ - file, - sessionId: header.id, - bindingCount: bindings.length, - binding, - }); - } - - return errors.length === 0 - ? { ok: true, specId: state.current?.specId ?? null, sessions } - : { ok: false, errors }; -} - -async function listSessionFiles(cwd: string): Promise { - try { - const entries = await readdir(sessionDir(cwd), { withFileTypes: true }); - return entries - .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) - .map((entry) => join(sessionDir(cwd), entry.name)) - .sort(); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return []; - } - throw error; - } -} - -async function readJsonl(file: string): Promise { - const content = await readFile(file, 'utf8'); - return content - .split('\n') - .filter((line) => line.trim().length > 0) - .map((line) => JSON.parse(line) as unknown); -} - -function isJsonParseError(error: unknown): error is SyntaxError { - return error instanceof SyntaxError; -} - -function isSessionHeader(value: unknown): value is SessionHeader { - return ( - typeof value === 'object' && - value !== null && - (value as { type?: unknown }).type === 'session' && - typeof (value as { id?: unknown }).id === 'string' - ); -} - -function isSessionInfoEntry(value: unknown): boolean { - return typeof value === 'object' && value !== null && (value as { type?: unknown }).type === 'session_info'; + return verifyCanonicalSessionStore({ + cwd, + expectedSessionCount: options.expectedSessionCount, + currentSpecId: state.current?.specId ?? null, + }); } diff --git a/src/session/workspace-session-coordinator/boot-session-store.ts b/src/session/workspace-session-coordinator/boot-session-store.ts new file mode 100644 index 000000000..1952597a0 --- /dev/null +++ b/src/session/workspace-session-coordinator/boot-session-store.ts @@ -0,0 +1,177 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import type { SessionHeader } from '@earendil-works/pi-coding-agent'; + +import { isSessionBindingEntry, SESSION_BINDING_TYPE, type SessionBindingData } from '../session-binding.js'; +import type { + WorkspaceLaunchSession, + WorkspaceStoreOracleResult, + WorkspaceUnavailableSession, +} from '../workspace-session-coordinator.js'; + +const BRUNCH_DIR = '.brunch'; +const SESSION_DIR = 'sessions'; + +interface BoundSessionFile extends Omit { + binding: SessionBindingData; + bindingCount: 1; +} + +type CanonicalSessionFile = BoundSessionFile | WorkspaceUnavailableSession; + +export async function inspectCanonicalSessionFiles(cwd: string): Promise { + const files = await listSessionFiles(cwd); + const sessions: CanonicalSessionFile[] = []; + for (const file of files) { + sessions.push(await inspectCanonicalSessionFile(file)); + } + return sessions; +} + +export async function verifyCanonicalSessionStore(options: { + cwd: string; + expectedSessionCount?: number | undefined; + currentSpecId: number | null; +}): Promise { + const classifiedSessions = await inspectCanonicalSessionFiles(options.cwd); + const errors: string[] = []; + + if ( + options.expectedSessionCount !== undefined && + classifiedSessions.length !== options.expectedSessionCount + ) { + errors.push( + `Expected ${options.expectedSessionCount} session file(s), found ${classifiedSessions.length}`, + ); + } + + const sessions: Array<{ + file: string; + sessionId: string; + bindingCount: number; + binding: SessionBindingData; + }> = []; + + for (const session of classifiedSessions) { + if (!session.available) { + errors.push(formatUnavailableSessionError(session)); + continue; + } + if (options.currentSpecId !== null && session.specId !== options.currentSpecId) { + errors.push( + `${session.file} binding spec ${session.specId} does not match state ${options.currentSpecId}`, + ); + } + sessions.push({ + file: session.file, + sessionId: session.id, + bindingCount: session.bindingCount, + binding: session.binding, + }); + } + + return errors.length === 0 ? { ok: true, specId: options.currentSpecId, sessions } : { ok: false, errors }; +} + +async function inspectCanonicalSessionFile(file: string): Promise { + let entries: unknown[]; + try { + entries = await readJsonl(file); + } catch (error) { + if (isJsonParseError(error)) { + return { file, reason: 'unreadable', available: false }; + } + throw error; + } + + const header = entries.find(isSessionHeader); + if (!header) { + return { file, reason: 'missing_header', available: false }; + } + + const bindings = entries.filter(isSessionBindingEntry); + if (bindings.length === 0) { + return { file, reason: 'missing_binding', available: false }; + } + + const binding = bindings[0]!; + if (bindings.length !== 1) { + return { file, reason: 'incompatible_binding', available: false }; + } + + const name = latestSessionName(entries); + + return { + id: header.id, + file, + specId: binding.data.specId, + binding: binding.data, + bindingCount: 1, + ...(name != null ? { name } : {}), + available: true, + }; +} + +async function listSessionFiles(cwd: string): Promise { + try { + const entries = await readdir(join(cwd, BRUNCH_DIR, SESSION_DIR), { withFileTypes: true }); + return entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) + .map((entry) => join(cwd, BRUNCH_DIR, SESSION_DIR, entry.name)) + .sort(); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; + } + throw error; + } +} + +async function readJsonl(file: string): Promise { + const content = await readFile(file, 'utf8'); + return content + .split('\n') + .filter((line) => line.trim().length > 0) + .map((line) => JSON.parse(line) as unknown); +} + +function latestSessionName(entries: unknown[]): string | undefined { + let name: string | undefined; + for (const entry of entries) { + if (isSessionInfoEntry(entry) && typeof entry.name === 'string') { + name = entry.name; + } + } + return name; +} + +function formatUnavailableSessionError(session: WorkspaceUnavailableSession): string { + switch (session.reason) { + case 'missing_header': + return `${session.file} has no session header`; + case 'missing_binding': + return `${session.file} has 0 ${SESSION_BINDING_TYPE} entries`; + case 'incompatible_binding': + return `${session.file} has incompatible ${SESSION_BINDING_TYPE} entries`; + case 'unreadable': + return `${session.file} is unreadable`; + } +} + +function isJsonParseError(error: unknown): error is SyntaxError { + return error instanceof SyntaxError; +} + +function isSessionHeader(value: unknown): value is SessionHeader { + return ( + typeof value === 'object' && + value !== null && + (value as { type?: unknown }).type === 'session' && + typeof (value as { id?: unknown }).id === 'string' + ); +} + +function isSessionInfoEntry(value: unknown): value is { type: 'session_info'; name?: unknown } { + return typeof value === 'object' && value !== null && (value as { type?: unknown }).type === 'session_info'; +} From b3dc4ef8b2cf6db527a91d04538c80fdb67bb31f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 15:03:47 +0200 Subject: [PATCH 23/34] Preserve session store oracle binding diagnostics --- .../workspace-session-coordinator.test.ts | 34 ++++++++++++++----- src/session/workspace-session-coordinator.ts | 2 +- .../boot-session-store.ts | 16 +++++---- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/session/workspace-session-coordinator.test.ts b/src/session/workspace-session-coordinator.test.ts index dd75b453a..0b0cef7b0 100644 --- a/src/session/workspace-session-coordinator.test.ts +++ b/src/session/workspace-session-coordinator.test.ts @@ -305,36 +305,54 @@ describe('WorkspaceSessionCoordinator', () => { const ready = await coordinator.createSetupSession({ specTitle: 'Alpha' }); const unboundFile = join(cwd, '.brunch', 'sessions', 'unbound.jsonl'); const mismatchedFile = join(cwd, '.brunch', 'sessions', 'mismatched.jsonl'); + const duplicateBindingFile = join(cwd, '.brunch', 'sessions', 'duplicate-binding.jsonl'); await writeFile( unboundFile, `${JSON.stringify({ type: 'session', id: 'unbound-session', cwd })}\n`, 'utf8', ); + const bindingEntry = JSON.stringify({ + type: 'custom', + customType: SESSION_BINDING_TYPE, + data: { + schemaVersion: 1, + specId: ready.spec.id, + }, + }); await writeFile( mismatchedFile, - `${JSON.stringify({ type: 'session', id: 'header-session', cwd })}\n${JSON.stringify({ - type: 'custom', - customType: SESSION_BINDING_TYPE, - data: { - schemaVersion: 1, - specId: ready.spec.id, - }, - })}\n`, + `${JSON.stringify({ type: 'session', id: 'header-session', cwd })}\n${bindingEntry}\n`, + 'utf8', + ); + await writeFile( + duplicateBindingFile, + `${JSON.stringify({ type: 'session', id: 'duplicate-binding-session', cwd })}\n${bindingEntry}\n${bindingEntry}\n`, 'utf8', ); const beforeUnbound = await readFile(unboundFile, 'utf8'); const beforeMismatched = await readFile(mismatchedFile, 'utf8'); + const beforeDuplicateBinding = await readFile(duplicateBindingFile, 'utf8'); const inventory = await coordinator.inspectWorkspace(); + const oracle = await verifyWorkspaceSessionStores({ cwd, expectedSessionCount: 4 }); expect(inventory.specs).toHaveLength(1); expect(inventory.specs[0]?.sessions).toHaveLength(2); expect(inventory.specs[0]?.sessions.map((session) => session.file)).toContain(mismatchedFile); expect(inventory.unavailableSessions).toEqual([ + expect.objectContaining({ file: duplicateBindingFile, reason: 'incompatible_binding' }), expect.objectContaining({ file: unboundFile, reason: 'missing_binding' }), ]); + expect(oracle.ok).toBe(false); + if (!oracle.ok) { + expect(oracle.errors).toEqual([ + expect.stringContaining(`${duplicateBindingFile} has 2 ${SESSION_BINDING_TYPE} entries`), + expect.stringContaining(`${unboundFile} has 0 ${SESSION_BINDING_TYPE} entries`), + ]); + } await expect(readFile(unboundFile, 'utf8')).resolves.toBe(beforeUnbound); await expect(readFile(mismatchedFile, 'utf8')).resolves.toBe(beforeMismatched); + await expect(readFile(duplicateBindingFile, 'utf8')).resolves.toBe(beforeDuplicateBinding); }); it('reports malformed session files without aborting inventory or store verification', async () => { diff --git a/src/session/workspace-session-coordinator.ts b/src/session/workspace-session-coordinator.ts index c34d7c44f..3845e5872 100644 --- a/src/session/workspace-session-coordinator.ts +++ b/src/session/workspace-session-coordinator.ts @@ -574,7 +574,7 @@ async function inspectWorkspaceInventory(cwd: string): Promise { bindingCount: 1; } -type CanonicalSessionFile = BoundSessionFile | WorkspaceUnavailableSession; +interface UnavailableSessionFile extends WorkspaceUnavailableSession { + bindingCount?: number; +} + +type CanonicalSessionFile = BoundSessionFile | UnavailableSessionFile; export async function inspectCanonicalSessionFiles(cwd: string): Promise { const files = await listSessionFiles(cwd); @@ -92,12 +96,12 @@ async function inspectCanonicalSessionFile(file: string): Promise Date: Tue, 2 Jun 2026 15:12:03 +0200 Subject: [PATCH 24/34] docs: reconcile workspace posture persistence --- memory/PLAN.md | 2 +- memory/SPEC.md | 2 +- src/agents/README.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 23a00a465..986037f66 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -137,7 +137,7 @@ The POC should maximize assumption falsification rather than merely implement mi - snapshot RENDER scales by lens-plane + grade-depth; `snapshot-{cwd,graph,nodes}` tools wrap the renderer (markdown in `content`, typed JSON in `details`); PULL stays read-only in `graph/`/`session/`. - `src/.pi/context/` removed; composition lives in `src/agents/`; `npm run verify` green. - **Verification:** Inner — `compose()` output assertions (manifest set/filtering/gating per axes; pinned-vs-AUTO router rules; code-owned `{name, description, location}` from `state.ts`); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip. Filed as I38-L in SPEC §Verification Design. Middle — `compose()` legality across projected runtime states/grades; AUTO-choice sensibility and whether the agent reads the selected resource before applying it are tracked as **probe fitness, not gates** (SPEC §Verification Design structural/behavioral split). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). No old-vs-new composer equivalence differential: the cutover is a clean delete-and-replace (the thin manifest is intentionally not equivalent to the old eager concatenation; pre-release regenerate-don't-preserve posture). -- **Cross-cutting obligations:** All of goals/strategies/lenses/methods are `read`-on-demand `.md` resources advertised through the per-turn manifest; manifest `{name, description, location}` metadata is code-owned in `agents/state.ts`, never filesystem-discovered (D39-L sealing). `contexts/` is the D60-L render layer only — no `` family. PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Workspace posture has no persisted home (deferred, SPEC §313). Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). +- **Cross-cutting obligations:** All of goals/strategies/lenses/methods are `read`-on-demand `.md` resources advertised through the per-turn manifest; manifest `{name, description, location}` metadata is code-owned in `agents/state.ts`, never filesystem-discovered (D39-L sealing). `contexts/` is the D60-L render layer only — no `` family. PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Workspace posture is workspace-scoped state persisted in `.brunch/workspace.json` and injected into the runtime header when known; it is not spec/session state or graph truth. Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). - **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L, I38-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location; the "Layer-2 eager packs / Layer-3 Pi-skill" framing; `capability` as a synonym for `method`. - **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture (concrete `agents/` topology), Lexicon. - **Current execution pointer:** Not started; **oracle design reconciled, not owed.** The premise change (eager packs → thin manifest, D58-L) already propagated into SPEC §Verification Design: I38-L is the manifest-legality inner gate, the structural/behavioral split (§Verification Stance) makes AUTO-choice and read-compliance fitness metrics rather than gates, and the "manifests before eager injection" design note is filed. Of the prior three grill questions, two are answered by that design (AUTO boundary = manifest-lists-the-legal-set is the inner gate, choice/read quality is fitness; tuple legality = model-based inner test) and the migration differential is resolved as **skip** (clean cutover, pre-release posture). The prompt-injection-automation question is resolved via the push/delegate/pull cut. Likely first slice (after `agent-runtime-vocabulary` lands the enums): `agents/state.ts` + `compose.ts` skeleton over the landed vocabulary, then resource authoring, then snapshots, then migration. diff --git a/memory/SPEC.md b/memory/SPEC.md index 42495ee14..825d5192f 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -331,7 +331,7 @@ src/agents/ - Manifest metadata is code-owned, not filesystem-discovered: `agents/state.ts` binds each legal axis value to its `{name, description, location}`, and `compose()` emits that binding; the agent `read`s the `.md` body at the listed `location` only when detail matters. This keeps the legal set and its labels in one tested place and honors D39-L sealing (no runtime resource discovery). Frontmatter-sourced manifest metadata is a deferred ergonomics option, not the POC mechanism. - `agents/contexts/` is the D60-L snapshot render layer (TypeScript), surfaced as the header's compact pushed context or via the `snapshot-*` tools; it is not part of the `read`-on-demand resource manifest and carries no `` family. -- Workspace **posture** has no persisted home in the POC: D57-L keeps it off the spec row and the graph. The runtime header surfaces posture when known and the always-on `capture-posture` goal (D59-L) confirms it conversationally; a durable store (a `.brunch/` workspace setting versus an optional `agent_runtime_state` field) is deferred until cross-session posture stability is needed. [open decision] +- Workspace **posture** is workspace-scoped product state persisted in `.brunch/workspace.json`, not spec state, session state, or graph truth. D57-L keeps it off the spec row and graph; D58-L composition injects known posture values into the runtime header as an axis of agent influence, and the `capture-posture` goal (D59-L) can confirm or refine those values conversationally. - Readiness is an internal forward gate, not a user-facing workflow stepper or session-local phase. `readiness_grade` lives on the spec row per D45-L; validators may warn when graph/transcript evidence and assigned grade diverge. Before readiness drives hard tool/agent authority beyond the POC, Brunch needs explicit rubrics for what evidence advances, blocks, or regresses grade. - Prompt resources and Pi skills are both progressive-disclosure mechanisms, but they are not authority. Brunch code owns runtime-state projection, legal tuple filtering, grade/allow-list gating, tool activation, and tool-call blocking. Pi-native skills may be used for startup-scoped capabilities; runtime-state-specific objective/method availability is advertised through Brunch's per-turn manifest so ambient user/project resources cannot leak into product behavior. diff --git a/src/agents/README.md b/src/agents/README.md index 68cfb2e2b..07dd6d8fc 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -29,8 +29,8 @@ Gates that condition composition but are not session-agent axes: ``` spec.readiness_grade grounding_onboarding → elicitation_ready → commitments_ready → planning_ready (forward gate, D45-L) -workspace posture no persisted home in the POC (D57-L: off the spec row and graph); - surfaced when known, confirmed via the capture-posture goal +workspace posture persisted in .brunch/workspace.json as workspace-scoped state; + surfaced in the runtime header and refined via capture-posture agent allow-list per-definition: which goals/strategies/lenses/methods are legal ``` From 2843c95335978b5a96c7737f68d8ba29f596662d Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 15:24:40 +0200 Subject: [PATCH 25/34] FE-789: Fix session-agent runtime vocabulary --- memory/PLAN.md | 19 ++-- memory/SPEC.md | 4 +- src/.pi/__tests__/chrome.test.ts | 8 +- src/.pi/__tests__/operational-mode.test.ts | 103 ++++++++++++++------- src/.pi/__tests__/prompting.test.ts | 24 +++-- src/.pi/context/compose-brunch-prompt.ts | 7 +- src/.pi/extensions/operational-mode.ts | 84 +++++++++++------ src/.pi/extensions/prompting.ts | 1 + src/.pi/pi-extension-shell.ts | 5 + src/rpc/handlers.test.ts | 6 +- src/rpc/handlers.ts | 10 +- src/session/elicitation-exchange.test.ts | 4 +- 12 files changed, 175 insertions(+), 100 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 986037f66..c183d4513 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -51,8 +51,7 @@ The POC should maximize assumption falsification rather than merely implement mi ### Active -1. `agent-runtime-vocabulary` — fix the session-agent record to the reconciled vocabulary (un-collapse lens, add `propose-graph`/`project-graph`/`goal`, derive role from `op_mode`). Foundational; gates the A14-L proof and the composition layer. -2. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, reviewer advisory writes; all writes via the command layer. Its A14-L real-LLM proof (the `propose-graph → commitGraph` path) waits on `agent-runtime-vocabulary`. +1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, reviewer advisory writes; all writes via the command layer. The `propose-graph → commitGraph` runtime vocabulary gate has landed; next M5 work can run the A14-L real-LLM proof, then scope capture/reviewer slices. ### Next @@ -102,23 +101,23 @@ The POC should maximize assumption falsification rather than merely implement mi ### agent-runtime-vocabulary - **Name:** Session-agent runtime vocabulary fix -- **Linear:** unassigned (create in FE / brunch when branch opens) -- **Branch:** to create — `ln/-agent-runtime-vocabulary` +- **Linear:** [FE-789](https://linear.app/hash/issue/FE-789/session-agent-runtime-vocabulary-fix) +- **Branch:** `ln/fe-789-session-agent-runtime-vocabulary-fix` - **Kind:** structural (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) -- **Status:** active +- **Status:** done - **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the `goal` axis (`grounding-advance | elicit-I | elicit-II | commitment-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. - **Why now / unlocks:** The code is the last place still carrying the pre-reconciliation model (collapsed lens, missing `propose-graph`/`project-graph`, role-as-state). `propose-graph` must exist before `agent-graph-integration` can run the A14-L real-LLM proof (the `propose-graph → commitGraph` path is the named proof target), and the orthogonal axes are the precondition for the `agents/` 3-layer composition. Foundational and small. - **Acceptance:** - `AgentLensId` is independent of `AgentStrategyId`; lens ∈ `intent | design | oracle`. - strategy ∈ `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; old names gone everywhere. - - `goal` axis present with grade-derived default and `auto` accepted; `op_mode` derives the foreground role; no stored `agentRole`. + - concrete `AgentStrategyId` / `AgentLensId` / `AgentGoalId` values stay separate from persisted `auto` selections; `goal` has a grade-derived default, and all three objective axes accept `auto`; `op_mode` derives the foreground role; no stored `agentRole`. - append/project/switch helpers round-trip the new record; malformed/illegal tuples rejected; I25-L tests updated and green. - all fixtures/tests regenerated to the new vocabulary; `npm run verify` green. - **Verification:** Inner — runtime-state append/project/switch unit tests over the new axes (I25-L); legal-tuple acceptance / illegal-tuple rejection. Middle — projection round-trip from real JSONL reload. No new outer-loop here; the A14-L behavioral proof rides on `agent-graph-integration`. - **Cross-cutting obligations:** Keep state linear-transcript-backed (D40-L); foreground role derived, not stored. Do **not** author prompt packs or build `compose()` here — that is `agents-composition-layer`. If `agents/state.ts` is introduced for the shared enums, keep it to type/enum + legal-combination homes only. - **Traceability:** D25-L (revised), D40-L (revised), D59-L, I25-L (revised) / A14-L (sharpens the proof). Retires: collapsed lens, stale strategy vocabulary, role-as-session-state. - **Design docs:** `memory/SPEC.md` §Active Decisions (D25-L, D40-L, D58-L, D59-L), Lexicon. -- **Current execution pointer:** Not started. Likely cards: (1) enums + `BrunchAgentState` shape + `DEFAULT` + projection/switch helpers; (2) fixture/test regeneration + I25-L update. +- **Current execution pointer:** Done. Reconciled `brunch.agent_runtime_state` now carries `operationalMode`, `agentGoal`, `agentStrategy`, and `agentLens` without stored `agentRole`; role is derived from `op_mode`, concrete axis id unions stay separate from the `auto` selection sentinel, deterministic elicitation lens metadata uses `intent`, and stale strategy/lens names were removed from source/tests. Verified with targeted runtime/prompting/chrome/RPC/exchange tests and `npm run verify`. ### agents-composition-layer @@ -352,10 +351,10 @@ The POC should maximize assumption falsification rather than merely implement mi ## Recently Completed +- 2026-06-02 `agent-runtime-vocabulary` (FE-789) — Done: session-agent runtime state uses the reconciled D25-L/D40-L/D59-L axes (`goal`, `strategy`, `lens`) with foreground role derived from `op_mode`; concrete axis id unions stay separate from the `auto` selection sentinel, and all three axes accept `auto`; `propose-graph`/`project-graph` are legal strategies; deterministic elicitation lens metadata now uses `intent`; stale collapsed strategy/lens names and stored `agentRole` are gone from runtime-state entries. Verified: targeted runtime/prompting/chrome/RPC/exchange tests and `npm run verify`. - 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. -- 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with target validation + LSN invariants; oracle-plane stub acceptance met by existing node kinds. Verified: `npm run verify` after each slice. `agent-graph-integration` (M5) is now unblocked. +- 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with LSN invariants. Verified: `npm run verify`. - 2026-06-01 `sealed-pi-profile-runtime-state` (FE-776) — Done: prep envelope tied off. Both strands complete: **(a)** Pi harness sealing including sealed profile, runtime-state transcript projection, session display names via Pi `session_info`; **(b)** graph-model lock-and-materialize with Phase 1 (edges) + Phase 2 (nodes) locked in `docs/design/GRAPH_MODEL.md`, code stubs under `src/graph/`, and A20-L persistence spike validating `drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`. `graph-data-plane` (M4 CRUD) is now unblocked. Verified: `npm run verify` after each slice. -- 2026-06-01 `pi-ui-extension-patterns` (FE-744) — Done. All Pi extension seam evidence for M5/M6/M7 landed. Detailed frontier definition archived to [docs/archive/PLAN_HISTORY.md §2026-06-01 Sync archive](file:///Users/lunelson/Code/hashintel/brunch-next/docs/archive/PLAN_HISTORY.md). Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` @@ -366,7 +365,7 @@ nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) spec-persistence-and-startup [done] (persistence-model fix + startup regression repair) - agent-runtime-vocabulary [active] (session-agent record vocabulary fix) + agent-runtime-vocabulary [done] (session-agent record vocabulary fix) agents-composition-layer [next] (agents/ 3-layer composition + snapshots + .pi/context migration) agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] diff --git a/memory/SPEC.md b/memory/SPEC.md index 825d5192f..e2ab0a462 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -238,7 +238,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D44-L — Subagents are main-agent-invoked, blocking Pi tool calls that gather data and propose variants for candidate-proposal generation.** Brunch may register a single `subagent` Pi tool whose parameters are `{ agent, task }` or `{ tasks: [] }` (parallel). Each invocation runs as an isolated `pi --mode json -p --no-session --no-skills --no-extensions` subprocess inheriting Brunch's sealed Pi Profile (D39-L); the subagent has no inherited conversation context so the task string must carry everything it needs. Agent definitions are declarative markdown files under `src/.pi/extensions/subagents/agents/*.md` with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. Concurrency cap lives in an externalized [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json) (default 4) so it can be reviewed and updated without SPEC churn. The subagent's result text is returned directly to the main agent as tool result content; subagents do not append custom messages to the session log on their own behalf, do not invoke the `CommandExecutor`, and do not gain access to the parent's Brunch RPC handlers. POC starter agents split into two families: - **Data gatherers** — read-only context fetchers whose output grounds proposals: **scout** (codebase recon: `read`, `grep`, `find`, `ls`), **researcher** (web research: `web_search`, `web_fetch`), and **graph-reader** (read-only Brunch graph projection tools). - **Variant proposer** — **proposer** (no tools): given a grounding bundle plus a batch-proposal lens frame, emits exactly one well-formed variant of a candidate proposal. The main agent achieves diversity by issuing parallel `tasks: []` invocations of `proposer` with intentionally distinct framings — the subagent realization of the "design it twice" pattern from `ln-design` and the parallel fan-out anticipated by `ln-oracles`. Each `proposer` invocation runs in its own isolated context so variants don't cross-contaminate; the main agent collects N outputs and composes the comparison via the D31-L meta-rubric (and/or project-specific axes) before writing a `brunch.review_set_proposal` entry through the elicitor flow. `proposer` is system-prompt-only by design: its grounding inputs come entirely through the task string the main agent assembles from preceding `scout` / `researcher` / `graph-reader` calls. - This division mirrors the batch-proposal flow in D26-L: `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, and `propose-oracle-ensembles` are the natural lenses that delegate to fan-out `proposer` invocations; `project-requirements-from-upstream` may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. + This division mirrors the batch-proposal flow in D26-L: `propose-graph` and `project-graph` strategies can delegate variant generation to fan-out `proposer` invocations while `intent` / `design` / `oracle` lenses frame the proposal subject; purely extractive single-exchange work may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. - **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and `` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; the `{name, description, location}` triples are code-owned in `agents/state.ts`, not filesystem-discovered, honoring D39-L sealing; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods/`, `contexts/`); `agents/contexts/` is the D60-L snapshot render layer (code), not a manifest resource family; composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; direct Layer-2 eager prompt-pack injection as the default mechanism; and `capability` as a parallel name for `method` / ``. @@ -273,7 +273,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | | I23-L | Every structured elicitation interaction that owns the response surface persists durable semantic display only through Pi `toolResult` rows rendered by `renderResult`; `renderCall` and live `ctx.ui.*` surfaces are transient. A structured-exchange tuple has a recoverable `present_*` result and, when required, exactly one matching terminal `request_*` result before the next agent turn consumes it. The target details model is checked by `schema` + `v`, `exchange_id`, and `tool_meta`; request outcomes are an exactly-one property-presence union; user-authored text is `comment` and runtime-authored text is `message`; present-side status/kind/expected-request aliases and capture graph payloads are invalid in the Zod-authored schema layer. `toolResult.content` is rich markdown suitable for both TUI transcript display and model context; `toolResult.details` carries structured projection/recovery data. | covered for current FE-744 structured-exchange tools (registered sequential `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices`; tests cover non-semantic `renderCall`, markdown `renderResult`, present/request details, unmatched-present recovery, active-vs-stub registry, JSON-editor fallback for multi-choice, terminal `answered`/`cancelled`/`unavailable` projection closure, option content/rationale parity, and same-assistant-message `present_options → request_choice` ordering over a real Pi RPC run. The Zod-authored schema layer is covered by JSON Schema export and drift-rejection tests for present/request/capture details; runtime tools still need a deliberate migration to those exports. `present_review_set`, `present_candidates`, and `request_review` remain named stubs and intentionally unregistered.) | D12-L, D13-L, D17-L, D37-L, D38-L, D41-L | | I24-L | A Brunch-launched Pi runtime does not load ambient user/project Pi context files, extensions, skills, prompt templates, themes, or behavior-shaping settings unless the Brunch Pi Profile explicitly allows them; Brunch-owned extension-discovered resources are identified as intentional product resources. | covered for TUI-launch profile boundary by contract tests: ambient resource flags and explicit extension factories are preserved; hostile ambient global/project settings are ignored by the in-memory Brunch settings policy before and after reload; audited Pi settings getters are tracked in `src/brunch-pi-profile.ts`. Subagent subprocess inheritance remains future coverage under I29-L. | D2-L, D39-L | -| I25-L | The active `op_mode`, `strategy`, `lens`, and `goal` are reconstructable from linear `brunch.agent_runtime_state` entries at turn start; the foreground session-agent role is derived from `op_mode`, not separately stored; tool gating follows the reconstructed `op_mode` so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted. | covered for the current runtime-state machine (append/project/switch helpers write and project `brunch.agent_runtime_state` snapshots from Pi custom entries, init idempotent, switches carry previous state, malformed entries rejected, latest valid state drives prompt/tool policy without extension-local memory); the axis vocabulary fix (un-collapse lens from strategy, add `propose-graph`/`project-graph`/`goal`, derive role from `op_mode`) is pending the agent-runtime frontier. | D17-L, D23-L, D40-L, D58-L, D59-L | +| I25-L | The active `op_mode`, `strategy`, `lens`, and `goal` are reconstructable from linear `brunch.agent_runtime_state` entries at turn start; concrete axis ids stay separate from the `auto` selection sentinel; the foreground session-agent role is derived from `op_mode`, not separately stored; tool gating follows the reconstructed `op_mode` so `elicit` cannot use execute/dangerous tools such as raw `bash`/`write` unless explicitly permitted. | covered (`src/.pi/__tests__/operational-mode.test.ts` covers append/project/switch helpers over the reconciled axis vocabulary, AUTO selection for every objective axis, init idempotence, previous-state snapshots, malformed/illegal tuple rejection, role derivation from `op_mode`, and Pi JSONL reload projection; `prompting.test.ts` covers prompt/tool-policy projection from the same transcript-backed runtime state). | D17-L, D23-L, D40-L, D58-L, D59-L | | I27-L | Session-name generation is best-effort presentation metadata only: lifecycle hooks may append Pi `session_info` entries, but naming failures never block shutdown/session replacement and generated names never mutate spec identity, session binding, or graph truth. | planned (session-lifecycle naming tests with empty transcript/auth failure/success paths; picker projection tests read session names when present) | D6-L, D21-L, D35-L, D42-L | | I26-L | Runtime schema-library imports stay deliberately scoped: Zod may appear only in D41-L-acknowledged product/protocol schema seams such as `src/.pi/extensions/structured-exchange/schemas/`; TypeBox remains valid for Pi tool parameters, small config/frontmatter contracts, and future Drizzle-derived row schemas; no boundary may hand-author parallel Zod and TypeBox sources for the same shape. Drizzle row/insert/update schemas are not hand-authored alongside their target tables. | covered (structured-exchange schema tests prove Zod parse/export; grep-based architectural boundary test in `architecture.test.ts` enforces no direct `db/` imports outside `graph/`; Drizzle derivation via `drizzle-typebox` in `row-schemas.ts`) | D41-L | | I28-L | Auto-compaction output preserves the configured anchor set byte-stable: every entry kind listed in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) is reconstructable post-compaction according to its `select` rule (`first | latest | active-leaves | all-unresolved`); LLM-generated narrative summary never replaces or rephrases preserved-anchor content; extension failure falls through to Pi default compaction rather than dropping anchors silently. | planned (compaction round-trip property tests at M9 plus inner-loop anchor-rendering unit tests and TypeBox schema validation of the anchor config) | D43-L; R15, R13; I3-L, I4-L, I8-L, I12-L | diff --git a/src/.pi/__tests__/chrome.test.ts b/src/.pi/__tests__/chrome.test.ts index 666cb876f..4c353177e 100644 --- a/src/.pi/__tests__/chrome.test.ts +++ b/src/.pi/__tests__/chrome.test.ts @@ -37,14 +37,14 @@ describe('Brunch chrome projection', () => { role: 'elicitor', model: 'claude-sonnet', thinking: 'medium', - lens: 'step-by-step', + lens: 'intent', }, }; expect(formatBrunchChromeHeaderLines(state)).toEqual([ '█▄▄ █▀█ █ █ █▄ █ █▀▀ █ █', '█▄█ █▀▄ █▄█ █ ▀█ █▄▄ █▀█', - 'runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step', + 'runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens intent', 'spec: Spec One · session: Interview #1 · phase: elicitation', ]); }); @@ -94,7 +94,7 @@ describe('Brunch chrome projection', () => { role: 'elicitor', model: 'claude-sonnet', thinking: 'medium', - lens: 'step-by-step', + lens: 'intent', }, build: { version: 'v0.0.0', dev: 'dev abc123' }, contextUsage: { usedTokens: 1024, maxTokens: 2048 }, @@ -103,7 +103,7 @@ describe('Brunch chrome projection', () => { }; expect(projectBrunchChromeFooterLines(state)).toEqual([ - 'brunch · runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens step-by-step · build: v0.0.0 dev abc123', + 'brunch · runtime: elicit-default · role elicitor · claude-sonnet · thinking medium · lens intent · build: v0.0.0 dev abc123', 'context: [█████░░░░░] 1,024/2,048 tokens (50%)', 'state: responding-to-elicitation · coherence: needs_review · worker: observer-review/queued', 'spec: Spec One · session: Interview #1', diff --git a/src/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts index d610167d8..6dbfd522e 100644 --- a/src/.pi/__tests__/operational-mode.test.ts +++ b/src/.pi/__tests__/operational-mode.test.ts @@ -51,6 +51,7 @@ describe('Brunch agent runtime-state projection', () => { it('projects the deterministic elicit/elicitor default when no runtime entries exist', () => { expect(projectBrunchAgentState([])).toMatchObject({ ...DEFAULT_BRUNCH_AGENT_STATE, + agentRole: 'elicitor', operationalModeDefinition: { id: 'elicit', defaultRole: 'elicitor', @@ -61,18 +62,34 @@ describe('Brunch agent runtime-state projection', () => { operationalMode: 'elicit', defaultStrategy: DEFAULT_BRUNCH_AGENT_STATE.agentStrategy, defaultLens: DEFAULT_BRUNCH_AGENT_STATE.agentLens, + defaultGoal: DEFAULT_BRUNCH_AGENT_STATE.agentGoal, }, }); }); + it('accepts AUTO as a selection sentinel for every objective axis', () => { + const autoState: BrunchAgentState = { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'auto', + agentLens: 'auto', + agentGoal: 'auto', + }; + + expect(projectBrunchAgentState([runtimeEntry(autoState)])).toMatchObject({ + ...autoState, + agentRole: 'elicitor', + }); + }); + it('uses the last valid runtime-state snapshot without mutating earlier transcript entries', () => { const first = runtimeEntry(DEFAULT_BRUNCH_AGENT_STATE); const latestState: BrunchAgentState = { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'propose-graph', + agentLens: 'design', + agentGoal: 'auto', }; const latest = runtimeEntry(latestState); @@ -85,9 +102,9 @@ describe('Brunch agent runtime-state projection', () => { const invalidCombination = runtimeEntry({ schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', agentStrategy: 'not-a-strategy', - agentLens: 'step-by-step', + agentLens: 'intent', + agentGoal: 'auto', } as unknown as BrunchAgentState); const malformed = { type: 'custom', @@ -104,9 +121,9 @@ describe('Brunch agent runtime-state projection', () => { const latestState: BrunchAgentState = { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'project-graph', + agentLens: 'oracle', + agentGoal: 'elicit-II', }; const events: Record unknown> = {}; const activeTools: string[][] = []; @@ -185,9 +202,9 @@ describe('Brunch agent runtime-state projection', () => { const latestState: BrunchAgentState = { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'step-wise-disambiguate', + agentLens: 'design', + agentGoal: 'capture-posture', }; expect(appendBrunchAgentRuntimeSwitch(manager, latestState, 'user')).toBe('entry-2'); @@ -209,30 +226,38 @@ describe('Brunch agent runtime-state projection', () => { { schemaVersion: 1, operationalMode: 'execute', - agentRole: 'elicitor', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'reviewer', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentRole: 'elicitor', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', agentStrategy: 'not-a-strategy', - agentLens: 'step-by-step', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'step-by-step', + agentStrategy: 'step-wise-decision-tree', agentLens: 'not-a-lens', + agentGoal: 'auto', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'not-a-goal', }, ]) { expect(() => @@ -242,35 +267,43 @@ describe('Brunch agent runtime-state projection', () => { expect(manager.entries).toEqual([]); }); - it('does not project invalid runtime mode, role, strategy, or lens entries', () => { + it('does not project invalid runtime mode, legacy role, strategy, lens, or goal entries', () => { for (const invalidState of [ { schemaVersion: 1, operationalMode: 'execute', - agentRole: 'elicitor', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'reviewer', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentRole: 'elicitor', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', agentStrategy: 'not-a-strategy', - agentLens: 'step-by-step', + agentLens: 'intent', + agentGoal: 'auto', }, { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'step-by-step', + agentStrategy: 'step-wise-decision-tree', agentLens: 'not-a-lens', + agentGoal: 'auto', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'not-a-goal', }, ]) { expect( @@ -309,9 +342,9 @@ describe('Brunch agent runtime-state projection', () => { const latestState: BrunchAgentState = { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'grounding-advance', }; manager.appendCustomEntry(BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE, { diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index 610bda7ba..31fec4609 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -51,8 +51,9 @@ describe('Brunch prompt-pack topology', () => { const result = composeBrunchPrompt({ operationalMode: 'elicit', agentRole: 'elicitor', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'auto', activeTools: ['read', 'grep', 'present_options'], }); @@ -67,6 +68,9 @@ describe('Brunch prompt-pack topology', () => { expect(result.prompt).toContain('[Brunch agent state]'); expect(result.prompt).toContain('Operational mode: elicit.'); expect(result.prompt).toContain('Agent role: elicitor.'); + expect(result.prompt).toContain('Agent goal: auto.'); + expect(result.prompt).toContain('Agent strategy: step-wise-decision-tree.'); + expect(result.prompt).toContain('Agent lens: intent.'); expect(result.prompt).toContain('Brunch exposes only elicit-safe tools: read, grep, present_options.'); expect(result.prompt.indexOf('# Brunch base')).toBeLessThan( result.prompt.indexOf('# Operational mode: elicit'), @@ -85,8 +89,9 @@ describe('Brunch prompt-pack topology', () => { it('appends composed Brunch prompting from runtime-state projection', async () => { const latestState: BrunchAgentState = { ...DEFAULT_BRUNCH_AGENT_STATE, - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'step-wise-disambiguate', + agentLens: 'design', + agentGoal: 'elicit-I', }; const events: Record unknown> = {}; @@ -115,7 +120,7 @@ describe('Brunch prompt-pack topology', () => { systemPrompt: expect.stringContaining('base\n\n[Brunch agent state]'), }); expect(result).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: disambiguate-via-examples.'), + systemPrompt: expect.stringContaining('Agent strategy: step-wise-disambiguate.'), }); expect(result).toMatchObject({ systemPrompt: expect.stringContaining( @@ -159,8 +164,9 @@ describe('Brunch prompt-pack topology', () => { ); const latestState: BrunchAgentState = { ...DEFAULT_BRUNCH_AGENT_STATE, - agentStrategy: 'disambiguate-via-examples', - agentLens: 'disambiguate-via-examples', + agentStrategy: 'propose-graph', + agentLens: 'oracle', + agentGoal: 'commitment-converge', }; appendBrunchAgentRuntimeSwitch(manager, latestState, 'user'); const switchedPromptResults = await Promise.all( @@ -185,10 +191,10 @@ describe('Brunch prompt-pack topology', () => { ['read', 'grep', 'present_options'], ]); expect(defaultPrompt).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: step-by-step.'), + systemPrompt: expect.stringContaining('Agent strategy: auto.'), }); expect(switchedPrompt).toMatchObject({ - systemPrompt: expect.stringContaining('Agent strategy: disambiguate-via-examples.'), + systemPrompt: expect.stringContaining('Agent strategy: propose-graph.'), }); }); diff --git a/src/.pi/context/compose-brunch-prompt.ts b/src/.pi/context/compose-brunch-prompt.ts index fdc8889d9..eb22dc46c 100644 --- a/src/.pi/context/compose-brunch-prompt.ts +++ b/src/.pi/context/compose-brunch-prompt.ts @@ -8,7 +8,8 @@ export interface BrunchPromptCompositionState { operationalMode: string; agentRole: string; agentStrategy: string; - agentLens: string | null; + agentLens: string; + agentGoal: string; activeTools: readonly string[]; } @@ -55,14 +56,14 @@ const PROMPT_PACKS = PROMPT_PACK_ORDER.map(readPromptPack); function renderAgentState(state: BrunchPromptCompositionState): string { const tools = state.activeTools.join(', ') || 'none'; - const lens = state.agentLens ?? 'none'; return [ '[Brunch agent state]', `- Operational mode: ${state.operationalMode}.`, `- Agent role: ${state.agentRole}.`, + `- Agent goal: ${state.agentGoal}.`, `- Agent strategy: ${state.agentStrategy}.`, - `- Agent lens: ${lens}.`, + `- Agent lens: ${state.agentLens}.`, `- Prompt packs: ${PROMPT_PACK_ORDER.join(', ')}.`, '', '[Brunch tool policy]', diff --git a/src/.pi/extensions/operational-mode.ts b/src/.pi/extensions/operational-mode.ts index e42b993eb..7f6c842b7 100644 --- a/src/.pi/extensions/operational-mode.ts +++ b/src/.pi/extensions/operational-mode.ts @@ -26,8 +26,22 @@ export const BRUNCH_AGENT_RUNTIME_STATE_CUSTOM_TYPE = 'brunch.agent_runtime_stat export type OperationalModeId = 'elicit'; export type AgentRoleId = 'elicitor'; -export type AgentStrategyId = 'step-by-step' | 'disambiguate-via-examples'; -export type AgentLensId = AgentStrategyId; +export type AutoAxisSelection = 'auto'; +export type AgentStrategyId = + | 'step-wise-decision-tree' + | 'step-wise-disambiguate' + | 'propose-graph' + | 'project-graph'; +export type AgentStrategySelection = AutoAxisSelection | AgentStrategyId; +export type AgentLensId = 'intent' | 'design' | 'oracle'; +export type AgentLensSelection = AutoAxisSelection | AgentLensId; +export type AgentGoalId = + | 'grounding-advance' + | 'elicit-I' + | 'elicit-II' + | 'commitment-converge' + | 'capture-posture'; +export type AgentGoalSelection = AutoAxisSelection | AgentGoalId; export type ToolPolicyId = 'elicit-read-only'; export type PromptPackId = 'brunch-base' | 'elicit' | 'elicitor'; export type ModelPreference = 'default'; @@ -36,9 +50,9 @@ export type ThinkingLevel = 'low' | 'medium' | 'high'; export interface BrunchAgentState { schemaVersion: 1; operationalMode: OperationalModeId; - agentRole: AgentRoleId; - agentStrategy: AgentStrategyId; - agentLens: AgentLensId | null; + agentStrategy: AgentStrategySelection; + agentLens: AgentLensSelection; + agentGoal: AgentGoalSelection; } export interface OperationalModeDefinition { @@ -52,16 +66,19 @@ export interface OperationalModeDefinition { export interface AgentRoleDefinition { id: AgentRoleId; operationalMode: OperationalModeId; - defaultStrategy: AgentStrategyId; + defaultStrategy: AgentStrategySelection; allowedStrategies: readonly AgentStrategyId[]; - defaultLens: AgentLensId | null; + defaultLens: AgentLensSelection; allowedLenses: readonly AgentLensId[]; + defaultGoal: AgentGoalSelection; + allowedGoals: readonly AgentGoalId[]; promptPackIds: readonly PromptPackId[]; modelPreference?: ModelPreference; thinkingLevel?: ThinkingLevel; } export interface ResolvedBrunchAgentState extends BrunchAgentState { + agentRole: AgentRoleId; operationalModeDefinition: OperationalModeDefinition; agentRoleDefinition: AgentRoleDefinition; } @@ -77,9 +94,9 @@ export interface BrunchAgentStateEntryData { export const DEFAULT_BRUNCH_AGENT_STATE: BrunchAgentState = { schemaVersion: 1, operationalMode: 'elicit', - agentRole: 'elicitor', - agentStrategy: 'step-by-step', - agentLens: 'step-by-step', + agentStrategy: 'auto', + agentLens: 'auto', + agentGoal: 'grounding-advance', }; export const OPERATIONAL_MODE_DEFINITIONS: Record = { @@ -96,10 +113,17 @@ export const AGENT_ROLE_DEFINITIONS: Record = elicitor: { id: 'elicitor', operationalMode: 'elicit', - defaultStrategy: 'step-by-step', - allowedStrategies: ['step-by-step', 'disambiguate-via-examples'], - defaultLens: 'step-by-step', - allowedLenses: ['step-by-step', 'disambiguate-via-examples'], + defaultStrategy: 'auto', + allowedStrategies: [ + 'step-wise-decision-tree', + 'step-wise-disambiguate', + 'propose-graph', + 'project-graph', + ], + defaultLens: 'auto', + allowedLenses: ['intent', 'design', 'oracle'], + defaultGoal: 'grounding-advance', + allowedGoals: ['grounding-advance', 'elicit-I', 'elicit-II', 'commitment-converge', 'capture-posture'], promptPackIds: ['elicitor'], }, }; @@ -118,30 +142,33 @@ function isOneOf(value: unknown, allowed: readonly T[]): value return typeof value === 'string' && allowed.includes(value as T); } +function isAxisSelection( + value: unknown, + allowed: readonly T[], +): value is AutoAxisSelection | T { + return value === 'auto' || isOneOf(value, allowed); +} + function parseBrunchAgentState(value: unknown): BrunchAgentState | undefined { if (!isRecord(value)) return undefined; const operationalModes = Object.keys(OPERATIONAL_MODE_DEFINITIONS) as OperationalModeId[]; - const agentRoles = Object.keys(AGENT_ROLE_DEFINITIONS) as AgentRoleId[]; if (value.schemaVersion !== 1) return undefined; if (!isOneOf(value.operationalMode, operationalModes)) return undefined; - if (!isOneOf(value.agentRole, agentRoles)) return undefined; + if ('agentRole' in value) return undefined; const mode = OPERATIONAL_MODE_DEFINITIONS[value.operationalMode]; - const role = AGENT_ROLE_DEFINITIONS[value.agentRole]; - if (!mode.allowedRoles.includes(value.agentRole)) return undefined; - if (role.operationalMode !== value.operationalMode) return undefined; - if (!isOneOf(value.agentStrategy, role.allowedStrategies)) return undefined; - if (value.agentLens !== null && !isOneOf(value.agentLens, role.allowedLenses)) { - return undefined; - } + const role = AGENT_ROLE_DEFINITIONS[mode.defaultRole]; + if (!isAxisSelection(value.agentStrategy, role.allowedStrategies)) return undefined; + if (!isAxisSelection(value.agentLens, role.allowedLenses)) return undefined; + if (!isAxisSelection(value.agentGoal, role.allowedGoals)) return undefined; return { schemaVersion: 1, operationalMode: value.operationalMode, - agentRole: value.agentRole, agentStrategy: value.agentStrategy, agentLens: value.agentLens, + agentGoal: value.agentGoal, }; } @@ -172,10 +199,13 @@ function parseBrunchAgentStateEntryData(value: unknown): BrunchAgentStateEntryDa } function resolveBrunchAgentState(state: BrunchAgentState): ResolvedBrunchAgentState { + const operationalModeDefinition = OPERATIONAL_MODE_DEFINITIONS[state.operationalMode]; + const agentRole = operationalModeDefinition.defaultRole; return { ...state, - operationalModeDefinition: OPERATIONAL_MODE_DEFINITIONS[state.operationalMode], - agentRoleDefinition: AGENT_ROLE_DEFINITIONS[state.agentRole], + agentRole, + operationalModeDefinition, + agentRoleDefinition: AGENT_ROLE_DEFINITIONS[agentRole], }; } @@ -245,9 +275,9 @@ export function appendBrunchAgentRuntimeSwitch( previous: { schemaVersion: previous.schemaVersion, operationalMode: previous.operationalMode, - agentRole: previous.agentRole, agentStrategy: previous.agentStrategy, agentLens: previous.agentLens, + agentGoal: previous.agentGoal, }, source, }); diff --git a/src/.pi/extensions/prompting.ts b/src/.pi/extensions/prompting.ts index 6a6a8cc63..26444d6b4 100644 --- a/src/.pi/extensions/prompting.ts +++ b/src/.pi/extensions/prompting.ts @@ -39,6 +39,7 @@ export function registerBrunchPrompting(pi: ExtensionAPI): void { agentRole: state.agentRole, agentStrategy: state.agentStrategy, agentLens: state.agentLens, + agentGoal: state.agentGoal, activeTools, }); diff --git a/src/.pi/pi-extension-shell.ts b/src/.pi/pi-extension-shell.ts index 4f410e0b8..4de54c4dd 100644 --- a/src/.pi/pi-extension-shell.ts +++ b/src/.pi/pi-extension-shell.ts @@ -33,10 +33,15 @@ export { appendBrunchAgentRuntimeSwitch, projectBrunchAgentState, registerBrunchOperationalModePolicy, + type AgentGoalSelection, + type AgentGoalId, type AgentLensId, + type AgentLensSelection, type AgentRoleDefinition, type AgentRoleId, type AgentStrategyId, + type AgentStrategySelection, + type AutoAxisSelection, type BrunchAgentState, type BrunchAgentStateEntryData, type BrunchAgentStateEntrySessionManager, diff --git a/src/rpc/handlers.test.ts b/src/rpc/handlers.test.ts index 10127ce39..3317c002b 100644 --- a/src/rpc/handlers.test.ts +++ b/src/rpc/handlers.test.ts @@ -551,7 +551,7 @@ describe('JSON-RPC handlers', () => { status: 'pending', exchange: { exchangeId: expect.any(String), - lens: 'step-by-step', + lens: 'intent', mode: 'single-select', prompt: expect.stringContaining('new product or feature'), options: expect.arrayContaining([ @@ -612,7 +612,7 @@ describe('JSON-RPC handlers', () => { expect(sessionText).toContain('brunch.structured_exchange.present'); expect(sessionText).toContain('present_options'); expect(sessionText).toContain(exchangeId); - expect(sessionText).toContain('"lens":"step-by-step"'); + expect(sessionText).toContain('"lens":"intent"'); }); it('reads the selected pending elicitation exchange from transcript truth', async () => { @@ -649,7 +649,7 @@ describe('JSON-RPC handlers', () => { } ).result.exchange.exchangeId, prompt: expect.stringContaining('new product or feature'), - lens: 'step-by-step', + lens: 'intent', options: expect.arrayContaining([ expect.objectContaining({ id: 'new-from-scratch', diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index 36591f70c..197ab3651 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -303,7 +303,7 @@ const TranscriptDisplayResultSchema = Type.Object( const PendingElicitationExchangeSchema = Type.Object( { exchangeId: NonBlankStringSchema, - lens: Type.Literal('step-by-step'), + lens: Type.Literal('intent'), mode: Type.Union([Type.Literal('text'), Type.Literal('single-select'), Type.Literal('multi-select')]), prompt: NonBlankStringSchema, details: Type.Optional(NonBlankStringSchema), @@ -831,7 +831,7 @@ function nextDeterministicElicitationExchange(completedCount: number): PendingEl const script: PendingElicitationExchange[] = [ { exchangeId: `deterministic-grounding-choice-${turnNumber}`, - lens: 'step-by-step', + lens: 'intent', mode: 'single-select', prompt: 'Is this a new product or feature from scratch?', details: 'Choose the best starting context so later elicitation can ask useful follow-ups.', @@ -859,7 +859,7 @@ function nextDeterministicElicitationExchange(completedCount: number): PendingEl }, { exchangeId: `deterministic-grounding-text-${turnNumber}`, - lens: 'step-by-step', + lens: 'intent', mode: 'text', prompt: 'What are we specifying?', details: @@ -869,7 +869,7 @@ function nextDeterministicElicitationExchange(completedCount: number): PendingEl }, { exchangeId: `deterministic-grounding-multi-${turnNumber}`, - lens: 'step-by-step', + lens: 'intent', mode: 'multi-select', prompt: 'Which proof qualities matter for this parity run?', details: @@ -1003,7 +1003,7 @@ function pendingExchangeFromStructuredPresent( const detailsText = typeof richDetails.details === 'string' ? richDetails.details : markdown; return { exchangeId: details.exchangeId, - lens: 'step-by-step', + lens: 'intent', mode: details.expectedRequest?.tool === 'request_choices' ? 'multi-select' diff --git a/src/session/elicitation-exchange.test.ts b/src/session/elicitation-exchange.test.ts index d4b0de331..2135a7b74 100644 --- a/src/session/elicitation-exchange.test.ts +++ b/src/session/elicitation-exchange.test.ts @@ -243,13 +243,13 @@ describe('elicitation exchange projection', () => { id: 'offer-1', type: 'custom', customType: 'brunch.establishment_offer', - data: { lens: 'step-by-step' }, + data: { lens: 'intent' }, }, { id: 'proposal-1', type: 'custom', customType: 'brunch.review_set_proposal', - data: { lens: 'propose-scenarios-with-tradeoffs' }, + data: { lens: 'design' }, }, user, ]); From d2af892280019612ece788c2179df27b9500625b Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 16:15:29 +0200 Subject: [PATCH 26/34] FE-785: Prove propose-graph commit path --- .../report.json | 57 +++ .../session.jsonl | 12 + .../transcript.md | 129 +++++ memory/PLAN.md | 2 +- memory/SPEC.md | 2 +- src/brunch-tui.test.ts | 25 + src/brunch-tui.ts | 29 +- src/graph/index.ts | 3 +- src/graph/workspace-store.ts | 27 +- src/probes/propose-graph-commit-proof.test.ts | 179 +++++++ src/probes/propose-graph-commit-proof.ts | 461 ++++++++++++++++++ 11 files changed, 910 insertions(+), 16 deletions(-) create mode 100644 .fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/report.json create mode 100644 .fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/session.jsonl create mode 100644 .fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/transcript.md create mode 100644 src/probes/propose-graph-commit-proof.test.ts create mode 100644 src/probes/propose-graph-commit-proof.ts diff --git a/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/report.json b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/report.json new file mode 100644 index 000000000..122035051 --- /dev/null +++ b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/report.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "probeId": "propose-graph-commit", + "runId": "2026-06-02-propose-graph-commit", + "generatedAt": "2026-06-02T14:11:25.395Z", + "mission": "Prove the propose-graph strategy can commit graph truth through commit_graph.", + "evaluationFocus": "A14-L structural legality for direct commitGraph batches.", + "success": true, + "cwd": "/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-propose-graph-commit-28lS9B", + "specId": 1, + "sessionId": "019e88ac-c864-7eb4-8ee0-85cd75c8dcc6", + "prompt": "Brunch A14-L probe: the user has accepted the following concept-level proposal and asked you to persist it now.\n\nConcept: A Brunch specification workspace needs an explicit launch-readiness subgraph that records the launch goal, the rollback requirement, the operator visibility criterion, and the assumption that users can recover from a failed launch.\n\nUse the read_graph tool once in overview mode, then use commit_graph to persist a coherent intent-plane graph. Requirements for the commit_graph call:\n- create at least four intent-plane nodes\n- include at least one goal, one requirement, one criterion, and one assumption\n- create at least three edges connecting the nodes\n- use only legal edge categories from the tool guidance\n- include stance only on support or proof edges\n- avoid decision and term nodes for this proof so detail schemas are not needed\n\nIf commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics and retry once with a corrected complete batch. Stop after a successful commit_graph result.", + "model": "claude-opus-4-7", + "maxAttempts": 2, + "attemptCount": 1, + "retryCount": 0, + "firstAttemptStatus": "success", + "finalStatus": "success", + "attempts": [ + { + "index": 1, + "status": "success", + "lsn": 2, + "nodeRefs": { + "g1": 1, + "r1": 2, + "c1": 3, + "a1": 4 + }, + "edgeIds": [ + 1, + 2, + 3, + 4 + ], + "content": "Graph committed successfully (LSN 2).\nNodes created: g1 → #1, r1 → #2, c1 → #3, a1 → #4\nEdges created: #1, #2, #3, #4" + } + ], + "finalGraph": { + "nodeCount": 4, + "edgeCount": 4, + "lsn": 2 + }, + "committedNodeTitles": [ + "Launch readiness", + "Rollback capability", + "Operator visibility", + "Users recover from failed launch" + ], + "friction": [], + "artifacts": { + "runDir": ".fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit", + "sessionJsonl": ".fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/session.jsonl", + "transcriptMarkdown": ".fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/transcript.md", + "reportJson": ".fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/report.json" + } +} diff --git a/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/session.jsonl b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/session.jsonl new file mode 100644 index 000000000..59b05e151 --- /dev/null +++ b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/session.jsonl @@ -0,0 +1,12 @@ +{"type":"session","version":3,"id":"019e88ac-c864-7eb4-8ee0-85cd75c8dcc6","timestamp":"2026-06-02T14:11:25.412Z","cwd":"/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-propose-graph-commit-28lS9B"} +{"type":"custom","customType":"brunch.session_binding","data":{"schemaVersion":1,"specId":1},"id":"bd8476b9","parentId":null,"timestamp":"2026-06-02T14:11:25.412Z"} +{"type":"session_info","id":"44a7abf9","parentId":"bd8476b9","timestamp":"2026-06-02T14:11:25.412Z","name":"A14 propose-graph commit proof — session 1"} +{"type":"custom","customType":"brunch.agent_runtime_state","data":{"schemaVersion":1,"reason":"switch","state":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"propose-graph","agentLens":"intent","agentGoal":"commitment-converge"},"previous":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"auto","agentLens":"auto","agentGoal":"grounding-advance"},"source":"extension"},"id":"a6dce430","parentId":"44a7abf9","timestamp":"2026-06-02T14:11:25.413Z"} +{"type":"model_change","id":"9d488815","parentId":"a6dce430","timestamp":"2026-06-02T14:11:25.425Z","provider":"anthropic","modelId":"claude-opus-4-7"} +{"type":"thinking_level_change","id":"dd57d5d2","parentId":"9d488815","timestamp":"2026-06-02T14:11:25.425Z","thinkingLevel":"medium"} +{"type":"message","id":"0eb8d639","parentId":"dd57d5d2","timestamp":"2026-06-02T14:11:25.469Z","message":{"role":"user","content":[{"type":"text","text":"Brunch A14-L probe: the user has accepted the following concept-level proposal and asked you to persist it now.\n\nConcept: A Brunch specification workspace needs an explicit launch-readiness subgraph that records the launch goal, the rollback requirement, the operator visibility criterion, and the assumption that users can recover from a failed launch.\n\nUse the read_graph tool once in overview mode, then use commit_graph to persist a coherent intent-plane graph. Requirements for the commit_graph call:\n- create at least four intent-plane nodes\n- include at least one goal, one requirement, one criterion, and one assumption\n- create at least three edges connecting the nodes\n- use only legal edge categories from the tool guidance\n- include stance only on support or proof edges\n- avoid decision and term nodes for this proof so detail schemas are not needed\n\nIf commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics and retry once with a corrected complete batch. Stop after a successful commit_graph result."}],"timestamp":1780409485427}} +{"type":"message","id":"9c0cf8a1","parentId":"0eb8d639","timestamp":"2026-06-02T14:11:30.935Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":" I should review the graph before making any changes.","thinkingSignature":"Eu4BCmMIDhgCKkDfi0PV+5Zhp834C+AQF8+Zsl5dp9wCXuecFW3oxmVAT/ZAsaFJsurV6/bOXXDyNJt7HlmTICMA6fpzO79lky58Mg9jbGF1ZGUtb3B1cy00LTc4AEIIdGhpbmtpbmcSDBlvobp/wsIxcg4wMxoMKDvy7u+bok1tlWZHIjD4ubCnT9I1JKzUl8sp5lLG2D1fN4EB30RYR4lTcPweGt3ZBWTEud/i1LwezTs3ehcqOfbAKPqLLIIG+2821+3lsmTuziEy3X2Tcdwh8aqYsP9IALDtr5BWcUjL4JNV2uQDr5HftaczGTiIwxgB"},{"type":"toolCall","id":"toolu_01DtKKmcMRAvKEB1SXyaP96d","name":"read_graph","arguments":{"mode":"overview"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":6,"output":98,"cacheRead":0,"cacheWrite":10518,"totalTokens":10622,"cost":{"input":0.000030000000000000004,"output":0.00245,"cacheRead":0,"cacheWrite":0.0657375,"total":0.0682175}},"stopReason":"toolUse","timestamp":1780409485470,"responseId":"msg_01XEsKg1GHnEDqphuKXMZxbD"}} +{"type":"message","id":"333d6bdc","parentId":"9c0cf8a1","timestamp":"2026-06-02T14:11:30.937Z","message":{"role":"toolResult","toolCallId":"toolu_01DtKKmcMRAvKEB1SXyaP96d","toolName":"read_graph","content":[{"type":"text","text":"The graph is empty (no nodes or edges)."}],"details":{},"isError":false,"timestamp":1780409490937}} +{"type":"message","id":"23c9ee36","parentId":"333d6bdc","timestamp":"2026-06-02T14:11:39.998Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01HJQLJQCtM9r3uCon8sa88x","name":"commit_graph","arguments":{"nodes":[{"ref":"g1","plane":"intent","kind":"goal","title":"Launch readiness","body":"Ship the launch in a state where failures can be detected and reversed without lasting user harm."},{"ref":"r1","plane":"intent","kind":"requirement","title":"Rollback capability","body":"The launched system must support a rollback path that returns users to the prior known-good state."},{"ref":"c1","plane":"intent","kind":"criterion","title":"Operator visibility","body":"Operators can observe launch health (errors, key flows, rollback triggers) in near-real-time during and after launch."},{"ref":"a1","plane":"intent","kind":"assumption","title":"Users recover from failed launch","body":"Users are able to resume their work after a failed launch once rollback completes, without manual data repair."}],"edges":[{"source":"r1","target":"g1","category":"support","stance":"for","rationale":"Rollback capability is a necessary condition for launch readiness."},{"source":"c1","target":"g1","category":"support","stance":"for","rationale":"Operator visibility is required to know whether the launch is healthy or needs rollback."},{"source":"a1","target":"r1","category":"support","stance":"for","rationale":"The rollback requirement presumes users can resume work after rollback; otherwise rollback alone would not suffice."},{"source":"c1","target":"r1","category":"dependency","rationale":"Deciding to invoke rollback depends on operator visibility into launch health."}]}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":1,"output":605,"cacheRead":10518,"cacheWrite":130,"totalTokens":11254,"cost":{"input":0.000005,"output":0.015125000000000001,"cacheRead":0.005259,"cacheWrite":0.0008125000000000001,"total":0.0212015}},"stopReason":"toolUse","timestamp":1780409490937,"responseId":"msg_01PfPpnssDTbF5A8KpMXoViy"}} +{"type":"message","id":"9905b0cb","parentId":"23c9ee36","timestamp":"2026-06-02T14:11:40.002Z","message":{"role":"toolResult","toolCallId":"toolu_01HJQLJQCtM9r3uCon8sa88x","toolName":"commit_graph","content":[{"type":"text","text":"Graph committed successfully (LSN 2).\nNodes created: g1 → #1, r1 → #2, c1 → #3, a1 → #4\nEdges created: #1, #2, #3, #4"}],"details":{"status":"success","lsn":2,"nodes":{"g1":1,"r1":2,"c1":3,"a1":4},"edges":[1,2,3,4]},"isError":false,"timestamp":1780409500002}} +{"type":"message","id":"e47855e1","parentId":"9905b0cb","timestamp":"2026-06-02T14:11:42.714Z","message":{"role":"assistant","content":[{"type":"text","text":"Persisted the launch-readiness subgraph (LSN 2):\n\n- #1 goal — Launch readiness\n- #2 requirement — Rollback capability → supports #1\n- #3 criterion — Operator visibility → supports #1; depends on by #2\n- #4 assumption — Users recover from failed launch → supports #2"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":1,"output":110,"cacheRead":10648,"cacheWrite":702,"totalTokens":11461,"cost":{"input":0.000005,"output":0.0027500000000000003,"cacheRead":0.005324,"cacheWrite":0.0043875,"total":0.012466499999999998}},"stopReason":"stop","timestamp":1780409500002,"responseId":"msg_016QhQDQBvksBViYK8Caro4w"}} diff --git a/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/transcript.md b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/transcript.md new file mode 100644 index 000000000..14cb4ac64 --- /dev/null +++ b/.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/transcript.md @@ -0,0 +1,129 @@ +# Transcript — session.jsonl + +## Session + +- session: 019e88ac-c864-7eb4-8ee0-85cd75c8dcc6 +- cwd: /var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-propose-graph-commit-28lS9B + +## Session binding + +```json +{ + "schemaVersion": 1, + "specId": 1 +} +``` + +## Entry 44a7abf9 + +```json +{ + "type": "session_info", + "id": "44a7abf9", + "parentId": "bd8476b9", + "timestamp": "2026-06-02T14:11:25.412Z", + "name": "A14 propose-graph commit proof — session 1" +} +``` + +## Custom: brunch.agent_runtime_state + +```json +{ + "schemaVersion": 1, + "reason": "switch", + "state": { + "schemaVersion": 1, + "operationalMode": "elicit", + "agentStrategy": "propose-graph", + "agentLens": "intent", + "agentGoal": "commitment-converge" + }, + "previous": { + "schemaVersion": 1, + "operationalMode": "elicit", + "agentStrategy": "auto", + "agentLens": "auto", + "agentGoal": "grounding-advance" + }, + "source": "extension" +} +``` + +## Entry 9d488815 + +```json +{ + "type": "model_change", + "id": "9d488815", + "parentId": "a6dce430", + "timestamp": "2026-06-02T14:11:25.425Z", + "provider": "anthropic", + "modelId": "claude-opus-4-7" +} +``` + +## Entry dd57d5d2 + +```json +{ + "type": "thinking_level_change", + "id": "dd57d5d2", + "parentId": "9d488815", + "timestamp": "2026-06-02T14:11:25.425Z", + "thinkingLevel": "medium" +} +``` + +## User + +Brunch A14-L probe: the user has accepted the following concept-level proposal and asked you to persist it now. + +Concept: A Brunch specification workspace needs an explicit launch-readiness subgraph that records the launch goal, the rollback requirement, the operator visibility criterion, and the assumption that users can recover from a failed launch. + +Use the read_graph tool once in overview mode, then use commit_graph to persist a coherent intent-plane graph. Requirements for the commit_graph call: +- create at least four intent-plane nodes +- include at least one goal, one requirement, one criterion, and one assumption +- create at least three edges connecting the nodes +- use only legal edge categories from the tool guidance +- include stance only on support or proof edges +- avoid decision and term nodes for this proof so detail schemas are not needed + +If commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics and retry once with a corrected complete batch. Stop after a successful commit_graph result. + +## Assistant + +_(empty)_ + + +## Assistant + +_(empty)_ + + +## Assistant + +Persisted the launch-readiness subgraph (LSN 2): + +- #1 goal — Launch readiness +- #2 requirement — Rollback capability → supports #1 +- #3 criterion — Operator visibility → supports #1; depends on by #2 +- #4 assumption — Users recover from failed launch → supports #2 + + +## Raw session JSONL + +```jsonl +{"type":"session","version":3,"id":"019e88ac-c864-7eb4-8ee0-85cd75c8dcc6","timestamp":"2026-06-02T14:11:25.412Z","cwd":"/var/folders/2c/ptn6jcrj61lck_yzfz_p3b5m0000gn/T/brunch-propose-graph-commit-28lS9B"} +{"type":"custom","customType":"brunch.session_binding","data":{"schemaVersion":1,"specId":1},"id":"bd8476b9","parentId":null,"timestamp":"2026-06-02T14:11:25.412Z"} +{"type":"session_info","id":"44a7abf9","parentId":"bd8476b9","timestamp":"2026-06-02T14:11:25.412Z","name":"A14 propose-graph commit proof — session 1"} +{"type":"custom","customType":"brunch.agent_runtime_state","data":{"schemaVersion":1,"reason":"switch","state":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"propose-graph","agentLens":"intent","agentGoal":"commitment-converge"},"previous":{"schemaVersion":1,"operationalMode":"elicit","agentStrategy":"auto","agentLens":"auto","agentGoal":"grounding-advance"},"source":"extension"},"id":"a6dce430","parentId":"44a7abf9","timestamp":"2026-06-02T14:11:25.413Z"} +{"type":"model_change","id":"9d488815","parentId":"a6dce430","timestamp":"2026-06-02T14:11:25.425Z","provider":"anthropic","modelId":"claude-opus-4-7"} +{"type":"thinking_level_change","id":"dd57d5d2","parentId":"9d488815","timestamp":"2026-06-02T14:11:25.425Z","thinkingLevel":"medium"} +{"type":"message","id":"0eb8d639","parentId":"dd57d5d2","timestamp":"2026-06-02T14:11:25.469Z","message":{"role":"user","content":[{"type":"text","text":"Brunch A14-L probe: the user has accepted the following concept-level proposal and asked you to persist it now.\n\nConcept: A Brunch specification workspace needs an explicit launch-readiness subgraph that records the launch goal, the rollback requirement, the operator visibility criterion, and the assumption that users can recover from a failed launch.\n\nUse the read_graph tool once in overview mode, then use commit_graph to persist a coherent intent-plane graph. Requirements for the commit_graph call:\n- create at least four intent-plane nodes\n- include at least one goal, one requirement, one criterion, and one assumption\n- create at least three edges connecting the nodes\n- use only legal edge categories from the tool guidance\n- include stance only on support or proof edges\n- avoid decision and term nodes for this proof so detail schemas are not needed\n\nIf commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics and retry once with a corrected complete batch. Stop after a successful commit_graph result."}],"timestamp":1780409485427}} +{"type":"message","id":"9c0cf8a1","parentId":"0eb8d639","timestamp":"2026-06-02T14:11:30.935Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":" I should review the graph before making any changes.","thinkingSignature":"Eu4BCmMIDhgCKkDfi0PV+5Zhp834C+AQF8+Zsl5dp9wCXuecFW3oxmVAT/ZAsaFJsurV6/bOXXDyNJt7HlmTICMA6fpzO79lky58Mg9jbGF1ZGUtb3B1cy00LTc4AEIIdGhpbmtpbmcSDBlvobp/wsIxcg4wMxoMKDvy7u+bok1tlWZHIjD4ubCnT9I1JKzUl8sp5lLG2D1fN4EB30RYR4lTcPweGt3ZBWTEud/i1LwezTs3ehcqOfbAKPqLLIIG+2821+3lsmTuziEy3X2Tcdwh8aqYsP9IALDtr5BWcUjL4JNV2uQDr5HftaczGTiIwxgB"},{"type":"toolCall","id":"toolu_01DtKKmcMRAvKEB1SXyaP96d","name":"read_graph","arguments":{"mode":"overview"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":6,"output":98,"cacheRead":0,"cacheWrite":10518,"totalTokens":10622,"cost":{"input":0.000030000000000000004,"output":0.00245,"cacheRead":0,"cacheWrite":0.0657375,"total":0.0682175}},"stopReason":"toolUse","timestamp":1780409485470,"responseId":"msg_01XEsKg1GHnEDqphuKXMZxbD"}} +{"type":"message","id":"333d6bdc","parentId":"9c0cf8a1","timestamp":"2026-06-02T14:11:30.937Z","message":{"role":"toolResult","toolCallId":"toolu_01DtKKmcMRAvKEB1SXyaP96d","toolName":"read_graph","content":[{"type":"text","text":"The graph is empty (no nodes or edges)."}],"details":{},"isError":false,"timestamp":1780409490937}} +{"type":"message","id":"23c9ee36","parentId":"333d6bdc","timestamp":"2026-06-02T14:11:39.998Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01HJQLJQCtM9r3uCon8sa88x","name":"commit_graph","arguments":{"nodes":[{"ref":"g1","plane":"intent","kind":"goal","title":"Launch readiness","body":"Ship the launch in a state where failures can be detected and reversed without lasting user harm."},{"ref":"r1","plane":"intent","kind":"requirement","title":"Rollback capability","body":"The launched system must support a rollback path that returns users to the prior known-good state."},{"ref":"c1","plane":"intent","kind":"criterion","title":"Operator visibility","body":"Operators can observe launch health (errors, key flows, rollback triggers) in near-real-time during and after launch."},{"ref":"a1","plane":"intent","kind":"assumption","title":"Users recover from failed launch","body":"Users are able to resume their work after a failed launch once rollback completes, without manual data repair."}],"edges":[{"source":"r1","target":"g1","category":"support","stance":"for","rationale":"Rollback capability is a necessary condition for launch readiness."},{"source":"c1","target":"g1","category":"support","stance":"for","rationale":"Operator visibility is required to know whether the launch is healthy or needs rollback."},{"source":"a1","target":"r1","category":"support","stance":"for","rationale":"The rollback requirement presumes users can resume work after rollback; otherwise rollback alone would not suffice."},{"source":"c1","target":"r1","category":"dependency","rationale":"Deciding to invoke rollback depends on operator visibility into launch health."}]}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":1,"output":605,"cacheRead":10518,"cacheWrite":130,"totalTokens":11254,"cost":{"input":0.000005,"output":0.015125000000000001,"cacheRead":0.005259,"cacheWrite":0.0008125000000000001,"total":0.0212015}},"stopReason":"toolUse","timestamp":1780409490937,"responseId":"msg_01PfPpnssDTbF5A8KpMXoViy"}} +{"type":"message","id":"9905b0cb","parentId":"23c9ee36","timestamp":"2026-06-02T14:11:40.002Z","message":{"role":"toolResult","toolCallId":"toolu_01HJQLJQCtM9r3uCon8sa88x","toolName":"commit_graph","content":[{"type":"text","text":"Graph committed successfully (LSN 2).\nNodes created: g1 → #1, r1 → #2, c1 → #3, a1 → #4\nEdges created: #1, #2, #3, #4"}],"details":{"status":"success","lsn":2,"nodes":{"g1":1,"r1":2,"c1":3,"a1":4},"edges":[1,2,3,4]},"isError":false,"timestamp":1780409500002}} +{"type":"message","id":"e47855e1","parentId":"9905b0cb","timestamp":"2026-06-02T14:11:42.714Z","message":{"role":"assistant","content":[{"type":"text","text":"Persisted the launch-readiness subgraph (LSN 2):\n\n- #1 goal — Launch readiness\n- #2 requirement — Rollback capability → supports #1\n- #3 criterion — Operator visibility → supports #1; depends on by #2\n- #4 assumption — Users recover from failed launch → supports #2"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-7","usage":{"input":1,"output":110,"cacheRead":10648,"cacheWrite":702,"totalTokens":11461,"cost":{"input":0.000005,"output":0.0027500000000000003,"cacheRead":0.005324,"cacheWrite":0.0043875,"total":0.012466499999999998}},"stopReason":"stop","timestamp":1780409500002,"responseId":"msg_016QhQDQBvksBViYK8Caro4w"}} +``` diff --git a/memory/PLAN.md b/memory/PLAN.md index c183d4513..80df972bf 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -223,7 +223,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L - **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. Next: validate the tools work with a real LLM (A14-L outer-loop proof), then scope capture/reviewer slices. +- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. Next: scope the `project-graph` review-set proposal/dry-run path or synchronous elicitor capture. ### subagents-for-proposal-diversity diff --git a/memory/SPEC.md b/memory/SPEC.md index e2ab0a462..bb89d44af 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -109,7 +109,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A9-L | A session-scoped mention ledger of (`entity_id`, `snapshotted_lsn`) is the right granularity for staleness hints; transcript-scoped or graph-scoped ledgers are not needed for the POC. | low | open | I7-L | M7 — turn-boundary reconciliation slice; observed via fixture runs that stress re-read decisions. | | A11-L | Pi's `prepareNextTurn` plus custom-message delivery are sufficient to express side-task result delivery without inventing a second event plane or forking pi. | medium | open | D15-L | M5 + M7: side-task registry wiring and next-turn delivery proof. | | A13-L | If Brunch later adds deferred observer/auditor jobs, a durable queue keyed by session id and elicitation-exchange entry range can recover async audit/backfill after process interruption without reintroducing canonical chat/turn tables; whether this shares storage with a generalized work-item/reconciliation table can be deferred. | medium | open | D18-L, I14-L | Deferred until async audit/backfill lands: restart/idempotence tests exercise exchange-keyed jobs once graph writes exist. | -| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | open | D27-L, D51-L, D53-L | Build graph layer (db/ + graph/ CommandExecutor) and commitGraph tool, then test LLM generation against real CommandExecutor validation. Bounded retry on structural_illegal. Fallback (constrained generation, retry-with-feedback, or NL-parse-at-accept) preserves the user-facing flow if reliability is insufficient. | +| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. Remaining open subclaim: review-set proposal legality/dry-run validation for `project-graph` (D27-L/I20-L). Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | | A15-L | Establishment hints as transcript-native custom entries (`brunch.establishment_offer`) provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms lens offers are reconstructable from transcript; chrome region renders ambient affordances from the latest such entry. | | A16-L | Reviewer triggering policy (always-on vs lens-keyed) and reviewer scope (batch + how-far-neighborhood) can be deferred to per-lens decisions without architectural commitment now. | low | open | D29-L | M5+: empirical — reviewer integration reveals which policy avoids unacceptable next-turn latency without losing relevant findings. | | A17-L | A user-level temperamental preference for interrogative vs proposal-based elicitation meaningfully affects adoption and eventually warrants expression as a user-level setting. | low | open | D25-L, D26-L | Deferred; surfaces from outer-loop walkthroughs and adversarial fixtures once both single-exchange and batch-proposal flows exist in product. | diff --git a/src/brunch-tui.test.ts b/src/brunch-tui.test.ts index d02d00853..7233d2761 100644 --- a/src/brunch-tui.test.ts +++ b/src/brunch-tui.test.ts @@ -28,6 +28,7 @@ import { applyBrunchOfflineDefault, brunchResourceLoaderOptions, createBrunchSettingsManager, + createBrunchAgentSessionRuntimeFactory, runBrunchTui, } from './brunch-tui.js'; import { userMessage } from './probes/test-helpers.js'; @@ -65,6 +66,30 @@ describe('Brunch TUI boot', () => { } }); + it('registers graph tools on the default product runtime path', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-graph-runtime-')); + const agentDir = await mkdtemp(join(tmpdir(), 'brunch-agent-dir-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const workspace = await coordinator.createSetupSession({ + specTitle: 'Graph runtime', + createNewSpec: true, + }); + const createRuntime = createBrunchAgentSessionRuntimeFactory({ workspace, coordinator }); + const created = await createRuntime({ + cwd, + agentDir, + sessionManager: workspace.session.manager, + }); + + try { + const toolNames = created.session.getAllTools().map((tool) => tool.name); + expect(toolNames).toContain('commit_graph'); + expect(toolNames).toContain('read_graph'); + } finally { + created.session.dispose(); + } + }); + it('runs inspect, preflight, and activation before launching interactive mode', async () => { const events: string[] = []; const workspace = readyWorkspace('/tmp/project', 'session-ready'); diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index f62c0e465..afa67552e 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -17,6 +17,7 @@ import { } from './.pi/brunch-pi-profile.js'; import { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; import { chromeStateForWorkspace, createBrunchPiExtensionShell } from './.pi/pi-extension-shell.js'; +import { openWorkspaceGraphRuntime } from './graph/index.js'; import { createWorkspaceSessionCoordinator, type WorkspaceLaunchInventory, @@ -99,23 +100,22 @@ async function chooseSpecSessionActivationDecision( return runWorkspaceDialogPreflight(inventory); } -async function launchPiInteractive({ workspace, coordinator }: BrunchTuiLaunchContext): Promise { - const agentDir = getAgentDir(); - const createRuntime: CreateAgentSessionRuntimeFactory = async ({ - cwd, - agentDir: runtimeAgentDir, - sessionManager, - }) => { +export function createBrunchAgentSessionRuntimeFactory({ + workspace, + coordinator, +}: BrunchTuiLaunchContext): CreateAgentSessionRuntimeFactory { + return async ({ cwd, agentDir: runtimeAgentDir, sessionManager }) => { + const graph = await openWorkspaceGraphRuntime(cwd); const profile = createBrunchPiProfile({ cwd, agentDir: runtimeAgentDir, extensionFactories: [ createBrunchPiExtensionShell( chromeStateForWorkspace(workspace), - async (sessionManager) => { - await coordinator.bindCurrentSpecToReplacementSession(sessionManager); + async (replacementSessionManager) => { + await coordinator.bindCurrentSpecToReplacementSession(replacementSessionManager); }, - { coordinator }, + { coordinator, graph }, ), ], }); @@ -135,11 +135,16 @@ async function launchPiInteractive({ workspace, coordinator }: BrunchTuiLaunchCo diagnostics: services.diagnostics, }; }; +} + +async function launchPiInteractive(context: BrunchTuiLaunchContext): Promise { + const agentDir = getAgentDir(); + const createRuntime = createBrunchAgentSessionRuntimeFactory(context); const runtime = await createAgentSessionRuntime(createRuntime, { - cwd: workspace.cwd, + cwd: context.workspace.cwd, agentDir, - sessionManager: workspace.session.manager, + sessionManager: context.workspace.session.manager, }); applyBrunchOfflineDefault(); diff --git a/src/graph/index.ts b/src/graph/index.ts index 65fbd9c1b..2279eda1e 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -65,7 +65,8 @@ export type { } from './snapshot.js'; export { CommandExecutor } from './command-executor.js'; -export { openWorkspaceCommandExecutor } from './workspace-store.js'; +export { openWorkspaceCommandExecutor, openWorkspaceGraphRuntime } from './workspace-store.js'; +export type { WorkspaceGraphRuntime } from './workspace-store.js'; export type { BatchEdgeInput, BatchEdgeRef, diff --git a/src/graph/workspace-store.ts b/src/graph/workspace-store.ts index 34b7ad0e4..5693a0b82 100644 --- a/src/graph/workspace-store.ts +++ b/src/graph/workspace-store.ts @@ -3,12 +3,37 @@ import { join } from 'node:path'; import { createDb } from '../db/connection.js'; import { CommandExecutor } from './command-executor.js'; +import { getGraphOverview, getNodeNeighborhood } from './snapshot.js'; +import type { GraphOverview, NeighborhoodOptions, NeighborhoodResult } from './snapshot.js'; const BRUNCH_DIR = '.brunch'; const DATA_DB_FILE = 'data.db'; +export interface WorkspaceGraphRuntime { + readonly commandExecutor: CommandExecutor; + readonly snapshots: { + readonly getGraphOverview: () => GraphOverview; + readonly getNodeNeighborhood: (nodeId: number, options?: NeighborhoodOptions) => NeighborhoodResult; + }; +} + +export async function openWorkspaceGraphRuntime(cwd: string): Promise { + const db = await openWorkspaceDb(cwd); + return { + commandExecutor: new CommandExecutor(db), + snapshots: { + getGraphOverview: () => getGraphOverview(db), + getNodeNeighborhood: (nodeId, options) => getNodeNeighborhood(db, nodeId, options), + }, + }; +} + export async function openWorkspaceCommandExecutor(cwd: string): Promise { + return (await openWorkspaceGraphRuntime(cwd)).commandExecutor; +} + +async function openWorkspaceDb(cwd: string) { const brunchDir = join(cwd, BRUNCH_DIR); await mkdir(brunchDir, { recursive: true }); - return new CommandExecutor(createDb(join(brunchDir, DATA_DB_FILE))); + return createDb(join(brunchDir, DATA_DB_FILE)); } diff --git a/src/probes/propose-graph-commit-proof.test.ts b/src/probes/propose-graph-commit-proof.test.ts new file mode 100644 index 000000000..05afc9998 --- /dev/null +++ b/src/probes/propose-graph-commit-proof.test.ts @@ -0,0 +1,179 @@ +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import type { GraphOverview } from '../graph/snapshot.js'; +import { + summarizeProposeGraphCommitProof, + writeProposeGraphCommitProofArtifacts, + type ProposeGraphCommitProofReport, +} from './propose-graph-commit-proof.js'; + +function messageEntry(toolName: string, details: unknown, content: string): string { + return JSON.stringify({ + type: 'message', + message: { + role: 'toolResult', + toolName, + content, + details, + }, + }); +} + +const successfulOverview: GraphOverview = { + nodes: [ + { + id: 1, + plane: 'intent', + kind: 'goal', + title: 'Clarify launch readiness', + basis: 'explicit', + createdAtLsn: 1, + updatedAtLsn: 1, + }, + { + id: 2, + plane: 'intent', + kind: 'requirement', + title: 'Expose rollback criteria', + basis: 'explicit', + createdAtLsn: 1, + updatedAtLsn: 1, + }, + ], + edges: [ + { + id: 1, + category: 'dependency', + sourceId: 2, + targetId: 1, + basis: 'explicit', + createdAtLsn: 1, + updatedAtLsn: 1, + }, + ], + nodeCount: 2, + edgeCount: 1, + lsn: 1, +}; + +describe('propose-graph commit proof report', () => { + it('classifies bounded retry evidence from commit_graph tool results', () => { + const sessionText = [ + messageEntry( + 'commit_graph', + { + status: 'structural_illegal', + diagnostics: [{ field: 'edges[0].stance', message: 'stance is required for support edges' }], + }, + 'STRUCTURAL_ILLEGAL', + ), + messageEntry( + 'commit_graph', + { + status: 'success', + lsn: 1, + nodes: { goal: 1, rollback: 2 }, + edges: [1], + }, + 'Graph committed successfully', + ), + ].join('\n'); + + const report = summarizeProposeGraphCommitProof({ + runId: 'run-1', + generatedAt: '2026-06-02T00:00:00.000Z', + cwd: '/tmp/brunch-proof', + specId: 7, + sessionId: 'session-1', + maxAttempts: 2, + sessionText, + overview: successfulOverview, + prompt: 'Commit the accepted concept.', + model: 'test-model', + }); + + expect(report.success).toBe(true); + expect(report.attempts).toHaveLength(2); + expect(report.firstAttemptStatus).toBe('structural_illegal'); + expect(report.finalStatus).toBe('success'); + expect(report.retryCount).toBe(1); + expect(report.finalGraph).toMatchObject({ nodeCount: 2, edgeCount: 1, lsn: 1 }); + expect(report.committedNodeTitles).toEqual(['Clarify launch readiness', 'Expose rollback criteria']); + expect(report.attempts[0]?.diagnostics).toEqual([ + { field: 'edges[0].stance', message: 'stance is required for support edges' }, + ]); + }); + + it('fails closed when no commit_graph attempt succeeds', () => { + const sessionText = messageEntry( + 'commit_graph', + { + status: 'structural_illegal', + diagnostics: [{ field: 'nodes[0].kind', message: 'invalid kind' }], + }, + 'STRUCTURAL_ILLEGAL', + ); + + const report = summarizeProposeGraphCommitProof({ + runId: 'run-2', + generatedAt: '2026-06-02T00:00:00.000Z', + cwd: '/tmp/brunch-proof', + specId: 7, + sessionId: 'session-1', + maxAttempts: 1, + sessionText, + overview: { ...successfulOverview, nodes: [], edges: [], nodeCount: 0, edgeCount: 0, lsn: 0 }, + prompt: 'Commit the accepted concept.', + }); + + expect(report.success).toBe(false); + expect(report.firstAttemptStatus).toBe('structural_illegal'); + expect(report.finalStatus).toBe('structural_illegal'); + expect(report.finalGraph).toMatchObject({ nodeCount: 0, edgeCount: 0, lsn: 0 }); + }); + + it('writes replayable probe artifacts', async () => { + const fixtureRoot = await mkdtemp(join(tmpdir(), 'brunch-propose-graph-artifacts-')); + const report: ProposeGraphCommitProofReport = { + schemaVersion: 1, + probeId: 'propose-graph-commit', + runId: 'artifact-run', + generatedAt: '2026-06-02T00:00:00.000Z', + mission: 'Prove the propose-graph strategy can commit graph truth through commit_graph.', + evaluationFocus: 'A14-L structural legality for direct commitGraph batches.', + success: true, + cwd: '/tmp/brunch-proof', + specId: 7, + sessionId: 'session-1', + prompt: 'Commit the accepted concept.', + maxAttempts: 2, + attemptCount: 1, + retryCount: 0, + firstAttemptStatus: 'success', + finalStatus: 'success', + attempts: [{ index: 1, status: 'success', lsn: 1, nodeRefs: { goal: 1 }, edgeIds: [] }], + finalGraph: { nodeCount: 1, edgeCount: 0, lsn: 1 }, + committedNodeTitles: ['Clarify launch readiness'], + friction: [], + }; + + const artifacts = await writeProposeGraphCommitProofArtifacts({ + fixtureRoot, + runId: report.runId, + sessionText: messageEntry( + 'commit_graph', + { status: 'success', lsn: 1, nodes: { goal: 1 }, edges: [] }, + 'Graph committed successfully', + ), + report, + }); + + expect(await readFile(artifacts.reportJson, 'utf8')).toContain('propose-graph-commit'); + expect(await readFile(artifacts.sessionJsonl, 'utf8')).toContain('commit_graph'); + expect(await readFile(artifacts.transcriptMarkdown, 'utf8')).toContain('Graph committed successfully'); + }); +}); diff --git a/src/probes/propose-graph-commit-proof.ts b/src/probes/propose-graph-commit-proof.ts new file mode 100644 index 000000000..fa88690f1 --- /dev/null +++ b/src/probes/propose-graph-commit-proof.ts @@ -0,0 +1,461 @@ +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { getAgentDir } from '@earendil-works/pi-coding-agent'; + +import { appendBrunchAgentRuntimeSwitch, type BrunchAgentState } from '../.pi/extensions/operational-mode.js'; +import { createBrunchAgentSessionRuntimeFactory } from '../brunch-tui.js'; +import { + openWorkspaceGraphRuntime, + type CommitGraphSuccess, + type Diagnostic, + type StructuralIllegal, +} from '../graph/index.js'; +import type { GraphOverview } from '../graph/snapshot.js'; +import { renderSessionTranscript } from '../session/session-transcript.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; + +const PROBE_ID = 'propose-graph-commit' as const; +const DEFAULT_MAX_ATTEMPTS = 2; + +export interface ProposeGraphCommitProofOptions { + cwd?: string; + fixtureRoot?: string; + runId?: string; + maxAttempts?: number; + prompt?: string; + agentDir?: string; +} + +export interface ProposeGraphCommitProofArtifacts { + runDir: string; + sessionJsonl: string; + transcriptMarkdown: string; + reportJson: string; +} + +export type CommitGraphAttemptStatus = + | CommitGraphSuccess['status'] + | StructuralIllegal['status'] + | 'needs_human' + | 'policy_blocked' + | 'version_conflict' + | 'unknown'; + +export interface CommitGraphAttemptReport { + index: number; + status: CommitGraphAttemptStatus; + lsn?: number; + nodeRefs?: Record; + edgeIds?: number[]; + diagnostics?: Diagnostic[]; + content?: string; +} + +export interface ProposeGraphCommitProofReport { + schemaVersion: 1; + probeId: typeof PROBE_ID; + runId: string; + generatedAt: string; + mission: string; + evaluationFocus: string; + success: boolean; + cwd: string; + specId: number; + sessionId: string; + prompt: string; + model?: string; + maxAttempts: number; + attemptCount: number; + retryCount: number; + firstAttemptStatus: CommitGraphAttemptStatus | 'not_called'; + finalStatus: CommitGraphAttemptStatus | 'not_called'; + attempts: CommitGraphAttemptReport[]; + finalGraph: { + nodeCount: number; + edgeCount: number; + lsn: number; + }; + committedNodeTitles: string[]; + friction: string[]; + artifacts?: ProposeGraphCommitProofArtifacts; +} + +export interface ProposeGraphCommitProofSummaryInput { + runId: string; + generatedAt: string; + cwd: string; + specId: number; + sessionId: string; + maxAttempts: number; + sessionText: string; + overview: GraphOverview; + prompt: string; + model?: string; + friction?: readonly string[]; +} + +export async function runProposeGraphCommitProof( + options: ProposeGraphCommitProofOptions = {}, +): Promise { + const cwd = options.cwd ?? (await mkdtemp(join(tmpdir(), 'brunch-propose-graph-commit-'))); + const runId = options.runId ?? defaultRunId(); + const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS; + const prompt = options.prompt ?? defaultProofPrompt(); + const generatedAt = new Date().toISOString(); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const workspace = await coordinator.createSetupSession({ + specTitle: 'A14 propose-graph commit proof', + createNewSpec: true, + }); + const runtimeState: BrunchAgentState = { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'propose-graph', + agentLens: 'intent', + agentGoal: 'commitment-converge', + }; + appendBrunchAgentRuntimeSwitch(workspace.session.manager, runtimeState, 'extension'); + const agentDir = options.agentDir ?? getAgentDir(); + const createRuntime = createBrunchAgentSessionRuntimeFactory({ workspace, coordinator }); + const created = await createRuntime({ + cwd, + agentDir, + sessionManager: workspace.session.manager, + }); + const session = created.session; + const friction = created.diagnostics.map((diagnostic) => `${diagnostic.type}: ${diagnostic.message}`); + const graph = await openWorkspaceGraphRuntime(cwd); + + try { + await session.sendUserMessage(prompt); + await session.agent.waitForIdle(); + + let report = await summarizeCurrentRun({ + runId, + generatedAt, + cwd, + specId: workspace.spec.id, + sessionId: workspace.session.id, + maxAttempts, + sessionFile: workspace.session.file, + overview: graph.snapshots.getGraphOverview(), + prompt, + ...(session.model?.id !== undefined ? { model: session.model.id } : {}), + friction, + }); + + if (shouldRetry(report)) { + await session.sendUserMessage(retryPrompt(report.attempts.at(-1)?.diagnostics ?? [])); + await session.agent.waitForIdle(); + report = await summarizeCurrentRun({ + runId, + generatedAt, + cwd, + specId: workspace.spec.id, + sessionId: workspace.session.id, + maxAttempts, + sessionFile: workspace.session.file, + overview: graph.snapshots.getGraphOverview(), + prompt, + ...(session.model?.id !== undefined ? { model: session.model.id } : {}), + friction, + }); + } + + if (options.fixtureRoot !== undefined) { + const sessionText = await readFile(workspace.session.file, 'utf8'); + report.artifacts = await writeProposeGraphCommitProofArtifacts({ + fixtureRoot: options.fixtureRoot, + runId, + sessionText, + report, + }); + } + + return report; + } finally { + session.dispose(); + } +} + +async function summarizeCurrentRun(options: { + runId: string; + generatedAt: string; + cwd: string; + specId: number; + sessionId: string; + maxAttempts: number; + sessionFile: string; + overview: GraphOverview; + prompt: string; + model?: string; + friction: readonly string[]; +}): Promise { + return summarizeProposeGraphCommitProof({ + runId: options.runId, + generatedAt: options.generatedAt, + cwd: options.cwd, + specId: options.specId, + sessionId: options.sessionId, + maxAttempts: options.maxAttempts, + sessionText: await readFile(options.sessionFile, 'utf8'), + overview: options.overview, + prompt: options.prompt, + ...(options.model !== undefined ? { model: options.model } : {}), + friction: options.friction, + }); +} + +function shouldRetry(report: ProposeGraphCommitProofReport): boolean { + return ( + !report.success && report.finalStatus === 'structural_illegal' && report.attemptCount < report.maxAttempts + ); +} + +export function summarizeProposeGraphCommitProof( + input: ProposeGraphCommitProofSummaryInput, +): ProposeGraphCommitProofReport { + const attempts = commitGraphAttemptsFromSession(input.sessionText); + const firstAttemptStatus = attempts[0]?.status ?? 'not_called'; + const finalStatus = attempts.at(-1)?.status ?? 'not_called'; + const successfulAttempt = lastSuccessfulAttempt(attempts); + const finalGraph = { + nodeCount: input.overview.nodeCount, + edgeCount: input.overview.edgeCount, + lsn: input.overview.lsn, + }; + const committedNodeTitles = input.overview.nodes.map((node) => node.title); + const friction = [...(input.friction ?? [])]; + const success = successfulAttempt !== undefined && input.overview.nodeCount > 0; + + if (attempts.length === 0) { + friction.push('No commit_graph tool result was recorded.'); + } + if (attempts.length > input.maxAttempts) { + friction.push(`commit_graph attempts exceeded maxAttempts=${input.maxAttempts}.`); + } + if (successfulAttempt === undefined && attempts.length > 0) { + friction.push(`No commit_graph attempt succeeded; final status was ${finalStatus}.`); + } + if (successfulAttempt !== undefined && input.overview.nodeCount === 0) { + friction.push('commit_graph reported success but graph overview is empty.'); + } + + return { + schemaVersion: 1, + probeId: PROBE_ID, + runId: input.runId, + generatedAt: input.generatedAt, + mission: 'Prove the propose-graph strategy can commit graph truth through commit_graph.', + evaluationFocus: 'A14-L structural legality for direct commitGraph batches.', + success, + cwd: input.cwd, + specId: input.specId, + sessionId: input.sessionId, + prompt: input.prompt, + ...(input.model !== undefined ? { model: input.model } : {}), + maxAttempts: input.maxAttempts, + attemptCount: attempts.length, + retryCount: + successfulAttempt === undefined + ? Math.max(0, attempts.length - 1) + : attempts.indexOf(successfulAttempt), + firstAttemptStatus, + finalStatus, + attempts, + finalGraph, + committedNodeTitles, + friction, + }; +} + +function lastSuccessfulAttempt( + attempts: readonly CommitGraphAttemptReport[], +): CommitGraphAttemptReport | undefined { + for (let index = attempts.length - 1; index >= 0; index -= 1) { + const attempt = attempts[index]; + if (attempt?.status === 'success') return attempt; + } + return undefined; +} + +function commitGraphAttemptsFromSession(sessionText: string): CommitGraphAttemptReport[] { + const attempts: CommitGraphAttemptReport[] = []; + for (const line of sessionText.split('\n')) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + const entry = parseJson(trimmed); + if (!isRecord(entry) || entry.type !== 'message') continue; + + const message = entry.message; + if (!isRecord(message) || message.role !== 'toolResult' || message.toolName !== 'commit_graph') { + continue; + } + attempts.push(commitGraphAttemptFromMessage(attempts.length + 1, message)); + } + return attempts; +} + +function commitGraphAttemptFromMessage( + index: number, + message: Record, +): CommitGraphAttemptReport { + const details = isRecord(message.details) ? message.details : undefined; + const status = commitGraphStatus(details?.status); + return { + index, + status, + ...(typeof details?.lsn === 'number' ? { lsn: details.lsn } : {}), + ...(isNumberRecord(details?.nodes) ? { nodeRefs: details.nodes } : {}), + ...(isNumberArray(details?.edges) ? { edgeIds: details.edges } : {}), + ...(isDiagnosticArray(details?.diagnostics) ? { diagnostics: details.diagnostics } : {}), + ...textContent(message.content), + }; +} + +function commitGraphStatus(value: unknown): CommitGraphAttemptStatus { + if ( + value === 'success' || + value === 'structural_illegal' || + value === 'needs_human' || + value === 'policy_blocked' || + value === 'version_conflict' + ) { + return value; + } + return 'unknown'; +} + +function isDiagnosticArray(value: unknown): value is Diagnostic[] { + return ( + Array.isArray(value) && + value.every( + (item) => isRecord(item) && typeof item.field === 'string' && typeof item.message === 'string', + ) + ); +} + +function isNumberRecord(value: unknown): value is Record { + return isRecord(value) && Object.values(value).every((entry): entry is number => typeof entry === 'number'); +} + +function isNumberArray(value: unknown): value is number[] { + return Array.isArray(value) && value.every((entry): entry is number => typeof entry === 'number'); +} + +function textContent(value: unknown): { content?: string } { + if (typeof value === 'string') return { content: value }; + if (!Array.isArray(value)) return {}; + const text = value + .flatMap((item) => (isRecord(item) && typeof item.text === 'string' ? [item.text] : [])) + .join('\n'); + return text.length > 0 ? { content: text } : {}; +} + +export async function writeProposeGraphCommitProofArtifacts(options: { + fixtureRoot: string; + runId: string; + sessionText: string; + report: ProposeGraphCommitProofReport; +}): Promise { + const runDir = join(options.fixtureRoot, 'runs', PROBE_ID, options.runId); + const artifacts: ProposeGraphCommitProofArtifacts = { + runDir, + sessionJsonl: join(runDir, 'session.jsonl'), + transcriptMarkdown: join(runDir, 'transcript.md'), + reportJson: join(runDir, 'report.json'), + }; + const report: ProposeGraphCommitProofReport = { + ...options.report, + artifacts, + }; + + await mkdir(runDir, { recursive: true }); + await writeFile(artifacts.sessionJsonl, options.sessionText, 'utf8'); + const transcriptMarkdown = [ + renderSessionTranscript(options.sessionText, { title: 'session.jsonl' }), + '', + '## Raw session JSONL', + '', + '```jsonl', + options.sessionText.trimEnd(), + '```', + '', + ].join('\n'); + await writeFile(artifacts.transcriptMarkdown, transcriptMarkdown, 'utf8'); + await writeFile(artifacts.reportJson, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + + return artifacts; +} + +function defaultProofPrompt(): string { + return `Brunch A14-L probe: the user has accepted the following concept-level proposal and asked you to persist it now.\n\nConcept: A Brunch specification workspace needs an explicit launch-readiness subgraph that records the launch goal, the rollback requirement, the operator visibility criterion, and the assumption that users can recover from a failed launch.\n\nUse the read_graph tool once in overview mode, then use commit_graph to persist a coherent intent-plane graph. Requirements for the commit_graph call:\n- create at least four intent-plane nodes\n- include at least one goal, one requirement, one criterion, and one assumption\n- create at least three edges connecting the nodes\n- use only legal edge categories from the tool guidance\n- include stance only on support or proof edges\n- avoid decision and term nodes for this proof so detail schemas are not needed\n\nIf commit_graph returns STRUCTURAL_ILLEGAL, read the diagnostics and retry once with a corrected complete batch. Stop after a successful commit_graph result.`; +} + +function retryPrompt(diagnostics: readonly Diagnostic[]): string { + const renderedDiagnostics = diagnostics + .map((diagnostic) => `- ${diagnostic.field}: ${diagnostic.message}`) + .join('\n'); + return `The previous commit_graph attempt was structurally illegal. Retry once with a complete corrected batch. Diagnostics:\n${renderedDiagnostics}`; +} + +function defaultRunId(): string { + return new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-'); +} + +function parseJson(value: string): unknown { + try { + return JSON.parse(value) as unknown; + } catch { + return undefined; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function parseCliArgs(argv: readonly string[]): ProposeGraphCommitProofOptions { + const options: ProposeGraphCommitProofOptions = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--cwd') { + options.cwd = requiredValue(argv, (index += 1), arg); + } else if (arg === '--fixture-root') { + options.fixtureRoot = requiredValue(argv, (index += 1), arg); + } else if (arg === '--run-id') { + options.runId = requiredValue(argv, (index += 1), arg); + } else if (arg === '--max-attempts') { + options.maxAttempts = Number(requiredValue(argv, (index += 1), arg)); + } else if (arg === '--prompt') { + options.prompt = requiredValue(argv, (index += 1), arg); + } + } + return options; +} + +function requiredValue(argv: readonly string[], index: number, flag: string): string { + const value = argv[index]; + if (value === undefined) { + throw new Error(`${flag} requires a value`); + } + return value; +} + +async function main(): Promise { + const report = await runProposeGraphCommitProof(parseCliArgs(process.argv.slice(2))); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + process.exitCode = report.success ? 0 : 1; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; + }); +} From 83d252efba88a70d65e4ad3d19ede848057cb41f Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 16:26:42 +0200 Subject: [PATCH 27/34] FE-785: Gate review-set proposals by dry-run validation --- memory/PLAN.md | 2 +- memory/SPEC.md | 8 +- src/.pi/__tests__/review-set-proposal.test.ts | 161 ++++++++++ .../extensions/graph/review-set-proposal.ts | 299 ++++++++++++++++++ src/graph/command-executor.ts | 108 +++++-- src/graph/index.ts | 2 + 6 files changed, 540 insertions(+), 40 deletions(-) create mode 100644 src/.pi/__tests__/review-set-proposal.test.ts create mode 100644 src/.pi/extensions/graph/review-set-proposal.ts diff --git a/memory/PLAN.md b/memory/PLAN.md index 80df972bf..09d9f44b8 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -223,7 +223,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L - **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. Next: scope the `project-graph` review-set proposal/dry-run path or synchronous elicitor capture. +- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates review-set proposal metadata, translates drafts to graph batches, and only surfaces dry-run-valid `brunch.review_set_proposal` entries. Next: wire real `project-graph` agent proposal generation or scope synchronous elicitor capture. ### subagents-for-proposal-diversity diff --git a/memory/SPEC.md b/memory/SPEC.md index bb89d44af..3813b08ba 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -109,7 +109,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A9-L | A session-scoped mention ledger of (`entity_id`, `snapshotted_lsn`) is the right granularity for staleness hints; transcript-scoped or graph-scoped ledgers are not needed for the POC. | low | open | I7-L | M7 — turn-boundary reconciliation slice; observed via fixture runs that stress re-read decisions. | | A11-L | Pi's `prepareNextTurn` plus custom-message delivery are sufficient to express side-task result delivery without inventing a second event plane or forking pi. | medium | open | D15-L | M5 + M7: side-task registry wiring and next-turn delivery proof. | | A13-L | If Brunch later adds deferred observer/auditor jobs, a durable queue keyed by session id and elicitation-exchange entry range can recover async audit/backfill after process interruption without reintroducing canonical chat/turn tables; whether this shares storage with a generalized work-item/reconciliation table can be deferred. | medium | open | D18-L, I14-L | Deferred until async audit/backfill lands: restart/idempotence tests exercise exchange-keyed jobs once graph writes exist. | -| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. Remaining open subclaim: review-set proposal legality/dry-run validation for `project-graph` (D27-L/I20-L). Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | +| A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. **Review-set dry-run substrate validated 2026-06-02** by `review-set-proposal.test.ts`: reviewable proposals must carry lens/epistemic/grounding metadata, translate to `commitGraph` input, pass `CommandExecutor.dryRunCommitGraph`, and stay off the review surface when structurally illegal. Remaining open subclaim: real LLM `project-graph` proposal generation against that substrate. Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | | A15-L | Establishment hints as transcript-native custom entries (`brunch.establishment_offer`) provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms lens offers are reconstructable from transcript; chrome region renders ambient affordances from the latest such entry. | | A16-L | Reviewer triggering policy (always-on vs lens-keyed) and reviewer scope (batch + how-far-neighborhood) can be deferred to per-lens decisions without architectural commitment now. | low | open | D29-L | M5+: empirical — reviewer integration reveals which policy avoids unacceptable next-turn latency without losing relevant findings. | | A17-L | A user-level temperamental preference for interrogative vs proposal-based elicitation meaningfully affects adoption and eventually warrants expression as a user-level setting. | low | open | D25-L, D26-L | Deferred; surfaces from outer-loop walkthroughs and adversarial fixtures once both single-exchange and batch-proposal flows exist in product. | @@ -265,10 +265,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I14-L | If Brunch introduces deferred observer/auditor jobs, they are keyed by session id plus elicitation-exchange entry-range ids and have durable status; replay/restart cannot enqueue duplicate jobs for the same exchange, and job writes never become the primary freshness path for the next elicitor turn. | deferred/planned only if observer-audit queue lands (M5+ restart/idempotence tests) | D18-L, D4-L | | I15-L | Every review-set acceptance routes through `CommandExecutor` as one atomic `acceptReviewSet` command producing one LSN, one change-log entry, and one transaction over the entire batch. Partial acceptance is not representable through any product API. | planned (M5+ batch-acceptance command tests; review-set fixture parity) | D20-L, D27-L; I1-L, I11-L | | I16-L | Reviewer-attributed writes target only the `reconciliation_need` substrate; no reviewer-attributed `CommandExecutor` call writes graph entities, edges, change-log entries directly, or any other record class. | planned (M5+ architectural test on reviewer command writers; reviewer-attributed command-result audit) | D29-L; I2-L, I11-L | -| I17-L | Every batch-proposal or review-set entry (`brunch.review_set_proposal`) declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | planned (M5+ proposal-entry schema test; fixture asserts status under thin and rich grounding) | D30-L, D46-L; A14-L | -| I18-L | Every elicitor-emitted prompt or proposal custom entry (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) carries a `lens` field; capture, reviewer, and future observer/auditor routing filters on this field. | planned (M5+ capture/reviewer routing tests; transcript-shape contract test) | D25-L, D26-L, D29-L | +| I17-L | Every batch-proposal or review-set entry (`brunch.review_set_proposal`) declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | partially covered (`review-set-proposal.test.ts` covers the product proposal helper rejecting missing `epistemicStatus` and empty grounding/support before surfacing a reviewable entry; thin-vs-rich grounding fixture semantics remain future work) | D30-L, D46-L; A14-L | +| I18-L | Every elicitor-emitted prompt or proposal custom entry (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) carries a `lens` field; capture, reviewer, and future observer/auditor routing filters on this field. | partially covered (`review-set-proposal.test.ts` covers review-set proposal lens validation; establishment/intent-hint routing tests remain planned with capture/reviewer slices) | D25-L, D26-L, D29-L | | I19-L | Brunch-controlled flows do not create or navigate Pi session branches, and Brunch transcript readers fail fast on non-linear JSONL rather than flattening, migrating, or branch-selecting. | partially covered (M3 transcript loader requires exactly one Pi session header, rejects malformed non-header entry shapes, and rejects non-linear child graphs, `parentSession`, and `branch_summary`; product-facing exchange projection helper preserves the non-linear error discriminant and is used by RPC and fixture replay assertions; `session.elicitationExchanges` returns a product-shaped error for non-linear selected sessions over stdio and WebSocket JSON-RPC; Brunch TUI extension cancels `session_before_tree` and `session_before_fork`; Pi command-containment source/RPC evidence shows `session_before_fork` can also cancel clone/fork effects but exact interactive built-ins still need product-shell policy if visibility must be strict; dynamic chrome remains projection-only and does not add branch or mutation authority) | D24-L, D6-L, D11-L, D13-L, D34-L, D35-L | -| I20-L | Every user-reviewable review-set proposal has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface as reviewable review sets. | planned (M5+ proposal-validation contract + differential tests) | D27-L; A14-L | +| I20-L | Every user-reviewable review-set proposal has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface as reviewable review sets. | partially covered (`CommandExecutor.dryRunCommitGraph` and `review-set-proposal.test.ts` cover product-helper dry-run validation, invalid proposal non-surfacing, no graph mutation during dry-run, and dry-run/commit validation parity; real agent-generated `project-graph` proposal fixtures remain planned) | D27-L; A14-L | | I21-L | WebSocket/stdio/TUI client attachment state never becomes the canonical spec/session binding: every session-consuming projection validates the durable `brunch.session_binding`, and write-capable session operations must target an explicit session or future write lease rather than whichever transport connection happens to be open. | partially covered (M3 RPC/WebSocket explicit session projection tests validate durable `brunch.session_binding` for read paths; FE-744 web live-update tests prove WebSocket notifications only invalidate/refetch canonical projection handlers after RPC-originated structured-exchange mutations; future write-lease tests remain planned when web input lands) | D10-L, D19-L, D21-L, D33-L | | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | | I23-L | Every structured elicitation interaction that owns the response surface persists durable semantic display only through Pi `toolResult` rows rendered by `renderResult`; `renderCall` and live `ctx.ui.*` surfaces are transient. A structured-exchange tuple has a recoverable `present_*` result and, when required, exactly one matching terminal `request_*` result before the next agent turn consumes it. The target details model is checked by `schema` + `v`, `exchange_id`, and `tool_meta`; request outcomes are an exactly-one property-presence union; user-authored text is `comment` and runtime-authored text is `message`; present-side status/kind/expected-request aliases and capture graph payloads are invalid in the Zod-authored schema layer. `toolResult.content` is rich markdown suitable for both TUI transcript display and model context; `toolResult.details` carries structured projection/recovery data. | covered for current FE-744 structured-exchange tools (registered sequential `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices`; tests cover non-semantic `renderCall`, markdown `renderResult`, present/request details, unmatched-present recovery, active-vs-stub registry, JSON-editor fallback for multi-choice, terminal `answered`/`cancelled`/`unavailable` projection closure, option content/rationale parity, and same-assistant-message `present_options → request_choice` ordering over a real Pi RPC run. The Zod-authored schema layer is covered by JSON Schema export and drift-rejection tests for present/request/capture details; runtime tools still need a deliberate migration to those exports. `present_review_set`, `present_candidates`, and `request_review` remain named stubs and intentionally unregistered.) | D12-L, D13-L, D17-L, D37-L, D38-L, D41-L | diff --git a/src/.pi/__tests__/review-set-proposal.test.ts b/src/.pi/__tests__/review-set-proposal.test.ts new file mode 100644 index 000000000..d40135c8e --- /dev/null +++ b/src/.pi/__tests__/review-set-proposal.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from 'vitest'; + +import { createDb } from '../../db/connection.js'; +import { CommandExecutor } from '../../graph/command-executor.js'; +import { getGraphOverview } from '../../graph/snapshot.js'; +import { + buildReviewableReviewSetProposalEntry, + projectLatestReviewableReviewSetProposal, + translateReviewSetProposalToCommitGraph, + type ReviewSetProposalDraft, +} from '../extensions/graph/review-set-proposal.js'; + +function validProposal(overrides: Partial = {}): ReviewSetProposalDraft { + return { + schemaVersion: 1, + lens: 'design', + epistemicStatus: 'inferred', + grounding: { + summary: 'The launch path is thin but enough to propose acceptance criteria.', + support: ['User accepted a launch-readiness concept.'], + }, + pitch: { + title: 'Launch readiness review set', + narrative: 'A small graph for deciding whether launch can proceed.', + }, + entityDrafts: [ + { + draftId: 'goal-launch', + plane: 'intent', + kind: 'goal', + title: 'Launch safely', + }, + { + draftId: 'req-rollback', + plane: 'intent', + kind: 'requirement', + title: 'Rollback path exists', + }, + { + draftId: 'crit-observable', + plane: 'intent', + kind: 'criterion', + title: 'Operators can observe failures', + }, + ], + edgeDrafts: [ + { + category: 'dependency', + sourceDraftId: 'req-rollback', + targetDraftId: 'goal-launch', + rationale: 'Rollback capability is required for safe launch.', + }, + { + category: 'support', + sourceDraftId: 'crit-observable', + targetDraftId: 'goal-launch', + stance: 'for', + rationale: 'Observability supports a safe launch decision.', + }, + ], + ...overrides, + }; +} + +describe('review-set proposal dry-run gate', () => { + it('surfaces dry-run-valid review-set proposals as transcript entries', () => { + const db = createDb(':memory:'); + const executor = new CommandExecutor(db); + const entry = buildReviewableReviewSetProposalEntry({ + proposal: validProposal(), + commandExecutor: executor, + source: 'agent', + }); + + expect(entry).toMatchObject({ + customType: 'brunch.review_set_proposal', + data: { + schemaVersion: 1, + lens: 'design', + epistemicStatus: 'inferred', + validation: { status: 'success' }, + }, + }); + expect(getGraphOverview(db)).toMatchObject({ nodeCount: 0, edgeCount: 0, lsn: 0 }); + expect(projectLatestReviewableReviewSetProposal([{ type: 'custom', ...entry }])).toMatchObject({ + pitch: { title: 'Launch readiness review set' }, + validation: { status: 'success' }, + }); + }); + + it('does not surface structurally invalid review-set proposals', () => { + const db = createDb(':memory:'); + const executor = new CommandExecutor(db); + const entry = buildReviewableReviewSetProposalEntry({ + proposal: validProposal({ + edgeDrafts: [ + { + category: 'support', + sourceDraftId: 'req-rollback', + targetDraftId: 'goal-launch', + }, + ], + }), + commandExecutor: executor, + source: 'agent', + }); + + expect(entry).toMatchObject({ + status: 'structural_illegal', + diagnostics: [{ field: 'edges[0].stance', message: expect.stringContaining('required') }], + }); + expect(getGraphOverview(db)).toMatchObject({ nodeCount: 0, edgeCount: 0, lsn: 0 }); + expect( + projectLatestReviewableReviewSetProposal([{ type: 'custom', customType: 'other', data: entry }]), + ).toBeUndefined(); + }); + + it('rejects proposal schema drift before CommandExecutor dry-run', () => { + const db = createDb(':memory:'); + const executor = new CommandExecutor(db); + + for (const proposal of [ + { ...validProposal(), epistemicStatus: undefined }, + { ...validProposal(), lens: 'propose-scenarios-with-tradeoffs' }, + { ...validProposal(), grounding: { summary: 'No support.', support: [] } }, + { + ...validProposal(), + edgeDrafts: [ + { + relation: 'supports', + sourceDraftId: 'req-rollback', + targetDraftId: 'goal-launch', + }, + ], + }, + ]) { + const result = buildReviewableReviewSetProposalEntry({ + proposal: proposal as unknown as ReviewSetProposalDraft, + commandExecutor: executor, + source: 'agent', + }); + expect(result.status).toBe('structural_illegal'); + } + }); + + it('keeps dry-run validation in parity with commitGraph validation', () => { + const db = createDb(':memory:'); + const executor = new CommandExecutor(db); + const proposal = validProposal(); + const entry = buildReviewableReviewSetProposalEntry({ + proposal, + commandExecutor: executor, + source: 'agent', + }); + expect(entry.status).toBe('reviewable'); + + const commitResult = executor.commitGraph(translateReviewSetProposalToCommitGraph(proposal)); + expect(commitResult).toMatchObject({ status: 'success' }); + expect(getGraphOverview(db)).toMatchObject({ nodeCount: 3, edgeCount: 2 }); + }); +}); diff --git a/src/.pi/extensions/graph/review-set-proposal.ts b/src/.pi/extensions/graph/review-set-proposal.ts new file mode 100644 index 000000000..d27b27614 --- /dev/null +++ b/src/.pi/extensions/graph/review-set-proposal.ts @@ -0,0 +1,299 @@ +import { + EDGE_CATEGORIES, + EDGE_STANCES, + type BatchEdgeInput, + type BatchNodeInput, + type CommandExecutor, + type CommitGraphInput, + type CommitGraphDryRunResult, + type Diagnostic, + type NodePlane, + type StructuralIllegal, +} from '../../../graph/index.js'; + +export const REVIEW_SET_PROPOSAL_CUSTOM_TYPE = 'brunch.review_set_proposal'; + +export type ReviewSetLens = 'intent' | 'design' | 'oracle'; +export type EpistemicStatus = 'inferred' | 'assumed' | 'asserted' | 'observed'; + +export interface ReviewSetProposalGrounding { + readonly summary: string; + readonly support: readonly string[]; +} + +export interface ReviewSetProposalPitch { + readonly title: string; + readonly narrative: string; +} + +export interface ReviewSetEntityDraft { + readonly draftId: string; + readonly plane: NodePlane; + readonly kind: string; + readonly title: string; + readonly body?: string; + readonly detail?: unknown; +} + +export interface ReviewSetEdgeDraft { + readonly category: string; + readonly sourceDraftId: string; + readonly targetDraftId: string; + readonly stance?: string; + readonly rationale?: string; +} + +export interface ReviewSetProposalDraft { + readonly schemaVersion: 1; + readonly lens: ReviewSetLens; + readonly epistemicStatus: EpistemicStatus; + readonly grounding: ReviewSetProposalGrounding; + readonly pitch: ReviewSetProposalPitch; + readonly entityDrafts: readonly ReviewSetEntityDraft[]; + readonly edgeDrafts: readonly ReviewSetEdgeDraft[]; + readonly proposalVersion?: number; + readonly supersedes?: string; +} + +export interface ReviewSetProposalData extends ReviewSetProposalDraft { + readonly validation: CommitGraphDryRunResult; + readonly source: 'agent' | 'system' | 'extension'; +} + +export interface ReviewableReviewSetProposalEntry { + readonly status: 'reviewable'; + readonly customType: typeof REVIEW_SET_PROPOSAL_CUSTOM_TYPE; + readonly content: string; + readonly display: true; + readonly data: ReviewSetProposalData; +} + +export interface CustomEntryLike { + readonly type?: unknown; + readonly customType?: unknown; + readonly data?: unknown; +} + +const VALID_LENSES = ['intent', 'design', 'oracle'] as const; +const VALID_EPISTEMIC_STATUSES = ['inferred', 'assumed', 'asserted', 'observed'] as const; +const VALID_PLANES = ['intent', 'oracle', 'design', 'plan'] as const; + +export function translateReviewSetProposalToCommitGraph(proposal: ReviewSetProposalDraft): CommitGraphInput { + return { + nodes: proposal.entityDrafts.map( + (draft): BatchNodeInput => ({ + ref: draft.draftId, + plane: draft.plane, + kind: draft.kind, + title: draft.title, + ...(draft.body !== undefined ? { body: draft.body } : {}), + basis: 'accepted_review_set', + ...(draft.detail !== undefined ? { detail: draft.detail } : {}), + }), + ), + edges: proposal.edgeDrafts.map( + (draft): BatchEdgeInput => ({ + category: draft.category, + source: draft.sourceDraftId, + target: draft.targetDraftId, + basis: 'accepted_review_set', + ...(draft.stance !== undefined ? { stance: draft.stance } : {}), + ...(draft.rationale !== undefined ? { rationale: draft.rationale } : {}), + }), + ), + }; +} + +export function buildReviewableReviewSetProposalEntry(options: { + readonly proposal: ReviewSetProposalDraft; + readonly commandExecutor: CommandExecutor; + readonly source: ReviewSetProposalData['source']; +}): ReviewableReviewSetProposalEntry | StructuralIllegal { + const diagnostics = validateReviewSetProposalDraft(options.proposal); + if (diagnostics.length > 0) { + return { status: 'structural_illegal', diagnostics }; + } + + const validation = options.commandExecutor.dryRunCommitGraph( + translateReviewSetProposalToCommitGraph(options.proposal), + ); + if (validation.status !== 'success') { + return validation; + } + + return { + status: 'reviewable', + customType: REVIEW_SET_PROPOSAL_CUSTOM_TYPE, + content: renderReviewSetProposalContent(options.proposal), + display: true, + data: { + ...options.proposal, + validation, + source: options.source, + }, + }; +} + +export function projectLatestReviewableReviewSetProposal( + entries: readonly CustomEntryLike[], +): ReviewSetProposalData | undefined { + let latest: ReviewSetProposalData | undefined; + for (const entry of entries) { + if (entry.type !== 'custom' || entry.customType !== REVIEW_SET_PROPOSAL_CUSTOM_TYPE) { + continue; + } + const data = parseReviewSetProposalData(entry.data); + if (data) latest = data; + } + return latest; +} + +function parseReviewSetProposalData(value: unknown): ReviewSetProposalData | undefined { + if (!isRecord(value)) return undefined; + if (!isRecord(value.validation) || value.validation.status !== 'success') return undefined; + const proposal = value as unknown as ReviewSetProposalDraft; + if (validateReviewSetProposalDraft(proposal).length > 0) return undefined; + const source = value.source; + if (source !== 'agent' && source !== 'system' && source !== 'extension') return undefined; + return { + ...proposal, + validation: { status: 'success' }, + source, + }; +} + +function validateReviewSetProposalDraft(value: ReviewSetProposalDraft): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const candidate = value as unknown; + if (!isRecord(candidate)) { + return [{ field: 'proposal', message: 'proposal must be an object' }]; + } + + if (candidate.schemaVersion !== 1) { + diagnostics.push({ field: 'schemaVersion', message: 'schemaVersion must be 1' }); + } + if (!isOneOf(candidate.lens, VALID_LENSES)) { + diagnostics.push({ field: 'lens', message: 'lens must be intent, design, or oracle' }); + } + if (!isOneOf(candidate.epistemicStatus, VALID_EPISTEMIC_STATUSES)) { + diagnostics.push({ field: 'epistemicStatus', message: 'epistemicStatus is required' }); + } + + validateGrounding(candidate.grounding, diagnostics); + validatePitch(candidate.pitch, diagnostics); + validateEntityDrafts(candidate.entityDrafts, diagnostics); + validateEdgeDrafts(candidate.edgeDrafts, diagnostics); + return diagnostics; +} + +function validateGrounding(value: unknown, diagnostics: Diagnostic[]): void { + if (!isRecord(value)) { + diagnostics.push({ field: 'grounding', message: 'grounding is required' }); + return; + } + if (typeof value.summary !== 'string' || value.summary.trim().length === 0) { + diagnostics.push({ field: 'grounding.summary', message: 'summary must be non-empty' }); + } + if (!isNonEmptyStringArray(value.support)) { + diagnostics.push({ field: 'grounding.support', message: 'support must be a non-empty string array' }); + } +} + +function validatePitch(value: unknown, diagnostics: Diagnostic[]): void { + if (!isRecord(value)) { + diagnostics.push({ field: 'pitch', message: 'pitch is required' }); + return; + } + if (typeof value.title !== 'string' || value.title.trim().length === 0) { + diagnostics.push({ field: 'pitch.title', message: 'title must be non-empty' }); + } + if (typeof value.narrative !== 'string' || value.narrative.trim().length === 0) { + diagnostics.push({ field: 'pitch.narrative', message: 'narrative must be non-empty' }); + } +} + +function validateEntityDrafts(value: unknown, diagnostics: Diagnostic[]): void { + if (!Array.isArray(value) || value.length === 0) { + diagnostics.push({ field: 'entityDrafts', message: 'entityDrafts must be non-empty' }); + return; + } + + const seen = new Set(); + value.forEach((draft, index) => { + const path = `entityDrafts[${index}]`; + if (!isRecord(draft)) { + diagnostics.push({ field: path, message: 'entity draft must be an object' }); + return; + } + if (typeof draft.draftId !== 'string' || draft.draftId.trim().length === 0) { + diagnostics.push({ field: `${path}.draftId`, message: 'draftId must be non-empty' }); + } else if (seen.has(draft.draftId)) { + diagnostics.push({ field: `${path}.draftId`, message: `duplicate draftId "${draft.draftId}"` }); + } else { + seen.add(draft.draftId); + } + if (!isOneOf(draft.plane, VALID_PLANES)) { + diagnostics.push({ field: `${path}.plane`, message: 'invalid plane' }); + } + if (typeof draft.kind !== 'string' || draft.kind.trim().length === 0) { + diagnostics.push({ field: `${path}.kind`, message: 'kind must be non-empty' }); + } + if (typeof draft.title !== 'string' || draft.title.trim().length === 0) { + diagnostics.push({ field: `${path}.title`, message: 'title must be non-empty' }); + } + }); +} + +function validateEdgeDrafts(value: unknown, diagnostics: Diagnostic[]): void { + if (!Array.isArray(value) || value.length === 0) { + diagnostics.push({ field: 'edgeDrafts', message: 'edgeDrafts must be non-empty' }); + return; + } + + value.forEach((draft, index) => { + const path = `edgeDrafts[${index}]`; + if (!isRecord(draft)) { + diagnostics.push({ field: path, message: 'edge draft must be an object' }); + return; + } + if ('relation' in draft) { + diagnostics.push({ field: `${path}.relation`, message: 'relation is retired; use category' }); + } + if (!isOneOf(draft.category, EDGE_CATEGORIES)) { + diagnostics.push({ field: `${path}.category`, message: 'invalid edge category' }); + } + if (draft.stance !== undefined && !isOneOf(draft.stance, EDGE_STANCES)) { + diagnostics.push({ field: `${path}.stance`, message: 'invalid stance' }); + } + if (typeof draft.sourceDraftId !== 'string' || draft.sourceDraftId.trim().length === 0) { + diagnostics.push({ field: `${path}.sourceDraftId`, message: 'sourceDraftId must be non-empty' }); + } + if (typeof draft.targetDraftId !== 'string' || draft.targetDraftId.trim().length === 0) { + diagnostics.push({ field: `${path}.targetDraftId`, message: 'targetDraftId must be non-empty' }); + } + }); +} + +function renderReviewSetProposalContent(proposal: ReviewSetProposalDraft): string { + return [ + `## ${proposal.pitch.title}`, + '', + proposal.pitch.narrative, + '', + `Epistemic status: ${proposal.epistemicStatus}`, + `Lens: ${proposal.lens}`, + `Drafts: ${proposal.entityDrafts.length} entities, ${proposal.edgeDrafts.length} edges`, + ].join('\n'); +} + +function isNonEmptyStringArray(value: unknown): value is readonly string[] { + return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string'); +} + +function isOneOf(value: unknown, allowed: readonly T[]): value is T { + return typeof value === 'string' && allowed.includes(value as T); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} diff --git a/src/graph/command-executor.ts b/src/graph/command-executor.ts index db6667428..ca2828e47 100644 --- a/src/graph/command-executor.ts +++ b/src/graph/command-executor.ts @@ -72,6 +72,11 @@ export interface CommitGraphSuccess { readonly edges: readonly number[]; } +/** Successful dry-run validation without mutation. */ +export interface DryRunSuccess { + readonly status: 'success'; +} + /** Successful reconciliation-need creation. */ export interface ReconNeedSuccess { readonly status: 'success'; @@ -125,6 +130,9 @@ export type CreateNodeResult = CommandSuccess | StructuralIllegal; /** Result of a commitGraph command. */ export type CommitGraphResult = CommitGraphSuccess | StructuralIllegal; +/** Result of a commitGraph dry-run validation. */ +export type CommitGraphDryRunResult = DryRunSuccess | StructuralIllegal; + /** Result of a createReconciliationNeed command. */ export type CreateReconNeedResult = ReconNeedSuccess | StructuralIllegal; @@ -674,6 +682,17 @@ export class CommandExecutor { }); } + /** + * Validate a commitGraph batch without mutating graph truth. + * + * This is the product gate for review-set proposals: a user-reviewable + * proposal must pass the same structural checks as the eventual commit. + */ + dryRunCommitGraph(input: CommitGraphInput): CommitGraphDryRunResult { + const diagnostics = this.validateCommitGraphInput(input); + return diagnostics.length > 0 ? { status: 'structural_illegal', diagnostics } : { status: 'success' }; + } + /** * Atomic batch creation of nodes and edges (D53-L). * @@ -683,41 +702,9 @@ export class CommandExecutor { * validation, the entire batch is rejected (I34-L). */ commitGraph(input: CommitGraphInput): CommitGraphResult { - // Empty batch is structural_illegal - if (input.nodes.length === 0 && input.edges.length === 0) { - return { - status: 'structural_illegal', - diagnostics: [{ field: 'batch', message: 'empty batch — nothing to commit' }], - }; - } - - // --- Pre-transaction: validate all batch nodes (pure checks) --- - const preDiagnostics: Diagnostic[] = []; - const seenRefs = new Set(); - - for (let i = 0; i < input.nodes.length; i++) { - const bn = input.nodes[i]!; - - // Duplicate ref check - if (seenRefs.has(bn.ref)) { - preDiagnostics.push({ - field: `nodes[${i}].ref`, - message: `duplicate batch ref "${bn.ref}"`, - }); - } - seenRefs.add(bn.ref); - - // Structural node validation (reuse) - for (const d of validateCreateNode(bn)) { - preDiagnostics.push({ - field: `nodes[${i}].${d.field}`, - message: d.message, - }); - } - } - - if (preDiagnostics.length > 0) { - return { status: 'structural_illegal', diagnostics: preDiagnostics }; + const diagnostics = this.validateCommitGraphInput(input); + if (diagnostics.length > 0) { + return { status: 'structural_illegal', diagnostics }; } // --- Transaction: insert nodes, resolve refs, validate + insert edges --- @@ -831,6 +818,57 @@ export class CommandExecutor { } } + private validateCommitGraphInput(input: CommitGraphInput): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + if (input.nodes.length === 0 && input.edges.length === 0) { + diagnostics.push({ field: 'batch', message: 'empty batch — nothing to commit' }); + return diagnostics; + } + + const refMap = new Map(); + for (let i = 0; i < input.nodes.length; i++) { + const bn = input.nodes[i]!; + if (refMap.has(bn.ref)) { + diagnostics.push({ + field: `nodes[${i}].ref`, + message: `duplicate batch ref "${bn.ref}"`, + }); + } + refMap.set(bn.ref, -(i + 1)); + + for (const diagnostic of validateCreateNode(bn)) { + diagnostics.push({ + field: `nodes[${i}].${diagnostic.field}`, + message: diagnostic.message, + }); + } + } + if (diagnostics.length > 0) return diagnostics; + + const existingRefs = new Set(); + for (const edge of input.edges) { + if (typeof edge.source !== 'string') existingRefs.add(edge.source.existing); + if (typeof edge.target !== 'string') existingRefs.add(edge.target.existing); + } + + const verifiedExisting = new Set(); + if (existingRefs.size > 0) { + const rows = this.db + .select({ id: schema.nodes.id }) + .from(schema.nodes) + .where(inArray(schema.nodes.id, [...existingRefs])) + .all(); + for (const row of rows) verifiedExisting.add(row.id); + } + + for (let i = 0; i < input.edges.length; i++) { + diagnostics.push( + ...validateAndResolveBatchEdge(input.edges[i]!, i, refMap, verifiedExisting).diagnostics, + ); + } + return diagnostics; + } + /** * Create a reconciliation need. * diff --git a/src/graph/index.ts b/src/graph/index.ts index 2279eda1e..6498b8fb3 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -74,6 +74,7 @@ export type { CommandResult, CommandSuccess, CommitGraphInput, + CommitGraphDryRunResult, CommitGraphResult, CommitGraphSuccess, CreateNodeInput, @@ -82,6 +83,7 @@ export type { CreateSpecInput, CreateSpecResult, CreateSpecSuccess, + DryRunSuccess, CreateReconNeedResult, Diagnostic, NeedsHuman, From 021c8d1546dc37ee0a518f170126b775872c67c7 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 16:36:44 +0200 Subject: [PATCH 28/34] FE-785: Fold runtime vocabulary into graph integration plan --- memory/PLAN.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 09d9f44b8..5529e4b68 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -55,7 +55,7 @@ The POC should maximize assumption falsification rather than merely implement mi ### Next -1. `agents-composition-layer` — the `agents/` 3-layer prompt composition (D58-L), goal/strategy/lens packs + AUTO selector, snapshot pull/render/surface (D60-L), and the `.pi/context → agents/` migration. Depends on `agent-runtime-vocabulary`. +1. `agents-composition-layer` — the `agents/` 3-layer prompt composition (D58-L), goal/strategy/lens packs + AUTO selector, snapshot pull/render/surface (D60-L), and the `.pi/context → agents/` migration. Depends on the runtime-vocabulary slice now folded into `agent-graph-integration`. 2. `probes-and-transcripts-evolution` — keep probe/transcript artifacts honest as new seams need evidence. ### Parallel / Low-conflict @@ -101,10 +101,10 @@ The POC should maximize assumption falsification rather than merely implement mi ### agent-runtime-vocabulary - **Name:** Session-agent runtime vocabulary fix -- **Linear:** [FE-789](https://linear.app/hash/issue/FE-789/session-agent-runtime-vocabulary-fix) -- **Branch:** `ln/fe-789-session-agent-runtime-vocabulary-fix` -- **Kind:** structural (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) -- **Status:** done +- **Linear:** none — folded into FE-785 (`agent-graph-integration`); FE-789 is no longer planned as a separate frontier/PR. +- **Branch:** landed as a small enabling slice on the FE-785 stack; do not submit a separate FE-789 PR. +- **Kind:** structural sub-slice inside `agent-graph-integration` (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) +- **Status:** folded into `agent-graph-integration` - **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the `goal` axis (`grounding-advance | elicit-I | elicit-II | commitment-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. - **Why now / unlocks:** The code is the last place still carrying the pre-reconciliation model (collapsed lens, missing `propose-graph`/`project-graph`, role-as-state). `propose-graph` must exist before `agent-graph-integration` can run the A14-L real-LLM proof (the `propose-graph → commitGraph` path is the named proof target), and the orthogonal axes are the precondition for the `agents/` 3-layer composition. Foundational and small. - **Acceptance:** @@ -182,7 +182,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Name:** Agent ↔ graph integration through the shared command layer (M5) - **Linear:** [FE-785](https://linear.app/hash/issue/FE-785) -- **Branch:** `ln/fe-785-agent-graph-integration` (stacked on `ln/fe-776-graph-layer-prep-profile`) +- **Branch:** `ln/fe-785-agent-graph-integration` (stacked on `ln/fe-776-graph-layer-prep-profile`; includes the folded runtime-vocabulary slice formerly tracked as FE-789) - **Kind:** structural - **Status:** in-progress - **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations — including `commitGraph` batch mutations for the `propose-graph` direct-commit path (D53-L, D26-L) — elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances for `project-graph`, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. **The primary A14-L proof runs here:** test whether the LLM can produce structurally-legal `commitGraph` batches against the real CommandExecutor with bounded retry. @@ -223,7 +223,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L - **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates review-set proposal metadata, translates drafts to graph batches, and only surfaces dry-run-valid `brunch.review_set_proposal` entries. Next: wire real `project-graph` agent proposal generation or scope synchronous elicitor capture. +- **Current execution pointer:** **(0)** ✓ Runtime vocabulary enabling slice folded into this frontier: `brunch.agent_runtime_state` now has the reconciled goal/strategy/lens/op-mode tuple and legal `propose-graph`/`project-graph` strategies. **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates review-set proposal metadata, translates drafts to graph batches, and only surfaces dry-run-valid `brunch.review_set_proposal` entries. Next: wire real `project-graph` agent proposal generation or scope synchronous elicitor capture. ### subagents-for-proposal-diversity @@ -351,7 +351,6 @@ The POC should maximize assumption falsification rather than merely implement mi ## Recently Completed -- 2026-06-02 `agent-runtime-vocabulary` (FE-789) — Done: session-agent runtime state uses the reconciled D25-L/D40-L/D59-L axes (`goal`, `strategy`, `lens`) with foreground role derived from `op_mode`; concrete axis id unions stay separate from the `auto` selection sentinel, and all three axes accept `auto`; `propose-graph`/`project-graph` are legal strategies; deterministic elicitation lens metadata now uses `intent`; stale collapsed strategy/lens names and stored `agentRole` are gone from runtime-state entries. Verified: targeted runtime/prompting/chrome/RPC/exchange tests and `npm run verify`. - 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. - 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with LSN invariants. Verified: `npm run verify`. - 2026-06-01 `sealed-pi-profile-runtime-state` (FE-776) — Done: prep envelope tied off. Both strands complete: **(a)** Pi harness sealing including sealed profile, runtime-state transcript projection, session display names via Pi `session_info`; **(b)** graph-model lock-and-materialize with Phase 1 (edges) + Phase 2 (nodes) locked in `docs/design/GRAPH_MODEL.md`, code stubs under `src/graph/`, and A20-L persistence spike validating `drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`. `graph-data-plane` (M4 CRUD) is now unblocked. Verified: `npm run verify` after each slice. @@ -365,7 +364,6 @@ nodes: sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) graph-data-plane [done] (M4 CRUD proper) spec-persistence-and-startup [done] (persistence-model fix + startup regression repair) - agent-runtime-vocabulary [done] (session-agent record vocabulary fix) agents-composition-layer [next] (agents/ 3-layer composition + snapshots + .pi/context migration) agent-graph-integration [in-progress] (M5) subagents-for-proposal-diversity [deferred · optional] @@ -380,9 +378,7 @@ edges: graph-data-plane -[hard]-> agent-graph-integration graph-data-plane -[hard]-> spec-persistence-and-startup spec-persistence-and-startup -[hard]-> agent-graph-integration - spec-persistence-and-startup -[hard]-> agent-runtime-vocabulary - agent-runtime-vocabulary -[hard]-> agents-composition-layer - agent-runtime-vocabulary -[hard]-> agent-graph-integration + agent-graph-integration -[hard]-> agents-composition-layer agent-graph-integration -[hard]-> authority-model agent-graph-integration -[hard]-> turn-boundary-reconciliation agent-graph-integration -[optional]-> subagents-for-proposal-diversity From a7b3e6b00ae58a788cb6f4e2028538423c3240ba Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 17:06:52 +0200 Subject: [PATCH 29/34] FE-785: Rename session-agent goals as objectives --- memory/PLAN.md | 2 +- memory/SPEC.md | 12 +++--- src/.pi/__tests__/operational-mode.test.ts | 44 +++++++++++++++++++++- src/.pi/__tests__/prompting.test.ts | 4 +- src/.pi/extensions/operational-mode.ts | 9 +---- src/agents/README.md | 4 +- src/probes/propose-graph-commit-proof.ts | 2 +- 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 5529e4b68..82a5729bd 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -105,7 +105,7 @@ The POC should maximize assumption falsification rather than merely implement mi - **Branch:** landed as a small enabling slice on the FE-785 stack; do not submit a separate FE-789 PR. - **Kind:** structural sub-slice inside `agent-graph-integration` (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) - **Status:** folded into `agent-graph-integration` -- **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the `goal` axis (`grounding-advance | elicit-I | elicit-II | commitment-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. +- **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the objective-shaped `goal` axis (`grounding-advance | elicit-expand | commit-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. - **Why now / unlocks:** The code is the last place still carrying the pre-reconciliation model (collapsed lens, missing `propose-graph`/`project-graph`, role-as-state). `propose-graph` must exist before `agent-graph-integration` can run the A14-L real-LLM proof (the `propose-graph → commitGraph` path is the named proof target), and the orthogonal axes are the precondition for the `agents/` 3-layer composition. Foundational and small. - **Acceptance:** - `AgentLensId` is independent of `AgentStrategyId`; lens ∈ `intent | design | oracle`. diff --git a/memory/SPEC.md b/memory/SPEC.md index 3813b08ba..61f9b65d6 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -227,7 +227,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text, linked structured response entries, and/or terminal structured-exchange toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. - **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. - **D25-L — Strategy and lens are two orthogonal session-agent axes within the `elicitor` role, not separate roles or operational modes.** *Strategies* describe interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`); *lenses* describe topical focus (`intent`, `design`, `oracle`; future execute-mode `plan`, `sync`, `scope`). Both are optional, AUTO-able fields of the projected session-agent record (D40-L) and are stamped onto elicitor-emitted custom entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) as provenance (I18-L); capture/reviewer/audit routing may filter on lens. Strategy determines the commitment mechanism (D26-L); the catalogue is expected to grow. Depends on: D12-L, D17-L, D23-L, D40-L. Supersedes: collapsing strategy and agent role into one vocabulary axis; treating strategies as a sub-kind of lenses; and the prior free-text lens-catalogue names (`propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`, `step-by-step`, `disambiguate-via-examples`), now retired in favor of the strategy×lens axes. -- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation; commitment (the `commitment-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. +- **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation; commitment (`commit-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. - **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** `brunch.establishment_offer` records the agent's current offer tree and recommended next move as durable transcript state. Ambient chrome or web affordances may render the latest offer, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. @@ -242,7 +242,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. - **D58-L — Brunch prompt composition is a thin runtime header plus a gated prompt-resource manifest, not eager selection of every objective pack.** `agents/compose(agentId, sessionState, spec, workspace, snapshots)` runs before Pi provider requests through Brunch's prompt extension and emits: **(1) agent control header** — keyed agent identity, model/thinking expectation, foreground role derived from `op_mode`, and mode/tool-authority summary; **(2) runtime-state header** — current pinned/AUTO `goal`, `strategy`, and `lens`, `spec.readiness_grade`, and workspace posture; **(3) resource manifests** — XML-style ``, ``, ``, and `` entries filtered by `agents/state.ts` legal tuples, grade, `op_mode`, and the agent allow-list, each carrying `{name, description, location}` for a Brunch-owned markdown resource under `src/agents/`; the `{name, description, location}` triples are code-owned in `agents/state.ts`, not filesystem-discovered, honoring D39-L sealing; **(4) compact pushed context** — only the minimal snapshot summary/handles needed to orient the turn, with detailed snapshot content still governed by D60-L. Detailed goal/strategy/lens/method instructions live in Brunch prompt resources and are loaded by the agent with `read` when needed, following the same simple mechanism Pi uses for skills. `AUTO` means the axis is unpinned: the manifest lists legal choices and router instructions tell the agent to choose only from the current manifest, reading the selected resource before applying it when detail matters. Pinned axes point to the pinned resource; code enforces legality and tool gating but does not choose or concatenate large semantic packs on the agent's behalf. Pi-native skills may still carry startup-scoped capabilities, but runtime-state-gated availability is Brunch's manifest, not ambient Pi discovery. `agents/` is a keyed resource registry (`definitions/`, `goals/`, `strategies/`, `lenses/`, `methods/`, `contexts/`); `agents/contexts/` is the D60-L snapshot render layer (code), not a manifest resource family; composition is projection, not a behavioral state machine. Depends on: D23-L, D25-L, D39-L, D40-L, D52-L, D59-L, D60-L. Supersedes: the flat "base + mode + role + strategy + lens + grade + …" layering; the fixed all-packs concatenation in `compose-brunch-prompt.ts`; "role preset / runtime bundle" as the composition unit; direct Layer-2 eager prompt-pack injection as the default mechanism; and `capability` as a parallel name for `method` / ``. -- **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-I` / `elicit-II` (main elicitation levels), `commitment-converge` (reduce / lock down), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO`; in either case D58-L manifests advertise the legal resource(s) rather than injecting the whole goal body. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. +- **D59-L — `goal` is a grade-derived, AUTO-able objective axis, distinct from strategy.** A *goal* is what the session agent currently pursues; a *strategy* is the reusable interaction shape used to pursue it — a goal is pursued *via* a strategy *through* a lens (three orthogonal axes). The goal set is derived/gated by `spec.readiness_grade`: `grounding-advance` (fill grounding and advance the grade), `elicit-expand` (expand the elicited specification graph while ambiguity remains productive), `commit-converge` (reduce / lock down reviewable commitments), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). `goal` defaults to the grade-derived objective, may be pinned, or left `AUTO`; in either case D58-L manifests advertise the legal resource(s) rather than injecting the whole goal body. `elicit-expand` and `commit-converge` intentionally form the diverge/converge pair for the elicitation diamond; `elicit-I` / `elicit-II` are retired because they were phase-like labels, not objectives. "Advance the grade" is a goal, not a strategy — though the `grounding-advance` goal may carry a dedicated default interaction pattern. Depends on: D45-L, D57-L, D58-L. Supersedes: conflating the elicit-lifecycle objective with strategy selection. - **D60-L — "Snapshot" splits into pull / render / surface, and names two distinct subjects.** **Agent-context snapshot** = content the agent reasons over: `cwd` (filesystem kickoff heuristic — `.brunch?`, session count/length, README/markdown sizes, file counts), `graph` (overview), `node` (variable-hop neighborhood). **PULL** is typed, read-only data access owned by the data layer (`graph/snapshot.ts` for graph/node; `session/` for cwd) and bypasses `CommandExecutor` (reads only); the typed value *is* the JSON form. **RENDER** turns the typed value into either an LLM-friendly string (owned solely by `agents/contexts/`, scaled by lens-plane and grade-depth) or JSON (trivial serialization). **SURFACE** delivers it: *pushed* (compose injects at turn boundary), *pulled* (thin `snapshot-*` Pi tools wrap the renderer — markdown in `toolResult.content`, typed JSON in `toolResult.details` per I33-L), or *rpc/ui*. The separate **workspace projection** (`workspace.snapshot` — workspace/session/spec/chrome product state) is a different subject and keeps that name; reserve "snapshot" for the agent-context family. Depends on: D35-L, D52-L, D53-L. Supersedes: pre-rendering snapshots to strings in the pull layer, and scattering snapshot build logic across `graph/`, `agents/contexts/`, and the `snapshot-*` tool stubs. ### Critical Invariants @@ -321,7 +321,7 @@ src/agents/ index.ts [ts] public entry / resource registry definitions/*.md [md+] keyed agents (elicitor foreground; reviewer side). frontmatter = model/thinking + tool authority + allow-lists; body = system prompt - goals/*.md [md] grounding-advance, elicit-I, elicit-II, commitment-converge, capture-posture + goals/*.md [md] grounding-advance, elicit-expand, commit-converge, capture-posture strategies/*.md [md] step-wise-decision-tree, step-wise-disambiguate, propose-graph, project-graph lenses/*.md [md] intent, design, oracle (future execute: plan, sync, scope) methods/*.md [md] run-structured-exchange, infer-and-capture, generate-proposal, @@ -384,7 +384,7 @@ src/agents/ | **Session agent** | The main-thread agent that drives the session forward — `elicitor` now, future `executor` — resolved from `op_mode`. It is the only agent represented in session state (D40-L); side/sub-agents are out-of-band. | | **Strategy** | An optional, AUTO-able session-agent axis (D25-L) describing interaction shape: `step-wise-decision-tree` (single-exchange Q&A), `step-wise-disambiguate` (contrastive examples), `propose-graph` (novel coherent subgraph via direct commit), `project-graph` (derived nodes/edges via review-set). Strategy determines the commitment mechanism (D26-L). Detailed strategy behavior lives in a Brunch prompt resource advertised through D58-L manifests. | | **Lens** | An optional, AUTO-able session-agent axis (D25-L) describing topical focus: `intent`, `design`, `oracle` for elicit mode; future execute-mode `plan`, `sync`, `scope`. Orthogonal to strategy; stamped onto elicitor-emitted entries as provenance (I18-L). Detailed lens behavior lives in a Brunch prompt resource advertised through D58-L manifests. | -| **Goal** | An optional, AUTO-able session-agent objective axis (D59-L): what the agent currently pursues, derived/gated by `spec.readiness_grade` — `grounding-advance`, `elicit-I`, `elicit-II`, `commitment-converge`, plus always-on `capture-posture`. Distinct from strategy (the *how*) and lens (the topical focus). | +| **Goal** | An optional, AUTO-able session-agent objective axis (D59-L): what the agent currently pursues, derived/gated by `spec.readiness_grade` — `grounding-advance`, `elicit-expand`, `commit-converge`, plus always-on `capture-posture`. Distinct from strategy (the *how*) and lens (the topical focus); `elicit-expand` / `commit-converge` are the diverge/converge pair in the elicitation diamond. | | **AUTO** | The unpinned state of an objective axis (`goal` / `strategy` / `lens`): composition advertises the legal choices in the current prompt-resource manifest and instructs the agent to self-select from that manifest only, reading the selected resource when detail matters (D58-L). | | **Brunch Pi Profile** | The sealed programmatic wrapper around embedded Pi: settings policy, resource-loader policy, extension factories, keybinding/command policy, tool policy, and prompt policy. It allows Brunch-owned resources while suppressing ambient `.pi/` behavior. | | **Prompt resource** | A Brunch-owned markdown file under `src/agents/` containing detailed goal, strategy, lens, method, or agent-definition guidance. Prompt resources are loaded by the agent with `read` when needed; they are product control-plane assets, not ambient Pi prompt templates. | @@ -479,8 +479,8 @@ src/agents/ | **Grounding anchor** | One sentence-scale fact captured during early elicitation that contributes to the grounding bundle. | | **Establishment offer** | A `brunch.establishment_offer` custom transcript entry summarising the elicitor's perceived gaps, the available lens strategies for the next move, the recommended lens, and the agent's confidence. Source of ambient affordances rendered in the chrome region; inspectable post-hoc and fixture-able. Orientation artifact, not a default exhaustive strategy menu. | | **Elicitor intent hint** | A `brunch.elicitor_intent_hint` custom transcript entry emitted alongside a prompt or proposal, declaring `lens` and semantic targets (e.g. expected ontological sub-type) for downstream capture/reviewer/future-auditor routing and extraction guidance. | -| **Review set** | A cohesive batch proposal presented to the user for review-cycle acceptance (approve / request changes / reject), modeled on the GitHub PR-review-cycle. Used for batch-proposal flows and for design/oracle commitment review sets (the `commitment-converge` goal, D59-L). | -| **Commitment review set** | A focus-primary review set: design-oriented sets primarily commit requirement/invariant-like intent claims; oracle-oriented sets primarily commit criterion/check/example-like verification claims. Support/provenance edges are part of the accepted batch. Driven by the `commitment-converge` goal (D59-L), not a persisted posture. | +| **Review set** | A cohesive batch proposal presented to the user for review-cycle acceptance (approve / request changes / reject), modeled on the GitHub PR-review-cycle. Used for batch-proposal flows and for design/oracle commitment review sets (the `commit-converge` goal, D59-L). | +| **Commitment review set** | A focus-primary review set: design-oriented sets primarily commit requirement/invariant-like intent claims; oracle-oriented sets primarily commit criterion/check/example-like verification claims. Support/provenance edges are part of the accepted batch. Driven by the `commit-converge` goal (D59-L), not a persisted posture. | | **Batch acceptance** | The single `CommandExecutor` call (`acceptReviewSet`) that commits an entire review set atomically as one LSN and one change-log entry, attributed to the user. Partial acceptance and accept-with-edits are not product operations. | | **Reviewer** | An agent role that runs async after batch acceptance, scoped to the accepted batch plus graph neighborhood, analyzing for coherence / completeness / gaps. Authority is narrow: writes only `reconciliation_need` records via `CommandExecutor`. | | **Anchor scenario** | A particular vignette embedded inside one alternative pitch to ground its framing. Transcript-rendered; not persisted as a graph entity. | diff --git a/src/.pi/__tests__/operational-mode.test.ts b/src/.pi/__tests__/operational-mode.test.ts index 6dbfd522e..48501e385 100644 --- a/src/.pi/__tests__/operational-mode.test.ts +++ b/src/.pi/__tests__/operational-mode.test.ts @@ -123,7 +123,7 @@ describe('Brunch agent runtime-state projection', () => { operationalMode: 'elicit', agentStrategy: 'project-graph', agentLens: 'oracle', - agentGoal: 'elicit-II', + agentGoal: 'commit-converge', }; const events: Record unknown> = {}; const activeTools: string[][] = []; @@ -259,6 +259,27 @@ describe('Brunch agent runtime-state projection', () => { agentLens: 'intent', agentGoal: 'not-a-goal', }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'elicit-I', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'elicit-II', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'commitment-converge', + }, ]) { expect(() => appendBrunchAgentRuntimeSwitch(manager, invalidState as unknown as BrunchAgentState), @@ -305,6 +326,27 @@ describe('Brunch agent runtime-state projection', () => { agentLens: 'intent', agentGoal: 'not-a-goal', }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'elicit-I', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'elicit-II', + }, + { + schemaVersion: 1, + operationalMode: 'elicit', + agentStrategy: 'step-wise-decision-tree', + agentLens: 'intent', + agentGoal: 'commitment-converge', + }, ]) { expect( projectBrunchAgentState([runtimeEntry(invalidState as unknown as BrunchAgentState)]), diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index 31fec4609..12d25e64c 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -91,7 +91,7 @@ describe('Brunch prompt-pack topology', () => { ...DEFAULT_BRUNCH_AGENT_STATE, agentStrategy: 'step-wise-disambiguate', agentLens: 'design', - agentGoal: 'elicit-I', + agentGoal: 'elicit-expand', }; const events: Record unknown> = {}; @@ -166,7 +166,7 @@ describe('Brunch prompt-pack topology', () => { ...DEFAULT_BRUNCH_AGENT_STATE, agentStrategy: 'propose-graph', agentLens: 'oracle', - agentGoal: 'commitment-converge', + agentGoal: 'commit-converge', }; appendBrunchAgentRuntimeSwitch(manager, latestState, 'user'); const switchedPromptResults = await Promise.all( diff --git a/src/.pi/extensions/operational-mode.ts b/src/.pi/extensions/operational-mode.ts index 7f6c842b7..cabeca73e 100644 --- a/src/.pi/extensions/operational-mode.ts +++ b/src/.pi/extensions/operational-mode.ts @@ -35,12 +35,7 @@ export type AgentStrategyId = export type AgentStrategySelection = AutoAxisSelection | AgentStrategyId; export type AgentLensId = 'intent' | 'design' | 'oracle'; export type AgentLensSelection = AutoAxisSelection | AgentLensId; -export type AgentGoalId = - | 'grounding-advance' - | 'elicit-I' - | 'elicit-II' - | 'commitment-converge' - | 'capture-posture'; +export type AgentGoalId = 'grounding-advance' | 'elicit-expand' | 'commit-converge' | 'capture-posture'; export type AgentGoalSelection = AutoAxisSelection | AgentGoalId; export type ToolPolicyId = 'elicit-read-only'; export type PromptPackId = 'brunch-base' | 'elicit' | 'elicitor'; @@ -123,7 +118,7 @@ export const AGENT_ROLE_DEFINITIONS: Record = defaultLens: 'auto', allowedLenses: ['intent', 'design', 'oracle'], defaultGoal: 'grounding-advance', - allowedGoals: ['grounding-advance', 'elicit-I', 'elicit-II', 'commitment-converge', 'capture-posture'], + allowedGoals: ['grounding-advance', 'elicit-expand', 'commit-converge', 'capture-posture'], promptPackIds: ['elicitor'], }, }; diff --git a/src/agents/README.md b/src/agents/README.md index 07dd6d8fc..f54e47494 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -17,7 +17,7 @@ Projected from linear `brunch.agent_runtime_state` entries at turn start ``` op_mode = elicit | execute (future) ← the only stored WHO field foreground role (elicitor) is DERIVED from op_mode, never stored -goal = grounding-advance | elicit-I | elicit-II | commitment-converge +goal = grounding-advance | elicit-expand | commit-converge | capture-posture [pinned | AUTO] grade-derived (D59-L) strategy = step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph [pinned | AUTO] (D25-L) @@ -68,7 +68,7 @@ agents/ ├── definitions/ keyed agents; frontmatter = model/thinking + tool authority + allow-lists, │ ├── elicitor.md body = system prompt │ └── reviewer.md -├── goals/ grounding-advance, elicit-I, elicit-II, commitment-converge, capture-posture +├── goals/ grounding-advance, elicit-expand, commit-converge, capture-posture ├── strategies/ step-wise-decision-tree, step-wise-disambiguate, propose-graph, project-graph ├── lenses/ intent, design, oracle ├── methods/ run-structured-exchange, infer-and-capture, generate-proposal, diff --git a/src/probes/propose-graph-commit-proof.ts b/src/probes/propose-graph-commit-proof.ts index fa88690f1..08f8b3f01 100644 --- a/src/probes/propose-graph-commit-proof.ts +++ b/src/probes/propose-graph-commit-proof.ts @@ -116,7 +116,7 @@ export async function runProposeGraphCommitProof( operationalMode: 'elicit', agentStrategy: 'propose-graph', agentLens: 'intent', - agentGoal: 'commitment-converge', + agentGoal: 'commit-converge', }; appendBrunchAgentRuntimeSwitch(workspace.session.manager, runtimeState, 'extension'); const agentDir = options.agentDir ?? getAgentDir(); From e7e8adf6c1f366bfe0855660466e756fc16ed953 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 17:26:04 +0200 Subject: [PATCH 30/34] Document public RPC exchange vocabulary --- memory/SPEC.md | 94 ++++++++++++------------ src/rpc/README.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 46 deletions(-) create mode 100644 src/rpc/README.md diff --git a/memory/SPEC.md b/memory/SPEC.md index 61f9b65d6..0617e998d 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -76,11 +76,11 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c 17. Brunch must support action, radio (single-select), checkbox (multi-select), freeform, and freeform-plus-choice response surfaces as typed transcript-backed interactions. Every option-selection structured exchange must allow an optional user-authored `comment` as additional context separate from custom/Other answers. Multi-question/questionnaire surfaces are deferred; when a complex shape is needed before a bespoke UI lands, Brunch may use just-in-time schema-tagged JSON over `ctx.ui.editor` or an equivalent product relay. In TUI mode a pending response request may replace the default input surface with custom UI; in RPC/probe/web-relay contexts the same semantic interaction may travel through Brunch product handlers or Pi's supported extension UI dialogs. Brunch must be able to project elicitation exchanges from Pi JSONL for post-exchange capture, including registered structured-exchange tool results whose `toolResult.details` is the self-contained structured response payload. 18. Brunch must support `#`-mentions of graph entities anchored to stable IDs, with session-scoped staleness tracking that produces discretionary re-read hints during `prepareNextTurn`. 19. Brunch must enforce a workspace state hierarchy `workspace(cwd) → spec → session`, where the workspace is only the current working directory invocation root, the user explicitly picks or creates one spec within that workspace before any agent loop runs, and then picks or creates a session within that spec. Spec selection persists across `/new`, and each session binds to exactly one spec. -20. Brunch must support multiple elicitation lenses within the `elicitor` agent role, with the agent owning lens selection and offer through transcript-native establishment offers; lens metadata is carried on elicitor-emitted custom entries for downstream routing. +20. Brunch must support multiple elicitation lenses within the `elicitor` agent role, with the agent owning lens selection and offer through transcript-native structured exchanges; lens metadata is carried on elicitor-emitted structured-exchange payload facets for downstream routing. 21. Brunch must distinguish single-exchange elicitation flows from batch-proposal/review-set flows by capture and commitment mechanism: single-exchange answers are captured synchronously by the elicitor at turn boundaries, while batch proposals carry structured entity-draft payloads and are committed only through review-set approval. 22. Brunch must maintain a spec-owned readiness grade as a forward gate inside the `elicit` operational mode. Grounding establishes the frame required for main elicitation; later grades unlock commitment and planning/export/execute posture without forbidding earlier gathering or refinement. 23. Brunch must support a review-cycle acceptance pattern for batch proposals and commitment review sets — approve / request changes (triggering regeneration) / reject — with batch acceptance committed atomically as one CommandExecutor call; partial acceptance is not representable. -28. Brunch must support assistant-first elicitation session driving over the public JSON-RPC surface: after workspace/spec/session activation, a client can start or resume elicitation, observe the current pending system/assistant-originated structured exchange, submit a response through Brunch product methods, and let Brunch advance the transcript-backed loop without ambient user prompt injection. +28. Brunch must support assistant-first session driving over the public JSON-RPC surface: after workspace/spec/session activation, a client can prompt or resume the agent loop, observe the current pending system/assistant-originated structured exchange, submit a typed exchange response through Brunch product methods, and let Brunch advance the transcript-backed loop without ambient user prompt injection. #### Verification & fixtures @@ -110,7 +110,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | A11-L | Pi's `prepareNextTurn` plus custom-message delivery are sufficient to express side-task result delivery without inventing a second event plane or forking pi. | medium | open | D15-L | M5 + M7: side-task registry wiring and next-turn delivery proof. | | A13-L | If Brunch later adds deferred observer/auditor jobs, a durable queue keyed by session id and elicitation-exchange entry range can recover async audit/backfill after process interruption without reintroducing canonical chat/turn tables; whether this shares storage with a generalized work-item/reconciliation table can be deferred. | medium | open | D18-L, I14-L | Deferred until async audit/backfill lands: restart/idempotence tests exercise exchange-keyed jobs once graph writes exist. | | A14-L | LLM elicitor agents can reliably produce graph-structurally-legal graph mutations — both `commitGraph` batches (D53-L) and review-set proposals (D27-L) — as well-formed entity drafts and category-typed edge drafts per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) that pass `CommandExecutor` structural validation. The `commitGraph` path under `propose-graph` strategy (D26-L) is the primary proof target: the agent must produce valid multi-node multi-edge batches with intra-batch references from a graph-vocabulary prompt after concept-level user acceptance. | medium | partially validated | D27-L, D51-L, D53-L | **CommitGraph subclaim validated 2026-06-02** by the product-path `propose-graph-commit` probe: the default Brunch runtime factory registered real `read_graph`/`commit_graph` tools, `claude-opus-4-7` produced one structurally legal `commit_graph` batch on the first attempt, and `CommandExecutor` persisted 4 nodes + 4 edges (LSN 2). Artifacts: `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`. **Review-set dry-run substrate validated 2026-06-02** by `review-set-proposal.test.ts`: reviewable proposals must carry lens/epistemic/grounding metadata, translate to `commitGraph` input, pass `CommandExecutor.dryRunCommitGraph`, and stay off the review surface when structurally illegal. Remaining open subclaim: real LLM `project-graph` proposal generation against that substrate. Fallback options remain constrained generation, retry-with-feedback, or NL-parse-at-accept if later legality rates regress. | -| A15-L | Establishment hints as transcript-native custom entries (`brunch.establishment_offer`) provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms lens offers are reconstructable from transcript; chrome region renders ambient affordances from the latest such entry. | +| A15-L | Establishment hints carried as structured-exchange payload facets provide sufficient inspectability, fixture-ability, and ambient-affordance source without a separate establishment-needs graph substrate or standalone `brunch.establishment_offer` entry family; whether such a substrate ever shares storage with reconciliation needs can be deferred. | medium | open | D25-L, D30-L | M5+: fixture inspection confirms establishment-offer facets are reconstructable from transcript-backed structured exchanges; chrome/web orientation regions render ambient affordances from the latest such facet. | | A16-L | Reviewer triggering policy (always-on vs lens-keyed) and reviewer scope (batch + how-far-neighborhood) can be deferred to per-lens decisions without architectural commitment now. | low | open | D29-L | M5+: empirical — reviewer integration reveals which policy avoids unacceptable next-turn latency without losing relevant findings. | | A17-L | A user-level temperamental preference for interrogative vs proposal-based elicitation meaningfully affects adoption and eventually warrants expression as a user-level setting. | low | open | D25-L, D26-L | Deferred; surfaces from outer-loop walkthroughs and adversarial fixtures once both single-exchange and batch-proposal flows exist in product. | | A18-L | Hiding unsupported Pi built-ins from autocomplete plus blocking dangerous session effects is sufficient for the POC product shell even though exact interactive built-ins remain callable until Pi exposes command policy. | medium | open | D2-L, D24-L, D34-L, D35-L | `pi-ui-extension-patterns` product-shell review after command-containment and dynamic Brunch chrome evidence; strict suppression requires a Pi upstream/API change if residual exposure is unacceptable. | @@ -147,7 +147,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D4-L — One shared mutation surface owns graph truth.** Every semantic graph mutation routes through Brunch-owned typed command handlers responsible for validation, structural legality, optimistic concurrency, event emission, audit attribution, and coherence triggering. Agents and adapters must not touch the ORM or SQLite directly. Depends on: A3-L. Supersedes: —. - **D20-L — Command execution owns the pre-M6 authority seam.** Callers submit product commands to a Brunch `CommandExecutor` and receive a structured result; they do not call a standalone authority service or graph persistence directly. The executor is the public mutation boundary that hides attribution, optimistic concurrency, structural validation, the minimal pre-M6 policy classifier, transaction execution, LSN allocation, change-log append, and coherence-trigger hooks. Before M6, the policy logic may be deliberately small, but the result shape must already include `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` so early RPC, print, agent-tool, deferred observer/auditor, and side-task code cannot bake in permissive mode-specific shortcuts. Depends on: D4-L, D16-L. Supersedes: the separate optional `AuthorityGate` / generic policy-service mental model. -- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's proposal custom entry (`brunch.review_set_proposal`) contains the graph entities and edges that *would* be created on acceptance — edge drafts follow the locked contract in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (`{category, sourceId, targetId, stance?, basis: 'accepted_review_set'}`) rather than a free-form `relation` string — in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, and enqueues any reviewer job. "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L. Supersedes: any caller-side multi-step "patch then commit" mental model. +- **D27-L — Review-set proposals are structured entity-draft payloads; batch acceptance is one atomic `CommandExecutor` call.** The elicitor's review-set proposal is carried inside a structured-exchange `present_review_set` / `request_review` flow rather than as a standalone `brunch.review_set_proposal` transcript-entry family. Its payload contains the graph entities and edges that *would* be created on acceptance — edge drafts follow the locked contract in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (`{category, sourceId, targetId, stance?, basis: 'accepted_review_set'}`) rather than a free-form `relation` string — in a form `CommandExecutor` can dry-run-validate at proposal time so `structural_illegal` / `policy_blocked` discriminants surface before the user reviews. Only proposals that pass this dry-run validation are surfaced as user-reviewable review sets; invalid generations stay internal to retry/regeneration paths rather than becoming review UI state. Acceptance is one `acceptReviewSet` command that consumes one LSN, writes the entire batch in one transaction, appends one change-log entry attributed to the user, triggers coherence updates, and enqueues any reviewer job. "Accept with edits" does not exist as a primitive: the cycle is approve / request changes (triggers regeneration of a successor proposal) / reject. Applies to batch-proposal flows and commitment review sets. Depends on: A14-L, D4-L, D20-L, D26-L. Supersedes: any caller-side multi-step "patch then commit" mental model or standalone review-set-proposal custom-entry contract. - **D53-L — `commitGraph` is a single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references.** The propose-graph strategy's load-bearing tool for direct graph commitment after concept-level user acceptance (D26-L). The agent produces one tool call with a `nodes` array (each carrying a temporary batch `ref`, `kind`, and content fields) and an `edges` array (each carrying `category`, source/target as either an intra-batch `ref` or an existing node id, optional `stance`, and `rationale`). The CommandExecutor validates all nodes structurally, assigns real NodeIds to each batch ref, resolves intra-batch and existing-node references, validates all edges per the closed category set and structural invariants in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), allocates one LSN, writes all nodes + edges + change-log in one SQLite transaction, and returns success with created ids or `structural_illegal` with diagnostics sufficient for agent self-correction. On validation failure the agent may retry within a bounded budget; the user does not see intermediate failures. `commitGraph` and `acceptReviewSet` (D27-L) are parallel paths to the same CommandExecutor — one for direct agent-authored commits after concept acceptance, one for user-reviewed batch proposals. Depends on: D4-L, D20-L, D51-L, D52-L. Supersedes: —. #### Transport & client @@ -155,7 +155,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D5-L — Brunch JSON-RPC is the single public product protocol.** Brunch exposes one public product RPC surface over stdio, WebSocket, and in-process handlers. Product clients — web UI, CLI probes, TUI adapters, and future relays — call Brunch method families and should not coordinate raw Pi RPC plus Brunch product RPC themselves. Pi RPC may be used behind a Brunch adapter for agent-loop mechanics and Pi extension UI, but it is not a second public product API. HTTP exists only as a transport shim (static bundle, health, uploads, webhooks). The Brunch stdio surface is also the agent-as-user probe driver interface, even when that driver internally relays Pi RPC events. Depends on: A5-L. Supersedes: treating raw Pi RPC as the product API for Brunch data. - **D10-L — Web client is a native Brunch React app over one WebSocket RPC client.** TanStack Router + TanStack Query + Brunch-owned elicitation/transcript primitives (Vercel AI SDK UI or TanStack AI style). `pi-web-ui` is not reused. The browser is a thin remote head over Brunch RPC method families, not a second product runtime or REST-backed data client. Depends on: D5-L. Supersedes: —. - **D17-L — Brunch semantics ride one transcript/event substrate, not parallel channels.** Pi JSONL transcript entries — ordinary messages, assistant tool-call/toolResult exchanges, and custom messages/entries — plus `deliverAs: "nextTurn" | "followUp"` and `prepareNextTurn` are the load-bearing mechanism for structured elicitation prompts/responses, `worldUpdate`, mention-staleness hints, and side-task-result delivery. New product semantics should compose onto this substrate before inventing a second event plane or a parallel chat/turn store. Depends on: D5-L, D6-L, D12-L, D15-L. Supersedes: custom-message-only interpretations of structured elicitation. -- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes named method families such as `rpc.*`, `workspace.*`, `session.*`, `elicitation.*`, `graph.*`, `coherence.*`, and `command.*`; each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch command layer. Subscriptions are first-class and may provide initial state plus updates, and adapter-only agent/UI events may be relayed into product-shaped notifications, but Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model and any two-public-RPC-surface split. +- **D19-L — Keep product RPC/read architecture thin: named method families over projection handlers.** Brunch exposes concrete named methods, not vague feature buckets or generic records. The canonical public RPC vocabulary is maintained in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md): `rpc.discover`; `workspace.snapshot`, `workspace.selectionState`, `workspace.activate`; `session.promptExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, `session.submitMessage`, and `session.exchanges`; future graph projections such as `graph.overview`, `graph.nodeNeighborhood`, `graph.changesSince`, and graph-adjacent `graph.coherenceSummary`. Each read handler projects from the canonical store that owns the fact (Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log), and each mutation handler routes to the Brunch-owned command/session layer. Subscriptions are first-class and may provide initial state plus updates, and adapter-only agent/UI events may be relayed into product-shaped notifications, but Brunch must not create a generic read-gateway platform, REST read model, DB-backed chat/turn projection, or canonical cross-store event spine merely to keep clients in sync. Proof-era names such as `session.startElicitation`, `elicitation.respond`, `session.elicitationExchanges`, and `session.transcriptDisplay` are implementation debt, not target vocabulary. Depends on: D5-L, D6-L, D10-L, D16-L. Supersedes: the heavier “unified read gateway” mental model, vague `elicitation.*` / `command.*` public families, and any two-public-RPC-surface split. - **D23-L — Transport modes, operational modes, agent roles, strategies, and lenses are separate axes.** TUI, RPC, print, and web are transport modes: ways of driving or observing the same Brunch host through Pi/Brunch harness seams. Operational modes are top-level authority/tooling postures such as `elicit` and future `execute`. Agent roles are active workers within an operational mode (`elicitor`, `reviewer`, `reconciler`, future `executor/orchestrator`, `scout`, `researcher`, and any deferred observer/auditor). Strategies are interaction plans; lenses are narrower interpretive/extraction/review framings. M1 print mode is therefore only a transport proof-of-life: it boots through the same host/coordinator, renders a snapshot of product-shaped state, and exits without running an agent turn. A future single-turn headless print run is deferred until runtime bundle selection/defaults are explicit. Depends on: D1-L, D5-L, D19-L, D21-L, D40-L. Supersedes: overloading “mode” to mean both transport and agent strategy, or using “agent mode” for role/preset/lens interchangeably. - **D33-L — Transport connections are client attachments, not Brunch sessions.** A Brunch session is a durable linear Pi JSONL transcript bound to exactly one spec; WebSocket connections, stdio streams, TUI instances, and browser tabs are ephemeral presentation attachments to product resources. Session-specific RPC methods should name their target spec/session explicitly or operate through an explicit client attachment; they must not infer durable session identity merely from the transport connection. `.brunch/workspace.json` remains launch/default acceleration, not concurrency authority. During the POC, Brunch targets a one-writer/many-observer local model: one interactive driver (typically TUI/agent) may write while web clients attach read-only for visual projections. Depends on: D5-L, D10-L, D11-L, D19-L, D21-L, D24-L. Supersedes: treating `/rpc`, a WebSocket, or workspace default state as the active session itself. @@ -166,9 +166,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c │ ▼ Brunch public JSON-RPC surface - ├─ workspace.* / session.* / graph.* / coherence.* reads - ├─ command.* named mutations ──► CommandExecutor ──► SQLite graph/change log - └─ agent.* / elicitation UI relay ──► Pi adapter + ├─ workspace.* / session.* reads and session transcript writes + ├─ graph.* reads and graph-adjacent coherence projections + ├─ agent/tool graph mutations ──► CommandExecutor ──► SQLite graph/change log └─► Pi AgentSession or pi --mode rpc └─► Pi JSONL transcript ``` @@ -186,10 +186,10 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c Brunch Pi adapter receives extension_ui_request(editor) │ ▼ - Brunch public surface exposes product-shaped pending elicitation + Brunch public surface exposes product-shaped pending exchange │ ▼ - Web/CLI responds through Brunch (e.g. elicitation.respond) + Web/CLI responds through Brunch (e.g. session.submitExchangeResponse) │ ▼ Adapter replies to Pi with extension_ui_response(value = JSON) @@ -199,7 +199,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c ``` - **D48-L — Brunch owns public RPC method discovery.** `rpc.discover` is the product-level discovery method for Brunch JSON-RPC. It returns Brunch method names, descriptions, parameter schemas, result schemas, and compact examples for the public surface that the current host supports. Schemas are JSON-Schema-shaped per D41-L, regardless of whether their source authoring library is Zod or TypeBox; discovery is not a promise to expose every internal handler or every raw Pi RPC command. Pi `get_commands` remains slash-command/prompt-template/skill discovery for Pi's `prompt` command and must not be treated as Brunch method discovery. Depends on: D5-L, D19-L, D41-L. Supersedes: hardcoded private probe knowledge and any plan to copy Pi's non-JSON-RPC command union as Brunch's protocol shape. -- **D49-L — Pending structured exchange lifecycle is Brunch-owned over public RPC.** The first product lifecycle is intentionally small: `session.startElicitation` starts or resumes the assistant-first elicitation loop for the activated spec/session; `session.pendingExchange` returns the current pending structured exchange or idle/completed status; `elicitation.respond` submits the terminal response for one pending exchange; `session.transcriptDisplay` and `session.elicitationExchanges` remain read projections over transcript truth. The implementation may delegate internally to Pi RPC/editor fallback or in-process structured-exchange handlers, but the client contract is Brunch JSON-RPC. Polling these methods is sufficient for the first proof; subscriptions stay required by R12 but are not prerequisite for the initial deterministic permutation parity run. Depends on: D5-L, D12-L, D19-L, D33-L, D37-L, D38-L, D48-L. Supersedes: command-first probes where the client sends a raw Pi slash command and answers `extension_ui_request(editor)` directly. +- **D49-L — Pending structured exchange lifecycle is Brunch-owned over public RPC.** The stable public lifecycle is session-native, not elicitation-mode-native: `session.promptExchange` starts, resumes, or advances the assistant-first loop until there is a pending exchange, idle/completed state, `needs_human`, or blocker; `session.pendingExchange` reads the current unresolved structured exchange without advancing the loop; `session.submitExchangeResponse` submits exactly one terminal response for a pending `request_*` tool shape (`request_answer`, `request_choice`, `request_choices`, `request_review`, or future variants); `session.submitMessage` handles ordinary non-exchange user text or explicit interruptions without silently answering a pending exchange; and `session.exchanges` projects structured exchange history from transcript truth. `session.transcriptDisplay` is a debug/print/diagnostic projection, not a core web-product state API. The implementation may delegate internally to Pi RPC/editor fallback or in-process structured-exchange handlers, but public clients speak the Brunch methods named in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md). Polling these methods is sufficient for the first proof; subscriptions stay required by R12 but are not prerequisite for the initial deterministic permutation parity run. Depends on: D5-L, D12-L, D19-L, D33-L, D37-L, D38-L, D48-L. Supersedes: command-first probes where the client sends a raw Pi slash command and answers `extension_ui_request(editor)` directly; the proof-era public names `session.startElicitation`, `elicitation.respond`, and `session.elicitationExchanges`. #### Persistence @@ -221,15 +221,15 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D11-L — Workspace state hierarchy `workspace(cwd) → spec → session`, with spec and session selection gated before any agent loop.** A Brunch workspace is the single cwd where the CLI is invoked; it is not a user-created container and there is only one per launch context. The cwd's human-readable label may be derived by `src/project-identity.ts` from shallow project manifests (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`) or directory basename, but that label is presentation metadata, not a second selectable container. The first durable choice is the spec: create a new DB-backed spec, or resume an existing spec. Within an existing spec, the second durable choice is the session: create a new session or resume an existing session. Creating a new spec implicitly creates its first session. Spec selection is durable across `/new` and persisted as `.brunch/workspace.json` `{project,current:{specId,sessionId},posture}`. Each Pi session is bound to exactly one spec by a collapsed `brunch.session_binding` custom entry `{schemaVersion,specId}` at session start; switching specs selects or creates another session rather than mutating the spec of the current session. Depends on: D6-L. Supersedes: treating “workspace” as the user-created product object in the boot dialog, string `spec-*` ids, and session self-id guards in the binding. - **D21-L — Workspace session coordination is the spec/session boot seam.** Brunch owns a narrow `WorkspaceSessionCoordinator` for boot, spec inventory, spec/session selection, selected-session reopening, and `/new` session creation. It is the only product module allowed to create or open Pi sessions for Brunch user flows and the only module allowed to write `brunch.session_binding`; callers inspect workspace inventory and activate a product decision rather than mutating a session's bound spec directly. The coordinator hides `SessionManager.create/open/continueRecent(cwd, ".brunch/sessions/")`, DB-backed spec lookup through `CommandExecutor`, internal session-start binding for pi-created replacement sessions, `.brunch/workspace.json` current spec/session acceleration, binding validation, and chrome-state derivation. Because pi defers appending session JSONL until an assistant message exists, the coordinator flushes Brunch's binding when it is created, refreshes it at `before_agent_start`, and performs the final pre-assistant flush from Brunch's internal assistant `message_start` hook after pi has persisted the user message but before assistant persistence; each flush reloads the session file so pi's next assistant append does not duplicate the already-written prefix. Depends on: D6-L, D11-L. Supersedes: the loose `SpecRegistry` + caller-orchestrated session-binding mental model, treating `.brunch/workspace.json` as an implicit instruction to resume without user-visible Brunch flow, or resolving spec names from JSONL. - **D22-L — TUI boot is Brunch-owned before Pi interactive runtime begins.** Brunch's TUI mode may use `@earendil-works/pi-tui` directly for a pre-Pi startup gate that selects or creates the active spec/session before `InteractiveMode.run()`. After activation, persistent chrome is mounted by an internal Brunch extension through Pi's public UI seams. Brunch does not fork pi, monkeypatch `InteractiveMode`, or expose generic pi extension configuration to users for product boot/chrome. Depends on: D2-L, D21-L, D36-L. Supersedes: private-header/monkeypatch approaches for M0 chrome and raw readline-only spec selection as the durable TUI product flow. -- **D12-L — Elicitation-first interaction, transcript-native structured prompts.** Brunch treats system/assistant prompts and user responses as Pi transcript truth. Structured action/choice/freeform surfaces may be represented by Brunch custom entries when needed, but there is no DB-owned prompt/response entity; at idle, the session waits on a system/assistant-originated elicitation prompt. Depends on: D6-L, D11-L. Supersedes: —. -- **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction may be represented through the thinnest Pi-supported transcript seam for its shape. The current preferred seam for Brunch structured exchanges is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details; runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof now drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Brunch custom messages/entries remain valid for establishment offers, review-set proposals, annotations, and future product-native displays, but they are not mandatory for every structured exchange. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. +- **D12-L — Elicitation-first interaction, transcript-native structured prompts.** Brunch treats system/assistant prompts and user responses as Pi transcript truth. Structured action/choice/freeform surfaces are preferably represented by registered structured-exchange `present_*` / `request_*` toolResult families when durable structure is needed; there is no DB-owned prompt/response entity. At idle, the session waits on a system/assistant-originated elicitation prompt. Depends on: D6-L, D11-L, D37-L. Supersedes: standalone custom-entry carriers as the default structured interaction shape. +- **D37-L — Structured elicitation is Pi-transcript-native; structured exchanges use durable toolResult families.** A system/assistant-originated structured interaction should use the thinnest Pi-supported transcript seam for its shape. The preferred Brunch seam is registered Pi tool results: `present_*` tools persist and display assistant-originated offer/question/proposal material, `request_*` tools collect and persist the user response, and future `capture_*` tools persist assistant analysis of candidate semantic changes without mutating graph truth. The assistant `toolCall` supplies call identity and arguments, but durable semantic display is the `toolResult` row rendered by that tool's `renderResult`; `renderCall` is transient header/progress only and must not carry Brunch semantic display. `toolResult.content` is rich markdown that is both transcript display content and model-readable context; `toolResult.details` is the structured projection/recovery payload. The landed Zod-authored target details model under `src/.pi/extensions/structured-exchange/schemas/` uses checked `schema` + `v` discriminants, `exchange_id`, compact `tool_meta` sequence/sibling metadata, exactly-one request outcome presence (`answered` | `cancelled` | `unavailable`), user-authored `comment` versus runtime-authored `message`, strict `present_candidates` rubrics/`graph_refs`, and intentionally minimal no-graph `capture_*` details. Runtime tools/projections still use the existing tuple details model until a deliberate migration slice rewires them to these exports. Implemented present/request tools use `executionMode: "sequential"`; FE-744's real Pi RPC ordering proof validates that same-assistant-message `present_options → request_choice` persists the present `toolResult` before the request `toolResult` and emits the present `tool_execution_end` before the request UI opens, and the public Brunch RPC parity proof drives the current deterministic tuple-shaped permutation set over product methods only. RPC event consumers should not assume `request_*` `tool_execution_start` precedes its extension UI request, because Pi may emit the UI request first. Standalone Brunch custom entries remain valid for genuinely non-exchange session facts such as `brunch.session_binding`, `brunch.agent_runtime_state`, lens switches, side-task results, and mention/world-update delivery; they are not the default carrier for establishment offers, review-set proposals, intent hints, or structured response surfaces. RPC/web paths answer the same semantic pending interaction through Brunch product handlers or Pi-supported dialog fallbacks rather than depending on TUI-only `ctx.ui.custom()`. Depends on: D12-L, D13-L, D17-L, D19-L, D38-L, D41-L. Supersedes: treating all structured offers as Brunch custom entries, treating render lifecycle state as durable transcript state, relying on ephemeral dialog results detached from transcript truth, modeling a structured exchange as one split-brain tool row whose present half lives in `renderCall`, or treating the retired scope-card contract as canonical after the schema README and tests have landed. - **D38-L — JSON-over-editor is the Pi-RPC compatibility seam for complex extension UI, not a second product API.** Pi RPC supports `ctx.ui.select`, `confirm`, `input`, and `editor`, but not `ctx.ui.custom()`. When a structured-exchange tool needs a complex shape (multi-select, review-style response, or a deferred multi-question/questionnaire shape) over raw Pi RPC, the tool may call `ctx.ui.editor()` with schema-tagged JSON prefill and validate the returned JSON before producing normal `toolResult.content` plus self-contained `toolResult.details`. A Brunch-aware adapter may render that JSON as a native product form and translate the user response back into Pi's documented `extension_ui_response`; public clients still speak Brunch RPC methods/events, not ad hoc raw Pi RPC extensions. Depends on: D5-L, D19-L, D33-L, D37-L. Supersedes: inventing unsupported Pi RPC command types for Brunch interactions or exposing raw editor JSON as the product UX. -- **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text, linked structured response entries, and/or terminal structured-exchange toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. +- **D13-L — Capture-aware elicitation exchange projection.** Post-exchange capture consumes derived elicitation exchanges: a prompt-side span (system/assistant/tool-side entries since the previous response, including structured/internal prompt content) plus a response-side span (user text and/or terminal structured-exchange `request_*` toolResults whose `details` encode the answer). Role/span alternation is the default projection in Brunch-supported linear sessions, but typed structured-exchange results override the naive "all toolResults are prompt side" rule where needed for deterministic replay. Depends on: D12-L, D24-L, D37-L. Supersedes: treating Pi message role alone as sufficient to classify structured elicitation response spans. - **D14-L — `#`-mentions are stable-handle text references resolved by Brunch, with a session-scoped mention ledger.** Pi autocomplete persists only the inserted `AutocompleteItem.value` as ordinary transcript text; popup labels/descriptions are UI-only. Brunch autocomplete may search by title/description, but insertion must rewrite to a stable handle (`#A12`, `#I7`, or equivalent node handle) that Brunch can resolve to the graph entity id through a read-only lookup/re-read tool when the agent needs detail. Brunch prompt injection (`before_agent_start`) teaches agents how to interpret the handles; Brunch-owned parsing/indexing, not Pi autocomplete, creates mention-ledger state. Per-session `(entity_id, snapshotted_lsn)` ledger drives discretionary `brunch.mention_staleness_hint` entries in `prepareNextTurn`. Depends on: A9-L, I4-L. Supersedes: assuming Pi autocomplete persists hidden mention metadata. -- **D25-L — Strategy and lens are two orthogonal session-agent axes within the `elicitor` role, not separate roles or operational modes.** *Strategies* describe interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`); *lenses* describe topical focus (`intent`, `design`, `oracle`; future execute-mode `plan`, `sync`, `scope`). Both are optional, AUTO-able fields of the projected session-agent record (D40-L) and are stamped onto elicitor-emitted custom entries (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) as provenance (I18-L); capture/reviewer/audit routing may filter on lens. Strategy determines the commitment mechanism (D26-L); the catalogue is expected to grow. Depends on: D12-L, D17-L, D23-L, D40-L. Supersedes: collapsing strategy and agent role into one vocabulary axis; treating strategies as a sub-kind of lenses; and the prior free-text lens-catalogue names (`propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`, `step-by-step`, `disambiguate-via-examples`), now retired in favor of the strategy×lens axes. +- **D25-L — Strategy and lens are two orthogonal session-agent axes within the `elicitor` role, not separate roles or operational modes.** *Strategies* describe interaction shape (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`); *lenses* describe topical focus (`intent`, `design`, `oracle`; future execute-mode `plan`, `sync`, `scope`). Both are optional, AUTO-able fields of the projected session-agent record (D40-L) and are stamped onto structured-exchange payload facets (for example establishment offers, intent hints, and review/proposal material) when those facets need downstream routing; capture/reviewer/audit routing may filter on lens. Strategy determines the commitment mechanism (D26-L); the catalogue is expected to grow. Depends on: D23-L, D40-L. Supersedes: lens-as-role, strategy-as-mode, and standalone elicitor-intent/establishment/review custom-entry families as the default carrier. - **D26-L — Elicitation flows split by capture and commitment mechanism, not by a hard extractive/generative phase boundary.** Three commitment mechanisms: (1) Single-exchange flows (`step-wise-decision-tree`, `step-wise-disambiguate`, and ordinary structured questions) are captured synchronously by the elicitor post-exchange per D18-L. (2) Review-set flows (`project-graph` strategy) carry structured entity-draft payloads at proposal time and become durable only through review-set approval (D27-L). (3) Direct-commit flows (`propose-graph` strategy) present a concept to the user via structured exchange with rubric axes, choices, and a recommendation; when the user accepts a concept, the agent autonomously generates and persists the full subgraph through `commitGraph` (D53-L) without intermediate entity-level user review — the user accepts a concept, not a graph shape. Design/oracle lenses may appear during ordinary elicitation; commitment (`commit-converge` goal and active review-set state, D59-L) changes what can be pinned, not what topics may be explored. Depends on: D18-L, D25-L, D45-L, D53-L. Supersedes: a single uniform "agent asks questions" mental model, the observer-owned extractive vs elicitor-owned generative split as the primary architecture, and assuming all batch-graph writes require review-set approval. - **D30-L — Grounding advances readiness for main elicitation; strategies remain available with honest epistemic signaling.** A minimum grounding bundle — *domain anchor*, *protagonist anchor*, *pain/pull anchor*, *constraint anchor* — establishes the frame required to move the spec from `grounding_onboarding` toward `elicitation_ready`. Lenses and strategies are not refused merely because grounding is thin, but their output resolution and epistemic load must honestly reflect what grounding supports: speculative outputs are visibly hedged and lower-authority, while grounded outputs may drive capture and later review-set projection. Grounding coverage should be explicit in offers/proposals where it affects confidence or gate transitions. Depends on: D26-L, D45-L. Supersedes: gating-by-refusal as a UX move and over-focusing readiness on generative lenses alone. -- **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** `brunch.establishment_offer` records the agent's current offer tree and recommended next move as durable transcript state. Ambient chrome or web affordances may render the latest offer, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu. +- **D32-L — Establishment offers are orientation artifacts, not a default next-action menu.** Establishment-offer material records the agent's current offer tree and recommended next move as durable structured-exchange payload state when it is part of an exchange, not as a mandatory standalone transcript entry family. Ambient chrome or web affordances may render the latest establishment-offer facet, and Brunch may expose a user-invoked orientation view summarizing what is established vs open, but Brunch does not surface an exhaustive lens/offer chooser by default; the agent still owns next-move selection unless the user explicitly asks to inspect alternatives. Depends on: D25-L, D30-L, A15-L. Supersedes: UI interpretations that turn establishment offers into a persistent strategy menu or separate transcript store. - **D31-L — A four-axis meta-rubric is a soft heuristic for fan-out comparison rubrics across all three flows; not architecturally enforced.** When generating comparison rubrics for fan-out alternatives across candidate-spec, technical-design, and verification-design flows, the elicitor attempts to express each axis in terms of (*legibility / cost-of-knowing*, *failure modes*, *coverage / range*, *commitment*). Project-specific axes are allowed alongside; the meta-frame is dropped when it doesn't fit. The hypothesis (uniform comparison UI across all three flows is more useful than per-flow improvisation) is testable via fixture comparison; promote to schema/UI only if it holds up. Depends on: D25-L, D26-L. Supersedes: a hardcoded per-flow rubric. - **D45-L — Spec readiness is stored as a DB-row grade, not as session-local phase, workflow location, or elicitation posture.** The `specs` row owns `readiness_grade = grounding_onboarding | elicitation_ready | commitments_ready | planning_ready`; it also owns identity fields `id`, `name`, and `slug`. Grade is a forward gate: it unlocks later strategies, commitment review sets, and eventual export/plan/execute operational modes, but it never forbids returning to earlier gathering/refinement when new ambiguity appears. `elicitation_posture` and `commitment_focus` are retired as spec fields; active review-set state, strategy/lens selection, and workspace posture should carry those concerns when they become concrete. Grade changes route through `CommandExecutor` and carry audit context in the change log. Depends on: D18-L, D20-L, D30-L. Supersedes: treating “phase” as a user-facing location/stepper or hidden session memory, storing `elicitation_posture`, or adding `commitment_focus` as canonical spec state. - **D46-L — Retired: commitment posture as persisted spec state.** Design and oracle lenses may still create accepted graph material, and cohesive review sets still commit atomically through `acceptReviewSet` per D27-L, but Brunch no longer models `pinning` or `commitment_focus` as spec-row state. Future commitment projection should derive from readiness grade, active strategy/lens/review-set state, and graph evidence rather than a persisted posture enum. Depends on: D27-L, D28-L, D45-L. Supersedes: per-item requirement/criterion confirmation, treating design/oracle commitment phases as first permission to discuss design/oracle topics, and storing commitment posture/focus on the spec. @@ -237,7 +237,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D50-L — `capture_*` tools persist transcript-native ANALYSIS, not graph mutations.** Brunch may add a third structured-exchange tool family such as `capture_analysis` alongside `present_*` and `request_*`. A `capture_*` tool returns a normal persisted Pi `toolResult` with Brunch details and markdown content describing likely graph/node/edge changes, grouped into high-confidence candidates that could be committed later and low-confidence candidates that should drive clarification. `capture_*` output is transcript-visible evidence for Markdown/ASCII review and later graph-mutation cross-checking, but it is not graph truth and never bypasses the `CommandExecutor`. Product UI should hide capture analysis entirely if Pi exposes a supported hide seam; otherwise `renderResult` should be maximally collapsed/minimal while preserving full persisted `toolResult.content`/`details` for transcript renderers. The current schema layer deliberately defines only minimum capture details (`schema`, `v`, `exchange_id`, `tool_meta`) and rejects graph payloads; richer analysis payloads and shared component subparts (`Preface`, prompt body, option list, answer summary, capture analysis) require a later `ln-design` pass before implementation. Depends on: D12-L, D17-L, D18-L, D37-L, D41-L, D47-L. Supersedes: using ad hoc hidden custom entries, probe-only side files, or graph writes as the first carrier for pre-graph analysis. - **D44-L — Subagents are main-agent-invoked, blocking Pi tool calls that gather data and propose variants for candidate-proposal generation.** Brunch may register a single `subagent` Pi tool whose parameters are `{ agent, task }` or `{ tasks: [] }` (parallel). Each invocation runs as an isolated `pi --mode json -p --no-session --no-skills --no-extensions` subprocess inheriting Brunch's sealed Pi Profile (D39-L); the subagent has no inherited conversation context so the task string must carry everything it needs. Agent definitions are declarative markdown files under `src/.pi/extensions/subagents/agents/*.md` with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. Concurrency cap lives in an externalized [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json) (default 4) so it can be reviewed and updated without SPEC churn. The subagent's result text is returned directly to the main agent as tool result content; subagents do not append custom messages to the session log on their own behalf, do not invoke the `CommandExecutor`, and do not gain access to the parent's Brunch RPC handlers. POC starter agents split into two families: - **Data gatherers** — read-only context fetchers whose output grounds proposals: **scout** (codebase recon: `read`, `grep`, `find`, `ls`), **researcher** (web research: `web_search`, `web_fetch`), and **graph-reader** (read-only Brunch graph projection tools). - - **Variant proposer** — **proposer** (no tools): given a grounding bundle plus a batch-proposal lens frame, emits exactly one well-formed variant of a candidate proposal. The main agent achieves diversity by issuing parallel `tasks: []` invocations of `proposer` with intentionally distinct framings — the subagent realization of the "design it twice" pattern from `ln-design` and the parallel fan-out anticipated by `ln-oracles`. Each `proposer` invocation runs in its own isolated context so variants don't cross-contaminate; the main agent collects N outputs and composes the comparison via the D31-L meta-rubric (and/or project-specific axes) before writing a `brunch.review_set_proposal` entry through the elicitor flow. `proposer` is system-prompt-only by design: its grounding inputs come entirely through the task string the main agent assembles from preceding `scout` / `researcher` / `graph-reader` calls. + - **Variant proposer** — **proposer** (no tools): given a grounding bundle plus a batch-proposal lens frame, emits exactly one well-formed variant of a candidate proposal. The main agent achieves diversity by issuing parallel `tasks: []` invocations of `proposer` with intentionally distinct framings — the subagent realization of the "design it twice" pattern from `ln-design` and the parallel fan-out anticipated by `ln-oracles`. Each `proposer` invocation runs in its own isolated context so variants don't cross-contaminate; the main agent collects N outputs and composes the comparison via the D31-L meta-rubric (and/or project-specific axes) before writing a review-set structured-exchange payload through the elicitor flow. `proposer` is system-prompt-only by design: it cannot read the graph, write files, or call `CommandExecutor`; the main agent supplies grounded inputs and owns any product write. Future per-lens proposers may exist only if the generic proposer proves too blunt. This division mirrors the batch-proposal flow in D26-L: `propose-graph` and `project-graph` strategies can delegate variant generation to fan-out `proposer` invocations while `intent` / `design` / `oracle` lenses frame the proposal subject; purely extractive single-exchange work may stay main-agent-only. Worker-style write-capable subagents are deferred until an execute operational mode lands. Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. NDJSON stream events from the subprocess drive TUI tool-progress UI; a `subagent.progress` RPC subscription for headless/web is deferred. Subagents are an optional enhancement to candidate-proposal diversity, not a load-bearing M0–M9 substrate: they enhance R20/D27-L proposal generation when bandwidth permits. Depends on: D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L. Distinct from: D15-L Side task (non-blocking, status-via-custom-message), the deferred Side chat (user-invoked overlay; see Future Direction Register). Supersedes: —. - **D36-L — Spec/session selection is a reusable hierarchical decision model with transport-specific presentations.** Brunch owns a pure spec/session selection model that renders cwd-scoped inventory without calling the user-created object a “workspace”. In TUI mode, the model may present a fast “continue last session” affordance when `.brunch/workspace.json` points to a valid spec+session; otherwise, or after “other spec/session”, the durable tree is: `create new spec → provide spec name → session created automatically`; `resume existing spec → choose existing spec → create a new session OR resume existing session → choose existing session`. The UI should not list every spec as a top-level action label; “resume existing spec” is the top-level intent, and the spec list is the next screen/scrollable selector. The model returns a product decision (`new spec`, `new session for spec`, `open session`, `continue selected session`, `cancel/quit`) without opening Pi sessions or mutating `.brunch/workspace.json` itself. The `WorkspaceSessionCoordinator` activates that decision and owns all persistence/session-binding effects. TUI startup and in-session paths share branded `pi-tui` components and colocated logo assets under `src/.pi/components/workspace-dialog`; adapters differ only in terminal lifecycle and Pi session-replacement mechanics (`ProcessTerminal`/`TUI.showOverlay` before Pi starts, `ctx.ui.custom(..., { overlay: true })` inside Pi), not in product semantics. RPC/headless transports must not invoke the TUI picker; they expose the same initial-selection requirement and activation decisions as JSON-RPC/product results so CLI JSON-RPC clients can select or create spec/session correctly. Depends on: D11-L, D21-L, D24-L, D33-L. Supersedes: implicit resume of `.brunch/workspace.json` on TUI launch, Pi `/resume`/`/new` as Brunch's product session chooser, one-off startup-only picker implementations, a flat action list that says “workspace” for specs, top-level `resume spec X` labels, and a separate intermediate action chooser for switching. - **D42-L — Session naming is a lifecycle side task over Pi `session_info`, not spec identity.** Brunch should use Pi session lifecycle hooks to opportunistically generate a short human-readable session name that characterizes what happened in the transcript. The preferred trigger is `session_shutdown` for `quit`, `new`, and `resume` replacements because it sees the just-finished transcript and can name it before later picker lists need to distinguish sessions; `session_before_compact` or post-compaction (`session_compact`) may be used to refresh names after major summarization, and a manual command can force regeneration for debugging. The naming call should mirror the model-selection pattern in the local `summarize.ts` extension example: choose a cheap/fast authorized model, extract user/assistant text plus salient tool calls from the current branch, ask for a concise title, and append a Pi `session_info` entry through `SessionManager.appendSessionInfo`. Naming must be best-effort and non-blocking with a tight budget: failures, missing auth, empty transcripts, or shutdown aborts leave the session unnamed rather than blocking session replacement or exit. Generated names label sessions in pickers and chrome, but do not affect spec ids, session bindings, graph truth, or replay semantics. Depends on: D6-L, D17-L, D21-L, D35-L. Supersedes: using spec title or session UUID alone as the only durable display label once transcripts have meaningful content. @@ -265,9 +265,9 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I14-L | If Brunch introduces deferred observer/auditor jobs, they are keyed by session id plus elicitation-exchange entry-range ids and have durable status; replay/restart cannot enqueue duplicate jobs for the same exchange, and job writes never become the primary freshness path for the next elicitor turn. | deferred/planned only if observer-audit queue lands (M5+ restart/idempotence tests) | D18-L, D4-L | | I15-L | Every review-set acceptance routes through `CommandExecutor` as one atomic `acceptReviewSet` command producing one LSN, one change-log entry, and one transaction over the entire batch. Partial acceptance is not representable through any product API. | planned (M5+ batch-acceptance command tests; review-set fixture parity) | D20-L, D27-L; I1-L, I11-L | | I16-L | Reviewer-attributed writes target only the `reconciliation_need` substrate; no reviewer-attributed `CommandExecutor` call writes graph entities, edges, change-log entries directly, or any other record class. | planned (M5+ architectural test on reviewer command writers; reviewer-attributed command-result audit) | D29-L; I2-L, I11-L | -| I17-L | Every batch-proposal or review-set entry (`brunch.review_set_proposal`) declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | partially covered (`review-set-proposal.test.ts` covers the product proposal helper rejecting missing `epistemicStatus` and empty grounding/support before surfacing a reviewable entry; thin-vs-rich grounding fixture semantics remain future work) | D30-L, D46-L; A14-L | -| I18-L | Every elicitor-emitted prompt or proposal custom entry (`brunch.elicitor_intent_hint`, `brunch.establishment_offer`, `brunch.review_set_proposal`) carries a `lens` field; capture, reviewer, and future observer/auditor routing filters on this field. | partially covered (`review-set-proposal.test.ts` covers review-set proposal lens validation; establishment/intent-hint routing tests remain planned with capture/reviewer slices) | D25-L, D26-L, D29-L | -| I19-L | Brunch-controlled flows do not create or navigate Pi session branches, and Brunch transcript readers fail fast on non-linear JSONL rather than flattening, migrating, or branch-selecting. | partially covered (M3 transcript loader requires exactly one Pi session header, rejects malformed non-header entry shapes, and rejects non-linear child graphs, `parentSession`, and `branch_summary`; product-facing exchange projection helper preserves the non-linear error discriminant and is used by RPC and fixture replay assertions; `session.elicitationExchanges` returns a product-shaped error for non-linear selected sessions over stdio and WebSocket JSON-RPC; Brunch TUI extension cancels `session_before_tree` and `session_before_fork`; Pi command-containment source/RPC evidence shows `session_before_fork` can also cancel clone/fork effects but exact interactive built-ins still need product-shell policy if visibility must be strict; dynamic chrome remains projection-only and does not add branch or mutation authority) | D24-L, D6-L, D11-L, D13-L, D34-L, D35-L | +| I17-L | Every batch-proposal or review-set structured-exchange payload declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | partially covered (`review-set-proposal.test.ts` covers the current product proposal helper rejecting missing `epistemicStatus` and empty grounding/support before surfacing a reviewable payload; thin-vs-rich grounding fixture semantics and structured-exchange carrier migration remain future work) | D30-L, D46-L; A14-L | +| I18-L | Every elicitor-emitted prompt/proposal payload facet that needs downstream routing (establishment offer, intent hint, review/proposal material) carries a `lens` field inside the structured-exchange details; capture, reviewer, and future observer/auditor routing filters on this field. | partially covered (`review-set-proposal.test.ts` covers current proposal lens validation; establishment/intent-hint routing tests and structured-exchange carrier migration remain planned with capture/reviewer slices) | D25-L, D26-L, D29-L | +| I19-L | Brunch-controlled flows do not create or navigate Pi session branches, and Brunch transcript readers fail fast on non-linear JSONL rather than flattening, migrating, or branch-selecting. | partially covered (M3 transcript loader requires exactly one Pi session header, rejects malformed non-header entry shapes, and rejects non-linear child graphs, `parentSession`, and `branch_summary`; product-facing exchange projection helper preserves the non-linear error discriminant and is used by RPC and fixture replay assertions; `session.exchanges` returns a product-shaped error for non-linear selected sessions over stdio and WebSocket JSON-RPC; Brunch TUI extension cancels `session_before_tree` and `session_before_fork`; Pi command-containment source tests prove no exposed Brunch command path creates branches) | D24-L, D34-L | | I20-L | Every user-reviewable review-set proposal has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface as reviewable review sets. | partially covered (`CommandExecutor.dryRunCommitGraph` and `review-set-proposal.test.ts` cover product-helper dry-run validation, invalid proposal non-surfacing, no graph mutation during dry-run, and dry-run/commit validation parity; real agent-generated `project-graph` proposal fixtures remain planned) | D27-L; A14-L | | I21-L | WebSocket/stdio/TUI client attachment state never becomes the canonical spec/session binding: every session-consuming projection validates the durable `brunch.session_binding`, and write-capable session operations must target an explicit session or future write lease rather than whichever transport connection happens to be open. | partially covered (M3 RPC/WebSocket explicit session projection tests validate durable `brunch.session_binding` for read paths; FE-744 web live-update tests prove WebSocket notifications only invalidate/refetch canonical projection handlers after RPC-originated structured-exchange mutations; future write-lease tests remain planned when web input lands) | D10-L, D19-L, D21-L, D33-L | | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | @@ -280,7 +280,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I29-L | Subagent subprocesses inherit Brunch Pi Profile sealing: every `subagent` tool invocation spawns `pi --mode json -p --no-session --no-skills --no-extensions` with an explicit per-agent tool allowlist and per-agent model; subagents never load ambient user/project `.pi/` skills, prompts, themes, extensions, context files, or behavior-shaping settings; subagents never gain direct access to the parent's `CommandExecutor`, Brunch RPC handlers, or graph persistence; subagent results return to the main agent only as tool result content (no side-effect transcript writes). | planned (subagent subprocess argv tests; isolation audit asserting absent ambient-resource leakage; tool-allowlist conformance test per starter agent) | D2-L, D39-L, D44-L; I2-L, I11-L, I24-L | | I30-L | Elicitor post-exchange capture only commits high-confidence extractive facts, concrete reconciliation needs, and justified spec readiness-grade updates; low-confidence implications remain in structured-exchange preface/question material and do not become graph truth until clarified, accepted, or explicitly escalated. | planned (M5 capture fixtures comparing committed graph facts and preface-only interpretations against transcript evidence) | D18-L, D47-L; A22-L | | I31-L | `readiness_grade` is a forward gate, not a workflow location: higher grades unlock later strategies/commitments/export paths but do not make earlier gathering/refinement invalid or unavailable; all grade mutations route through `CommandExecutor` and carry audit through the change log. | partial (Card 1: specs table plus `createSpec` / `getSpec` / `updateReadinessGrade` command tests; M5 prompt/tool-policy tests for grade-gated availability remain) | D20-L, D45-L | -| I32-L | Public RPC structured-exchange driving never requires a client to speak raw Pi RPC: after Brunch method discovery and workspace/spec/session activation, each pending assistant-originated exchange is answered exactly once through `elicitation.respond`, and the deterministic permutation run produces linear Pi JSONL whose transcript display and elicitation-exchange projections preserve the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity (`rpc.discover` contract tests, pending/respond lifecycle tests, current public-RPC structured-exchange permutations, terminal non-answered status handling, option content/rationale parity, no repeated deterministic prompts, and transcript/projection oracle in `src/probes/public-rpc-parity-proof.ts`) | R11, R16, R17, R24, R27, R28; D5-L, D12-L, D37-L, D48-L, D49-L | +| I32-L | Public RPC structured-exchange driving never requires a client to speak raw Pi RPC: after Brunch method discovery and workspace/spec/session activation, each pending assistant-originated exchange is answered exactly once through `session.submitExchangeResponse`, and the deterministic permutation run produces linear Pi JSONL whose structured exchange projection preserves the same prompt/answer/status/comment artifacts as the equivalent TUI structured-exchange path. | covered for deterministic FE-744 parity under proof-era method names (`rpc.discover` contract tests, pending/respond lifecycle tests, current public-RPC structured-exchange permutations, terminal non-answered status handling, option content/rationale parity, no repeated deterministic prompts, and transcript/exchange parity assertions); target method rename coverage remains to be implemented per D49-L. | D5-L, D48-L, D49-L; I10-L, I13-L, I21-L, I23-L | | I33-L | `capture_*` analysis entries are transcript evidence only: they persist as Brunch structured-exchange `toolResult` rows, are included by Brunch-semantic transcript renderers, are hidden or collapsed in TUI display, and never mutate graph truth or bypass `CommandExecutor`. | partially covered (minimum capture details schemas parse/export and reject graph payload fields; future runtime capture-analysis schema/rendering tests plus transcript renderer fixtures still need to prove persisted result rendering and TUI hide/collapse behavior; later graph-capture fixtures compare analysis candidates against committed graph mutations) | D17-L, D18-L, D37-L, D47-L, D50-L; I2-L, I11-L, I23-L, I30-L | | I34-L | `commitGraph` batch validation is all-or-nothing: if any node or edge in the batch is structurally illegal, the entire batch is rejected and no partial state is persisted; the agent receives diagnostics sufficient for bounded self-correction retry. | covered (22 tests in `command-executor.test.ts` — edge failure rolls back nodes, mixed-batch rejection, diagnostic sufficiency) | D53-L; I1-L, I11-L | | I35-L | Graph context snapshots support multiple detail levels: a cursory/compact full-graph overview for orientation, and detailed node-neighborhood snapshots with configurable hop depth for focused work. Context builders in `agents/contexts/` orchestrate which level to inject or advertise based on mode/goal/strategy/lens/grade. | partially covered (`getGraphOverview` + `getNodeNeighborhood` in `snapshot.ts` with 10 tests; context-builder integration deferred to M5) | D52-L, D53-L, D58-L | @@ -430,10 +430,10 @@ src/agents/ | **Canonical store** | The persistence surface that owns a fact: Pi JSONL for session transcript truth, `.brunch/workspace.json` for lightweight workspace binding state, SQLite graph/change log for graph truth and coherence substrates. | | **Elicitation prompt** | System- or assistant-originated transcript span that prompts/directs the user's next response. At idle, a Brunch-supported linear session ends with an unresolved elicitation prompt. | | **User response** | User-originated text and/or structured action selection responding to the current elicitation prompt. There is no ambient chat input in the POC model. | -| **Elicitation exchange** | A derived projection over Brunch-supported linear Pi JSONL: prompt-side span (system/assistant/tool-side entries since the prior response, excluding terminal structured-exchange results) plus response-side span (the user's text, linked structured action entries, and/or terminal structured-exchange toolResult details). This is the default post-exchange capture unit. | -| **Structured elicitation entry** | Optional Brunch custom transcript entry used when an elicitation prompt/offer or response carries actions, choices, or other deterministic UI structure. Plain generative prompts can remain ordinary Pi messages. | -| **Structured offer** | A system/assistant-originated prompt, proposal, or question that owns the response surface until answered, cancelled, marked unavailable, or explicitly declared display-only. A distinct `skipped` terminal state is deferred until product pressure distinguishes “declined but continue” from cancellation or an explicit `none`/`other` answer. Depending on shape, it may be represented by a Brunch custom entry/message, a review-set proposal entry, or a registered Pi `present_*`/`request_*` tool tuple whose result details carry the structured display and response. | -| **Pending exchange** | Product-shaped view of the current unresolved structured offer for one activated spec/session. Public RPC clients read it through `session.pendingExchange` and close it through `elicitation.respond`; it is a projection/adapter state over transcript truth and in-flight Pi extension UI, not a canonical turn table. | +| **Elicitation exchange** | A derived projection over Brunch-supported linear Pi JSONL: prompt-side span (system/assistant/tool-side entries since the prior response, excluding terminal structured-exchange results) plus response-side span (the user's text and/or terminal structured-exchange `request_*` toolResult details). This is the default post-exchange capture unit. | +| **Structured exchange** | Transcript-native `present_*` / `request_*` / future `capture_*` toolResult tuple used when an elicitation prompt/offer/response carries durable actions, choices, review payloads, or other deterministic UI structure. Plain generative prompts can remain ordinary Pi messages. | +| **Structured offer** | A system/assistant-originated prompt, proposal, or question that owns the response surface until answered, cancelled, marked unavailable, or explicitly declared display-only. A distinct `skipped` terminal state is deferred until product pressure distinguishes “declined but continue” from cancellation or an explicit `none`/`other` answer. The target carrier is a registered Pi `present_*`/`request_*` tool tuple whose result details carry the structured display and response; older custom-entry carriers are proof/history, not the preferred product shape. | +| **Pending exchange** | Product-shaped view of the current unresolved structured offer for one activated spec/session. Public RPC clients read it through `session.pendingExchange` and close it through `session.submitExchangeResponse`; it is a projection/adapter state over transcript truth and in-flight Pi extension UI, not a canonical turn table. | | **Agent-as-user driver** | A scripted or generative client that drives Brunch only through the public JSON-RPC surface as if it were a user: discover methods, activate workspace/spec/session, observe prompts, answer pending exchanges, and report blockers/frictions for probe reports. | | **RPC structured-exchange parity proof** | The FE-744 product proof that a public Brunch RPC agent-as-user can complete the current deterministic structured-exchange permutations and leave Pi JSONL plus Brunch projections comparable in semantic kind and quality to a TUI-driven session. Contrasts with future generative elicitation-quality probes and with the raw Pi RPC structured-exchange editor fallback proof, which is supporting evidence only. | | **Structured-exchange preface** | Plain prose in a structured-exchange payload that summarizes non-committed working interpretation before asking the next question. It may mention exploratory tool findings or implied graph candidates, but it is not graph truth. | @@ -443,14 +443,14 @@ src/agents/ | **Capture tool** | A future `capture_*` structured-exchange tool (for example `capture_analysis`) whose normal persisted `toolResult` records ANALYSIS: high-confidence candidate graph mutations and low-confidence clarification candidates grounded in transcript evidence. It is transcript-visible but UI-hidden when possible, otherwise maximally collapsed; it is never a graph mutation. | | **ANALYSIS transcript section** | Human-reviewable transcript rendering of `capture_*` tool results. ANALYSIS explains candidate node/edge changes and uncertainties before graph persistence or before comparing later graph mutations to the transcript; it is evidence, not authority. | | **Structured exchange result details** | The structured payload in a structured-exchange toolResult. The target Zod-authored model uses checked `schema` + `v`, `exchange_id`, and `tool_meta`; request details use property presence (`answered`, `cancelled`, or `unavailable`) plus typed answer/choice/review `comment` data; `message` is reserved for runtime-authored cancellation/unavailable explanations; minimal capture details carry sequence identity only until a later design approves richer analysis payloads. Brunch projection should not need render lifecycle state to rebuild the exchange. | -| **Offer response** | The terminal structured answer to a structured offer, represented either as a linked Brunch custom entry or as self-contained `request_*` toolResult details. It is transcript truth, not an ephemeral UI return value. | +| **Offer response** | The terminal structured answer to a structured offer, represented as self-contained `request_*` toolResult details. It is transcript truth, not an ephemeral UI return value. | | **JSON-editor fallback** | A Pi-RPC-compatible adapter for complex interactive shapes: the tool calls `ctx.ui.editor()` with schema-tagged JSON prefill; a Brunch-aware client renders a real form and returns filled JSON through Pi's documented `extension_ui_response`; the tool validates and persists a normal structured result. | -| **Elicitation UI relay** | The adapter path that translates Pi extension UI requests (including JSON-editor fallback) into Brunch public RPC pending-elicitation events/methods, then translates product responses back into Pi `extension_ui_response` messages. | +| **Elicitation UI relay** | The adapter path that translates Pi extension UI requests (including JSON-editor fallback) into Brunch public RPC pending-exchange events/methods, then translates product responses back into Pi `extension_ui_response` messages. | | **Deferred observer/auditor job** | Optional durable async work item keyed by session id and elicitation-exchange entry-range ids. If introduced, it audits or backfills exchange analysis and survives process restart, but it is not the primary path for next-turn graph freshness. | | **Lens switch** | A durable `brunch.lens_switch` transcript entry recording that the active agent/session changed lenses. The switch event is distinct from the lens concept itself. | | **Side task** | Main-agent-invoked, non-blocking work item tracked by the Brunch `SideTaskRegistry`. The main agent fires it and does not await a return value; the only path it influences the main agent is by appending a custom-message status update to the session log that arrives at the next-turn boundary via `prepareNextTurn`. Side-task writes route through the `CommandExecutor`. Distinct from Subagent (blocking) and Side chat (user-invoked). | | **Subagent** | Main-agent-invoked, **blocking** Pi tool call (`subagent`) that runs an isolated `pi` subprocess with a per-agent tool allowlist and per-agent model. Has no inherited conversation context, no `CommandExecutor` access, and no Brunch RPC access. Result text returns directly as tool result content. POC starter agents split into **data gatherers** (scout / researcher / graph-reader — read-only context fetchers that ground proposals) and a **variant proposer** (proposer — system-prompt-only; one variant per invocation, fan-out via parallel mode realizes the "design it twice" pattern). | -| **Proposer subagent** | The system-prompt-only starter subagent that emits exactly one well-formed candidate-proposal variant per invocation given a grounding bundle plus a batch-proposal lens frame. Diversity arises from parallel `tasks: []` invocations with intentionally distinct framings; the main agent assembles outputs into a `brunch.review_set_proposal` via the D31-L meta-rubric. Realizes the "design it twice" / parallel-fan-out pattern from `ln-design` and `ln-oracles` skills in subagent form. | +| **Proposer subagent** | The system-prompt-only starter subagent that emits exactly one well-formed candidate-proposal variant per invocation given a grounding bundle plus a batch-proposal lens frame. Diversity arises from parallel `tasks: []` invocations with intentionally distinct framings; the main agent assembles outputs into review-set structured-exchange proposal details via the D31-L meta-rubric. Realizes the "design it twice" / parallel-fan-out pattern from `ln-design` and `ln-oracles` skills in subagent form. | | **Subagent registry** | The set of registered subagent definitions loaded from `src/.pi/extensions/subagents/agents/*.md` at extension activation. Brunch-owned only for the POC; cross-extension agent registration is deferred. | | **Subagent agent definition** | A markdown file with TypeBox-validated frontmatter (`name`, `description`, `tools`, `model`) plus a system-prompt body. The frontmatter is the registry contract; the body is the subagent's standing instructions. | | **Auto-compaction extension** | The Brunch-owned `session_before_compact` extension (`src/.pi/extensions/auto-compaction.ts`) that renders the preserved anchor set as a deterministic markdown header and prepends it to an LLM-generated narrative summary. Resolves its summarization model through the active agent definition's model preference; falls through to Pi default compaction on auth/empty-output/unexpected errors. | @@ -474,11 +474,11 @@ src/agents/ | **Probe brief** | Optional future input text for an agent-as-user probe. A brief is not a canonical artifact family by itself; if brief-based golden fixtures return, they produce normal probe runs and transcript artifacts. | | **Elicitation lens** | Retired term. The interaction-shape axis is now **Strategy** (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`) and the topical-focus axis is **Lens** (`intent`, `design`, `oracle`) — two orthogonal session-agent axes (D25-L). The prior free-text catalogue (`step-by-step`, `disambiguate-via-examples`, `propose-scenarios-with-tradeoffs`, `propose-design-shapes`, `propose-oracle-ensembles`, `project-requirements-from-upstream`) is superseded. | | **Single-exchange elicitation flow** | A prompt/answer exchange such as step-by-step questioning or contrastive disambiguation. The elicitor captures high-confidence extractive content synchronously post-exchange; low-confidence implications stay in preface/question material. | -| **Batch-proposal flow** | A proposal/review flow with structured entity-draft payloads in `brunch.review_set_proposal` entries. Durable graph changes land only through review-set approval. | +| **Batch-proposal flow** | A proposal/review flow with structured entity-draft payloads in structured-exchange proposal details. Durable graph changes land only through review-set approval. | | **Grounding bundle** | The minimum set of anchors required to establish the frame for main elicitation: a *domain anchor*, a *protagonist anchor*, a *pain/pull anchor*, and a *constraint anchor*. Captured technical constraints land in the constraint anchor and bound subsequent technical-design fan-outs. | | **Grounding anchor** | One sentence-scale fact captured during early elicitation that contributes to the grounding bundle. | -| **Establishment offer** | A `brunch.establishment_offer` custom transcript entry summarising the elicitor's perceived gaps, the available lens strategies for the next move, the recommended lens, and the agent's confidence. Source of ambient affordances rendered in the chrome region; inspectable post-hoc and fixture-able. Orientation artifact, not a default exhaustive strategy menu. | -| **Elicitor intent hint** | A `brunch.elicitor_intent_hint` custom transcript entry emitted alongside a prompt or proposal, declaring `lens` and semantic targets (e.g. expected ontological sub-type) for downstream capture/reviewer/future-auditor routing and extraction guidance. | +| **Establishment offer** | A structured-exchange payload facet summarising the elicitor's perceived gaps, available strategies for the next move, recommendation, and confidence. Source of ambient affordances rendered in chrome/web orientation regions; inspectable post-hoc and fixture-able through transcript replay. Orientation artifact, not a default exhaustive strategy menu. | +| **Elicitor intent hint** | A structured-exchange payload facet emitted alongside a prompt or proposal, declaring `lens` and semantic targets for downstream capture/reviewer/future-auditor routing and extraction guidance. | | **Review set** | A cohesive batch proposal presented to the user for review-cycle acceptance (approve / request changes / reject), modeled on the GitHub PR-review-cycle. Used for batch-proposal flows and for design/oracle commitment review sets (the `commit-converge` goal, D59-L). | | **Commitment review set** | A focus-primary review set: design-oriented sets primarily commit requirement/invariant-like intent claims; oracle-oriented sets primarily commit criterion/check/example-like verification claims. Support/provenance edges are part of the accepted batch. Driven by the `commit-converge` goal (D59-L), not a persisted posture. | | **Batch acceptance** | The single `CommandExecutor` call (`acceptReviewSet`) that commits an entire review set atomically as one LSN and one change-log entry, attributed to the user. Partial acceptance and accept-with-edits are not product operations. | @@ -514,7 +514,7 @@ The structural/behavioral split is the key discipline: never let a behavioral fi | Dimension | Score | Notes | Raised by | | --- | --- | --- | --- | -| Observability | partial, improving to high by M4/M5 | Text-native artifacts are planned (`.brunch/workspace.json`, Pi JSONL, command results, graph exports, coherence exports, fixture bundles). Generative-lens material adds further text-native surfaces: `brunch.review_set_proposal`, `brunch.establishment_offer`, `brunch.elicitor_intent_hint` entries plus reviewer-finding `reconciliation_need` records. *Structural* observability is high; *behavioral* observability (proposal quality, lens-recommendation appropriateness, reviewer precision) remains low and outer-loop only. M0 TUI chrome and M3 browser UX remain partly visual unless paired with artifact/query checks. | Probe oracles; projection handlers; graph/coherence exports; transcript projection of lens/establishment/proposal entries. | +| Observability | partial, improving to high by M4/M5 | Text-native artifacts are planned (`.brunch/workspace.json`, Pi JSONL, command results, graph exports, coherence exports, fixture bundles). Generative-lens material adds further text-native surfaces through structured-exchange payload facets for review proposals, establishment offers, and elicitor intent hints, plus reviewer-finding `reconciliation_need` records. *Structural* observability is high; *behavioral* observability (proposal quality, lens-recommendation appropriateness, reviewer precision) remains low and outer-loop only. M0 TUI chrome and M3 browser UX remain partly visual unless paired with artifact/query checks. | Probe oracles; projection handlers; graph/coherence exports; transcript projection of lens/offer/proposal facets. | | Reproducibility | partial | Fixture briefs and captured runs create a repeatable path. M1/M2 proved the agent-as-user harness and JSONL projection/reload discipline. LLM runs remain variable, so deterministic postcondition checks and property assertions are required; batch-proposal/review-set flows additionally need seeded multi-run probes to characterize structural-legality rate at all. Driver extension for review-cycle flows (approve / request-changes / reject) is conditional on cost being worth the controllability gain. | Deterministic probe checks; captured-run metadata; replay/property fixtures; (planned) review-cycle driver extension. | | Controllability | partial → high (conditional) | `npm run fix` / `npm run verify` are agent-controllable. The agent-as-user stdio RPC driver covers single-exchange flows end-to-end; extending it to drive review-cycle acceptance/regeneration would lift batch-proposal/review-set controllability to "high" but carries implementation cost. TUI/browser/manual flows for ambient affordances, in-flight reviewer signals, and chrome rendering remain probe-oracle territory. | Store/projection postcondition checkers; stdio/WebSocket drivers; (planned) review-cycle driver extension; probe oracles for chrome surfaces. | @@ -524,16 +524,18 @@ Infrastructure is not yet fully laid (Phase 3 of POC bootstrapping). Commands fo | Step | Check | Command | | --- | --- | --- | -| 1 | Lint-fix + format (inner loop) | `npm run fix` | -| 2 | Format check + lint (no writes) | `npm run check` | +| 1 | Lint:fix + format (inner loop, writes) | `npm run fix` | +| 2 | Lint + format check (no writes; CI use) | `npm run check` | | 3 | Unit tests | `npm run test` | | 4 | Build | `npm run build` | -| all | Full gate | `npm run verify` (= check + test + build) | +| all | Full gate (writes via `fix`) | `npm run verify` (= fix + test + build) | + +`fix` and `check` share the same lint-then-format order; `fix` writes, `check` does not. There is no separate `typecheck` script — type-checking runs inside oxlint via tsgolint (`.oxlintrc.json` sets `typeAware: true` and `typeCheck: true`). ### Verification Policy -- **Inner loop:** run `npm run fix` after every meaningful edit. Tooling: oxlint (lint + type-aware via tsgolint), oxfmt (format), vitest (test). See AGENTS.md. -- **Gate before commit:** `npm run verify`. All steps must pass; no override. +- **Inner loop:** run `npm run fix` after every meaningful edit. Tooling: oxlint (lint + type-aware + type-check via tsgolint), oxfmt (format), vitest (test). See AGENTS.md. +- **Gate before commit:** `npm run verify`. The gate auto-applies inner-loop fixes; remaining failures must be fixed before proceeding. No override. - **Failure protocol:** stop on first failure; the failure becomes the must-fix task; re-run the stack from step 1; only proceed when all checks pass. - **Frontier completion:** manual smoke can prove presentation life, but any durable product claim must also have an artifact/query oracle, property/round-trip test, contract test, or fixture assertion tied to the canonical store or projection handler that owns the fact. - **Probe/transcript architecture:** the POC uses transcript-backed probe runs as the current verification artifact model. Committed probe evidence lives under `.fixtures/runs///` with colocated `session.jsonl`, rendered `transcript.md`, and `report.json` so the executable oracle and human transcript oracle stay paired. Brief-based golden fixtures are deferred; if they return, briefs are probe inputs and the resulting transcript-backed probe run is canonical. Transcript renderers default to the Brunch-semantic view: structured-exchange and Brunch custom transcript evidence included, unrelated generic tool results skipped unless an explicit raw/debug mode is requested. @@ -543,17 +545,17 @@ Infrastructure is not yet fully laid (Phase 3 of POC bootstrapping). Commands fo | Loop | Oracle family | Proves | Primary claims | | --- | --- | --- | --- | | Inner | Type-aware lint, type checks, fast unit tests | Local module correctness, typed command/result shapes (including `acceptReviewSet` and reviewer-writable record-class types), projection helper behavior (including `supersedes`-chain filtering). | D12-L, D13-L, D20-L, D21-L, D27-L, D28-L, D29-L. | -| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, runtime-gated prompt-resource manifests, `brunch.review_set_proposal` / `brunch.establishment_offer` / `brunch.elicitor_intent_hint` custom-entry payloads (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L, I38-L. | -| Middle | **Probe oracles**: prose manual actions plus executable postcondition checkers | Interactive seams leave correct durable state. Early M0 checkers may inspect stores only; once handlers exist, prefer projection-including checks. Extends to workspace-dialog startup behavior, in-flight reviewer-signal chrome behavior, and ambient-affordance rendering from latest establishment-offer entry. | D11-L, D21-L, D22-L, D25-L, D29-L, D36-L; I8-L, I13-L, I22-L. | +| Inner | Schema/shape validation at boundaries | JSON-RPC payloads, command results, structured elicitation entries, Zod-authored structured-exchange present/request/capture details with JSON Schema export, probe report metadata, graph exports, runtime-gated prompt-resource manifests, and structured-exchange payload facets for review proposals, establishment offers, and elicitor intent hints (lens presence, `epistemic_status`, grounding coverage, entity-draft shape). | R8, R10, R11, R17, R20, R21, R23; I3-L, I10-L, I11-L, I17-L, I18-L, I23-L, I26-L, I38-L. | +| Middle | **Probe oracles**: prose manual actions plus executable postcondition checkers | Interactive seams leave correct durable state. Early M0 checkers may inspect stores only; once handlers exist, prefer projection-including checks. Extends to workspace-dialog startup behavior, in-flight reviewer-signal chrome behavior, and ambient-affordance rendering from latest establishment-offer structured-exchange facet. | D11-L, D21-L, D22-L, D25-L, D29-L, D36-L; I8-L, I13-L, I22-L. | | Middle | Round-trip tests | JSONL reload, linear transcript validation, elicitation exchange projection, compaction, graph export/import, command result serialization, `supersedes`-chain reconstruction across regeneration. | D6-L, D13-L, D24-L, D28-L; I3-L, I8-L, I10-L, I19-L. | | Middle | Property-based / model-based tests | LSN monotonicity, change-log replay, reconciliation-need invariants, mention staleness, interest-set recomputation, side-task delivery ordering, **batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible even under mid-batch validation failure)**, **`supersedes`-chain acyclicity and unique-leaf-per-thread**, **lens-routing correctness (generated elicitor entries route to the right consumer)**, **reviewer-finding turn-boundary delivery ordering**. | A4-L, A8-L, A9-L, A11-L; I1-L, I4-L, I5-L, I6-L, I9-L, I12-L, I15-L, I16-L, I18-L. | -| Middle | Contract tests | Named RPC method families and transport adapters share handler semantics; `rpc.discover` describes public methods with usable schemas/examples; pending-exchange start/read/respond handlers preserve transcript truth; subscriptions deliver initial snapshot plus ordered updates; `CommandExecutor` hides policy/transaction details; `acceptReviewSet` returns expected structured discriminants; only prevalidated proposals become reviewable review sets. | D5-L, D19-L, D20-L, D27-L, D48-L, D49-L; R11, R12, R27, R28. | +| Middle | Contract tests | Named RPC method families and transport adapters share handler semantics; `rpc.discover` describes public methods with usable schemas/examples; `session.promptExchange` / `session.pendingExchange` / `session.submitExchangeResponse` / `session.exchanges` preserve transcript truth; subscriptions deliver initial snapshot plus ordered updates; `CommandExecutor` hides policy/transaction details; `acceptReviewSet` returns expected structured discriminants; only prevalidated proposals become reviewable review sets. | D5-L, D19-L, D20-L, D27-L, D48-L, D49-L; R11, R12, R27, R28. | | Middle | Architectural boundary tests | No direct ORM/SQLite mutation outside `CommandExecutor`; no canonical chat/turn store; TUI/RPC/fixture code does not write `brunch.session_binding`; spec/session picker UI returns decisions rather than opening/mutating sessions; RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code; Brunch wrappers do not expose Pi branch creation/navigation as product behavior; spec readiness-grade mutations route through commands rather than session-local memory; reviewer-attributed writes target only `reconciliation_need`; Brunch-launched Pi runtimes do not load ambient `.pi/` resources or behavior-shaping settings outside the Brunch Pi Profile; Brunch product extensions load through the explicit static shell list rather than filesystem discovery or a runtime extension-metadata protocol. | D4-L, D6-L, D18-L, D21-L, D24-L, D29-L, D36-L, D39-L, D45-L; I2-L, I10-L, I11-L, I16-L, I19-L, I22-L, I24-L, I31-L. | | Middle | **Differential testing** | Dry-run validation at proposal time matches real-run validation at acceptance time (no drift between modes); free-form-generation vs constrained-generation legality rates (informs whether fallback path is needed per A14-L). | D27-L; A14-L. | | Middle | Probe transcript replay and property assertions | Probe runs preserve transcript evidence that can be replayed, rendered, and compared against current Brunch projections. Future brief-driven sessions, if revived, must produce the same probe-run artifact shape. For batch proposals/review sets: **structural-legality rate of LLM proposals tracked per-run in probe metadata as POC-phase fitness, not a merge gate**; first-attempt vs retry-with-feedback rates surfaced for human review. | A5-L, A6-L, A7-L, A14-L; I7-L; R20, R21, R22, R23. | -| Middle | Deterministic public-RPC parity proof | A scripted agent-as-user discovers Brunch methods, activates workspace/spec/session, drives the current structured-exchange permutations through Brunch JSON-RPC only, compares Pi JSONL plus `session.transcriptDisplay` / `session.elicitationExchanges` projections against TUI-shaped structured-exchange expectations, rejects repeated deterministic prompts, and can persist a `.fixtures/runs/public-rpc-parity//` review bundle containing source `session.jsonl`, Brunch-semantic `transcript.md`, and `report.json`. | A5-L; D5-L, D48-L, D49-L; I23-L, I32-L; R24, R27, R28. | +| Middle | Deterministic public-RPC parity proof | A scripted agent-as-user discovers Brunch methods, activates workspace/spec/session, drives the current structured-exchange permutations through Brunch JSON-RPC only, compares Pi JSONL plus `session.exchanges` projections against TUI-shaped structured-exchange expectations, rejects repeated deterministic prompts, and can persist a `.fixtures/runs/public-rpc-parity//` review bundle containing source `session.jsonl`, Brunch-semantic `transcript.md`, and `report.json`. The landed FE-744 proof uses older public names; D49-L defines the target rename. | A5-L; D5-L, D48-L, D49-L; I23-L, I32-L; R24, R27, R28. | | Middle | Capture-analysis transcript oracle | Future `capture_*` probes persist ANALYSIS as normal Brunch toolResults, assert no graph writes occur, render full analysis in Markdown/ASCII transcripts, and assert the TUI path hides or collapses the same result without losing persisted content/details. | D17-L, D18-L, D37-L, D47-L, D50-L; I23-L, I30-L, I33-L. | -| Outer | Manual walkthrough with checklist | UX/presentation life: TUI chrome, spec/session picker, web shell feel, coherence visibility, elicitation usefulness. Adds: ambient-affordance rendering from establishment-offer entries; proposal/framing quality review; lens-recommendation appropriateness; review-cycle UX (approve / request-changes / reject); meta-rubric comparative-usefulness review (D31-L hypothesis test). | A17-L; R4, R14, R16, R20, R21. | +| Outer | Manual walkthrough with checklist | UX/presentation life: TUI chrome, spec/session picker, web shell feel, coherence visibility, elicitation usefulness. Adds: ambient-affordance rendering from establishment-offer structured-exchange facets; proposal/framing quality review; lens-recommendation appropriateness; review-cycle UX (approve / request-changes / reject); meta-rubric comparative-usefulness review (D31-L hypothesis test). | A17-L; R4, R14, R16, R20, R21. | | Outer | Adversarial / generative probe runs | Elicitation quality, human-gated `needs_human`, contradictory requirements, cross-session updates, long-horizon compaction, and reviewer-finding precision through small targeted probe scenarios (brief-shaped inputs are allowed, but the probe run and transcript artifacts are canonical). POC scope remains one or two known-bad scenarios per relevant invariant, not exhaustive coverage. | A5-L, A8-L, A9-L, A11-L, A14-L; I4-L, I6-L, I12-L, I13-L, I16-L. | ### Probe Oracle Design @@ -587,13 +589,13 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I14-L | Deferred unless observer/auditor queue lands: restart/idempotence tests over exchange-keyed jobs, plus proof that next-turn freshness does not depend on the async job completing. | | I15-L | M5+ middle-loop property tests for batch-acceptance atomicity (one LSN / one change-log entry, partial-batch impossible under mid-batch validation failure) paired with `acceptReviewSet` contract tests; review-set fixture parity in replay. | | I16-L | M5+ middle-loop architectural boundary test on reviewer-attributed `CommandExecutor` writers (rejects any non-`reconciliation_need` target); paired with reviewer-attributed command-result audit fixture. | -| I17-L | M5+ inner-loop schema validation on `brunch.review_set_proposal` entries (must declare `epistemic_status`); paired with outer-loop fixture assertion that status varies appropriately with grounding density (POC-phase fitness, not gate). | -| I18-L | M5+ inner-loop schema validation on elicitor-emitted custom entries (must declare `lens`); paired with middle-loop property test that generated entries route to the correct capture/reviewer/future-auditor consumer. | +| I17-L | M5+ inner-loop schema validation on review-set structured-exchange payloads (must declare `epistemic_status`); paired with outer-loop fixture assertion that status varies appropriately with grounding density (POC-phase fitness, not gate). | +| I18-L | M5+ inner-loop schema validation on elicitor-emitted structured-exchange payload facets that need routing (must declare `lens`); paired with middle-loop property test that generated payloads route to the correct capture/reviewer/future-auditor consumer. | | I19-L | Brunch extension/runtime guard tests for `/tree`/`/fork`/`/clone` blocking plus transcript-reader non-linearity rejection tests. | | I20-L | M5+ proposal-validation contract and differential tests proving only dry-run-valid proposals become reviewable review sets. | | I21-L | M3 RPC/WebSocket explicit-session projection tests; future write-lease tests when browser writes land. | | I22-L | FE-744 coordinator inventory/activation tests plus pty/ANSI-stripped TUI probe assertions: no stale transcript before explicit resume, new-spec path creates an implicit first session, new-session path yields binding-only JSONL, resume path renders the chosen transcript, chrome includes activated session id, and RPC/headless boot exposes structured initial-selection state instead of invoking TUI picker code. | -| I23-L | FE-744 structured-exchange tests: `present_*` results persist rich markdown display through `toolResult.content`/`renderResult`; `request_*` tools mount an input-replacing TUI response surface when available; single-choice, multi-choice, freeform, and freeform-plus-choice answers persist as self-contained request result details or linked custom entries; RPC/fixture paths submit the same semantic response through JSON-editor fallback or Brunch product handlers; recovery helpers detect unmatched required presents; elicitation-exchange projection pairs the prompt-side present/custom entry with the terminal request result. Structured-exchange schema tests cover the landed target details model: checked `schema`/`v`, `tool_meta`, candidate rubric/graph-ref drift rejection, exactly-one request outcomes, comment/message placement, minimal capture details, and JSON Schema export. | +| I23-L | FE-744 structured-exchange tests: `present_*` results persist rich markdown display through `toolResult.content`/`renderResult`; `request_*` tools mount an input-replacing TUI response surface when available; single-choice, multi-choice, freeform, and freeform-plus-choice answers persist as self-contained request result details; RPC/fixture paths submit the same semantic response through JSON-editor fallback or Brunch product handlers; recovery helpers detect unmatched required presents; elicitation-exchange projection pairs the prompt-side present with the terminal request result. Structured-exchange schema tests cover the landed target details model: checked `schema`/`v`, `tool_meta`, candidate rubric/graph-ref shapes, review-set pointer shape, request answered/cancelled/unavailable unions, `comment` vs runtime `message`, and capture no-graph-payload minimum. | | I24-L | Sealed-profile tests: resource-loader options disable ambient discovery; inline Brunch extension resources still load intentionally through `resources_discover`; settings/keybinding/tool/prompt policy audit proves no ambient user/project `.pi/` setting changes Brunch product behavior. | | I25-L | Runtime-state tests: append init/switch custom entries, reload the linear transcript, reconstruct the active `op_mode` / `strategy` / `lens` / `goal` (foreground role derived from `op_mode`), and verify before-agent-start/tool-call policy suppresses disallowed tools for `elicit`. | | I26-L | Structured-exchange schema tests prove the acknowledged Zod seam parses and exports JSON Schema; future M4 architectural tests should grep/import-audit schema libraries and Drizzle row-schema derivation boundaries. | @@ -611,7 +613,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` - **Prompt-resource manifests before eager prompt injection.** For goal, strategy, lens, and method guidance, prefer a deterministic per-turn manifest plus agent-driven `read` loading over a Brunch state machine that selects and concatenates large semantic prompt bodies. Inner-loop tests prove manifest legality and filtering; behavioral probes judge whether the agent loads and applies the right resource. - **Deterministic before generative.** Probe runs should prefer deterministic or tightly scripted paths before relying on LLM persona variance. Generative/adversarial probes come after the transcript substrate is trusted. Retired M1 scripted captures proved the early transport/projection substrate on then-current terms, but tuple-shaped FE-744 public-RPC probes are the current evidence path. -- **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, pending/respond lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. +- **Public RPC parity before LLM quality.** FE-744's product proof uses a deterministic dummy elicitor rather than a real LLM: the point is to prove Brunch's public RPC contract, assistant-first turn model, structured-exchange prompt/read/submit lifecycle, current structured-exchange permutations, JSONL/projection parity, and reviewable probe artifacts. The target method names live in [`src/rpc/README.md`](file:///Users/lunelson/Code/hashintel/brunch-next/src/rpc/README.md); proof-era names in current code are rename debt, not design vocabulary. LLM elicitation quality and coherent ten-turn progress remain outer-loop generative fixture concerns after the transport/turn substrate is trustworthy. - **Capture analysis before graph persistence.** `capture_*` ANALYSIS is the transcript-native bridge for reviewing likely graph changes before graph persistence or before comparing later graph mutations against transcript evidence. The landed schema layer defines only the checked minimum capture details and rejects graph payloads; richer analysis payloads and shared rendering components still require a separate design pass before runtime implementation. - **Projection handlers are oracles, not stores.** Read/subscription tests should prove handlers reconstruct truth from Brunch-supported linear Pi JSONL, `.brunch/workspace.json`, or SQLite graph/change log; they should not introduce a canonical view-store just for testing. - **Behavioral quality boundary.** Inner/middle loops prove structural validity, durable state, invariants, and expected graph/property coverage. “Good interview”, “good question”, and “coherent UX feel” remain outer-loop checklist/generative-fixture judgments until enough examples justify sharper metrics. diff --git a/src/rpc/README.md b/src/rpc/README.md new file mode 100644 index 000000000..5dd33cfdc --- /dev/null +++ b/src/rpc/README.md @@ -0,0 +1,179 @@ +# Brunch public RPC + +This directory owns Brunch's public JSON-RPC boundary. This README is the findable naming contract for RPC methods that product clients and designers should reason about. `memory/SPEC.md` records the architectural decision; this file names the concrete surface. + +## Boundary + +Brunch exposes one product RPC surface over stdio, WebSocket, and in-process handlers. Browser clients, CLI probes, TUI adapters, and future relays speak Brunch method names; they do not coordinate raw Pi RPC plus Brunch product RPC themselves. + +RPC handlers project from canonical stores: + +```pseudo +canonical stores: + .brunch/workspace.json + project + posture + current/default spec/session acceleration + + SQLite graph DB + specs + nodes + edges + change_log + reconciliation_needs + coherence_state + + Pi JSONL transcript + session_binding + agent_runtime_state + structured_exchange toolResult tuples + worldUpdate entries +``` + +RPC handlers must not become a generic records API, REST read model, or canonical view store. Reads are named projections over the store that owns the fact. Mutations route through the owning product seam: session transcript operations through `session.*`, graph mutations through the agent/tool or `CommandExecutor` path that owns them. + +## Product method vocabulary + +Use these names in product design, SPEC text, and new public handlers: + +```pseudo +rpc.discover + Returns supported Brunch methods, schemas, and examples. + +workspace.snapshot + Returns cwd-scoped workspace product state: + project + posture + current/default spec/session + activation/chrome state + +workspace.selectionState + Returns boot/picker inventory and whether explicit spec/session activation is required. + +workspace.activate + Applies an explicit workspace -> spec -> session decision. + +session.promptExchange + Starts, resumes, or advances the assistant-first session loop until one of: + pending structured exchange + idle/completed state + needs_human blocker + policy/authority blocker + +session.pendingExchange + Reads the current unresolved structured exchange without advancing the agent loop. + +session.submitExchangeResponse + Submits the terminal response for one pending structured exchange. + The payload is generic over request_* variants: + request_answer + request_choice + request_choices + request_review + future request_* tools + +session.submitMessage + Submits ordinary non-exchange user text or an explicit interruption. + It is not a structured exchange answer. + +session.exchanges + Projects structured exchange history from transcript truth. + +future graph projection methods + graph.overview + graph.nodeNeighborhood + graph.changesSince / graph.recentChanges + +future graph-adjacent coherence projection method + graph.coherenceSummary +``` + +## Names to avoid + +These names are proof-era, stale, or too narrow for the stable product contract: + +```pseudo +session.startElicitation + too mode/lifecycle specific; use session.promptExchange + +elicitation.respond + too mode-specific and too narrow; use session.submitExchangeResponse + +session.elicitationExchanges + too mode-specific; use session.exchanges + +session.transcriptDisplay + render/debug concern, not a core web-product state API + +command.* + not a web UI primitive for propose-graph; command execution is an internal authority seam +``` + +Existing code may still expose some proof-era names from the deterministic public-RPC parity slice. Treat those as rename debt, not as product vocabulary for new work. + +## Structured exchange lifecycle + +A structured exchange is transcript-native. Its durable semantic content lives in Pi JSONL `toolResult` tuples, not in UI local state. + +```pseudo +present_* toolResult + assistant-side display and recovery payload: + exchange id + display markdown/data + strategy/lens metadata when applicable + preface/proposal/rubric/offer material when applicable + expected request_* tool + +request_* toolResult + terminal user-side response: + answered | cancelled | unavailable + selected choice(s) or freeform answer + optional user-authored comment + runtime-authored message for cancellation/unavailable states + +capture_* toolResult (future) + assistant analysis only: + transcript evidence + possible semantic candidates + no graph mutation +``` + +Payload facets such as establishment offers, elicitor intent hints, and review/proposal material belong inside structured exchange payloads when they are part of an exchange. They are not separate public RPC entities. + +## Web UI rules + +```pseudo +if session.pendingExchange returns pending: + render the structured exchange form + submit via session.submitExchangeResponse + do not also treat freeform text as ambient chat + +if no exchange is pending: + session.promptExchange may ask the agent for the next exchange + session.submitMessage may append ordinary user text or an explicit interruption +``` + +`session.submitMessage` must not silently answer a pending exchange. If interruption is allowed, it should be explicit in the payload and transcript-visible. + +## `propose-graph` flow + +In `propose-graph`, the browser does not submit graph nodes or edges and does not call `commitGraph` directly. + +```pseudo +session.promptExchange + -> agent presents a concept proposal as a structured exchange + +session.pendingExchange + -> web reads proposal/rubric/choice surface + +session.submitExchangeResponse + -> user chooses accept_concept | request_revision | reject + with optional comment + +agent continues after acceptance + -> agent calls commitGraph({ nodes, edges }) internally + -> CommandExecutor validates and commits atomically + -> graph/coherence projections update +``` + +The user reviews the concept-level proposal. The graph becomes product truth only after the internal `commitGraph` path succeeds. From c3047b0b95310ac264ff757082b75aed7aba2edd Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 17:41:04 +0200 Subject: [PATCH 31/34] FE-785: Align review-set proposal state with RPC contract --- memory/PLAN.md | 8 +- memory/SPEC.md | 10 +-- src/.pi/__tests__/review-set-proposal.test.ts | 36 +++------ .../extensions/auto-compaction-anchors.json | 5 -- .../extensions/graph/review-set-proposal.ts | 73 +++---------------- src/session/elicitation-exchange.test.ts | 10 +-- src/session/elicitation-exchange.ts | 1 - 7 files changed, 32 insertions(+), 111 deletions(-) diff --git a/memory/PLAN.md b/memory/PLAN.md index 82a5729bd..c6467fa91 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -219,11 +219,11 @@ The POC should maximize assumption falsification rather than merely implement mi └── if observer/auditor queues land → backstops only, not primary capture freshness path ``` - **Implementation layout:** Per D52-L, graph domain logic lives in `src/graph/` (CommandExecutor, readers, policy, validators, snapshot functions) and persistence in `src/db/`. The Pi-facing adapter goes in one explicit product extension directory, `src/.pi/extensions/graph/`, imported by `src/.pi/pi-extension-shell.ts` as `registerBrunchGraph` rather than discovered dynamically. Use `graph/index.ts` only to register Pi tools, message renderers, and event hooks. Keep tool definitions in `graph/tools/*` (`read-graph`, `commit-graph`, `create-intent-node`, `update-intent-node`, `link-intent-nodes`, `accept-review-set`), boundary schemas in `graph/schemas/*` (`tool-inputs`, `tool-results`, `custom-entries`), transcript helpers in `graph/transcript/*` (`entries`, `projections`, `renderers`), synchronous capture in `graph/capture/post-exchange-capture.ts`, reviewer target enforcement in `graph/reviewer/reviewer-writes.ts`, and the Pi→CommandExecutor translation seam in `graph/command-adapter.ts`. The extension directory must not own SQLite/Drizzle persistence, LSN allocation, structural graph validators, reviewer-agent implementation, or capture model/prompt machinery; those are Brunch product/core modules passed into the extension through explicit shell options such as `{ graph: { commandExecutor, capturePostExchange?, reviewerWrites? } }`. Agent prompts, strategy definitions (including `propose-graph` and `project-graph`), lens definitions, and context builders live in `src/agents/` per D52-L. -- **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, proposal-entry schema validation (`brunch.review_set_proposal` must declare `epistemic_status` and support/grounding coverage), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposals become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — kernel-card-output coverage assertions begin landing through targeted probe runs; first batch-proposal probe (e.g. `propose-scenarios-with-tradeoffs`) replays through review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. +- **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, review-set payload schema validation for the `present_review_set` structured-exchange flow (`epistemic_status` and support/grounding coverage required), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposal payloads become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — targeted batch-proposal probes replay through structured-exchange review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. - **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. - **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L - **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** **(0)** ✓ Runtime vocabulary enabling slice folded into this frontier: `brunch.agent_runtime_state` now has the reconciled goal/strategy/lens/op-mode tuple and legal `propose-graph`/`project-graph` strategies. **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates review-set proposal metadata, translates drafts to graph batches, and only surfaces dry-run-valid `brunch.review_set_proposal` entries. Next: wire real `project-graph` agent proposal generation or scope synchronous elicitor capture. +- **Current execution pointer:** **(0)** ✓ Runtime vocabulary enabling slice folded into this frontier: `brunch.agent_runtime_state` now has the reconciled goal/strategy/lens/op-mode tuple and legal `propose-graph`/`project-graph` strategies. **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate reconciled to the public RPC contract: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates structured-exchange review-set payload metadata and translates drafts to graph batches without creating a standalone public review-set proposal entity. Next: wire real `project-graph` agent proposal generation through `present_review_set` / `request_review`, or scope synchronous elicitor capture. ### subagents-for-proposal-diversity @@ -231,8 +231,8 @@ The POC should maximize assumption falsification rather than merely implement mi - **Linear:** unassigned - **Kind:** optional enhancement - **Status:** deferred (lands when `agent-and-graph-integration` is far enough along to benefit; never a blocker for M0–M9) -- **Objective:** Register a single `subagent` Pi tool per D44-L so the main agent can (a) fan out blocking data-gathering calls (scout / researcher / graph-reader) in parallel to ground proposals, then (b) fan out parallel `proposer` invocations to generate diverse candidate variants — the subagent realization of `ln-design`'s "design it twice" pattern and `ln-oracles`'s parallel-fan-out — and finally compose `brunch.review_set_proposal` entries from those variants via the D31-L meta-rubric. Subagent results return as tool content; no `CommandExecutor` access; no Brunch RPC access; isolated `pi --no-session --no-skills --no-extensions` subprocesses inheriting Brunch Pi Profile sealing. -- **Acceptance:** `subagent` tool registered with `{ agent, task }` and `{ tasks: [] }` parameters; starter agents scout/researcher/graph-reader/proposer land as markdown files with TypeBox-validated frontmatter under `src/.pi/extensions/subagents/agents/`; proposer is system-prompt-only (no tools) and produces exactly one variant per invocation; argv shape per spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent tool allowlist / model / system-prompt path; concurrency cap honored from [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json); subagents have no inherited conversation context so the task string must carry everything; result text returns as tool result content with no transcript side-effects; at least one batch-proposal probe exercises a `tasks: []` parallel `proposer` fan-out (≥ 2 variants) feeding a single `brunch.review_set_proposal` composed by the main agent via the D31-L meta-rubric. +- **Objective:** Register a single `subagent` Pi tool per D44-L so the main agent can (a) fan out blocking data-gathering calls (scout / researcher / graph-reader) in parallel to ground proposals, then (b) fan out parallel `proposer` invocations to generate diverse candidate variants — the subagent realization of `ln-design`'s "design it twice" pattern and `ln-oracles`'s parallel-fan-out — and finally compose a `present_review_set` structured-exchange payload from those variants via the D31-L meta-rubric. Subagent results return as tool content; no `CommandExecutor` access; no Brunch RPC access; isolated `pi --no-session --no-skills --no-extensions` subprocesses inheriting Brunch Pi Profile sealing. +- **Acceptance:** `subagent` tool registered with `{ agent, task }` and `{ tasks: [] }` parameters; starter agents scout/researcher/graph-reader/proposer land as markdown files with TypeBox-validated frontmatter under `src/.pi/extensions/subagents/agents/`; proposer is system-prompt-only (no tools) and produces exactly one variant per invocation; argv shape per spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent tool allowlist / model / system-prompt path; concurrency cap honored from [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json); subagents have no inherited conversation context so the task string must carry everything; result text returns as tool result content with no transcript side-effects; at least one batch-proposal probe exercises a `tasks: []` parallel `proposer` fan-out (≥ 2 variants) feeding a single `present_review_set` payload composed by the main agent via the D31-L meta-rubric. - **Verification:** Inner — `subagent` tool argv-shape tests; TypeBox schema validation of agent frontmatter and `config.json`; per-starter-agent tool-allowlist conformance (proposer must have an empty tool set). Middle — isolation audit (no ambient `.pi/` resources reachable; parent `CommandExecutor` / Brunch RPC handlers absent from subprocess environment); subprocess streaming / abort propagation tests; parallel-fan-out independence test (two `proposer` invocations with distinct framings produce structurally distinct outputs). Outer — proposal-generation probe invokes scout/researcher/graph-reader to ground, then parallel `proposer` variants, and surfaces the composed review-set proposal with grounding-bundle coverage and `epistemic_status` consistent with the gathered evidence; meta-rubric application visible in the comparison rendering. - **Cross-cutting obligations:** Preserve the single-authority mutation rule (`CommandExecutor` only — subagents never bypass it) and the sealed Pi Profile (no ambient `.pi/` leakage through the subprocess boundary). Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. Worker-style write-capable subagents are deferred until an execute operational mode exists. - **Traceability:** R20 / D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L, D44-L / I2-L, I11-L, I24-L, I29-L diff --git a/memory/SPEC.md b/memory/SPEC.md index 0617e998d..00799a0f7 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -207,7 +207,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D15-L — Side tasks are a first-class Brunch subsystem delivered through the same transcript/event substrate.** Side tasks are main-agent-invoked, non-blocking work items: the main agent fires them and continues without awaiting a return value. A Brunch-owned `SideTaskRegistry` tracks status; the only path a side task influences the main agent is by appending a custom-message status update to the session log that arrives at the next-turn boundary through the existing `prepareNextTurn` path — never mid-turn. Side-task writes remain subject to the same command-layer authority as primary-agent writes. This is distinct from D44-L Subagent (main-agent-invoked **blocking** tool call whose result is returned directly as tool content). Depends on: A11-L, D4-L. Supersedes: —. - **D16-L — Graph persistence uses Drizzle over `better-sqlite3`, with one-LSN-per-commit and no bypass paths.** The command layer owns precondition checks, structural validation, entity writes, LSN allocation, change-log append, and any coherence updates inside one transaction. This rule applies equally to migrations and maintenance code; there is no privileged write path outside the command-executor protocol. Runtime row/insert/update schemas are derived from Drizzle table definitions through `drizzle-typebox` (`createInsertSchema`, `createSelectSchema`) rather than hand-authored alongside the table. **Settled by A20-L spike (2026-06-01):** `drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` + `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14`. Pi tool parameter schemas use `typebox` v1.x (Pi's package) separately; Drizzle-derived row schemas stay internal to `db/`→`graph/`; shared enum `const` arrays bridge both. Depends on: A3-L, A4-L, A20-L (validated). Refined by: D41-L. Supersedes: —. - **D18-L — Post-exchange capture is synchronous elicitor work for the POC; observer/auditor queues are deferred backstops, not primary extraction authority.** After a user response closes an elicitation exchange, the elicitor may run a post-exchange capture step in the same turn-boundary flow: commit high-confidence extractive facts, concrete reconciliation needs, and justified spec-readiness updates through the `CommandExecutor`; fold low-confidence implications into later questions rather than graph truth. Brunch may still introduce durable observer/auditor jobs keyed by session id plus exchange entry ids for restartable audit, quality checks, or later backfill, but those jobs are not the load-bearing path for keeping the next turn's world fresh. Any async job writes still route through the command layer and remain operational queue state unless they surface semantic work as reconciliation needs. Depends on: A13-L, A22-L, D4-L, D13-L, D16-L. Supersedes: the old DB-backed `chat` / `turn` mental model and the earlier observer-owned primary extraction path. -- **D28-L — Regenerated review-set proposals are appended as successor entries in the linear Pi JSONL session; projection helpers filter to the accepted set for context economy.** When the user requests changes, the agent appends a successor proposal entry that references its predecessor via `supersedes`; prior proposals are *not* deleted from JSONL but remain visible as raw transcript history. This stays within Brunch's linear transcript policy — no Pi branching is created. Pi JSONL is treated as a "capture everything" store for replay and audit. Projection helpers used to drive the agent (context injection, summarization) walk the `supersedes` chain and surface only the latest (or ultimately accepted) proposal — the agent does not re-process every superseded proposal as live context. The reviewer likewise sees only the accepted set, not the regeneration history. Depends on: D6-L, D12-L, D17-L, D24-L, D27-L. Supersedes: any "in-place edit" or "fork-on-regenerate" mental model. +- **D28-L — Regenerated review-set proposals are appended as successor `present_review_set` toolResult payloads in the linear Pi JSONL session; projection helpers filter to the accepted set for context economy.** When the user requests changes, the agent appends a successor structured-exchange proposal payload that references its predecessor via `supersedes`; prior proposal payloads are *not* deleted from JSONL but remain visible as raw transcript history. This stays within Brunch's linear transcript policy — no Pi branching is created. Pi JSONL is treated as a "capture everything" store for replay and audit. Projection helpers used to drive the agent (context injection, summarization) walk the `supersedes` chain and surface only the latest (or ultimately accepted) proposal — the agent does not re-process every superseded proposal as live context. The reviewer likewise sees only the accepted set, not the regeneration history. Depends on: D6-L, D12-L, D17-L, D24-L, D27-L. Supersedes: any "in-place edit" or "fork-on-regenerate" mental model, and the retired standalone `brunch.review_set_proposal` entry family. - **D29-L — Reviewer is an async advisory role with narrow write authority.** After a batch acceptance closes, Brunch may enqueue a reviewer job keyed by session id plus the batch-acceptance entry id; the job survives process restart and analyzes the accepted batch plus its graph neighborhood for coherence, completeness, and gaps. **Reviewer writes only `reconciliation_need` records via the `CommandExecutor`**; it never writes graph entities, edges, change-log entries directly, or any other record class. Findings reach the user through next-turn delivery as advisory items on the reconciliation-need surface — the batch acceptance remains the user's atomic commitment and the reviewer cannot amend it. (Suggestion-shaped findings may later route to candidate-artefacts when that substrate exists; the POC routes everything to reconciliation needs.) Depends on: A16-L, D4-L, D8-L, D15-L, D17-L, D18-L, D20-L, D27-L. Supersedes: any "reviewer may quietly amend the graph" mental model. - **D24-L — Brunch POC enforces a linear transcript policy over Pi JSONL.** Pi's session tree is a substrate capability, not a Brunch product surface. Until branch-aware continuity/coherence is explicitly designed, Brunch-controlled interactive/runtime flows block `/tree`, `/fork`, and `/clone` through the thinnest available Pi hooks; transcript readers reject non-linear session files instead of flattening, adapting, migrating, or selecting a branch. This is intentional fail-fast pre-release posture: avoid compatibility debt with Pi internals or earlier Brunch revisions, and keep wrapper/adapter layers minimal. Depends on: D6-L, D11-L, D13-L. Supersedes: treating active-branch projection as Brunch product semantics. - **D43-L — Auto-compaction is a Brunch-owned `session_before_compact` extension whose anchor preservation contract is an externalized JSON config.** Brunch always owns this hook because Pi's default summary cannot know about Brunch's transcript-native continuity entries. The extension composes a deterministic preserved-anchor header (rendered byte-stable from the configured anchor set against the pre-compaction branch) with an LLM-generated narrative summary, then returns Pi's standard `{ compaction: { summary, firstKeptEntryId, tokensBefore } }` shape. The summarization model is resolved through the active runtime bundle (D40-L) — typically a cheap/fast "compaction" preset (e.g. Gemini Flash, Haiku) — with fallback to Pi's default compaction on missing auth, empty output, or unexpected error so compaction is never gated on extension success. The anchor contract lives in [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json) as `{ kind, select, rationale }` rules (`select ∈ first | latest | active-leaves | all-unresolved`) so it can be reviewed and updated without SPEC churn; the file is validated through a D41-L-compatible runtime schema when the module lands. Brunch-initiated proactive compaction (post-`acceptReviewSet`, on shutdown) and reactor-side compaction triggers are deferred. Session-scoped continuity metadata (`lastSeenLsn`, interest sets) is *projected* from the change log plus the preserved anchor entries — it is not itself an anchor and never appears in the JSON. Depends on: D6-L, D15-L, D17-L, D40-L, D41-L. Supersedes: relying on Pi's default `session_before_compact` summary to keep Brunch-specific continuity intelligible. @@ -268,7 +268,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I17-L | Every batch-proposal or review-set structured-exchange payload declares an `epistemic_status` (`inferred | assumed | asserted | observed`) and enough grounding/support coverage to justify that status at proposal time; UI renderings honor this status as a presentation contract. | partially covered (`review-set-proposal.test.ts` covers the current product proposal helper rejecting missing `epistemicStatus` and empty grounding/support before surfacing a reviewable payload; thin-vs-rich grounding fixture semantics and structured-exchange carrier migration remain future work) | D30-L, D46-L; A14-L | | I18-L | Every elicitor-emitted prompt/proposal payload facet that needs downstream routing (establishment offer, intent hint, review/proposal material) carries a `lens` field inside the structured-exchange details; capture, reviewer, and future observer/auditor routing filters on this field. | partially covered (`review-set-proposal.test.ts` covers current proposal lens validation; establishment/intent-hint routing tests and structured-exchange carrier migration remain planned with capture/reviewer slices) | D25-L, D26-L, D29-L | | I19-L | Brunch-controlled flows do not create or navigate Pi session branches, and Brunch transcript readers fail fast on non-linear JSONL rather than flattening, migrating, or branch-selecting. | partially covered (M3 transcript loader requires exactly one Pi session header, rejects malformed non-header entry shapes, and rejects non-linear child graphs, `parentSession`, and `branch_summary`; product-facing exchange projection helper preserves the non-linear error discriminant and is used by RPC and fixture replay assertions; `session.exchanges` returns a product-shaped error for non-linear selected sessions over stdio and WebSocket JSON-RPC; Brunch TUI extension cancels `session_before_tree` and `session_before_fork`; Pi command-containment source tests prove no exposed Brunch command path creates branches) | D24-L, D34-L | -| I20-L | Every user-reviewable review-set proposal has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface as reviewable review sets. | partially covered (`CommandExecutor.dryRunCommitGraph` and `review-set-proposal.test.ts` cover product-helper dry-run validation, invalid proposal non-surfacing, no graph mutation during dry-run, and dry-run/commit validation parity; real agent-generated `project-graph` proposal fixtures remain planned) | D27-L; A14-L | +| I20-L | Every user-reviewable review-set proposal payload has already passed proposal-time dry-run structural/policy validation against `CommandExecutor`; proposals that fail dry-run validation do not surface through `present_review_set` as reviewable review sets. | partially covered (`CommandExecutor.dryRunCommitGraph` and `review-set-proposal.test.ts` cover product-helper dry-run validation, invalid proposal-payload rejection, no graph mutation during dry-run, and dry-run/commit validation parity; real agent-generated `project-graph` proposal fixtures remain planned) | D27-L; A14-L | | I21-L | WebSocket/stdio/TUI client attachment state never becomes the canonical spec/session binding: every session-consuming projection validates the durable `brunch.session_binding`, and write-capable session operations must target an explicit session or future write lease rather than whichever transport connection happens to be open. | partially covered (M3 RPC/WebSocket explicit session projection tests validate durable `brunch.session_binding` for read paths; FE-744 web live-update tests prove WebSocket notifications only invalidate/refetch canonical projection handlers after RPC-originated structured-exchange mutations; future write-lease tests remain planned when web input lands) | D10-L, D19-L, D21-L, D33-L | | I22-L | Brunch TUI startup must not render prior session transcript entries or enter an agent loop until the user has explicitly activated a spec/session decision; creating a new spec implicitly creates its first session, creating a new session for an existing spec lands in a binding-only session, resuming a prior transcript is opt-in, and RPC/headless startup exposes structured initial-selection state rather than invoking TUI picker code. | covered (FE-744 coordinator tests; hierarchical spec/session picker model + component tests; `workspace.selectionState` / `workspace.activate` JSON-RPC contract tests with source assertion that RPC does not import TUI picker code; `src/probes/scripts/verify-startup-no-resume.sh` pty/ANSI-stripped TUI probe oracle proving stale transcript text is absent before explicit activation) | D11-L, D21-L, D22-L, D36-L | | I23-L | Every structured elicitation interaction that owns the response surface persists durable semantic display only through Pi `toolResult` rows rendered by `renderResult`; `renderCall` and live `ctx.ui.*` surfaces are transient. A structured-exchange tuple has a recoverable `present_*` result and, when required, exactly one matching terminal `request_*` result before the next agent turn consumes it. The target details model is checked by `schema` + `v`, `exchange_id`, and `tool_meta`; request outcomes are an exactly-one property-presence union; user-authored text is `comment` and runtime-authored text is `message`; present-side status/kind/expected-request aliases and capture graph payloads are invalid in the Zod-authored schema layer. `toolResult.content` is rich markdown suitable for both TUI transcript display and model context; `toolResult.details` carries structured projection/recovery data. | covered for current FE-744 structured-exchange tools (registered sequential `present_question`, `present_options`, `request_answer`, `request_choice`, and `request_choices`; tests cover non-semantic `renderCall`, markdown `renderResult`, present/request details, unmatched-present recovery, active-vs-stub registry, JSON-editor fallback for multi-choice, terminal `answered`/`cancelled`/`unavailable` projection closure, option content/rationale parity, and same-assistant-message `present_options → request_choice` ordering over a real Pi RPC run. The Zod-authored schema layer is covered by JSON Schema export and drift-rejection tests for present/request/capture details; runtime tools still need a deliberate migration to those exports. `present_review_set`, `present_candidates`, and `request_review` remain named stubs and intentionally unregistered.) | D12-L, D13-L, D17-L, D37-L, D38-L, D41-L | @@ -420,9 +420,9 @@ src/agents/ | **commitGraph** | Single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references. One tool call, one LSN, all-or-nothing (I34-L). The load-bearing tool for the `propose-graph` strategy's direct-commit path (D53-L). | | **propose-graph** | Elicitor strategy for generative lenses where the agent proposes a novel coherent subgraph. The concept is presented to the user with rubric axes, choices, and recommendation via structured exchange; upon acceptance the agent generates and persists the full subgraph through `commitGraph` without intermediate entity-level review (D26-L, D53-L). The hardest thing to get structurally legal and the primary proof target for A14-L. | | **project-graph** | Elicitor strategy for deriving nodes and edges from existing graph truth (e.g. projecting requirements from upstream goals/constraints). Uses review-set commitment (D27-L). Extractive rather than inventive; lower structural-legality risk than propose-graph. | -| **Brunch public RPC surface** | The one product-facing JSON-RPC surface exposed over stdio, WebSocket, and in-process handlers. Product clients use this surface for workspace, session, graph, coherence, command, agent, and elicitation behavior; raw Pi RPC is hidden behind adapters when needed. | -| **RPC discovery** | Brunch-owned `rpc.discover` method output: public method names, descriptions, parameter/result schemas, and examples for the current Brunch host. It is distinct from Pi `get_commands`, which only lists slash commands/prompt templates/skills invokable through Pi's `prompt` command. | -| **RPC method family** | A named group of Brunch JSON-RPC methods (`rpc.*`, `workspace.*`, `session.*`, `elicitation.*`, `graph.*`, `coherence.*`, `command.*`) that exposes product behavior through stdio, WebSocket, or in-process handler calls without creating a second public API surface. | +| **Brunch public RPC surface** | The one product-facing JSON-RPC surface exposed over stdio, WebSocket, and in-process handlers. Product clients use this surface for workspace, session, graph, and coherence projections plus session-native interaction methods; raw Pi RPC is hidden behind adapters when needed. | +| **RPC discovery** | Brunch-owned `rpc.discover` method output: public method names, descriptions, parameter/result schemas, and examples for the current Brunch host. It is distinct from Pi `get_commands`, which only lists slash commands/prompt templates/skills invokable through Pi's `prompt` command. The concrete public vocabulary is maintained in `src/rpc/README.md`. | +| **RPC method family** | A named group of Brunch JSON-RPC methods (`rpc.*`, `workspace.*`, `session.*`, future `graph.*`) that exposes product behavior through stdio, WebSocket, or in-process handler calls without creating a second public API surface. Proof-era `elicitation.*`, `session.startElicitation`, `session.elicitationExchanges`, `session.transcriptDisplay`, and public `command.*` method names are rename debt, not product vocabulary for new work. | | **Projection handler** | A thin handler that reads or subscribes to a canonical store and returns product-shaped state for a mode/client. It is not a canonical store itself. | | **Subscription** | A long-lived RPC operation that delivers live updates, often with an initial snapshot, for views that must stay current with session, workspace, graph, or coherence state. | | **Transport adapter** | The stdio, WebSocket, HTTP-shim, Pi-RPC relay, or in-process wrapper around the same Brunch handlers. Transport adapters do not own product semantics. | diff --git a/src/.pi/__tests__/review-set-proposal.test.ts b/src/.pi/__tests__/review-set-proposal.test.ts index d40135c8e..4732ef751 100644 --- a/src/.pi/__tests__/review-set-proposal.test.ts +++ b/src/.pi/__tests__/review-set-proposal.test.ts @@ -4,9 +4,8 @@ import { createDb } from '../../db/connection.js'; import { CommandExecutor } from '../../graph/command-executor.js'; import { getGraphOverview } from '../../graph/snapshot.js'; import { - buildReviewableReviewSetProposalEntry, - projectLatestReviewableReviewSetProposal, translateReviewSetProposalToCommitGraph, + validateReviewSetProposalPayload, type ReviewSetProposalDraft, } from '../extensions/graph/review-set-proposal.js'; @@ -63,18 +62,17 @@ function validProposal(overrides: Partial = {}): ReviewS } describe('review-set proposal dry-run gate', () => { - it('surfaces dry-run-valid review-set proposals as transcript entries', () => { + it('validates dry-run-valid review-set proposal payloads for structured exchanges', () => { const db = createDb(':memory:'); const executor = new CommandExecutor(db); - const entry = buildReviewableReviewSetProposalEntry({ + const result = validateReviewSetProposalPayload({ proposal: validProposal(), commandExecutor: executor, - source: 'agent', }); - expect(entry).toMatchObject({ - customType: 'brunch.review_set_proposal', - data: { + expect(result).toMatchObject({ + status: 'success', + proposal: { schemaVersion: 1, lens: 'design', epistemicStatus: 'inferred', @@ -82,16 +80,12 @@ describe('review-set proposal dry-run gate', () => { }, }); expect(getGraphOverview(db)).toMatchObject({ nodeCount: 0, edgeCount: 0, lsn: 0 }); - expect(projectLatestReviewableReviewSetProposal([{ type: 'custom', ...entry }])).toMatchObject({ - pitch: { title: 'Launch readiness review set' }, - validation: { status: 'success' }, - }); }); - it('does not surface structurally invalid review-set proposals', () => { + it('rejects structurally invalid review-set proposal payloads', () => { const db = createDb(':memory:'); const executor = new CommandExecutor(db); - const entry = buildReviewableReviewSetProposalEntry({ + const result = validateReviewSetProposalPayload({ proposal: validProposal({ edgeDrafts: [ { @@ -102,17 +96,13 @@ describe('review-set proposal dry-run gate', () => { ], }), commandExecutor: executor, - source: 'agent', }); - expect(entry).toMatchObject({ + expect(result).toMatchObject({ status: 'structural_illegal', diagnostics: [{ field: 'edges[0].stance', message: expect.stringContaining('required') }], }); expect(getGraphOverview(db)).toMatchObject({ nodeCount: 0, edgeCount: 0, lsn: 0 }); - expect( - projectLatestReviewableReviewSetProposal([{ type: 'custom', customType: 'other', data: entry }]), - ).toBeUndefined(); }); it('rejects proposal schema drift before CommandExecutor dry-run', () => { @@ -134,10 +124,9 @@ describe('review-set proposal dry-run gate', () => { ], }, ]) { - const result = buildReviewableReviewSetProposalEntry({ + const result = validateReviewSetProposalPayload({ proposal: proposal as unknown as ReviewSetProposalDraft, commandExecutor: executor, - source: 'agent', }); expect(result.status).toBe('structural_illegal'); } @@ -147,12 +136,11 @@ describe('review-set proposal dry-run gate', () => { const db = createDb(':memory:'); const executor = new CommandExecutor(db); const proposal = validProposal(); - const entry = buildReviewableReviewSetProposalEntry({ + const entry = validateReviewSetProposalPayload({ proposal, commandExecutor: executor, - source: 'agent', }); - expect(entry.status).toBe('reviewable'); + expect(entry.status).toBe('success'); const commitResult = executor.commitGraph(translateReviewSetProposalToCommitGraph(proposal)); expect(commitResult).toMatchObject({ status: 'success' }); diff --git a/src/.pi/extensions/auto-compaction-anchors.json b/src/.pi/extensions/auto-compaction-anchors.json index 82017563c..97550832c 100644 --- a/src/.pi/extensions/auto-compaction-anchors.json +++ b/src/.pi/extensions/auto-compaction-anchors.json @@ -22,11 +22,6 @@ "select": "latest", "rationale": "D25-L — observer/reviewer routing and prompt composition depend on the active lens; the latest switch is the authoritative lens marker post-compaction." }, - { - "kind": "brunch.review_set_proposal", - "select": "active-leaves", - "rationale": "D27-L, D28-L — proposals not yet accepted/rejected and not superseded must remain reviewable after compaction; superseded ancestors do not." - }, { "kind": "brunch.side_task_result", "select": "all-unresolved", diff --git a/src/.pi/extensions/graph/review-set-proposal.ts b/src/.pi/extensions/graph/review-set-proposal.ts index d27b27614..edc83344f 100644 --- a/src/.pi/extensions/graph/review-set-proposal.ts +++ b/src/.pi/extensions/graph/review-set-proposal.ts @@ -11,8 +11,6 @@ import { type StructuralIllegal, } from '../../../graph/index.js'; -export const REVIEW_SET_PROPOSAL_CUSTOM_TYPE = 'brunch.review_set_proposal'; - export type ReviewSetLens = 'intent' | 'design' | 'oracle'; export type EpistemicStatus = 'inferred' | 'assumed' | 'asserted' | 'observed'; @@ -55,24 +53,16 @@ export interface ReviewSetProposalDraft { readonly supersedes?: string; } -export interface ReviewSetProposalData extends ReviewSetProposalDraft { +export interface ReviewSetProposalPayload extends ReviewSetProposalDraft { readonly validation: CommitGraphDryRunResult; - readonly source: 'agent' | 'system' | 'extension'; } -export interface ReviewableReviewSetProposalEntry { - readonly status: 'reviewable'; - readonly customType: typeof REVIEW_SET_PROPOSAL_CUSTOM_TYPE; - readonly content: string; - readonly display: true; - readonly data: ReviewSetProposalData; +export interface ReviewSetProposalValidationSuccess { + readonly status: 'success'; + readonly proposal: ReviewSetProposalPayload; } -export interface CustomEntryLike { - readonly type?: unknown; - readonly customType?: unknown; - readonly data?: unknown; -} +export type ReviewSetProposalValidationResult = ReviewSetProposalValidationSuccess | StructuralIllegal; const VALID_LENSES = ['intent', 'design', 'oracle'] as const; const VALID_EPISTEMIC_STATUSES = ['inferred', 'assumed', 'asserted', 'observed'] as const; @@ -104,11 +94,10 @@ export function translateReviewSetProposalToCommitGraph(proposal: ReviewSetPropo }; } -export function buildReviewableReviewSetProposalEntry(options: { +export function validateReviewSetProposalPayload(options: { readonly proposal: ReviewSetProposalDraft; readonly commandExecutor: CommandExecutor; - readonly source: ReviewSetProposalData['source']; -}): ReviewableReviewSetProposalEntry | StructuralIllegal { +}): ReviewSetProposalValidationResult { const diagnostics = validateReviewSetProposalDraft(options.proposal); if (diagnostics.length > 0) { return { status: 'structural_illegal', diagnostics }; @@ -122,46 +111,14 @@ export function buildReviewableReviewSetProposalEntry(options: { } return { - status: 'reviewable', - customType: REVIEW_SET_PROPOSAL_CUSTOM_TYPE, - content: renderReviewSetProposalContent(options.proposal), - display: true, - data: { + status: 'success', + proposal: { ...options.proposal, validation, - source: options.source, }, }; } -export function projectLatestReviewableReviewSetProposal( - entries: readonly CustomEntryLike[], -): ReviewSetProposalData | undefined { - let latest: ReviewSetProposalData | undefined; - for (const entry of entries) { - if (entry.type !== 'custom' || entry.customType !== REVIEW_SET_PROPOSAL_CUSTOM_TYPE) { - continue; - } - const data = parseReviewSetProposalData(entry.data); - if (data) latest = data; - } - return latest; -} - -function parseReviewSetProposalData(value: unknown): ReviewSetProposalData | undefined { - if (!isRecord(value)) return undefined; - if (!isRecord(value.validation) || value.validation.status !== 'success') return undefined; - const proposal = value as unknown as ReviewSetProposalDraft; - if (validateReviewSetProposalDraft(proposal).length > 0) return undefined; - const source = value.source; - if (source !== 'agent' && source !== 'system' && source !== 'extension') return undefined; - return { - ...proposal, - validation: { status: 'success' }, - source, - }; -} - function validateReviewSetProposalDraft(value: ReviewSetProposalDraft): Diagnostic[] { const diagnostics: Diagnostic[] = []; const candidate = value as unknown; @@ -274,18 +231,6 @@ function validateEdgeDrafts(value: unknown, diagnostics: Diagnostic[]): void { }); } -function renderReviewSetProposalContent(proposal: ReviewSetProposalDraft): string { - return [ - `## ${proposal.pitch.title}`, - '', - proposal.pitch.narrative, - '', - `Epistemic status: ${proposal.epistemicStatus}`, - `Lens: ${proposal.lens}`, - `Drafts: ${proposal.entityDrafts.length} entities, ${proposal.edgeDrafts.length} edges`, - ].join('\n'); -} - function isNonEmptyStringArray(value: unknown): value is readonly string[] { return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string'); } diff --git a/src/session/elicitation-exchange.test.ts b/src/session/elicitation-exchange.test.ts index 2135a7b74..ebc53f830 100644 --- a/src/session/elicitation-exchange.test.ts +++ b/src/session/elicitation-exchange.test.ts @@ -236,7 +236,7 @@ describe('elicitation exchange projection', () => { }); }); - it('includes known elicitor custom entries on the prompt side', () => { + it('includes known standalone elicitor custom entries on the prompt side', () => { const projection = projectElicitationExchanges([ assistant, { @@ -245,16 +245,10 @@ describe('elicitation exchange projection', () => { customType: 'brunch.establishment_offer', data: { lens: 'intent' }, }, - { - id: 'proposal-1', - type: 'custom', - customType: 'brunch.review_set_proposal', - data: { lens: 'design' }, - }, user, ]); - expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1', 'offer-1', 'proposal-1']); + expect(projection.exchanges[0]?.promptEntryIds).toEqual(['a1', 'offer-1']); }); it('ignores unknown custom entries even when their type contains prompt', () => { diff --git a/src/session/elicitation-exchange.ts b/src/session/elicitation-exchange.ts index 5dac99b55..dd25d1fdf 100644 --- a/src/session/elicitation-exchange.ts +++ b/src/session/elicitation-exchange.ts @@ -28,7 +28,6 @@ const PROMPT_SIDE_CUSTOM_TYPES = new Set([ 'brunch.elicitation_prompt', 'brunch.elicitor_intent_hint', 'brunch.establishment_offer', - 'brunch.review_set_proposal', ]); const STRUCTURED_RESPONSE_TYPES = new Set([ From f04b48d2d05192bf8f70b764de6d7702b6dd84d6 Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 17:58:48 +0200 Subject: [PATCH 32/34] Lock graph persistence topology --- .oxfmtrc.json | 3 +- .oxlintrc.json | 3 +- AGENTS.md | 18 +- drizzle.config.ts | 10 + drizzle/0000_jazzy_warbound.sql | 63 + drizzle/meta/0000_snapshot.json | 404 ++ drizzle/meta/_journal.json | 13 + package-lock.json | 6406 +++++++++++------ package.json | 41 +- src/.pi/__tests__/chrome.test.ts | 14 +- src/.pi/__tests__/extension-registry.test.ts | 6 +- src/.pi/__tests__/graph-tools.test.ts | 2 +- src/.pi/__tests__/prompting.test.ts | 6 +- ...tructured-exchange-present-request.test.ts | 4 - src/.pi/__tests__/workspace-dialog.test.ts | 34 +- src/.pi/brunch-pi-profile.ts | 1 + src/.pi/extensions/graph/command-adapter.ts | 33 +- src/.pi/extensions/graph/index.ts | 74 +- src/.pi/extensions/graph/tool-schemas.ts | 83 + .../extensions/structured-exchange/index.ts | 10 +- .../structured-exchange/shared/markdown.ts | 2 +- src/brunch-tui.ts | 7 +- src/db/README.md | 122 +- src/db/connection.ts | 92 +- src/graph/README.md | 163 +- src/graph/command-executor.ts | 4 +- ...structured-exchange-ordering-proof.test.ts | 2 +- src/rpc/README.md | 6 +- 28 files changed, 4978 insertions(+), 2648 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_jazzy_warbound.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/.pi/extensions/graph/tool-schemas.ts diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 11282bf29..6b8cd51c9 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -17,6 +17,7 @@ "tmp/**", "dist-web/**", "bin/**", - "dist/**" + "dist/**", + "drizzle/**" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index b821e6c8b..a471a1fe5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -19,6 +19,7 @@ "tmp/**", "dist-web/**", "bin/**", - "dist/**" + "dist/**", + "drizzle/**" ] } diff --git a/AGENTS.md b/AGENTS.md index 87cb1bfda..7666ec87a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,17 +67,21 @@ Verification boundary: /ln-spec owns inner-loop verification (commands, policy). ### verification -**Inner loop** (run after every meaningful edit): `npm run fix` — lint-fixes then auto-formats. +**Inner loop** (run after every meaningful edit): `npm run fix` — lint:fix then format. -**Gate** (run before committing): `npm run verify` — check (fmt + lint, no writes) → test → build. All must pass. +**Gate** (run before committing): `npm run verify` — fix → test → build. The gate auto-applies inner-loop fixes; if anything else fails, stop and fix it. -| Script | Purpose | Writes? | +**CI / read-only check**: `npm run check` — lint then fmt:check, no writes. Use this where the gate must not mutate the worktree. + +| Script | Steps | Writes? | | --- | --- | --- | -| `npm run fix` | lint:fix + fmt (inner loop) | yes | -| `npm run check` | fmt:check + lint (CI gate) | no | -| `npm run verify` | check + test + build (full gate) | no | +| `npm run fix` | lint:fix → fmt | yes | +| `npm run check` | lint → fmt:check | no | +| `npm run verify` | fix → test → build | yes (via fix) | + +Ordering rationale: `fix` must run lint:fix before fmt because lint fixes can rewrite code that then needs reformatting. `check` mirrors that order (lint before fmt:check) so both scripts read as the same recipe in different modes. -Tooling: oxlint (lint + type-aware + type-check via tsgolint), oxfmt (format). Verification strategy details in SPEC.md §Verification Design. +Type-checking is done by oxlint via tsgolint (`.oxlintrc.json` sets `typeAware: true` and `typeCheck: true`); there is no separate `typecheck` script. Tooling: oxlint (lint + type-aware + type-check via tsgolint), oxfmt (format), vitest (test). Verification strategy details in SPEC.md §Verification Design. ## critical file-safety rule diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 000000000..f53f7f080 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + out: './drizzle', + schema: './src/db/schema.ts', + dialect: 'sqlite', + dbCredentials: { + url: process.env.BRUNCH_DB?.trim() || './.brunch/data.db', + }, +}); diff --git a/drizzle/0000_jazzy_warbound.sql b/drizzle/0000_jazzy_warbound.sql new file mode 100644 index 000000000..5d5fe6fea --- /dev/null +++ b/drizzle/0000_jazzy_warbound.sql @@ -0,0 +1,63 @@ +CREATE TABLE `change_log` ( + `lsn` integer PRIMARY KEY NOT NULL, + `operation` text NOT NULL, + `payload` text NOT NULL, + `created_at` text DEFAULT (datetime('now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `edges` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `category` text NOT NULL, + `source_id` integer NOT NULL, + `target_id` integer NOT NULL, + `stance` text, + `basis` text DEFAULT 'explicit' NOT NULL, + `rationale` text, + `created_at_lsn` integer NOT NULL, + `updated_at_lsn` integer NOT NULL, + FOREIGN KEY (`source_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`target_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `graph_clock` ( + `id` integer PRIMARY KEY NOT NULL, + `lsn` integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +INSERT INTO `graph_clock` (`id`, `lsn`) VALUES (1, 0); +--> statement-breakpoint +CREATE TABLE `nodes` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `plane` text NOT NULL, + `kind` text NOT NULL, + `title` text NOT NULL, + `body` text, + `basis` text DEFAULT 'explicit' NOT NULL, + `source` text, + `detail` text, + `created_at_lsn` integer NOT NULL, + `updated_at_lsn` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `reconciliation_need` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `target_kind` text NOT NULL, + `target_edge_id` integer, + `target_a_id` integer, + `target_b_id` integer, + `kind` text NOT NULL, + `status` text DEFAULT 'open' NOT NULL, + `reason` text, + `created_at_lsn` integer NOT NULL, + `resolved_at_lsn` integer, + FOREIGN KEY (`target_edge_id`) REFERENCES `edges`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`target_a_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`target_b_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `specs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `slug` text NOT NULL, + `readiness_grade` text DEFAULT 'grounding_onboarding' NOT NULL +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 000000000..2e7def4c5 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,404 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9a22b1aa-7dfa-4f47-95f8-5dd93ffcf7c1", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "change_log": { + "name": "change_log", + "columns": { + "lsn": { + "name": "lsn", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "edges": { + "name": "edges", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stance": { + "name": "stance", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basis": { + "name": "basis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'explicit'" + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_lsn": { + "name": "created_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at_lsn": { + "name": "updated_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "edges_source_id_nodes_id_fk": { + "name": "edges_source_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "edges_target_id_nodes_id_fk": { + "name": "edges_target_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graph_clock": { + "name": "graph_clock", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "lsn": { + "name": "lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "nodes": { + "name": "nodes", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plane": { + "name": "plane", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basis": { + "name": "basis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'explicit'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_lsn": { + "name": "created_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at_lsn": { + "name": "updated_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reconciliation_need": { + "name": "reconciliation_need", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_edge_id": { + "name": "target_edge_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_a_id": { + "name": "target_a_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_b_id": { + "name": "target_b_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_lsn": { + "name": "created_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at_lsn": { + "name": "resolved_at_lsn", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "reconciliation_need_target_edge_id_edges_id_fk": { + "name": "reconciliation_need_target_edge_id_edges_id_fk", + "tableFrom": "reconciliation_need", + "tableTo": "edges", + "columnsFrom": ["target_edge_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reconciliation_need_target_a_id_nodes_id_fk": { + "name": "reconciliation_need_target_a_id_nodes_id_fk", + "tableFrom": "reconciliation_need", + "tableTo": "nodes", + "columnsFrom": ["target_a_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reconciliation_need_target_b_id_nodes_id_fk": { + "name": "reconciliation_need_target_b_id_nodes_id_fk", + "tableFrom": "reconciliation_need", + "tableTo": "nodes", + "columnsFrom": ["target_b_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "specs": { + "name": "specs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "readiness_grade": { + "name": "readiness_grade", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grounding_onboarding'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 000000000..f6240af31 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1780414439815, + "tag": "0000_jazzy_warbound", + "breakpoints": true + } + ] +} diff --git a/package-lock.json b/package-lock.json index f344b3d18..e6469f722 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,36 +8,39 @@ "name": "brunch-next", "version": "0.0.0", "dependencies": { - "@earendil-works/pi-coding-agent": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.4", - "@tanstack/react-query": "^5.100.11", - "@tanstack/react-router": "^1.170.6", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.20.1", + "@earendil-works/pi-ai": "^0.75.5", + "@earendil-works/pi-coding-agent": "^0.75.5", + "@earendil-works/pi-tui": "^0.75.5", + "@tanstack/react-query": "^5.100.14", + "@tanstack/react-router": "^1.170.10", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typebox": "^1.1.39", + "ws": "^8.21.0", "zod": "^4.4.3" }, "bin": { "brunch-next": "bin/brunch.js" }, "devDependencies": { - "@sinclair/typebox": "^0.34.14", + "@sinclair/typebox": "^0.34.49", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "@types/react": "^19.2.15", + "@types/node": "^22.19.19", + "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^6.0.2", - "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.18.1", + "better-sqlite3": "^12.10.0", + "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", "oxfmt": "latest", - "oxlint": "latest", - "tsx": "^4.19.0", + "oxlint": "^1.68.0", + "oxlint-tsgolint": "^0.23.0", + "tsx": "^4.22.4", "typescript": "^5.7.0", "vite": "^8.0.16", "vitest": "^4.1.8" @@ -181,19 +184,19 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1050.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1050.0.tgz", - "integrity": "sha512-KbQqWGSyXh1c0opFTEcwNu6PcGd/IRyTnihDh8fpdiVCu62/53469AN+Xe6cKSuM6W2oOBbY12Pbj3zrdRK5mA==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/credential-provider-node": "^3.972.43", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", - "@aws-sdk/middleware-websocket": "^3.972.20", - "@aws-sdk/token-providers": "3.1050.0", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", @@ -206,17 +209,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.12.tgz", - "integrity": "sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==", + "version": "3.974.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz", + "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.24.5", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -225,15 +228,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.38.tgz", - "integrity": "sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz", + "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -241,41 +244,55 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.40.tgz", - "integrity": "sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==", + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz", + "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz", + "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.42.tgz", - "integrity": "sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz", + "integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/credential-provider-env": "^3.972.38", - "@aws-sdk/credential-provider-http": "^3.972.40", - "@aws-sdk/credential-provider-login": "^3.972.42", - "@aws-sdk/credential-provider-process": "^3.972.38", - "@aws-sdk/credential-provider-sso": "^3.972.42", - "@aws-sdk/credential-provider-web-identity": "^3.972.42", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-login": "^3.972.45", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -283,16 +300,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.42.tgz", - "integrity": "sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz", + "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -300,21 +317,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.43.tgz", - "integrity": "sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==", + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.48.tgz", + "integrity": "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.38", - "@aws-sdk/credential-provider-http": "^3.972.40", - "@aws-sdk/credential-provider-ini": "^3.972.42", - "@aws-sdk/credential-provider-process": "^3.972.38", - "@aws-sdk/credential-provider-sso": "^3.972.42", - "@aws-sdk/credential-provider-web-identity": "^3.972.42", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-ini": "^3.972.46", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -322,15 +339,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.38.tgz", - "integrity": "sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz", + "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -338,17 +355,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.42.tgz", - "integrity": "sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz", + "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/token-providers": "3.1049.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/token-providers": "3.1056.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -356,16 +373,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1049.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1049.0.tgz", - "integrity": "sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==", + "version": "3.1056.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz", + "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -373,16 +390,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.42.tgz", - "integrity": "sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz", + "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -390,14 +407,14 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", - "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.18.tgz", + "integrity": "sha512-QPQhwY/fstR8fMZFWrsJRNoTP6D1RjRPHGRX7u9/VkF3opCsvD0oXPz6qzkX94SchzvuS5vyFZbJbPcMEs2Jeg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -405,14 +422,14 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", - "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.14.tgz", + "integrity": "sha512-DoZ4djVj/74XQ6M/IwxuKh543tTvLCL7u1Dx+VDHMgW9yGNrFSJJ1l0LrUQRaekic5CB12wUiiOoHL0VI6H0gg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -420,17 +437,17 @@ } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.20.tgz", - "integrity": "sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.23.tgz", + "integrity": "sha512-F0d4A9pJFiwljyKgSwU1Z5n+CXSv8bp+V5SthbS2rftB8wBN9z1K2Yyv3xbeK0AM2T0g4q6Ptf0shFF+oQZyiA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -438,36 +455,49 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.10.tgz", - "integrity": "sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==", + "version": "3.997.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz", + "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz", + "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", - "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "version": "3.996.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", + "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.973.9", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -475,13 +505,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1050.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1050.0.tgz", - "integrity": "sha512-LVw+bW8LKWdus3U4v7Ojm5XmIXv1ZlQ3rsQrlkEt5fss+SsWfTTzVxoo8kl6ZCY5gl5kL8lPGluHPIDGR8bntQ==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.12", - "@aws-sdk/nested-clients": "^3.997.10", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", @@ -492,12 +522,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -517,13 +547,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz", + "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==", "license": "Apache-2.0", "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" }, @@ -727,36 +756,29 @@ "node": ">=20.19.0" } }, - "node_modules/@earendil-works/pi-agent-core": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", - "integrity": "sha512-azg09GSrckQa3ffbH09YEZC7DyHgmNSX+vmWEoEhQvp4icbzqbqLfIeMayMNEK/aGusm1SghZC4bPlDdagDALg==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", - "ignore": "^7.0.5", - "typebox": "^1.1.24", - "yaml": "^2.8.2" - }, - "engines": { - "node": ">=22.19.0" - } + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/@earendil-works/pi-ai": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.3.tgz", - "integrity": "sha512-UKccS+ADlkSVJ49a00346jUfXmUi6zzzB+pPWotsyA6SxhKr2ejjkGQksGyR1DyNVrsEP/WWlsOSTUUwVlzNaA==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.5.tgz", + "integrity": "sha512-zf1F5kXk1pqZeFShXOqq9ibUk8QdtRoLCDPAjO+hj44e3EUs9/GFO2qnhTC5+JA2uwVCx+WCNe1PiCjlBYWm5w==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.1", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", "openai": "6.26.0", - "partial-json": "^0.1.7", - "typebox": "^1.1.24" + "partial-json": "0.1.7", + "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" @@ -765,29 +787,36 @@ "node": ">=22.19.0" } }, + "node_modules/@earendil-works/pi-ai/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.3.tgz", - "integrity": "sha512-LIi5/CdUBfcLp3BAtpLx1BfnHDLmDOQVdzYfS1H9fjjCw2dcPr9voSI5ncrhvZdgyFSnfHck4BCbNcfZk+TEHQ==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.3", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cross-spawn": "^7.0.6", - "diff": "^8.0.2", - "glob": "^13.0.1", - "highlight.js": "^10.7.3", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "jiti": "^2.7.0", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "typebox": "^1.1.24", - "undici": "^8.3.0", - "yaml": "^2.8.2" + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.75.5.tgz", + "integrity": "sha512-O3CCQDYy28D4uwtP6zZkdEwzHN6X22v49Sb0+SZTC7x37V/YfmogrWPiaFoWeoc2hmdKhSATI7ZAK5bQbJG5NA==", + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.75.5", + "@earendil-works/pi-ai": "^0.75.5", + "@earendil-works/pi-tui": "^0.75.5", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "typebox": "1.1.38", + "undici": "8.3.0", + "yaml": "2.9.0" }, "bin": { "pi": "dist/cli.js" @@ -796,1128 +825,3116 @@ "node": ">=22.19.0" }, "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.6" + "@mariozechner/clipboard": "0.3.6" } }, - "node_modules/@earendil-works/pi-tui": { - "version": "0.75.4", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.4.tgz", - "integrity": "sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", "dependencies": { - "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "json-schema-to-ts": "^3.1.1" }, - "engines": { - "node": ">=22.19.0" + "bin": { + "anthropic-ai-sdk": "bin/cli" }, - "optionalDependencies": { - "koffi": "2.16.2" + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.5.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.75.5", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.5.tgz", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.1", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "./dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.5.tgz", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.6", + "@mariozechner/clipboard-darwin-universal": "0.3.6", + "@mariozechner/clipboard-darwin-x64": "0.3.6", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", + "@mariozechner/clipboard-linux-x64-musl": "0.3.6", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", "cpu": [ - "ppc64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", "cpu": [ - "s390x" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", "cpu": [ - "x64" + "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "win32" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } ], - "engines": { - "node": ">=18" + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", "license": "Apache-2.0", "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", - "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.6", - "@mariozechner/clipboard-darwin-universal": "0.3.6", - "@mariozechner/clipboard-darwin-x64": "0.3.6", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", - "@mariozechner/clipboard-linux-x64-musl": "0.3.6", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", - "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 10" + "node": ">=14.0.0" } }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", - "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 10" + "node": ">=14.0.0" } }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", - "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", - "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": "18 || 20 || >=22" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", - "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": "*" } }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", - "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">= 10" + "node": "18 || 20 || >=22" } }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", - "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", - "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">= 10" + "node": ">= 8" } }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", - "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10" + "node": ">= 12" } }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", - "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">= 10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "safe-buffer": "^5.0.1" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/nodable" + "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, - "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", - "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", - "cpu": [ - "arm" + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", - "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", - "cpu": [ - "arm64" + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^12.20 || >= 14.13" } }, - "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", - "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "fetch-blob": "^3.1.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=12.20.0" } }, - "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", - "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", - "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", - "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", - "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", - "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", - "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14" } }, - "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", - "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, - "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", - "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "*" } }, - "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", - "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", - "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 14" } }, - "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", - "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 14" } }, - "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", - "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 4" } }, - "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", - "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", - "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "bignumber.js": "^9.0.0" } }, - "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", - "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=16" } }, - "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.53.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", + "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@earendil-works/pi-tui": { + "version": "0.75.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.5.tgz", + "integrity": "sha512-LkXUM1/49pvzzeI39Y5wjBMlgafcCf67HCLhB9Z7yuXHy4XgT+VqxWcZVW5hBdhQsHZd0znjJotfGH1BzxMfiA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.53.0.tgz", + "integrity": "sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.53.0.tgz", + "integrity": "sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.53.0.tgz", + "integrity": "sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.53.0.tgz", + "integrity": "sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.53.0.tgz", + "integrity": "sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.53.0.tgz", + "integrity": "sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.53.0.tgz", + "integrity": "sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.53.0.tgz", + "integrity": "sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.53.0.tgz", + "integrity": "sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.53.0.tgz", + "integrity": "sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.53.0.tgz", + "integrity": "sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.53.0.tgz", + "integrity": "sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.53.0.tgz", + "integrity": "sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.53.0.tgz", + "integrity": "sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.53.0.tgz", + "integrity": "sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.53.0.tgz", + "integrity": "sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.53.0.tgz", + "integrity": "sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.53.0.tgz", + "integrity": "sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.53.0", "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.53.0.tgz", "integrity": "sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==", "cpu": [ @@ -1933,10 +3950,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/darwin-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.18.1.tgz", - "integrity": "sha512-FqDrcQJmEGNkgmZgI4wbCrGyJl1tiRZa3udxvyYaXag8W80A0zLFNCyWVvHIgUJ0DHlZjRc7O72xUGjiyvQrqQ==", + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==", "cpu": [ "arm64" ], @@ -1947,10 +3964,10 @@ "darwin" ] }, - "node_modules/@oxlint/darwin-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-0.18.1.tgz", - "integrity": "sha512-YUcyWBJdNuMcJxAwdV/i25/kvnKrVsA+vLn7SsL87cAwiD//rqGdOixk0r8sKUYa71Kx3h0Fg2ToUOjdE6ddYw==", + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==", "cpu": [ "x64" ], @@ -1961,14 +3978,289 @@ "darwin" ] }, - "node_modules/@oxlint/linux-arm64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-0.18.1.tgz", - "integrity": "sha512-ol3jhmUv5VI/omMrt6DkwY/jVTSVJlflFyU1SnSb/BuVVf3TyBiCHmZ4wVtcrcT5k3sWjrvYWw2kSozvmuE4tg==", + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.68.0.tgz", + "integrity": "sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.68.0.tgz", + "integrity": "sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.68.0.tgz", + "integrity": "sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.68.0.tgz", + "integrity": "sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.68.0.tgz", + "integrity": "sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.68.0.tgz", + "integrity": "sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.68.0.tgz", + "integrity": "sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.68.0.tgz", + "integrity": "sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.68.0.tgz", + "integrity": "sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.68.0.tgz", + "integrity": "sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.68.0.tgz", + "integrity": "sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.68.0.tgz", + "integrity": "sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.68.0.tgz", + "integrity": "sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==", + "cpu": [ + "s390x" + ], + "dev": true, "libc": [ "glibc" ], @@ -1976,63 +4268,72 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxlint/linux-arm64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-0.18.1.tgz", - "integrity": "sha512-iKDj1ZwlU4KpXuIL1qkVP6NJzri2VSJreqXCIAe1Bf5RZXMAGSO3xjldgiX+HBvFOKSBIarLcqONYDbYco9uaQ==", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.68.0.tgz", + "integrity": "sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "libc": [ - "musl" + "glibc" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxlint/linux-x64-gnu": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-0.18.1.tgz", - "integrity": "sha512-A3g+fZhlOivUdK7xU/IrbhBcMHig5GLrfMX0HYjXL1fiSqKYu9n1o1p42WpT6KfPL3L2uncSg/iyg7hspcN6qA==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.68.0.tgz", + "integrity": "sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==", "cpu": [ "x64" ], "dev": true, "libc": [ - "glibc" + "musl" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxlint/linux-x64-musl": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-0.18.1.tgz", - "integrity": "sha512-LA02SdATWZEZBy8ZZpR2GlUbDg7+Jq1/WKkywMXqxdClkcoyyFozj8aQD2iTMKELSra4OSyqqZpOYroqjSSKmw==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.68.0.tgz", + "integrity": "sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxlint/win32-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-0.18.1.tgz", - "integrity": "sha512-FNL+OxDflqLGXRgLxfBM/X4RnLYgtOKTsb1mNSqsjSCEfUi1Oqivh7KvZ09IfAMZeJ85/fL6EI6hSOyY7nNYUg==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.68.0.tgz", + "integrity": "sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A==", "cpu": [ "arm64" ], @@ -2041,12 +4342,32 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.68.0.tgz", + "integrity": "sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@oxlint/win32-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-0.18.1.tgz", - "integrity": "sha512-W+aVE9Siqs6Oe3NDaDOTTOYsN9X3znl+whfqWK1EcLpqJXX1kdB8Hf45HkGjqnHoFoP96GRgUnXQHQvmUybjvg==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.68.0.tgz", + "integrity": "sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g==", "cpu": [ "x64" ], @@ -2055,7 +4376,10 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", @@ -2076,9 +4400,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { @@ -2402,27 +4726,21 @@ "dev": true, "license": "MIT" }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0" - }, "node_modules/@sinclair/typebox": { - "version": "0.34.14", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.14.tgz", - "integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==", + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "dev": true, "license": "MIT" }, "node_modules/@smithy/core": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2430,13 +4748,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", - "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz", + "integrity": "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2444,13 +4762,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", - "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2484,13 +4802,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", - "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2498,9 +4816,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2556,9 +4874,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.11.tgz", - "integrity": "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==", + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz", + "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==", "license": "MIT", "funding": { "type": "github", @@ -2566,12 +4884,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.100.11", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.11.tgz", - "integrity": "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==", + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz", + "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.100.11" + "@tanstack/query-core": "5.100.14" }, "funding": { "type": "github", @@ -2582,14 +4900,14 @@ } }, "node_modules/@tanstack/react-router": { - "version": "1.170.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.6.tgz", - "integrity": "sha512-tGQkOjcMESBbfw+iz9zE/ivcuw4D2zdhW8PA4wMmpOyA2bLiqpc6bg5MnJPxamdXoO3GZBiHYOOoEwi5qxpPgA==", + "version": "1.170.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.10.tgz", + "integrity": "sha512-gVmWYq0ucWr+OB97Nud0YhKa9NOipB7/QrWI7wRZJJWEL0qUS8WPqAs0vA1f3IBXZpXmf8xxzf/tl5cmo4tlmA==", "license": "MIT", "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", - "@tanstack/router-core": "1.171.4", + "@tanstack/router-core": "1.171.8", "isbot": "^5.1.22" }, "engines": { @@ -2623,9 +4941,9 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.171.4", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.4.tgz", - "integrity": "sha512-LU9YuVdgaP+h4MEXRvokyjIEelulylgsromHMfYwVfgo1PF9oJP3NHyy7qtjxGLJq6zoZMCfoa1frDJlPo7I8g==", + "version": "1.171.8", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.8.tgz", + "integrity": "sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw==", "license": "MIT", "dependencies": { "@tanstack/history": "1.162.0", @@ -2762,9 +5080,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "version": "19.2.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", + "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2988,15 +5306,6 @@ "node": ">=12" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3018,9 +5327,9 @@ "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3029,7 +5338,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, "node_modules/bidi-js": { @@ -3079,18 +5388,6 @@ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -3122,18 +5419,12 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/chai": { "version": "6.2.2", @@ -3145,18 +5436,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -3164,33 +5443,6 @@ "dev": true, "license": "ISC" }, - "node_modules/cli-color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", - "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.15", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3204,20 +5456,6 @@ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -3239,20 +5477,6 @@ "dev": true, "license": "MIT" }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3346,614 +5570,339 @@ "node": ">=8" } }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/difflib": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", - "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", - "dev": true, - "dependencies": { - "heap": ">= 0.2.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dreamopt": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", - "integrity": "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==", - "dev": true, - "dependencies": { - "wordwrap": ">=0.0.2" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/drizzle-kit": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.18.1.tgz", - "integrity": "sha512-Oqie227W2Dd7FuqX4pvQWeClSvnoPCIn2cO9JueeLWZqj3tpdBhnbgt4nLHhBbOdWRlTLYwXnkTDW3hYym/gGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "commander": "^9.4.1", - "esbuild": "^0.15.18", - "esbuild-register": "^3.4.2", - "glob": "^8.1.0", - "hanji": "^0.0.5", - "json-diff": "0.9.0", - "minimatch": "^7.4.3", - "zod": "^3.20.2" - }, - "bin": { - "drizzle-kit": "index.js" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", - "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", - "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/drizzle-kit/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/drizzle-kit/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/drizzle-kit/node_modules/esbuild": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", - "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.15.18", - "@esbuild/linux-loong64": "0.15.18", - "esbuild-android-64": "0.15.18", - "esbuild-android-arm64": "0.15.18", - "esbuild-darwin-64": "0.15.18", - "esbuild-darwin-arm64": "0.15.18", - "esbuild-freebsd-64": "0.15.18", - "esbuild-freebsd-arm64": "0.15.18", - "esbuild-linux-32": "0.15.18", - "esbuild-linux-64": "0.15.18", - "esbuild-linux-arm": "0.15.18", - "esbuild-linux-arm64": "0.15.18", - "esbuild-linux-mips64le": "0.15.18", - "esbuild-linux-ppc64le": "0.15.18", - "esbuild-linux-riscv64": "0.15.18", - "esbuild-linux-s390x": "0.15.18", - "esbuild-netbsd-64": "0.15.18", - "esbuild-openbsd-64": "0.15.18", - "esbuild-sunos-64": "0.15.18", - "esbuild-windows-32": "0.15.18", - "esbuild-windows-64": "0.15.18", - "esbuild-windows-arm64": "0.15.18" - } - }, - "node_modules/drizzle-kit/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/drizzle-kit/node_modules/glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/drizzle-kit/node_modules/minimatch": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", - "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=10" + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "bin": { + "drizzle-kit": "bin.cjs" } }, - "node_modules/drizzle-kit/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=4", - "@electric-sql/pglite": ">=0.2.0", - "@libsql/client": ">=0.10.0", - "@libsql/client-wasm": ">=0.10.0", - "@neondatabase/serverless": ">=0.10.0", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=14.0.0", - "gel": ">=2", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "sql.js": ">=1", - "sqlite3": ">=5" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@libsql/client-wasm": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { - "optional": true - }, - "expo-sqlite": { - "optional": true - }, - "gel": { - "optional": true - }, - "knex": { - "optional": true - }, - "kysely": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "postgres": { - "optional": true - }, - "prisma": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - } + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/drizzle-typebox": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/drizzle-typebox/-/drizzle-typebox-0.3.3.tgz", - "integrity": "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "@sinclair/typebox": ">=0.34.8", - "drizzle-orm": ">=0.36.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=18" } }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.10" + "node": ">=18" } }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.12" + "node": ">=18" } }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" } }, - "node_modules/esbuild-android-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", - "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ - "x64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", - "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ - "arm64" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", - "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", - "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", - "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ - "x64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", - "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", - "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", - "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -3961,118 +5910,118 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", - "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", - "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", - "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ - "mips64el" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", - "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", - "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", - "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ - "s390x" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", - "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -4080,124 +6029,270 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", - "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", - "cpu": [ - "x64" - ], + "node_modules/drizzle-typebox": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/drizzle-typebox/-/drizzle-typebox-0.3.3.tgz", + "integrity": "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "license": "Apache-2.0", + "peerDependencies": { + "@sinclair/typebox": ">=0.34.8", + "drizzle-orm": ">=0.36.0" } }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "dev": true, - "license": "MIT", + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" + "safe-buffer": "^5.0.1" } }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", - "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", - "cpu": [ - "x64" - ], + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "once": "^1.4.0" } }, - "node_modules/esbuild-windows-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", - "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", - "cpu": [ - "ia32" - ], + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/esbuild-windows-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", - "cpu": [ - "x64" - ], + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", - "cpu": [ - "arm64" - ], + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=0.10" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/estree-walker": { @@ -4210,17 +6305,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -4241,16 +6325,6 @@ "node": ">=12.0.0" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "license": "ISC", - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4361,13 +6435,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4423,6 +6490,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4430,23 +6510,6 @@ "dev": true, "license": "MIT" }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/google-auth-library": { "version": "10.6.2", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", @@ -4461,61 +6524,16 @@ "jws": "^4.0.0" }, "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hanji": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", - "integrity": "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lodash.throttle": "^4.1.1", - "sisteransi": "^1.0.5" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", - "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", - "dev": true, - "license": "MIT" - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=14" } }, "node_modules/html-encoding-sniffer": { @@ -4578,27 +6596,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4620,13 +6617,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isbot": { "version": "5.1.40", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", @@ -4636,21 +6626,6 @@ "node": ">=18" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4718,24 +6693,6 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/json-diff": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.9.0.tgz", - "integrity": "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-color": "^2.0.0", - "difflib": "~0.2.1", - "dreamopt": "~0.8.0" - }, - "bin": { - "json-diff": "bin/json-diff.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/json-schema-to-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", @@ -4770,17 +6727,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -5054,13 +7000,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -5071,21 +7010,12 @@ "version": "11.5.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es5-ext": "~0.10.2" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -5125,26 +7055,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/memoizee": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", - "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", - "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "es5-ext": "^0.10.64", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -5158,21 +7068,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5183,15 +7078,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5231,13 +7117,6 @@ "dev": true, "license": "MIT" }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true, - "license": "ISC" - }, "node_modules/node-abi": { "version": "3.92.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", @@ -5407,30 +7286,70 @@ } }, "node_modules/oxlint": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-0.18.1.tgz", - "integrity": "sha512-JGcQvbhd00Qb+nq4f9sYYRh7mZIb0K/7rbMepNdJDMzo8pbmBpx1N2XOG61RjHDsNnY6ImAmVk3h4QVwFenwUQ==", + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.68.0.tgz", + "integrity": "sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA==", "dev": true, "license": "MIT", "bin": { - "oxc_language_server": "bin/oxc_language_server", "oxlint": "bin/oxlint" }, "engines": { - "node": ">=8.*" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/darwin-arm64": "0.18.1", - "@oxlint/darwin-x64": "0.18.1", - "@oxlint/linux-arm64-gnu": "0.18.1", - "@oxlint/linux-arm64-musl": "0.18.1", - "@oxlint/linux-x64-gnu": "0.18.1", - "@oxlint/linux-x64-musl": "0.18.1", - "@oxlint/win32-arm64": "0.18.1", - "@oxlint/win32-x64": "0.18.1" + "@oxlint/binding-android-arm-eabi": "1.68.0", + "@oxlint/binding-android-arm64": "1.68.0", + "@oxlint/binding-darwin-arm64": "1.68.0", + "@oxlint/binding-darwin-x64": "1.68.0", + "@oxlint/binding-freebsd-x64": "1.68.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.68.0", + "@oxlint/binding-linux-arm-musleabihf": "1.68.0", + "@oxlint/binding-linux-arm64-gnu": "1.68.0", + "@oxlint/binding-linux-arm64-musl": "1.68.0", + "@oxlint/binding-linux-ppc64-gnu": "1.68.0", + "@oxlint/binding-linux-riscv64-gnu": "1.68.0", + "@oxlint/binding-linux-riscv64-musl": "1.68.0", + "@oxlint/binding-linux-s390x-gnu": "1.68.0", + "@oxlint/binding-linux-x64-gnu": "1.68.0", + "@oxlint/binding-linux-x64-musl": "1.68.0", + "@oxlint/binding-openharmony-arm64": "1.68.0", + "@oxlint/binding-win32-arm64-msvc": "1.68.0", + "@oxlint/binding-win32-ia32-msvc": "1.68.0", + "@oxlint/binding-win32-x64-msvc": "1.68.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.22.1", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.23.0.tgz", + "integrity": "sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "0.23.0", + "@oxlint-tsgolint/darwin-x64": "0.23.0", + "@oxlint-tsgolint/linux-arm64": "0.23.0", + "@oxlint-tsgolint/linux-x64": "0.23.0", + "@oxlint-tsgolint/win32-arm64": "0.23.0", + "@oxlint-tsgolint/win32-x64": "0.23.0" } }, "node_modules/p-retry": { @@ -5480,31 +7399,6 @@ "node": ">=14.0.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5604,37 +7498,17 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/protobufjs": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", - "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", + "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", @@ -5686,24 +7560,24 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/react-is": { @@ -5738,6 +7612,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -5841,27 +7725,6 @@ "seroval": "^1.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -5869,12 +7732,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -5922,12 +7779,15 @@ "simple-concat": "^1.0.0" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/source-map-js": { "version": "1.2.1", @@ -5939,6 +7799,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6022,20 +7893,6 @@ "node": ">=6" } }, - "node_modules/timers-ext": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", - "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", - "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6139,9 +7996,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { @@ -6170,17 +8027,10 @@ "node": "*" } }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true, - "license": "ISC" - }, "node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.1.39", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", + "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", "license": "MIT" }, "node_modules/typescript": { @@ -6197,15 +8047,6 @@ "node": ">=14.17" } }, - "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6453,21 +8294,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -6485,13 +8311,6 @@ "node": ">=8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6500,9 +8319,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6552,21 +8371,6 @@ "dev": true, "license": "MIT" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index abc545f68..430a21773 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dist", "dist-web", "bin", + "drizzle", "assets" ], "type": "module", @@ -20,6 +21,8 @@ "build": "tsc -p tsconfig.build.json && npm run build:pi-assets && npm run build:web", "build:pi-assets": "mkdir -p dist/.pi/components/workspace-dialog dist/.pi/context/prompt-packs && cp -R src/.pi/components/workspace-dialog/assets dist/.pi/components/workspace-dialog/ && cp src/.pi/context/prompt-packs/*.md dist/.pi/context/prompt-packs/", "build:web": "vite build", + "db:generate": "drizzle-kit generate", + "db:studio": "drizzle-kit studio", "test": "vitest --run", "test:watch": "vitest", "lint": "oxlint", @@ -27,38 +30,40 @@ "fmt": "oxfmt", "fmt:check": "oxfmt --check", "fix": "npm run lint:fix && npm run fmt", - "check": "npm run fmt:check && npm run lint && npm run typecheck", - "verify": "npm run check && npm run test && npm run build", - "typecheck": "tsc -p tsconfig.json" + "check": "npm run lint && npm run fmt:check", + "verify": "npm run fix && npm run test && npm run build" }, "dependencies": { - "@earendil-works/pi-coding-agent": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.4", - "@tanstack/react-query": "^5.100.11", - "@tanstack/react-router": "^1.170.6", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.20.1", + "@earendil-works/pi-ai": "^0.75.5", + "@earendil-works/pi-coding-agent": "^0.75.5", + "@earendil-works/pi-tui": "^0.75.5", + "@tanstack/react-query": "^5.100.14", + "@tanstack/react-router": "^1.170.10", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typebox": "^1.1.39", + "ws": "^8.21.0", "zod": "^4.4.3" }, "devDependencies": { - "@sinclair/typebox": "^0.34.14", + "@sinclair/typebox": "^0.34.49", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0", - "@types/react": "^19.2.15", + "@types/node": "^22.19.19", + "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^6.0.2", - "better-sqlite3": "^12.8.0", - "drizzle-kit": "^0.18.1", + "better-sqlite3": "^12.10.0", + "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "drizzle-typebox": "^0.3.3", "jsdom": "^29.1.1", "oxfmt": "latest", - "oxlint": "latest", - "tsx": "^4.19.0", + "oxlint": "^1.68.0", + "oxlint-tsgolint": "^0.23.0", + "tsx": "^4.22.4", "typescript": "^5.7.0", "vite": "^8.0.16", "vitest": "^4.1.8" @@ -67,6 +72,6 @@ "node": ">=20" }, "allowScripts": { - "better-sqlite3@12.8.0": true + "better-sqlite3@12.10.0": true } } diff --git a/src/.pi/__tests__/chrome.test.ts b/src/.pi/__tests__/chrome.test.ts index 4c353177e..b958f96a7 100644 --- a/src/.pi/__tests__/chrome.test.ts +++ b/src/.pi/__tests__/chrome.test.ts @@ -1,7 +1,7 @@ import type { ExtensionUIContext } from '@earendil-works/pi-coding-agent'; import { describe, expect, it } from 'vitest'; -import type { WorkspaceSessionReadyState } from '../../../workspace-session-coordinator.js'; +import type { WorkspaceSessionReadyState } from '../../session/workspace-session-coordinator.js'; import { chromeStateForWorkspace, formatBrunchChromeHeaderLines, @@ -28,7 +28,7 @@ describe('Brunch chrome projection', () => { it('formats chrome header as wordmark plus runtime-state summary', async () => { const state = { cwd: '/tmp/project', - spec: { id: 'spec-1', title: 'Spec One' }, + spec: { id: 1, title: 'Spec One' }, session: { id: 'session-1', label: 'Interview #1' }, phase: 'elicitation' as const, chatMode: 'responding-to-elicitation' as const, @@ -52,7 +52,7 @@ describe('Brunch chrome projection', () => { it('formats honest Brunch chrome from one product-state snapshot', async () => { const state = { cwd: '/tmp/project', - spec: { id: 'spec-1', title: 'Spec One' }, + spec: { id: 1, title: 'Spec One' }, session: { id: 'session-1', label: 'Interview #1' }, phase: 'elicitation' as const, chatMode: 'responding-to-elicitation' as const, @@ -85,7 +85,7 @@ describe('Brunch chrome projection', () => { it('formats rich optional runtime and context metadata without fabricating missing fields', () => { const state = { cwd: '/tmp/project', - spec: { id: 'spec-1', title: 'Spec One' }, + spec: { id: 1, title: 'Spec One' }, session: { id: 'session-1', label: 'Interview #1' }, phase: 'elicitation' as const, chatMode: 'responding-to-elicitation' as const, @@ -116,7 +116,7 @@ describe('Brunch chrome projection', () => { const footer = projectBrunchChromeFooterLines( { cwd: '/tmp/project', - spec: { id: 'spec-1', title: 'Spec One' }, + spec: { id: 1, title: 'Spec One' }, session: { id: 'session-1', label: 'Interview #1' }, phase: 'elicitation', chatMode: 'responding-to-elicitation', @@ -162,7 +162,7 @@ describe('Brunch chrome projection', () => { renderBrunchChrome(ui, { cwd: '/tmp/project', - spec: { id: 'spec-1', title: 'Spec One' }, + spec: { id: 1, title: 'Spec One' }, session: { id: 'session-1' }, phase: 'elicitation', chatMode: 'responding-to-elicitation', @@ -189,7 +189,7 @@ describe('Brunch chrome projection', () => { }); function readyWorkspace(cwd: string, sessionId: string, sessionName?: string): WorkspaceSessionReadyState { - const spec = { id: 'spec-1', title: 'Spec One' }; + const spec = { id: 1, title: 'Spec One' }; return { status: 'ready', cwd, diff --git a/src/.pi/__tests__/extension-registry.test.ts b/src/.pi/__tests__/extension-registry.test.ts index 84724beb6..a6277157f 100644 --- a/src/.pi/__tests__/extension-registry.test.ts +++ b/src/.pi/__tests__/extension-registry.test.ts @@ -109,10 +109,10 @@ describe('Brunch explicit Pi extension registry', () => { const brunchChromeFixture = { cwd: '/tmp/brunch', - chatMode: 'interactive' as const, - phase: 'ready' as const, + chatMode: 'responding-to-elicitation' as const, + phase: 'elicitation' as const, spec: { - id: 'spec-1', + id: 1, title: 'Fixture spec', }, session: { diff --git a/src/.pi/__tests__/graph-tools.test.ts b/src/.pi/__tests__/graph-tools.test.ts index b1c8f31c5..f85cdd882 100644 --- a/src/.pi/__tests__/graph-tools.test.ts +++ b/src/.pi/__tests__/graph-tools.test.ts @@ -169,7 +169,7 @@ describe('graph tools end-to-end', () => { it('commit_graph returns diagnostics on invalid batch', () => { const input = translateCommitGraph({ - nodes: [{ ref: 'n1', plane: 'intent', kind: 'not_a_kind', title: 'Bad' }], + nodes: [{ ref: 'n1', plane: 'intent', kind: 'not_a_kind' as never, title: 'Bad' }], edges: [], }); const result = executor.commitGraph(input); diff --git a/src/.pi/__tests__/prompting.test.ts b/src/.pi/__tests__/prompting.test.ts index 12d25e64c..1f0a616a4 100644 --- a/src/.pi/__tests__/prompting.test.ts +++ b/src/.pi/__tests__/prompting.test.ts @@ -204,9 +204,9 @@ describe('Brunch prompt-pack topology', () => { await createBrunchPiExtensionShell( { cwd: '/tmp/brunch', - chatMode: 'interactive', - phase: 'ready', - spec: { id: 'spec-1', title: 'Spec' }, + chatMode: 'responding-to-elicitation', + phase: 'elicitation', + spec: { id: 1, title: 'Spec' }, session: { id: 'session-1', label: 'Session' }, }, undefined, diff --git a/src/.pi/__tests__/structured-exchange-present-request.test.ts b/src/.pi/__tests__/structured-exchange-present-request.test.ts index 42339ba5a..550052f39 100644 --- a/src/.pi/__tests__/structured-exchange-present-request.test.ts +++ b/src/.pi/__tests__/structured-exchange-present-request.test.ts @@ -273,9 +273,6 @@ describe('structured exchange present/request tools', () => { type: 'message', message: { role: 'toolResult', - toolName: PRESENT_OPTIONS_TOOL, - toolCallId: 'present-call-1', - content: [{ type: 'text', text: '## Offer' }], details: { schema: 'brunch.structured_exchange.present', schemaVersion: 1, @@ -286,7 +283,6 @@ describe('structured exchange present/request tools', () => { expectedRequest: { tool: REQUEST_CHOICE_TOOL, required: true }, createdAtToolCallId: 'present-call-1', }, - isError: false, }, }, ]); diff --git a/src/.pi/__tests__/workspace-dialog.test.ts b/src/.pi/__tests__/workspace-dialog.test.ts index 582f1ce25..98be16efe 100644 --- a/src/.pi/__tests__/workspace-dialog.test.ts +++ b/src/.pi/__tests__/workspace-dialog.test.ts @@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises'; import { type Terminal } from '@earendil-works/pi-tui'; import { describe, expect, it } from 'vitest'; -import type { WorkspaceLaunchInventory } from '../../../workspace-session-coordinator.js'; +import type { WorkspaceLaunchInventory } from '../../session/workspace-session-coordinator.js'; import { formatBrunchProductIdentity, readBrunchAnsiLogo } from '../components/brunch-identity.js'; import { buildWorkspaceSelectionView, @@ -35,7 +35,7 @@ describe('spec/session picker', () => { expect(selectWorkspaceSelectionOption(view, 0)).toEqual({ decision: { action: 'continue', - specId: 'spec-alpha', + specId: 1, sessionFile: '/sessions/alpha-current.jsonl', }, }); @@ -59,14 +59,14 @@ describe('spec/session picker', () => { 'Resume existing session', ]); expect(selectWorkspaceSelectionOption(specAction.view, 0)).toEqual({ - decision: { action: 'newSession', specId: 'spec-alpha' }, + decision: { action: 'newSession', specId: 1 }, }); }); it('emits open-session only after a session is selected', () => { const sessionList = buildWorkspaceSelectionView(inventory(), { stage: 'sessionList', - specId: 'spec-alpha', + specId: 1, }); expect(sessionList.options.map((option) => option.label)).toEqual([ @@ -76,7 +76,7 @@ describe('spec/session picker', () => { expect(selectWorkspaceSelectionOption(sessionList, 1)).toEqual({ decision: { action: 'openSession', - specId: 'spec-alpha', + specId: 1, sessionFile: '/sessions/alpha-older.jsonl', }, }); @@ -99,7 +99,7 @@ describe('spec/session picker', () => { it('only shows resume-existing-session when the chosen spec has sessions', () => { const view = buildWorkspaceSelectionView(emptySessionInventory(), { stage: 'specAction', - specId: 'spec-empty', + specId: 3, }); expect(view.options.map((option) => option.label)).toEqual(['Create new session']); @@ -150,7 +150,7 @@ describe('spec/session picker', () => { expect(decisions).toEqual([ { action: 'continue', - specId: 'spec-alpha', + specId: 1, sessionFile: '/sessions/alpha-current.jsonl', }, ]); @@ -168,7 +168,7 @@ describe('spec/session picker', () => { component.handleInput!('\r'); component.handleInput!('\r'); - expect(decisions).toEqual([{ action: 'newSession', specId: 'spec-alpha' }]); + expect(decisions).toEqual([{ action: 'newSession', specId: 1 }]); }); it('returns open-session through the hierarchical keyboard path', () => { @@ -189,7 +189,7 @@ describe('spec/session picker', () => { expect(decisions).toEqual([ { action: 'openSession', - specId: 'spec-alpha', + specId: 1, sessionFile: '/sessions/alpha-older.jsonl', }, ]); @@ -414,10 +414,10 @@ function emptyInventory(): WorkspaceLaunchInventory { function emptySessionInventory(): WorkspaceLaunchInventory { return { cwd: '/project', - currentSpec: { id: 'spec-empty', title: 'Empty' }, + currentSpec: { id: 3, title: 'Empty' }, currentSessionFile: null, needsNewSpec: false, - specs: [{ spec: { id: 'spec-empty', title: 'Empty' }, sessions: [] }], + specs: [{ spec: { id: 3, title: 'Empty' }, sessions: [] }], unavailableSessions: [], }; } @@ -425,36 +425,36 @@ function emptySessionInventory(): WorkspaceLaunchInventory { function inventory(): WorkspaceLaunchInventory { return { cwd: '/project', - currentSpec: { id: 'spec-alpha', title: 'Alpha' }, + currentSpec: { id: 1, title: 'Alpha' }, currentSessionFile: '/sessions/alpha-current.jsonl', needsNewSpec: false, specs: [ { - spec: { id: 'spec-alpha', title: 'Alpha' }, + spec: { id: 1, title: 'Alpha' }, sessions: [ { id: 'session-alpha-current', file: '/sessions/alpha-current.jsonl', - specId: 'spec-alpha', + specId: 1, specTitle: 'Alpha', available: true, }, { id: 'session-alpha-older', file: '/sessions/alpha-older.jsonl', - specId: 'spec-alpha', + specId: 1, specTitle: 'Alpha', available: true, }, ], }, { - spec: { id: 'spec-beta', title: 'Beta' }, + spec: { id: 2, title: 'Beta' }, sessions: [ { id: 'session-beta', file: '/sessions/beta.jsonl', - specId: 'spec-beta', + specId: 2, specTitle: 'Beta', available: true, }, diff --git a/src/.pi/brunch-pi-profile.ts b/src/.pi/brunch-pi-profile.ts index ab5a01aa1..74986301f 100644 --- a/src/.pi/brunch-pi-profile.ts +++ b/src/.pi/brunch-pi-profile.ts @@ -78,6 +78,7 @@ export const BRUNCH_SETTINGS_AUDITED_GETTERS = [ 'getNpmCommand', 'getCollapseChangelog', 'getEnableInstallTelemetry', + 'getHttpIdleTimeoutMs', 'getPackages', 'getExtensionPaths', 'getSkillPaths', diff --git a/src/.pi/extensions/graph/command-adapter.ts b/src/.pi/extensions/graph/command-adapter.ts index bf0db18b3..37fa62f30 100644 --- a/src/.pi/extensions/graph/command-adapter.ts +++ b/src/.pi/extensions/graph/command-adapter.ts @@ -16,41 +16,10 @@ import type { CommitGraphInput, CommitGraphResult, CommitGraphSuccess, - Diagnostic, StructuralIllegal, } from '../../../graph/command-executor.js'; import type { GraphOverview, NeighborhoodResult } from '../../../graph/snapshot.js'; - -// --------------------------------------------------------------------------- -// commit-graph: Pi params → CommitGraphInput -// --------------------------------------------------------------------------- - -/** Shape of a node as received from the LLM tool call. */ -export interface ToolCommitNode { - readonly ref: string; - readonly plane: string; - readonly kind: string; - readonly title: string; - readonly body?: string; - readonly basis?: string; - readonly source?: string; - readonly detail?: unknown; -} - -/** Shape of an edge as received from the LLM tool call. */ -export interface ToolCommitEdge { - readonly category: string; - readonly source: string | { readonly existing: number }; - readonly target: string | { readonly existing: number }; - readonly stance?: string; - readonly rationale?: string; -} - -/** Shape of the commit_graph tool params from the LLM. */ -export interface ToolCommitGraphParams { - readonly nodes: readonly ToolCommitNode[]; - readonly edges: readonly ToolCommitEdge[]; -} +import type { ToolCommitGraphParams } from './tool-schemas.js'; /** * Translate Pi tool params into a CommandExecutor CommitGraphInput. diff --git a/src/.pi/extensions/graph/index.ts b/src/.pi/extensions/graph/index.ts index 7f4f9ae07..c36994e9b 100644 --- a/src/.pi/extensions/graph/index.ts +++ b/src/.pi/extensions/graph/index.ts @@ -10,20 +10,9 @@ * dependencies from the extension shell. */ -import { StringEnum } from '@earendil-works/pi-ai'; import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; -import { Type } from 'typebox'; import type { CommandExecutor } from '../../../graph/command-executor.js'; -import { - INTENT_KINDS, - ORACLE_KINDS, - DESIGN_KINDS, - PLAN_KINDS, - EDGE_CATEGORIES, - EDGE_STANCES, - NODE_BASES, -} from '../../../graph/index.js'; import type { GraphOverview, NeighborhoodResult } from '../../../graph/snapshot.js'; import { translateCommitGraph, @@ -31,6 +20,7 @@ import { formatGraphOverview, formatNeighborhoodResult, } from './command-adapter.js'; +import { CommitGraphParams, ReadGraphParams } from './tool-schemas.js'; // --------------------------------------------------------------------------- // Dependencies injected by the extension shell @@ -47,68 +37,6 @@ export interface BrunchGraphDeps { readonly snapshots: GraphSnapshotReaders; } -// --------------------------------------------------------------------------- -// Tool parameter schemas (TypeBox v1.x / Pi's typebox) -// --------------------------------------------------------------------------- - -const ALL_KINDS = [...INTENT_KINDS, ...ORACLE_KINDS, ...DESIGN_KINDS, ...PLAN_KINDS] as const; - -const CommitNodeSchema = Type.Object({ - ref: Type.String({ - description: "Temporary batch reference id (e.g. 'n1', 'n2')", - }), - plane: StringEnum(['intent', 'oracle', 'design', 'plan'] as const), - kind: StringEnum([...ALL_KINDS]), - title: Type.String({ description: 'Node title — must be non-empty' }), - body: Type.Optional(Type.String({ description: 'Extended description' })), - basis: Type.Optional(StringEnum([...NODE_BASES])), - source: Type.Optional( - Type.String({ - description: "Epistemic attribution (e.g. 'stakeholder', 'derived')", - }), - ), - detail: Type.Optional( - Type.Unknown({ - description: - 'Per-kind detail: decision requires {chosen_option, rejected, rationale}; term requires {definition, aliases?}', - }), - ), -}); - -const EdgeRefSchema = Type.Union([ - Type.String({ description: "Intra-batch ref (e.g. 'n1')" }), - Type.Object({ - existing: Type.Number({ description: 'Id of an existing node' }), - }), -]); - -const CommitEdgeSchema = Type.Object({ - category: StringEnum([...EDGE_CATEGORIES]), - source: EdgeRefSchema, - target: EdgeRefSchema, - stance: Type.Optional(StringEnum([...EDGE_STANCES])), - rationale: Type.Optional(Type.String()), -}); - -const CommitGraphParams = Type.Object({ - nodes: Type.Array(CommitNodeSchema, { - description: 'Nodes to create in this batch', - }), - edges: Type.Array(CommitEdgeSchema, { - description: 'Edges to create, referencing batch refs or existing node ids', - }), -}); - -const ReadGraphParams = Type.Object({ - mode: StringEnum(['overview', 'neighborhood'] as const), - node_id: Type.Optional( - Type.Number({ - description: 'Required for neighborhood mode — the anchor node id', - }), - ), - hops: Type.Optional(Type.Number({ description: 'Neighborhood traversal depth (default: 1)' })), -}); - // --------------------------------------------------------------------------- // Registrar // --------------------------------------------------------------------------- diff --git a/src/.pi/extensions/graph/tool-schemas.ts b/src/.pi/extensions/graph/tool-schemas.ts new file mode 100644 index 000000000..e1f1ab3e6 --- /dev/null +++ b/src/.pi/extensions/graph/tool-schemas.ts @@ -0,0 +1,83 @@ +/** + * Pi-tool-facing graph parameter schemas. + * + * This is the adapter-boundary schema layer for agent tools. It derives enum + * literals from graph/index.ts, but it deliberately does not import db/ or + * Drizzle row schemas: commit_graph accepts a product command shape, not raw + * SQLite rows. + */ + +import { StringEnum, Type, type Static } from '@earendil-works/pi-ai'; + +import { + DESIGN_KINDS, + EDGE_CATEGORIES, + EDGE_STANCES, + INTENT_KINDS, + NODE_BASES, + ORACLE_KINDS, + PLAN_KINDS, +} from '../../../graph/index.js'; + +const ALL_KINDS = [...INTENT_KINDS, ...ORACLE_KINDS, ...DESIGN_KINDS, ...PLAN_KINDS] as const; + +export const CommitNodeSchema = Type.Object({ + ref: Type.String({ + description: "Temporary batch reference id (e.g. 'n1', 'n2')", + }), + plane: StringEnum(['intent', 'oracle', 'design', 'plan'] as const), + kind: StringEnum([...ALL_KINDS]), + title: Type.String({ description: 'Node title — must be non-empty' }), + body: Type.Optional(Type.String({ description: 'Extended description' })), + basis: Type.Optional(StringEnum([...NODE_BASES])), + source: Type.Optional( + Type.String({ + description: "Epistemic attribution (e.g. 'stakeholder', 'derived')", + }), + ), + detail: Type.Optional( + Type.Unknown({ + description: + 'Per-kind detail: decision requires {chosen_option, rejected, rationale}; term requires {definition, aliases?}', + }), + ), +}); + +export const EdgeRefSchema = Type.Union([ + Type.String({ description: "Intra-batch ref (e.g. 'n1')" }), + Type.Object({ + existing: Type.Number({ description: 'Id of an existing node' }), + }), +]); + +export const CommitEdgeSchema = Type.Object({ + category: StringEnum([...EDGE_CATEGORIES]), + source: EdgeRefSchema, + target: EdgeRefSchema, + stance: Type.Optional(StringEnum([...EDGE_STANCES])), + rationale: Type.Optional(Type.String()), +}); + +export const CommitGraphParams = Type.Object({ + nodes: Type.Array(CommitNodeSchema, { + description: 'Nodes to create in this batch', + }), + edges: Type.Array(CommitEdgeSchema, { + description: 'Edges to create, referencing batch refs or existing node ids', + }), +}); + +export const ReadGraphParams = Type.Object({ + mode: StringEnum(['overview', 'neighborhood'] as const), + node_id: Type.Optional( + Type.Number({ + description: 'Required for neighborhood mode — the anchor node id', + }), + ), + hops: Type.Optional(Type.Number({ description: 'Neighborhood traversal depth (default: 1)' })), +}); + +export type ToolCommitNode = Static; +export type ToolCommitEdge = Static; +export type ToolCommitGraphParams = Static; +export type ToolReadGraphParams = Static; diff --git a/src/.pi/extensions/structured-exchange/index.ts b/src/.pi/extensions/structured-exchange/index.ts index a0ed6b61b..1e3b53393 100644 --- a/src/.pi/extensions/structured-exchange/index.ts +++ b/src/.pi/extensions/structured-exchange/index.ts @@ -1,13 +1,13 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; -import { PRESENT_CANDIDATES_TOOL, presentCandidatesTool } from './present-candidates.js'; +import { PRESENT_CANDIDATES_TOOL } from './present-candidates.js'; import { PRESENT_OPTIONS_TOOL, presentOptionsTool } from './present-options.js'; import { PRESENT_QUESTION_TOOL, presentQuestionTool } from './present-question.js'; -import { PRESENT_REVIEW_SET_TOOL, presentReviewSetTool } from './present-review-set.js'; +import { PRESENT_REVIEW_SET_TOOL } from './present-review-set.js'; import { REQUEST_ANSWER_TOOL, requestAnswerTool } from './request-answer.js'; import { REQUEST_CHOICE_TOOL, requestChoiceTool } from './request-choice.js'; import { REQUEST_CHOICES_TOOL, requestChoicesTool } from './request-choices.js'; -import { REQUEST_REVIEW_TOOL, requestReviewTool } from './request-review.js'; +import { REQUEST_REVIEW_TOOL } from './request-review.js'; export type { StructuredExchangeResultDetails as StructuredExchangeToolResultDetails } from '../../../session/structured-exchange.js'; @@ -55,10 +55,6 @@ export const STRUCTURED_EXCHANGE_STUB_TOOL_NAMES = [ REQUEST_REVIEW_TOOL, ] as const; -void presentReviewSetTool; -void presentCandidatesTool; -void requestReviewTool; - export function registerStructuredExchange(pi: ExtensionAPI) { for (const tool of STRUCTURED_EXCHANGE_IMPLEMENTED_TOOLS) { pi.registerTool(tool); diff --git a/src/.pi/extensions/structured-exchange/shared/markdown.ts b/src/.pi/extensions/structured-exchange/shared/markdown.ts index 00d050030..5664425d0 100644 --- a/src/.pi/extensions/structured-exchange/shared/markdown.ts +++ b/src/.pi/extensions/structured-exchange/shared/markdown.ts @@ -53,7 +53,7 @@ export function renderPlainResult(result: ToolResultLike) { } export function markdownEscape(text: string): string { - return text.replace(/([\\`*_{}\[\]()#+\-.!|>])/g, '\\$1'); + return text.replace(/([\\`*_{}[\]()#+\-.!|>])/g, '\\$1'); } export function normalizeOptionalText(value: unknown): string | undefined { diff --git a/src/brunch-tui.ts b/src/brunch-tui.ts index afa67552e..34afc30de 100644 --- a/src/brunch-tui.ts +++ b/src/brunch-tui.ts @@ -9,12 +9,7 @@ import { type CreateAgentSessionRuntimeFactory, } from '@earendil-works/pi-coding-agent'; -import { - applyBrunchOfflineDefault, - brunchResourceLoaderOptions, - createBrunchPiProfile, - createBrunchSettingsManager, -} from './.pi/brunch-pi-profile.js'; +import { applyBrunchOfflineDefault, createBrunchPiProfile } from './.pi/brunch-pi-profile.js'; import { runWorkspaceDialogPreflight } from './.pi/components/workspace-dialog.js'; import { chromeStateForWorkspace, createBrunchPiExtensionShell } from './.pi/pi-extension-shell.js'; import { openWorkspaceGraphRuntime } from './graph/index.js'; diff --git a/src/db/README.md b/src/db/README.md index c8a908487..230a4f80c 100644 --- a/src/db/README.md +++ b/src/db/README.md @@ -4,56 +4,106 @@ SPEC decisions: D16-L, D41-L, D52-L ## Owns -- **Drizzle table definitions** (`schema.ts`) — specs, nodes, edges, - change_log, graph_clock, reconciliation_need. Canonical column-level source - of truth for persisted shapes. `specs` stores `{id, name, slug, - readiness_grade}` only; `elicitation_posture` and `commitment_focus` are - retired. Exports shared enum `const` arrays (`INTENT_KINDS`, - `READINESS_GRADES`, `EDGE_CATEGORIES`, etc.) reused by `graph/` domain types - and Pi tool parameter schemas. - -- **Row schema derivation** (`row-schemas.ts`) — runtime insert/select - schemas derived from Drizzle tables via `drizzle-typebox`. Do not - hand-author parallel row schemas alongside table definitions. +- **Drizzle table definitions** (`schema.ts`) — the canonical column-level + source of truth for persisted graph/workspace rows. It owns the SQLite table + names, column names, and shared enum `const` arrays (`INTENT_KINDS`, + `READINESS_GRADES`, `EDGE_CATEGORIES`, etc.). + +- **Row schema derivation** (`row-schemas.ts`) — runtime insert/select schemas + derived from Drizzle tables via `drizzle-typebox`. Do not hand-author parallel + row schemas alongside table definitions. - **Connection lifecycle** (`connection.ts`) — `better-sqlite3` connection - creation, WAL mode, pragmas, migration runner. + creation, WAL mode, foreign-key pragma, and Drizzle-managed migration + execution. -- **Migrations** — Drizzle-managed schema migrations for `.brunch/data.db`. - Wired when the first `drizzle-kit generate` run lands. +- **Migrations** (`../../drizzle/`) — generated by `npm run db:generate` from + `src/db/schema.ts` and run by `createDb`. Custom data/bootstrap statements + that are part of schema initialization, such as the singleton `graph_clock` + seed row, live in migrations. -## Does NOT own +## Does not own -- Domain logic, validation, policy, CommandExecutor, readers, change-log - replay — all of that lives in `graph/`. -- Query construction beyond simple helpers — domain queries live in `graph/`. +- Domain logic, structural validation, graph policy, CommandExecutor, readers, + change-log replay, or reconciliation semantics. Those live in `graph/`. +- Public RPC request/result contracts. Those live in `rpc/` and should describe + product projections, not raw rows. +- Agent tool parameter schemas. Those live at the Pi adapter boundary and should + describe product command shapes, not raw rows. +- React Query hook shapes. Those live in `web/` and should mirror RPC product + resources. ## Imported by -- `graph/` — the only layer that imports `db/` directly. - No other layer should import from this directory. +- `graph/` is the only application layer that imports `db/` directly. + Non-`graph/` code imports graph-domain APIs instead. ## Enum flow -`db/schema.ts` owns the single `const` arrays (`INTENT_KINDS`, -`EDGE_CATEGORIES`, `NODE_BASES`, etc.). Other layers derive from them: +```pseudo +db/schema.ts + owns: + enum const arrays + Drizzle table definitions -``` -db/schema.ts const arrays + Drizzle tables - │ (single source of truth) - ├──► db/row-schemas.ts drizzle-typebox insert/select - │ (@sinclair/typebox 0.34) - ├──► graph/schema/nodes.ts type IntentKind = (typeof INTENT_KINDS)[number] - │ graph/schema/edges.ts type EdgeCategory = (typeof EDGE_CATEGORIES)[number] + ├─► db/row-schemas.ts + │ drizzle-typebox insert/select schemas + │ @sinclair/typebox 0.34 + │ persistence boundary only + │ + ├─► graph/schema/nodes.ts + │ type IntentKind = typeof INTENT_KINDS[number] + │ + ├─► graph/schema/edges.ts + │ type EdgeCategory = typeof EDGE_CATEGORIES[number] │ - └──► Pi tool parameter schemas Type.Union(INTENT_KINDS.map(Type.Literal)) - (typebox v1.x — Pi's package) + └─► graph/index.ts + re-exports enum arrays so adapters can derive schemas without + importing db/ directly +``` + +## Shape boundaries + +```pseudo +DB row shape: + persisted columns + snake_case + includes LSN columns and foreign-key columns + +Graph domain shape: + GraphNode / GraphEdge / ReconciliationNeed + camelCase + structural invariants validated by graph/ + +Agent tool shape: + commit_graph({ nodes, edges }) + includes batch refs and existing-node refs + excludes DB-owned LSN columns + +RPC shape: + named product projections such as graph.overview + spec/session-addressed where applicable + not a generic table/records API ``` -Do not redeclare enum literals in `graph/` or tool definitions. -Import the `const` array from `db/schema.ts` and derive. +Do not derive agent tools or RPC contracts directly from Drizzle row schemas. +Only enum literals should flow outward from `db/schema.ts`; object shapes are +owned by their boundary. + +## Current schema posture + +The current tables are the M4 graph substrate: `specs`, `nodes`, `edges`, +`graph_clock`, `change_log`, and `reconciliation_need`. + +Multi-spec is the product direction, but graph rows are not yet spec-scoped in +this schema. Before stable `graph.*` RPC or multi-spec UI work lands, add +spec-scoping columns and update graph readers/commands so projections cannot mix +initiative-local graph truth. + +`coherence_state` is intentionally not present yet. Coherence is a product +concept, but the durable table/result contract is still undefined. -## Stack (settled by A20-L spike) +## Stack -`drizzle-orm@0.45.2` + `drizzle-kit@0.31.10` + `better-sqlite3@12.8.0` -+ `drizzle-typebox@0.3.3` + `@sinclair/typebox@0.34.14` +`drizzle-orm@0.45.x` + `drizzle-kit@0.31.x` + `better-sqlite3@12.x` + +`drizzle-typebox@0.3.x` + `@sinclair/typebox@0.34.x` diff --git a/src/db/connection.ts b/src/db/connection.ts index 4d83e5700..b4732a5e3 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -1,101 +1,37 @@ /** + * Uses drizzle-kit migrations generated from `src/db/schema.ts`. + * * better-sqlite3 connection lifecycle. * * SPEC decisions: D16-L (settled by A20-L spike) - * Stack: drizzle-orm@0.45.2 + better-sqlite3@12.8.0 + * Stack: drizzle-orm@0.45.x + better-sqlite3@12.x */ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; import * as schema from './schema.js'; export type BrunchDb = ReturnType>; /** - * Create a Brunch database connection with schema initialized. - * - * Creates all tables if they don't exist and seeds the graph_clock - * with lsn=0. For tests, pass `":memory:"` for an in-memory database. + * Create a Brunch database connection and run Drizzle-managed migrations. * - * When real migrations are needed (existing data to transform), - * replace `initSchema` with `drizzle-kit`-managed migrations. + * For tests, pass `":memory:"` for an in-memory database. */ export function createDb(path: string): BrunchDb { const sqlite = new Database(path); sqlite.pragma('journal_mode = WAL'); sqlite.pragma('foreign_keys = ON'); - initSchema(sqlite); - return drizzle(sqlite, { schema }); + const db = drizzle(sqlite, { schema }); + migrate(db, { migrationsFolder: migrationsFolder() }); + return db; } -/** - * Push schema DDL and seed initial data. - * - * This replaces drizzle-kit migrations for the initial M4 slice. - * Pre-release posture: no existing data to preserve, so CREATE IF - * NOT EXISTS is sufficient. Add migration files when schema evolution - * needs data transformation. - */ -function initSchema(sqlite: Database.Database): void { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS specs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - slug TEXT NOT NULL, - readiness_grade TEXT NOT NULL DEFAULT 'grounding_onboarding' - ); - - CREATE TABLE IF NOT EXISTS nodes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - plane TEXT NOT NULL, - kind TEXT NOT NULL, - title TEXT NOT NULL, - body TEXT, - basis TEXT NOT NULL DEFAULT 'explicit', - source TEXT, - detail TEXT, - created_at_lsn INTEGER NOT NULL, - updated_at_lsn INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS edges ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category TEXT NOT NULL, - source_id INTEGER NOT NULL REFERENCES nodes(id), - target_id INTEGER NOT NULL REFERENCES nodes(id), - stance TEXT, - basis TEXT NOT NULL DEFAULT 'explicit', - rationale TEXT, - created_at_lsn INTEGER NOT NULL, - updated_at_lsn INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS graph_clock ( - id INTEGER PRIMARY KEY, - lsn INTEGER NOT NULL DEFAULT 0 - ); - - CREATE TABLE IF NOT EXISTS change_log ( - lsn INTEGER PRIMARY KEY, - operation TEXT NOT NULL, - payload TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS reconciliation_need ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - target_kind TEXT NOT NULL, - target_edge_id INTEGER REFERENCES edges(id), - target_a_id INTEGER REFERENCES nodes(id), - target_b_id INTEGER REFERENCES nodes(id), - kind TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'open', - reason TEXT, - created_at_lsn INTEGER NOT NULL, - resolved_at_lsn INTEGER - ); - - INSERT OR IGNORE INTO graph_clock (id, lsn) VALUES (1, 0); - `); +function migrationsFolder(): string { + return resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle'); } diff --git a/src/graph/README.md b/src/graph/README.md index 82851e931..ec0672e37 100644 --- a/src/graph/README.md +++ b/src/graph/README.md @@ -5,69 +5,136 @@ SPEC decisions: D4-L, D20-L, D51-L, D52-L, D53-L ## Owns -- **CommandExecutor** — the single mutation boundary for all graph writes. - Hides validation, LSN allocation, change-log append, transaction mechanics. - Returns structured results: `ok`, `needs_human`, `policy_blocked`, - `version_conflict`, `structural_illegal`. +- **CommandExecutor** (`command-executor.ts`) — the single mutation boundary for + graph/spec writes. It hides structural validation, transaction mechanics, LSN + allocation, change-log append, and structured command results. -- **commitGraph** (D53-L) — atomic batch mutation accepting `{ nodes, edges }` - with intra-batch refs (`"n1"`) and existing-node refs. One tool call, - one LSN, all-or-nothing (I34-L). The load-bearing tool for propose-graph. +- **commitGraph** — atomic batch mutation for `propose-graph`: one tool call, + one transaction, one LSN, all-or-nothing. It accepts product command input + (`nodes[]` with batch refs, `edges[]` with batch/existing refs), not raw DB + rows. -- **Readers / snapshot functions** — graph queries at multiple detail levels: - cursory full-graph overview, node-neighborhood with configurable hops (I35-L). - Called by `agents/contexts/` for prompt injection. +- **Readers / snapshot functions** (`snapshot.ts`) — graph projections at + multiple detail levels: full overview, node neighborhood, and open + reconciliation needs. These return typed domain objects, not Drizzle rows. -- **Policy** — per-category edge policy (cascade, recon-need triggers, - criteria-help signals, projection effects). +- **Domain schema types** (`schema/`) — `GraphNode`, `GraphEdge`, + `ReconciliationNeed`, kind/category types, and derived intent-kind grouping. -- **Validators** — structural legality checks: closed edge-category set, - stance rules, supersession acyclicity, framing matrix, intra-batch - reference resolution. +- **Policy** (`policy/category-policy.ts`) — edge-category semantics such as + cascade behavior, reconciliation triggers, and projection effects. -- **Change-log replay** — ordered mutation history keyed by LSN. - -- **Reconciliation-need substrate** — separate from graph edges; - target is `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}`. +- **Workspace graph runtime** (`workspace-store.ts`) — opens `.brunch/data.db` + through `db/connection.ts` and returns a `CommandExecutor` plus bound snapshot + readers for adapters. ## Imports from -- `db/` — Drizzle table definitions, connection handle. - This is the only layer that touches `db/` directly. +- `db/` — Drizzle table definitions, enum arrays, and connection handle. + `graph/` is the only application layer that should import `db/` directly. ## Imported by -- `.pi/extensions/graph/` — Pi tool adapters call CommandExecutor -- `rpc/` — graph.* RPC handlers call readers and CommandExecutor -- `agents/contexts/` — snapshot functions for prompt context +- `.pi/extensions/graph/` — Pi tool adapters for `commit_graph` and `read_graph`. +- `rpc/` — future `graph.*` projection handlers and graph-adjacent state. +- `agents/contexts/` — future prompt context renderers. +- `probes/` — graph proof drivers. -## Current state (Phase 1 stubs) +## Current topology -``` +```pseudo graph/ -├── atoms.ts NodeId, EdgeId, Lsn type aliases -├── index.ts public re-exports -├── schema/ -│ ├── edges.ts GraphEdge, EdgeCategory, EdgeStance, EdgeBasis -│ └── reconciliation-need.ts ReconciliationNeed types -└── policy/ - └── category-policy.ts CATEGORY_POLICY table + atoms.ts + NodeId / EdgeId / Lsn aliases + + index.ts + public graph-layer export barrel + re-exports enum arrays for non-db consumers + + command-executor.ts + CommandExecutor + command input/result types + createSpec + updateReadinessGrade + createNode + commitGraph / dryRunCommitGraph + create/resolve reconciliation need + + snapshot.ts + getGraphOverview + getNodeNeighborhood + getOpenReconciliationNeeds + row -> domain mapping + + workspace-store.ts + openWorkspaceGraphRuntime(cwd) + openWorkspaceCommandExecutor(cwd) + + schema/ + nodes.ts + edges.ts + reconciliation-need.ts + + policy/ + category-policy.ts ``` -## Target state (after M4) - +## Boundary flow + +```pseudo +db/schema.ts + Drizzle rows + enum literals + │ + ▼ +graph/schema/*.ts + domain types derived from enum literals + │ + ▼ +CommandExecutor + validates product command input + writes rows transactionally + appends change_log + │ + ├─► .pi/extensions/graph + │ agent tool adapter + │ + ├─► rpc/ future graph handlers + │ public product projections + │ + └─► agents/contexts future renderers + prompt context snapshots ``` -graph/ -├── atoms.ts -├── index.ts -├── command-executor.ts CommandExecutor + result types -├── commit-graph.ts batch validation + intra-batch ref resolution -├── readers.ts snapshot queries (cursory, neighborhood) -├── change-log.ts replay, changesSince -├── schema/ -│ ├── edges.ts -│ ├── nodes.ts Phase 2 — per-plane node kinds -│ └── reconciliation-need.ts -└── policy/ - └── category-policy.ts + +## Fractal split points + +Keep `command-executor.ts` and `snapshot.ts` as public entry points. When either +file needs to split, use same-named private folders rather than exposing more +entry points: + +```pseudo +graph/command-executor/ + commit-graph.ts + specs.ts + reconciliation-needs.ts + diagnostics.ts + lsn.ts + change-log.ts + +graph/snapshot/ + row-mappers.ts + overview.ts + neighborhood.ts + reconciliation-needs.ts + changes-since.ts ``` + +Do not create these files until pressure is real or an importer/test names the +seam. The desired shape is documented here so future splits preserve topology. + +## Known near-term schema pressure + +- Add spec scoping before stable `graph.*` RPC / multi-spec UI projections. + The current table set has `specs`, but graph rows are not yet scoped to a spec. +- Keep `coherence_state` deferred until its durable semantics are defined. +- Begin consuming `db/row-schemas.ts` at persistence-facing validation seams; + do not use row schemas as public RPC or agent-tool object contracts. diff --git a/src/graph/command-executor.ts b/src/graph/command-executor.ts index ca2828e47..2062b1551 100644 --- a/src/graph/command-executor.ts +++ b/src/graph/command-executor.ts @@ -527,7 +527,7 @@ export class CommandExecutor { if (!isReadinessGrade(readinessGrade)) { diagnostics.push({ field: 'readinessGrade', - message: `"${readinessGrade}" is not a valid readiness grade`, + message: `"${String(readinessGrade)}" is not a valid readiness grade`, }); } if (diagnostics.length > 0) return { status: 'structural_illegal', diagnostics }; @@ -579,7 +579,7 @@ export class CommandExecutor { diagnostics: [ { field: 'readinessGrade', - message: `"${input.readinessGrade}" is not a valid readiness grade`, + message: `"${String(input.readinessGrade)}" is not a valid readiness grade`, }, ], }; diff --git a/src/probes/structured-exchange-ordering-proof.test.ts b/src/probes/structured-exchange-ordering-proof.test.ts index 6236b08d7..5116fb136 100644 --- a/src/probes/structured-exchange-ordering-proof.test.ts +++ b/src/probes/structured-exchange-ordering-proof.test.ts @@ -18,8 +18,8 @@ describe('structured-exchange ordering proof', () => { expect(proof.eventOrder).toEqual([ 'present_options:start', 'present_options:end', - 'ui:select', 'request_choice:start', + 'ui:select', 'ui:input', 'request_choice:end', ]); diff --git a/src/rpc/README.md b/src/rpc/README.md index 5dd33cfdc..14b00a115 100644 --- a/src/rpc/README.md +++ b/src/rpc/README.md @@ -21,7 +21,10 @@ canonical stores: edges change_log reconciliation_needs + + deferred graph-adjacent store coherence_state + not yet defined; do not expose a durable coherence contract until modeled Pi JSONL transcript session_binding @@ -173,7 +176,8 @@ session.submitExchangeResponse agent continues after acceptance -> agent calls commitGraph({ nodes, edges }) internally -> CommandExecutor validates and commits atomically - -> graph/coherence projections update + -> graph projections update + -> future graph.coherenceSummary updates only after coherence semantics are defined ``` The user reviews the concept-level proposal. The graph becomes product truth only after the internal `commitGraph` path succeeds. From c77a3fdafa7a425113bab6c49f25d23251a38c8d Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 18:41:33 +0200 Subject: [PATCH 33/34] mega sync and archive of docs vs SPEC --- .../session-re-extending-sessions.jsonl | 0 .../smart-brunch-poc-architecture-prd.md | 0 .../transcript-of-pi-architecture-review.md | 0 .../design/PLANNING_PERSISTENCE_MODEL.md | 0 docs/README.md | 1 - docs/architecture/prd.md | 4 +- docs/design/SPEC_INITIATIVE_MODEL.md | 4 +- memory/SPEC.md | 12 +- src/web/README.md | 399 ++++++++++++++++++ 9 files changed, 414 insertions(+), 6 deletions(-) rename {docs => archive/docs}/architecture/artifacts/session-re-extending-sessions.jsonl (100%) rename {docs => archive/docs}/architecture/artifacts/smart-brunch-poc-architecture-prd.md (100%) rename {docs => archive/docs}/architecture/artifacts/transcript-of-pi-architecture-review.md (100%) rename {docs => archive/docs}/design/PLANNING_PERSISTENCE_MODEL.md (100%) create mode 100644 src/web/README.md diff --git a/docs/architecture/artifacts/session-re-extending-sessions.jsonl b/archive/docs/architecture/artifacts/session-re-extending-sessions.jsonl similarity index 100% rename from docs/architecture/artifacts/session-re-extending-sessions.jsonl rename to archive/docs/architecture/artifacts/session-re-extending-sessions.jsonl diff --git a/docs/architecture/artifacts/smart-brunch-poc-architecture-prd.md b/archive/docs/architecture/artifacts/smart-brunch-poc-architecture-prd.md similarity index 100% rename from docs/architecture/artifacts/smart-brunch-poc-architecture-prd.md rename to archive/docs/architecture/artifacts/smart-brunch-poc-architecture-prd.md diff --git a/docs/architecture/artifacts/transcript-of-pi-architecture-review.md b/archive/docs/architecture/artifacts/transcript-of-pi-architecture-review.md similarity index 100% rename from docs/architecture/artifacts/transcript-of-pi-architecture-review.md rename to archive/docs/architecture/artifacts/transcript-of-pi-architecture-review.md diff --git a/docs/design/PLANNING_PERSISTENCE_MODEL.md b/archive/docs/design/PLANNING_PERSISTENCE_MODEL.md similarity index 100% rename from docs/design/PLANNING_PERSISTENCE_MODEL.md rename to archive/docs/design/PLANNING_PERSISTENCE_MODEL.md diff --git a/docs/README.md b/docs/README.md index 9282a68b2..e351f5470 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,6 @@ Older brief-library examples were retired; future behavioral-kernel evidence sho ## Horizon design notes - [`docs/design/SPEC_INITIATIVE_MODEL.md`](../design/SPEC_INITIATIVE_MODEL.md) — working design proposal for spec as initiative/problem lifecycle, claim as truth-bearing unit, projected current truth, and repo-native branching/merge implications for planning data. -- [`docs/design/PLANNING_PERSISTENCE_MODEL.md`](../design/PLANNING_PERSISTENCE_MODEL.md) — working design proposal for repo-native canonical planning history, SQLite materialization, markdown projections, and the recommended changeset-oriented persistence posture. ## Working conventions diff --git a/docs/architecture/prd.md b/docs/architecture/prd.md index cafed4bc7..939054dfa 100644 --- a/docs/architecture/prd.md +++ b/docs/architecture/prd.md @@ -1,6 +1,6 @@ # Brunch POC Architecture PRD -This document extracts the final architectural position from [the source transcript](./transcript-of-pi-architecture-review.md). The transcript used the placeholder product name `foobar`; this document maps that proposal onto `brunch` and prefers later corrections over earlier statements when they conflict. +This document extracts the final architectural position from [the source transcript](../../archive/docs/architecture/artifacts/transcript-of-pi-architecture-review.md) (archived). The transcript used the placeholder product name `foobar`; this document maps that proposal onto `brunch` and prefers later corrections over earlier statements when they conflict. This is a POC PRD, not a declaration of shipped product truth. Its job is to state the architecture clearly enough that Brunch can prove or falsify the core bets in a deliberate order. @@ -58,7 +58,7 @@ The POC may start intent-first, but storage, transport, and naming should not lo The transcript store should be treated as a first-class architectural decision, not as an accidental side effect of whichever pi default happens to be present. -The session export in [docs/architecture/artifacts/session-re-extending-sessions.jsonl](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/artifacts/session-re-extending-sessions.jsonl) sharpens the near-term posture: +The session export in [archive/docs/architecture/artifacts/session-re-extending-sessions.jsonl](../../archive/docs/architecture/artifacts/session-re-extending-sessions.jsonl) (archived) sharpens the near-term posture: - pi JSONL sessions are richer than a flat append log - they already support tree structure, branch summaries, compaction entries, labels, model/thinking changes, `custom` entries, and `custom_message` entries diff --git a/docs/design/SPEC_INITIATIVE_MODEL.md b/docs/design/SPEC_INITIATIVE_MODEL.md index c808e9048..39f81335a 100644 --- a/docs/design/SPEC_INITIATIVE_MODEL.md +++ b/docs/design/SPEC_INITIATIVE_MODEL.md @@ -1,12 +1,12 @@ # Spec Initiative Model -> Status: **working design proposal**. +> Status: **vocabulary locked (D61-L); richer model deferred as Future Direction**. > Date: 2026-05-20. > Scope: the horizon model for how Brunch should represent specifications, claims, planning lifecycles, and collaboration over time. > > This note captures a design conclusion that emerged while pressure-testing the current `ln-*` workflow against the product direction in [`docs/architecture/prd.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [`docs/architecture/pi-seam-extensions.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), and [`memory/SPEC.md`](file:///Users/lunelson/Code/hashintel/brunch-next/memory/SPEC.md): a **single start-to-finish spec for the whole product** is a naive and impractical model. > -> This document does not yet change the canonical architecture register. It is a design note intended to sharpen the next round of architecture and planning decisions. +> The spec-as-initiative identity and the spec↔claim vocabulary are now locked in [`memory/SPEC.md`](file:///Users/lunelson/Code/hashintel/brunch-next/memory/SPEC.md) (D61-L, plus Lexicon `Spec` / `Claim`). The richer model below — cross-spec claim survival/adoption, initiative-status lifecycle, spec-to-spec relationships, current-truth-as-projection — remains a deferred directional bet (SPEC §Future Direction → Spec initiative & claim model), not yet product contract. ## Why this note exists diff --git a/memory/SPEC.md b/memory/SPEC.md index 00799a0f7..b08e0de88 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -142,6 +142,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D56-L — Intent node kinds: 11 kinds in 3 derived categories (basic / structural / reasoning); canonical contract is [`docs/design/GRAPH_MODEL.md` §Per-plane node kinds](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md#per-plane-node-kinds).** `basic` (goal, thesis, term, context) carries grounding material; `structural` (requirement, assumption, constraint, invariant) carries core specification; `reasoning` (decision, criterion, example) carries decisions and evidence. Category is a pure function of `kind` — not stored on the node. The `basic` category maps to spec-grade progression: grounding-gate readiness depends on satisficing threshold of basic-category nodes (D57-L). `thesis` carries "what/who/why/for whom" material (La Carte Blanche style). `term` carries canonical naming commitments (ubiquitous language). `invariant` is first-class (not a constraint subtype) because its operational role differs: invariants get `dependency` and `proof` edges, constraints get `boundary` edges. Each intent kind has a modality-of-claim and source-question rubric for agent prompting (GRAPH_MODEL.md §"Prompting guidance"). Oracle (check, validation_method, evidence, obligation), design (module, interface), and plan (milestone, frontier, slice) kinds are stable from worked examples. Depends on: D54-L. Supersedes: D7-L (`framing_as`), A7-L. - **D57-L — Spec-grade grounding gate is LLM-judged satisficiency with a count floor on basic-category nodes.** The gate from `grounding_onboarding` toward `elicitation_ready` is not structurally enforced by rubric coverage checks. The agent judges readiness using prompt-embedded abstract drivers (Walter-style: what is it, who is it for, what problem, what value, when used, how measured) but cannot declare grounding complete with zero `basic`-category nodes. Grounding elicitation may establish workspace posture, but posture is not a spec-row field or graph node kind in the POC. Depends on: D45-L, D56-L. Supersedes: D30-L grounding-bundle anchor vocabulary as the sole readiness gate description. Refines: D30-L, D45-L. - **D51-L — Graph edge model is a closed structural-category set with a separate ReconciliationNeed substrate; canonical contract is [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md).** Every accepted edge is one of eight closed categories (`dependency`, `proof`, `support`, `realization`, `boundary`, `composition`, `association`, `supersession`); `stance: for | against` is valid only on `proof` and `support`; `basis ∈ explicit | accepted_review_set` (no `inferred`). Accepted edges have no mutable `status` field — `proposed` lives in review-set drafts, `rejected` is absent + change-log audit, `stale` is represented by a `ReconciliationNeed`. Identity fields (`category`, `sourceId`, `targetId`, `stance`) are immutable on an accepted edge; a "category change" is delete + recreate. Only `dependency` cascades automatically; other categories surface advisory recon-needs rather than auto-blocking. Cross-plane edges are unrestricted at the POC stage; `realization` subtypes (implementation/establishment/assertion/etc.) may be derived from node-tuple lookup later rather than encoded on the edge. `ReconciliationNeed` is a separate substrate whose target is exactly `{kind:'edge', edgeId}` or `{kind:'node_pair', aId, bId}` — it is not itself a graph edge. Depends on: D4-L, D8-L, D16-L, D27-L, A14-L. Supersedes: the named-relation catalogue in `docs/architecture/pi-seam-extensions.md` §"Edge types" (`validates`, `instance_of`, `produces`, `discharges`, `depends_on`, `derived_from`, `counterexample_for`, `witnesses`), the per-relation policy registry / lookup, the brainstormed expanded edge taxonomy in `archive/docs/design/GRAPH_EDGE_CATEGORIES.md`, and any `concerns`-edge wiring from reconciliation needs to graph nodes. +- **D61-L — A spec is an initiative answering a problem; its truth-bearing units are claims resolved at node level.** A spec's identity is its problem-answering initiative, not the product areas, seams, or domains it touches; it may reach a done-state while those keep evolving. Its truth-bearing units ("claims") are the existing `structural` and `reasoning` intent node kinds (requirement, assumption, constraint, invariant, decision, criterion, example) under D54-L/D56-L — `claim` is a vocabulary umbrella, not a new node kind — so revision, conflict, and supersession resolve at node level (supersession edges per D51-L), not at whole-spec level. POC scope: each spec owns its own intent graph (no cross-spec claim sharing); the `workspace → spec → session` hierarchy (D11-L) is unchanged and `spec.readiness_grade` (D45-L) remains the only persisted spec-state — no initiative-status column is added. The full initiative/claim model (cross-spec claim survival/adoption, initiative-status lifecycle, spec-to-spec relationships, current-truth-as-projection) is deferred to Future Direction §Spec initiative & claim model; rationale: [`docs/design/SPEC_INITIATIVE_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/SPEC_INITIATIVE_MODEL.md). Depends on: D11-L, D45-L, D54-L, D56-L. Supersedes: —. #### Authority & mutation @@ -372,6 +373,14 @@ src/agents/ - **Title and hidden-thinking-label as state-indicative chrome.** Pi exposes `ctx.ui.setTitle()` and `ctx.ui.setHiddenThinkingLabel()` as small dynamic chrome surfaces. Brunch now uses `setTitle()` narrowly as part of the D35-L chrome wrapper: the title is a stateless projection from the activated product snapshot, currently `brunch — `, and must not synthesize working-state it does not have. Richer title states tied to active role/lens/workflow remain deferred until stable producers exist. Hidden-thinking-label remains deferred: candidate labels vary by agent role or lens (e.g. "Eliciting…", "Reviewing batch…", "Reconciling…") and depend on the relevant subsystems (agent-role dispatcher, lens registry) landing first. - **Status keys as the dynamic contribution channel.** `ctx.ui.setStatus(key, text)` remains the multi-extension-friendly seam for other Brunch extensions and future dynamic Brunch state to surface in the footer's status row. Brunch's chrome wrapper does not contribute its own status key by default; it merges all foreign status entries via `footerData.getExtensionStatuses()` into the footer's right column so contributions surface without anyone owning the whole footer. +### Planning persistence evolution + +- Brunch's own planning truth (`memory/SPEC.md`, `memory/PLAN.md`) is canonical for the POC, but the directional bet is to demote markdown to **projections** over a repo-native structured canonical planning history — stable-ID `spec` / `claim` / changeset records, branchable and semantically mergeable alongside code — materialized into local SQLite for fast query/validation/traversal. This converges meta-planning persistence with Brunch's own product-native graph thinking. It is **not yet adopted**: the canonical-only rule (durable planning state lives only in `memory/SPEC.md` + `memory/PLAN.md`) holds until a concrete frontier owns the migration. Rationale: [`archive/docs/design/PLANNING_PERSISTENCE_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/archive/docs/design/PLANNING_PERSISTENCE_MODEL.md). + +### Spec initiative & claim model + +- D61-L locks only the vocabulary (spec = initiative answering a problem; claims = truth-bearing nodes resolved at node level). The richer model in [`docs/design/SPEC_INITIATIVE_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/SPEC_INITIATIVE_MODEL.md) is deferred and surfaces only when multi-spec work lands: **claims survive their parent spec** and may be adopted by later specs (a cross-spec claim graph, not per-spec); an **initiative-status lifecycle** (`proposed` / `drafting` / `active` / `adopted` / `done` / `superseded` / `abandoned`) distinct from `readiness_grade`; a small closed set of **spec-to-spec relationships** (`informed_by`, `supersedes`, `parallel_to`, `depends_on`, `conflicts_with`) rather than a generic "related" edge; and **current truth as a projection** over surviving claims with explicit precedence — explicit supersession wins, `adopted` / `active` / `done` outrank `drafting` / `abandoned`, and unresolved overlap surfaces a reconciliation need instead of silent last-writer-wins. Adopting any of these is a frontier decision, not yet product contract. + ## Lexicon | Term | Definition | @@ -398,7 +407,8 @@ src/agents/ | **Structural legality** | Synchronous schema/ontology validity of graph mutations: edge categories from the closed set in `docs/design/GRAPH_MODEL.md`, per-category stance/cardinality/acyclicity rules, immutable accepted-edge identity (`category`, `sourceId`, `targetId`, `stance`), per-plane closed node `kind` enums, required `detail` sub-schemas for `decision`/`term`, `constraint.subtype` enum, and transaction invariants. Structural legality can fail even before semantic coherence is evaluated. | | **Print snapshot** | The M1 meaning of the print transport mode: boot the Brunch host, resolve workspace/spec/session state through the coordinator, render product-shaped state, and exit without running an agent turn. | | **Workspace** | The current working directory where the Brunch CLI was invoked. It scopes `.brunch/` state for the launch context. It is not user-created, not selectable within the dialog, and there is only one active workspace per Brunch process. The UI may display a project identity/name derived from cwd-local manifests or directory basename, but that name labels the cwd; it does not create a separate workspace object. | -| **Spec / specification** | The user-created specification container within a workspace, identified by its intent-graph root. Multiple specs may coexist under one workspace. A spec contains sessions and the graph data gathered through those sessions (intent nodes, design nodes, oracle/plan data as they land). Future plan-execution mode operates on a selected spec. | +| **Spec / specification** | A user-created **initiative that exists to answer a problem** well enough to guide coordinated work, and that can reach a done-state even though the product, domains, and architecture keep evolving (D61-L). Concretely it is a container within a workspace, identified by its intent-graph root, holding sessions and the truth-bearing graph data (claims) gathered through them; the areas, seams, and domains it touches are not its identity. Multiple specs may coexist under one workspace; future plan-execution mode operates on a selected spec. | +| **Claim** | Umbrella term for a truth-bearing graph node — the `structural` and `reasoning` intent kinds (requirement, assumption, constraint, invariant, decision, criterion, example) under D54-L/D56-L. Not a separate node kind: revision, conflict, supersession, and current-truth resolution happen at claim (node) level via supersession edges (D51-L), not at whole-spec level (D61-L). A claim is created within a spec; cross-spec claim survival/adoption is deferred (Future Direction §Spec initiative & claim model). | | **Session** | An elicitation transcript belonging to one spec. Backed by a linear pi JSONL session under `.brunch/sessions/`. A spec may have many sessions over time; a session never changes specs. Pi branch/tree mechanics are unsupported Brunch product behavior in the POC. | | **Session display name** | Optional human-readable label for a session, stored as Pi `session_info` metadata and used by pickers/chrome to distinguish sessions. It may be user-set or Brunch-generated from transcript content; it is not canonical spec/session identity. | | **Session binding** | The first Brunch custom entry in a session that binds the Pi session id to exactly one spec id and schema version. Makes JSONL self-describing; registry/index state is an acceleration, not the canonical binding. | diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 000000000..9fc5d102b --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,399 @@ +# web/ — Brunch React client + +Canonical references: `docs/architecture/prd.md` §Browser / web client, `src/rpc/README.md` + +This directory owns the browser client for `brunch --mode web`. The browser is a thin remote head over the Brunch host: one React app, one WebSocket-backed Brunch JSON-RPC client, TanStack Router for route/data preloading, and TanStack Query for cache ownership and update scheduling. + +The web client must not read SQLite, Pi RPC, local JSONL, or `.brunch/workspace.json` directly. It speaks Brunch public RPC method names and renders product projections. + +## Current topology + +```pseudo +web/ + main.tsx + browser entrypoint + creates root-owned WebSocketRpcClient + creates BrunchWebRuntime + disposes runtime on pagehide + + rpc-client.ts + one WebSocket JSON-RPC client + request(method, params) -> Promise + subscribe(listener) for server notifications + close() + + app.tsx + creates QueryClient + TanStack Router runtime + root route loader ensureQueryData(workspace.snapshot) + current proof UI: + workspace.snapshot + session.transcriptDisplay # proof-era method; rename debt per rpc/README + brunch.updated notification -> invalidate relevant queries + + *.test.tsx / *.test.ts + component and transport oracles for current web proof +``` + +Current `app.tsx` intentionally keeps query options in-file because the surface is still tiny. Split to the topology below as soon as a second route, mutation, or graph projection lands. + +## Host / asset boundary + +`src/rpc/web-host.ts` serves the built Vite bundle and attaches Brunch JSON-RPC at `/rpc`: + +```pseudo +GET / + -> dist-web/index.html + +GET /assets/* + -> static built assets + +WS /rpc + -> Brunch public JSON-RPC handlers +``` + +Useful pattern from `../brunch`: a CLI-launched local service can choose a random localhost port, print/open the URL, serve static client assets, and keep the browser as a local attachment to the same process authority. Brunch-next already follows the same shape through `startWebHost`; future launch polish can copy the old runtime guard / browser-open ergonomics if needed. + +## Framework contract + +```pseudo +React + component/runtime layer only + +TanStack Router + route ownership + route params + loaders that prewarm Query caches via ensureQueryData + defaultPreloadStaleTime: 0 + +TanStack Query + query/mutation cache + request deduplication + invalidation on Brunch RPC notifications + optimistic mutation scaffolding when web writes arrive + +WebSocketRpcClient + transport only + no React state + no product-specific cache + no method-specific helpers +``` + +Do not add a second client state container for server truth. Local UI state is fine for transient form controls, expansion state, canvas viewport, selected graph node, etc.; product facts live in Query cache entries derived from RPC projections. + +## Source-of-truth flow + +```pseudo +Brunch host canonical stores + .brunch/workspace.json + SQLite graph DB + Pi JSONL transcript + │ + ▼ +rpc/ handlers + named product projections and mutations + │ + ▼ +web/rpc-client.ts + one WebSocket request/notification transport + │ + ▼ +web/queries/* and web/mutations/* + Query options, mutation options, subscription bridges + │ + ▼ +routes/features/components + render product projections +``` + +## Target file topology + +Introduce these files incrementally when an importer or test needs the seam. Do not create empty markers. + +```pseudo +web/ + app.tsx + createBrunchWebRuntime + createBrunchWebRouter + BrunchWebApp shell + + main.tsx + DOM mounting only + + rpc-client.ts + generic WebSocket JSON-RPC transport + + query-client.ts + QueryClient factory/defaults once defaults matter outside tests + + query-keys.ts + one stable key factory object for all product resources + + queries/ + workspace.ts + workspaceSnapshotQueryOptions(rpc) + workspaceSelectionStateQueryOptions(rpc) + + session.ts + pendingExchangeQueryOptions(rpc, specId, sessionId) + sessionExchangesQueryOptions(rpc, specId, sessionId) + # proof-era compatibility may live here temporarily: + transcriptDisplayQueryOptions(rpc, specId, sessionId) + + graph.ts + graphOverviewQueryOptions(rpc, specId) + graphNodeNeighborhoodQueryOptions(rpc, specId, nodeId, hops) + graphRecentChangesQueryOptions(rpc, specId, sinceLsn) + + coherence.ts + graphCoherenceSummaryQueryOptions(rpc, specId) + # only after durable coherence semantics are modeled + + mutations/ + workspace.ts + activateWorkspaceMutationOptions(rpc) + + session.ts + promptExchangeMutationOptions(rpc) + submitExchangeResponseMutationOptions(rpc) + submitMessageMutationOptions(rpc) + + subscriptions/ + brunch-updates.ts + useBrunchUpdateInvalidation(rpc, queryClient) + maps notification topics/LSNs -> exact Query keys + + routes/ + root.tsx + workspace shell and loader + + workspace.tsx + spec/session selection dashboard + + session.tsx + transcript + pending exchange surface + + graph.tsx + graph overview / node-neighborhood route + + features/ + structured-exchange/ + PendingExchangePanel.tsx + response controls for request_answer / request_choice / request_choices / request_review + + propose-graph/ + ProposeGraphExchange.tsx + ProposalConceptCard.tsx + GraphContextPanel.tsx + + graph/ + GraphOverview.tsx + NodeNeighborhood.tsx + ReconciliationBadges.tsx +``` + +## Query key contract + +Keys should mirror Brunch product resources, not database tables: + +```pseudo +queryKeys = { + workspace: { + snapshot: ['workspace.snapshot'], + selectionState: ['workspace.selectionState'], + }, + + session: { + pendingExchange: (specId, sessionId) => + ['session.pendingExchange', specId, sessionId], + + exchanges: (specId, sessionId) => + ['session.exchanges', specId, sessionId], + + transcriptDisplay: (specId, sessionId) => + ['session.transcriptDisplay', specId, sessionId], # proof-era only + }, + + graph: { + overview: (specId) => ['graph.overview', specId], + nodeNeighborhood: (specId, nodeId, hops) => + ['graph.nodeNeighborhood', specId, nodeId, hops], + recentChanges: (specId, sinceLsn) => + ['graph.recentChanges', specId, sinceLsn], + coherenceSummary: (specId) => + ['graph.coherenceSummary', specId], + }, +} +``` + +Avoid: + +```pseudo +['nodes'] +['edges'] +['records'] +['sqlite', tableName] +['pi-rpc', command] +``` + +## RPC methods to web hooks + +Method names follow `src/rpc/README.md`. Existing proof-era methods may remain until renamed, but new web work should use the stable vocabulary below. + +```pseudo +rpc.discover + useRpcDiscoveryQuery(rpc) + Purpose: optional capability/schema introspection for debug panels and adaptive clients. + +workspace.snapshot + workspaceSnapshotQueryOptions(rpc) + Purpose: cwd product state, project/posture, current/default spec/session, chrome state. + Route loader: root route. + +workspace.selectionState + workspaceSelectionStateQueryOptions(rpc) + Purpose: boot/picker inventory and whether explicit activation is required. + Route: workspace/spec-session picker. + +workspace.activate + activateWorkspaceMutationOptions(rpc) + Purpose: apply explicit workspace -> spec -> session decision. + On success: invalidate workspace.snapshot, workspace.selectionState, session/graph keys for selected resources. + +session.promptExchange + promptExchangeMutationOptions(rpc) + Purpose: start/resume/advance assistant-first loop until pending exchange, idle, needs_human, or blocker. + On success: invalidate session.pendingExchange and session.exchanges. + +session.pendingExchange + pendingExchangeQueryOptions(rpc, specId, sessionId) + Purpose: current unresolved structured exchange. + Route: session/propose-graph panel. + +session.submitExchangeResponse + submitExchangeResponseMutationOptions(rpc) + Purpose: submit terminal response for one pending structured exchange. + On success: invalidate session.pendingExchange, session.exchanges, graph.overview, graph.coherenceSummary as applicable. + +session.submitMessage + submitMessageMutationOptions(rpc) + Purpose: ordinary non-exchange user text or explicit interruption. + Must not silently answer a pending exchange. + +session.exchanges + sessionExchangesQueryOptions(rpc, specId, sessionId) + Purpose: transcript-derived structured exchange history. + +future graph.overview + graphOverviewQueryOptions(rpc, specId) + Purpose: committed graph projection, node/edge counts, LSN. + +future graph.nodeNeighborhood + graphNodeNeighborhoodQueryOptions(rpc, specId, nodeId, hops) + Purpose: focused graph context around selected/mentioned node. + +future graph.recentChanges / graph.changesSince + graphRecentChangesQueryOptions(rpc, specId, sinceLsn) + Purpose: worldUpdate panels and cache patching. + +future graph.coherenceSummary + graphCoherenceSummaryQueryOptions(rpc, specId) + Purpose: coherence banner/badges after durable semantics are defined. +``` + +## Subscription / notification bridge + +Current proof code listens for `brunch.updated` and invalidates broad keys. Target shape: + +```pseudo +useBrunchUpdateInvalidation(rpc, queryClient) + subscribe to server notifications once at app/root level + + for each notification: + if topic == workspace.snapshot: + invalidate queryKeys.workspace.snapshot + + if topic == session.pendingExchange: + invalidate exact pendingExchange key + + if topic == session.exchanges: + invalidate exact exchanges key + + if topic == graph.overview: + invalidate or patch exact graph.overview(specId) + + if topic == graph.nodeNeighborhood: + invalidate neighborhoods that include changed node ids + + if topic == graph.coherenceSummary: + invalidate exact graph.coherenceSummary(specId) +``` + +Prefer exact invalidation when the notification includes `{specId, sessionId, lsn, nodeIds, edgeIds}`. Broad invalidation is acceptable in proof code, but it should not become the product cache policy. + +## Route/data ownership pattern + +Use the old `../brunch/src/client/routes/specification/$id/-specification-data.ts` as a cautionary reference: centralizing route query keys, loader priming, and invalidation helpers is useful, but REST fetchers and large all-in-one route data modules should not be copied directly. + +Target Brunch-next pattern: + +```pseudo +route loader + context.queryClient.ensureQueryData(queryOptionsFrom(web/queries/*)) + +route component + useSuspenseQuery(same query options) for required data + useQuery(enabled: target != null) for optional panels + +feature component + receives loaded product projection props or calls a narrow feature hook + never constructs raw RPC method strings ad hoc +``` + +## `propose-graph` UI data dependencies + +```pseudo +ProposeGraphExchange route/panel + required: + workspace.snapshot + session.pendingExchange(specId, sessionId) + + context panels: + graph.overview(specId) + graph.nodeNeighborhood(specId, selectedNodeId, hops) + graph.coherenceSummary(specId) # future, once modeled + + mutations: + session.submitExchangeResponse + decision: accept_concept | request_revision | reject + comment?: string + + after submit: + pendingExchange invalidates immediately + graph projections update only after agent-internal commitGraph succeeds +``` + +The browser does not call `commitGraph`, does not submit node/edge drafts in `propose-graph`, and does not treat proposal prose as graph truth. + +## Testing expectations + +```pseudo +rpc-client.test.ts + transport ordering + request id correlation + malformed frame failure + notifications independent from requests + +app / route tests + one runtime-owned QueryClient and router + loaders call expected queryOptions + no optional session query when no session is selected + notifications invalidate expected keys + +future hook tests + query keys are exact and stable + mutation success invalidates/patches intended domains only + pending exchange response does not submit ambient message text + graph updates do not remount unrelated session routes +``` + +Keep tests at the seam that owns the behavior: transport tests for `rpc-client.ts`, hook tests for `queries/` / `mutations/`, route integration tests for loader/cache ownership, and component tests for rendering/accessibility. From b1fed926a445a19eb28413c15430f44471482e0e Mon Sep 17 00:00:00 2001 From: Lu Nelson Date: Tue, 2 Jun 2026 20:39:29 +0200 Subject: [PATCH 34/34] FE-785: Reframe plan around POC delivery --- docs/archive/PLAN_HISTORY.md | 6 + memory/PLAN.md | 551 ++++++++++++++--------------------- 2 files changed, 226 insertions(+), 331 deletions(-) diff --git a/docs/archive/PLAN_HISTORY.md b/docs/archive/PLAN_HISTORY.md index 9cd00e2a7..3b324a3d6 100644 --- a/docs/archive/PLAN_HISTORY.md +++ b/docs/archive/PLAN_HISTORY.md @@ -3,6 +3,12 @@ This file is the active POC-line plan archive for `memory/PLAN.md`. Legacy pre-`next` history was moved out of the live docs tree with the old archived implementation. +## 2026-06-02 Delivery-cut archive + +Archived from `memory/PLAN.md` when the live plan shifted from concept/frontier proving to the POC delivery spine. The delivery plan now keeps only the P0/P1 marks and the last few completed summaries live. + +- 2026-06-01 — **sealed-pi-profile-runtime-state** (FE-776) — Prep envelope tied off. Brunch now runs through a sealed Pi profile with programmatic resource/settings policy, transcript-backed runtime state projection, explicit product extension shell under `src/.pi/`, and session display names via Pi `session_info`. Graph-model prep locked the closed edge contract, common node shape, 11 intent kinds, retired `provenance`/`framing_as`, and validated the A20 persistence line (`drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`). Verified with profile/runtime/schema/persistence tests and repeated `npm run verify`. Residue: strict built-in command suppression remains an A18-L Pi API seam; runtime prompt composition moved to the delivery-critical `agents-composition-layer` frontier. + ## 2026-05-20 Origin - 2026-05-20 — **Pre-POC archive and reseed** — razed pre-POC implementation, archived legacy docs and planning memory under `archive/`, tagged `next-baseline`, and reseeded `memory/SPEC.md` and `memory/PLAN.md` from the three canonical POC architecture docs. Phase 3 infra bootstrap was folded into `walking-skeleton` rather than remaining an independent frontier. diff --git a/memory/PLAN.md b/memory/PLAN.md index c6467fa91..cb0286c81 100644 --- a/memory/PLAN.md +++ b/memory/PLAN.md @@ -7,395 +7,284 @@ Keep this file light. Archive older completed work to docs/archive/PLAN_HISTORY.md. Edit Sequencing for ordering/status churn; keep Frontier Definitions relatively stable. - - Anchored on SPEC.md and the three POC architecture docs. --> + Do not spread retired work history across handoff files, refactor plans, or ad hoc status notes. --> # Plan ## Context -Brunch-next is proceeding on the razed `next` line (tag `next-baseline`) as a thin product layer over `pi-coding-agent`. M0–M3 plus `pi-ui-extension-patterns` (FE-744) proved the basic host, JSONL transcript viability, probe/RPC substrate, read-only web shell, Pi extension seams, and public-RPC structured-exchange parity; detailed completed frontier definitions live in `docs/archive/PLAN_HISTORY.md`. The active frontier is now `sealed-pi-profile-runtime-state`, expanded in place into a **prep envelope before `graph-data-plane` (M4) CRUD**. It carries two strands under one branch (`ln/fe-776-graph-layer-prep-profile`): **(a) Pi harness sealing** — Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`, and operational-mode / role-preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries reconstructed at turn boundaries; **(b) graph-model lock-and-materialize** — lock the conceptual edge and node contracts in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), stub the type/policy surface under `src/graph/`, and prove the A20-L Drizzle 1.0-beta + `drizzle-orm/typebox` + `better-sqlite3` + Pi `registerTool` round-trip so M4 CRUD lands on settled persistence/schema-derivation foundations. Phase 1 (edges) has landed; Phase 2 (nodes) and the Drizzle spike are the remaining moves before `graph-data-plane` resumes. A18-L strict command containment is still carried as a residual Pi API risk to route as a narrow upstream ask if the embedded-harness strand surfaces a clean seam. - -Architecture grill (2026-06-01) locked several decisions that shape graph-data-plane and agent-graph-integration: **(1)** source topology `src/{.pi, agents, db, graph, session, rpc, web}` with directed layer dependencies (D52-L); **(2)** `commitGraph` — a single-tool atomic batch mutation accepting `{ nodes, edges }` with intra-batch and existing-node references, one LSN, all-or-nothing (D53-L, I34-L); **(3)** the `propose-graph` strategy bypasses review-set — user accepts a concept, agent generates and persists the full subgraph through `commitGraph` directly (D26-L updated); **(4)** strategy/lens axis split — strategies are interaction shapes (`step-wise-decision-tree`, `step-wise-disambiguate`, `propose-graph`, `project-graph`), lenses are topical focus (`intent`, `design`, `oracle`) (D25-L updated). The `commitGraph` path under `propose-graph` is the primary A14-L proof target: if LLMs cannot produce structurally-legal multi-node multi-edge batches, the core flow must be re-architected. +Brunch-next is now in a **POC delivery cut**. The earlier concept-driven frontier work proved the host, transcript, public RPC, sealed Pi profile, SQLite graph data plane, `CommandExecutor`, real graph tools, and one real `propose-graph → commitGraph` agent proof. The remaining POC work is not to prove Brunch is good at specification work in the broad product-quality sense; that belongs beyond this POC. The delivery question is narrower and stricter: can the real product entrypoints compose without the harness secretly supplying wiring? -Phase 2 node grill (2026-06-01) locked the node layer: **(1)** common flat `GraphNode` shape with `title`, `body`, `basis`, `source` (free-form epistemic attribution), and `detail` JSON column (D54-L); **(2)** `provenance` retired from both nodes and edges — `change_log` owns audit trail (D55-L); **(3)** 11 intent kinds in 3 derived categories: basic (`goal`, `thesis`, `term`, `context`), structural (`requirement`, `assumption`, `constraint`, `invariant`), reasoning (`decision`, `criterion`, `example`) (D56-L); **(4)** `framing_as` retired, absorbed by thesis/term/constraint/invariant/goal (A7-L retired, D7-L retired, I7-L retired); **(5)** spec-grade grounding gate: LLM-judged satisficiency with count floor on basic-category nodes, Walter-style rubric in prompt (D57-L); **(6)** `posture` is spec-level, not a graph node; **(7)** modality-of-claim + source-question rubric and context promotion heuristic for agent prompting. +The black triangles for this cut are: -### POC assumption pressure +1. **Live graph observability:** the TUI remains the writer/agent session while the web app attaches over Brunch WebSocket RPC and shows the selected spec's graph changing. +2. **Behavioral runtime posture:** operational goal/strategy/lens state changes the actual prompt/resource/tool posture, not just a stored label. +3. **Capture to graph truth:** a structured elicitation response can become high-confidence graph truth through `CommandExecutor`, visible to web/TUI projections. +4. **Graph tool resilience:** the direct agent graph path survives more than the one A14 happy path: existing-node refs, structural-illegal diagnostics/retry, and ambiguity/no-overcommit cases. +5. **Review cycle, if included in the POC story:** `project-graph` proposal generation surfaces a dry-run-valid review set, and approval commits atomically. -The POC should maximize assumption falsification rather than merely implement milestone labels. Treat the table below as the live consequence map from SPEC assumptions to frontier pressure; when scoping a frontier, prefer the thinnest slice that can validate or falsify its assigned assumptions. Retired/validated assumptions A10-L and A23-L are now carried by the SPEC decision/invariant residues for chrome, structured exchange, and public RPC parity; FE-744's remaining residue is the still-open A18-L containment limitation, not missing chrome work. - -| Assumption | Pressure / what could falsify it | Plan consequence | -| --- | --- | --- | -| A1-L Pi substrate seams | A needed host/session/RPC/extension seam cannot be expressed without forking Pi. | Mostly exercised by M0-M3; FE-744 and `sealed-pi-profile-runtime-state` close the remaining UI/profile seams before graph-agent work depends on them. | -| A3-L command layer sufficiency | Agent, UI, reviewer, or capture writes need shortcuts around one `CommandExecutor`. | `graph-data-plane`, `agent-graph-integration`, and `authority-model` must prove one command boundary for every write path. | -| A4-L global LSN adequacy | Replay, staleness, or reconciliation ordering needs per-entity/vector clocks. | `graph-data-plane` establishes one-LSN-per-transaction; `turn-boundary-reconciliation` tries to break it with cross-session traces. | -| A5-L probe/transcript driver quality | Agent-as-user probes fail to catch regressions or cannot produce reviewable transcript evidence for realistic Brunch seams. | FE-744 has proved a deterministic public-RPC structured-exchange permutation driver; future brief-based or generative golden runs must pass through the `.fixtures/runs///` probe/transcript artifact path. | -| A6-L unified `graph.*` namespace | Intent/oracle/design/plan semantics become confusing or unsafe under one umbrella. | `graph-data-plane` and `agent-graph-integration` should start unified but watch for namespace pressure. | -| A7-L `framing_as` modality | ~~Product framings need a node-shape carrier that `framing_as` cannot express.~~ | **Retired.** Phase 2 node lock absorbed `framing_as` into first-class `thesis`, `term`, `context`, `constraint`, `invariant`, and `goal` kinds (D54-L, D56-L). | -| A8-L reconciliation substrate | Gaps, contradictions, process debt, and conflicts need separate substrates immediately. | `graph-data-plane` builds the shared substrate; `coherence-first-class` and known-bad briefs test subtype pressure. | -| A9-L mention ledger granularity | Session-scoped snapshots miss necessary staleness or create noisy hints. | Defer until `turn-boundary-reconciliation`, after graph ids/LSNs exist. | -| A11-L next-turn delivery | Side-task/reviewer results require mid-turn delivery or another event plane. | Keep deferred until M5/M7 side-task/reviewer paths exist; test at turn-boundary rendezvous. | -| A13-L deferred observer/auditor queue | Async audit/backfill needs canonical chat/turn tables or privileged writes. | Not load-bearing after D18-L; defer until a backstop queue is actually introduced. | -| A14-L graph-mutation structural legality | LLMs cannot produce structurally-legal `commitGraph` batches (multi-node multi-edge with intra-batch refs) or review-set entity drafts reliably enough. The `propose-graph` → `commitGraph` path (D53-L) is the primary proof target. | `graph-data-plane` must land `commitGraph` batch validation (I34-L); `agent-graph-integration` must test LLM generation against real CommandExecutor. This is the highest-stakes assumption: failure requires re-architecture of the propose-graph flow. | -| A15-L establishment hints | Offers are not reconstructable or useful from transcript entries alone. | M5 establishment-offer probe runs and FE-744 chrome affordances exercise this. | -| A16-L reviewer trigger/scope | Reviewer findings are too slow, noisy, or incomplete under deferred policy. | Do not overbuild early; first accepted review-set probe runs should make reviewer policy empirical. | -| A17-L elicitation temperament preference | Users do not need persistent interrogative/proposal preference. | Outer-loop adoption signal only; do not block POC. | -| A18-L command containment | Hiding suggestions + lifecycle blocking leaves unsafe Pi built-ins reachable. | FE-744 product-shell evidence must name any Pi upstream seam before M5/M6 authority work relies on it. | -| A19-L sealed Pi profile | Ambient `.pi` settings/resources still shape Brunch product behavior. | `sealed-pi-profile-runtime-state` is a gate before graph tools and authority-sensitive agent work. | -| A20-L Drizzle line + schema path | The chosen Drizzle line blocks migrations, SQLite fidelity, monotonic counter/change-log mechanics, or runtime-schema derivation. | Prove the persistence seam now inside `sealed-pi-profile-runtime-state`: one representative table plus monotonic counter / change-log skeleton, then let `graph-data-plane` inherit the settled choice. | -| A21-L bounded coherence | Contradiction/gap verdicts cannot represent useful coherence without broader judgment. | Keep implementation late (M8), but design known-bad probe scenarios earlier so the rubric is falsifiable. | -| A22-L synchronous elicitor capture | Elicitor over-captures, misses obvious facts, or cannot use preface to resolve uncertainty. | `agent-graph-integration` needs targeted capture probe runs before async observer backstops are reconsidered. | +All delivery frontiers must also continue materializing the locked source topology (D52-L): `src/{.pi, agents, db, graph, session, rpc, web}` with directed dependencies. Treat topology completion as a product-delivery dimension, not cleanup. Each frontier definition names the files/directories it should move toward their final home. +The multi-spec workspace model is now explicit: a workspace is the cwd; multiple specs may coexist under it; each session binds to exactly one spec; each POC spec owns its own intent graph; cross-spec claim sharing/adoption is deferred (D11-L, D21-L, D61-L). Delivery work must target an explicit selected/current spec and must not accidentally recreate a workspace-global graph. ## Sequencing ### Active -1. `agent-graph-integration` — M5. Graph tools, synchronous elicitor capture, review-set acceptance, reviewer advisory writes; all writes via the command layer. The `propose-graph → commitGraph` runtime vocabulary gate has landed; next M5 work can run the A14-L real-LLM proof, then scope capture/reviewer slices. +1. `live-graph-observer` — not-started — P0 visual black triangle: TUI writer + web observer + selected-spec graph view over WebSocket RPC. ### Next -1. `agents-composition-layer` — the `agents/` 3-layer prompt composition (D58-L), goal/strategy/lens packs + AUTO selector, snapshot pull/render/surface (D60-L), and the `.pi/context → agents/` migration. Depends on the runtime-vocabulary slice now folded into `agent-graph-integration`. -2. `probes-and-transcripts-evolution` — keep probe/transcript artifacts honest as new seams need evidence. +1. `agents-composition-layer` — P0 behavior black triangle: make goal/strategy/lens/grade/posture change prompt manifests and agent posture; retire `src/.pi/context` into `src/agents`. +2. `capture-response-to-graph` — P0 product loop: structured exchange answer → narrow high-confidence capture → `CommandExecutor` commit → web graph update. +3. `graph-tool-resilience` — P0 hardening: broaden the A14 proof beyond the single happy path and capture retry/diagnostic evidence. +4. `project-graph-review-cycle` — P1 unless demo narrative promotes it: real `project-graph` review-set proposal/approval loop. +5. `minimal-authority-shell` — P1 safety: thin POC authority posture over already-existing command-result seams and `elicit` tool policy. +6. `poc-live-ship-gate` — P1 final gate: fresh-cwd runbook exercising the composed product path end to end. ### Parallel / Low-conflict -- `probes-and-transcripts-evolution` — Harden the probe/transcript artifact path as new seams need evidence: report schemas, transcript renderers, targeted probe scenarios, and optional brief inputs that feed normal `.fixtures/runs///` runs. Doc/test-heavy, but assumption-critical. -- `subagents-for-proposal-diversity` — Optional enhancement to candidate-proposal generation (D44-L). Lands when `agent-and-graph-integration` (M5) is far enough along that batch-proposal flow exists and would benefit from parallel data-gathering; never a blocker. +- `probes-and-transcripts-evolution` — continuous probe/report/transcript hardening as each delivery frontier lands evidence. +- `topology-readmes-and-boundaries` — small doc/test hardening when a frontier moves files or exposes a boundary; should remain attached to the frontier when possible rather than becoming an abstract cleanup project. ### Horizon -- `authority-model` — M6. Three-tier policy (autonomous / requires-confirmation / human-only) end-to-end across modes. -- `turn-boundary-reconciliation` — M7. Graph-revision tracking, session interest sets, `worldUpdate` injection, and the mention-staleness hint synthesiser. -- `coherence-first-class` — M8. Clarify the product meaning of coherence, then implement synchronous structural legality plus stored semantic coherence verdicts visible to UI and agent. -- `compaction-and-conflict-widening` — M9. Compaction preserves graph + coherence anchors; interest sets can widen; conflict signals remain intelligible at long horizons. -- `flue-pattern-adoption` — Sandbox abstraction (SessionEnv/SandboxApi style), remote-deploy shape, MCP adapter. Post-POC. -- `oracle-design-plan-graphs` — Lift oracle / design / plan planes from stub status to durable persistence + commands. Post-POC. -- `framework-direction-stubs` — Lightweight structural stubs for Context layer, capability tiers, candidate artefacts. Discretionary; only when downstream pressure makes a stub cheaper than a hole. -- `geolog-and-petri-execution` — Datalog-shaped intent store and petri-net plan execution. Exploratory; parallel to Brunch proper. +- `turn-boundary-reconciliation` — M7; graph revisions, `worldUpdate`, mention staleness, side-task/reviewer drains. +- `coherence-first-class` — M8; bounded coherence verdicts backed by reconciliation needs. +- `compaction-and-conflict-widening` — M9; long-horizon continuity through compaction. +- `subagents-for-proposal-diversity` — optional proposal-quality enhancement; never a POC blocker. +- `oracle-design-plan-graphs` — lift oracle/design/plan planes from stubs after the POC delivery spine works. +- `flue-pattern-adoption` — post-POC harness-pattern adoption. +- `framework-direction-stubs` — discretionary structural stubs only when downstream pressure makes a stub cheaper than a hole. +- `geolog-and-petri-execution` — exploratory, parallel to Brunch proper. ## Frontier Definitions -### spec-persistence-and-startup +### live-graph-observer -- **Name:** Spec persistence and startup integrity -- **Linear:** none — folded into FE-785 (`agent-graph-integration`); no separate issue created. -- **Branch:** landed on `ln/fe-785-agent-graph-integration` (commits `c1053963`, `5e5cf1a1`); not branched separately, contrary to the one-branch-per-frontier default. -- **Kind:** structural (persistence-model correction + startup-path regression repair) -- **Status:** done -- **Objective:** Restore the DB-on-startup path lost in the rebuild and correct the workspace/spec/session persistence model so Brunch initializes and boots correctly under all foreseeable conditions, reading each fact from its canonical home. Concretely: **(a)** model specs as DB rows (`specs{id:int, name, slug, readiness_grade}`), retiring `elicitation_posture` and `commitment_focus`; **(b)** create `.brunch/data.db` if absent at startup and route spec create/read/grade-update through the `CommandExecutor`; **(c)** rename `.brunch/state.json` → `.brunch/workspace.json` and reshape to `{project:{name,slug}, current:{specId:int, sessionId}, posture:}`, dropping the dead `source` field; **(d)** collapse the `brunch.session_binding` entry to `{specId:int}`, dropping `sessionId`/`specTitle` and the `sessionId !== header.id` self-guard (fork-portable); **(e)** resolve spec names from the DB, not from JSONL or workspace state. -- **Why now / unlocks:** The rebuild branch regressed two capabilities the prior version had — DB-on-startup and specs-in-DB — because work optimized for green tests over working product runs (`createDb`/`new CommandExecutor` appear only in `:memory:` tests; the TUI shell builds extensions with no graph deps via `{ coordinator }`; no `.brunch/data.db` is ever created at runtime; mention-autocomplete reads `FIXTURE_GRAPH_MENTION_SOURCE`). This frontier repairs that and lands the spec row D45-L already assumes exists, unblocking the runtime portion of `agent-graph-integration` (the A14-L real-LLM proof needs the DB live at startup) and the deferred agent-runtime vocabulary work. Retires the spec-row persistence hole and the unscoped global graph namespace concern. -- **Acceptance:** - - `specs` table exists; spec create/get/grade-update route through `CommandExecutor`; `elicitation_posture` and `commitment_focus` are absent from the model. - - Startup creates `.brunch/data.db` if missing and opens it; spec creation writes a `specs` row with an integer id (no `spec-${uuid}` mint). - - `.brunch/workspace.json` (renamed from `state.json`) persists `{project:{name,slug}, current:{specId:int, sessionId}, posture}`; first run scaffolds `posture` with empty-string axes; no `source` field; `activation`/`chrome` are not persisted. - - `brunch.session_binding` is `{schemaVersion, specId:int}`; the `sessionId !== header.id` guard is gone; a forked session retains its spec linkage through the inherited binding. - - The spec picker/inventory resolves spec names from the DB by `specId`. - - Init/startup succeeds across the foreseeable condition matrix: no `.brunch/`; workspace.json + DB present; workspace.json present + DB missing; current spec/session missing or stale; multiple specs; new-spec; new-session-for-current-spec; cancel. -- **Verification:** Inner — verify gate plus spec-`CommandExecutor` unit tests, `workspace.json` read/write/scaffold tests, binding-schema tests. Middle — the startup-condition-matrix integration tests (the north-star oracle), coordinator↔DB integration, picker-name-from-DB, architectural no-bypass (spec writes only via `CommandExecutor`). Outer — a real `brunch` boot/`--mode print` run against a regenerated `.brunch/` proving a live `.brunch/data.db` is created and read. -- **Cross-cutting obligations:** All spec writes route through `CommandExecutor` (D16-L/D20-L no-bypass). Session identity stays Pi-owned — Brunch contributes only `{specId}`; keep runtime state linear-transcript-backed and fork-portable. Do **not** wire graph-agent tools here — this frontier owns the DB lifecycle; `agent-graph-integration` inherits it to register graph tools. **SPEC reconciliation required (with the build):** retire the `elicitation_posture` half of D45-L, gut/retire D46-L, trim requirement #22, simplify I31-L to grade-only, delete prompt-assembly layer 7, and flip the SPEC §Vocabulary-evolution `posture` note from spec-level → workspace-level (POC-stubbed). Update `src/README.md`, `src/db/README.md`, `src/session/README.md` migration notes. -- **Traceability:** R22, R28 / D16-L, D20-L, D40-L, D45-L (revise), D46-L (retire), D52-L / I31-L (revise) / A19-L, A20-L. Retires: spec-row persistence hole; DB-on-startup regression. -- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (posture deferral note, §Vocabulary evolution); this session's locked state-model decisions. -- **Current execution pointer:** Done. Card queue exhausted and removed after Card 1 (spec table + CommandExecutor create/read/grade update) and Card 2 (DB-backed startup, `.brunch/workspace.json`, collapsed binding, picker names from DB). Verified with `npm run verify` plus real `brunch --mode print` against a fresh cwd. - -### agent-runtime-vocabulary - -- **Name:** Session-agent runtime vocabulary fix -- **Linear:** none — folded into FE-785 (`agent-graph-integration`); FE-789 is no longer planned as a separate frontier/PR. -- **Branch:** landed as a small enabling slice on the FE-785 stack; do not submit a separate FE-789 PR. -- **Kind:** structural sub-slice inside `agent-graph-integration` (corrects a projected, persisted session-agent custom-entry record to match revised D25-L/D40-L/D59-L) -- **Status:** folded into `agent-graph-integration` -- **Objective:** Bring the live `brunch.agent_runtime_state` machinery (`src/.pi/extensions/operational-mode.ts`) into conformance with the reconciled SPEC: **(a)** un-collapse `AgentLensId` from `AgentStrategyId` — lens becomes the orthogonal topical axis `intent | design | oracle`; **(b)** replace the stale strategy vocabulary (`step-by-step`, `disambiguate-via-examples`) with `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; **(c)** add the objective-shaped `goal` axis (`grounding-advance | elicit-expand | commit-converge | capture-posture`, grade-derived, AUTO-able) per D59-L; **(d)** make `op_mode` the only WHO field and **derive** the foreground role (`elicitor`) from it, dropping `agentRole` as stored state; **(e)** make `strategy`/`lens`/`goal` optional and `auto`-able. Regenerate `DEFAULT_BRUNCH_AGENT_STATE`, the append/project/switch helpers, and all fixtures. -- **Why now / unlocks:** The code is the last place still carrying the pre-reconciliation model (collapsed lens, missing `propose-graph`/`project-graph`, role-as-state). `propose-graph` must exist before `agent-graph-integration` can run the A14-L real-LLM proof (the `propose-graph → commitGraph` path is the named proof target), and the orthogonal axes are the precondition for the `agents/` 3-layer composition. Foundational and small. +- **Name:** Live selected-spec graph observer over web RPC +- **Linear:** unassigned +- **Branch:** to create — `ln/-live-graph-observer` +- **Kind:** bounded feature / tracer bullet +- **Status:** not-started +- **Objective:** Make the graph visible as live product state while the TUI remains the writer. Add product RPC graph reads/subscriptions and a minimal web graph panel so a graph mutation from the TUI/agent path updates the browser's selected-spec graph view. +- **Why now / unlocks:** This is the primary POC observability mark. Without a simultaneous TUI session and web graph view, the graph-native workspace remains mostly invisible even though persistence and tools exist. - **Acceptance:** - - `AgentLensId` is independent of `AgentStrategyId`; lens ∈ `intent | design | oracle`. - - strategy ∈ `step-wise-decision-tree | step-wise-disambiguate | propose-graph | project-graph`; old names gone everywhere. - - concrete `AgentStrategyId` / `AgentLensId` / `AgentGoalId` values stay separate from persisted `auto` selections; `goal` has a grade-derived default, and all three objective axes accept `auto`; `op_mode` derives the foreground role; no stored `agentRole`. - - append/project/switch helpers round-trip the new record; malformed/illegal tuples rejected; I25-L tests updated and green. - - all fixtures/tests regenerated to the new vocabulary; `npm run verify` green. -- **Verification:** Inner — runtime-state append/project/switch unit tests over the new axes (I25-L); legal-tuple acceptance / illegal-tuple rejection. Middle — projection round-trip from real JSONL reload. No new outer-loop here; the A14-L behavioral proof rides on `agent-graph-integration`. -- **Cross-cutting obligations:** Keep state linear-transcript-backed (D40-L); foreground role derived, not stored. Do **not** author prompt packs or build `compose()` here — that is `agents-composition-layer`. If `agents/state.ts` is introduced for the shared enums, keep it to type/enum + legal-combination homes only. -- **Traceability:** D25-L (revised), D40-L (revised), D59-L, I25-L (revised) / A14-L (sharpens the proof). Retires: collapsed lens, stale strategy vocabulary, role-as-session-state. -- **Design docs:** `memory/SPEC.md` §Active Decisions (D25-L, D40-L, D58-L, D59-L), Lexicon. -- **Current execution pointer:** Done. Reconciled `brunch.agent_runtime_state` now carries `operationalMode`, `agentGoal`, `agentStrategy`, and `agentLens` without stored `agentRole`; role is derived from `op_mode`, concrete axis id unions stay separate from the `auto` selection sentinel, deterministic elicitation lens metadata uses `intent`, and stale strategy/lens names were removed from source/tests. Verified with targeted runtime/prompting/chrome/RPC/exchange tests and `npm run verify`. + - `graph.overview` and a focused graph read such as `graph.nodeNeighborhood` or equivalent target names are exposed through Brunch JSON-RPC discovery with schemas/examples. + - Web attaches over the existing WebSocket RPC client, selects/uses explicit `{specId, sessionId?}` product resources, and renders a minimal intelligible graph projection (list/table/graph visualization is acceptable; polish is not the point). + - A graph commit made through the real product path invalidates/notifies the web client, which refetches from canonical graph readers and shows the updated selected-spec graph without page reload. + - The TUI remains the writer; the web surface is read-only unless a later explicit product command changes that. + - Multi-spec discipline: graph reads target the selected/current spec; no workspace-global graph projection is introduced. +- **Verification:** Inner — RPC handler/discovery/schema tests; web query/render tests around empty graph and populated graph. Middle — integration test or probe that performs a real `commitGraph`/graph-tool write and observes WebSocket notification/refetch over the public RPC surface. Outer — manual TUI + web smoke: fresh cwd, activate spec/session in TUI, open web dashboard, create/commit graph, see web update. +- **Topology materialization:** `graph/` remains the read/domain owner; `rpc/` owns graph method handlers and subscriptions; `web/` owns graph rendering; no `web/` or `.pi/` import of `db/`; no duplicate graph DTOs outside projected/read-model types. +- **Cross-cutting obligations:** Preserve D19-L thin named RPC methods, D33-L client attachment semantics, D35-L product chrome/projection discipline, and D52-L source dependency direction. Do not introduce a generic read gateway or view store. +- **Traceability:** R7, R10, R11, R12 / D5-L, D10-L, D19-L, D33-L, D52-L, D60-L / I21-L, I35-L / A3-L, A4-L. +- **Design docs:** `memory/SPEC.md` D19-L, D33-L, D52-L, D60-L; `src/rpc/README.md`; `src/web/README.md`; `docs/design/GRAPH_MODEL.md`. ### agents-composition-layer -- **Name:** agents/ 3-layer prompt composition, snapshot tiers, and .pi/context migration +- **Name:** Agent prompt-resource composition, runtime manifests, and snapshot contexts - **Linear:** unassigned - **Branch:** to create — `ln/-agents-composition-layer` -- **Kind:** structural (new composition seam + held Class-B migration; spans `agents/`, `graph/`, `session/`, `.pi/extensions/`) -- **Status:** next -- **Objective:** Build the `agents/` layer per D58-L/D59-L/D60-L and migrate `.pi/context/` into it. **(1)** `agents/state.ts` — the legal `(op_mode × goal × strategy × lens)` combination table, shared axis enums, and the code-owned `{name, description, location}` manifest entry for every goal/strategy/lens/method resource. **(2)** `agents/compose.ts` — the per-agent projection that emits the runtime header (agent control + runtime-state) plus the gated prompt-resource manifest filtered by tuple/grade/`op_mode`/allow-list, with router rules for pinned/AUTO axes; it is projection, not a selector that concatenates semantic bodies, and it replaces the fixed `PROMPT_PACK_ORDER`. **(3)** `agents/{definitions,goals,strategies,lenses,methods}/*.md` — author the (currently absent) goal/strategy/lens/method resources as `read`-on-demand markdown; rehome `brunch-base`/`elicit`/`elicitor` as `definitions/*.md` (frontmatter = model/thinking + tool authority + allow-lists; body = system prompt); convert `structured-exchange`/`capture-analysis`/`candidate-proposals` guidance into `methods/*.md` resources. **(4)** `agents/contexts/*.ts` — snapshot RENDER (D60-L): LLM-string/JSON from typed pulls, scaled by lens-plane + grade-depth; wire `snapshot-*` Pi tool wrappers (SURFACE) over the renderer; PULL stays read-only in `graph/snapshot.ts` (+ a `session/` cwd-overview pull). `contexts/` is render-only and carries no manifest family. **(5)** Migrate `src/.pi/context/* → src/agents/` (the held Class-B move) and delete the old composer. -- **Why now / unlocks:** Turns the locked composition model into a real composer so mode/strategy/lens/goal actually drive the prompt body (today they only change a header sentence), and finally injects the M4 graph snapshots into context. Unblocks grade-gated, goal-driven elicitation behavior. -- **Acceptance:** - - `compose(agentId, sessionState, spec, workspace, snapshots)` emits the runtime header + a gated prompt-resource manifest; the manifest set varies by `(op_mode × goal × strategy × lens)`/grade/allow-list, and detailed bodies are `read`-on-demand `.md`, not concatenated into the prompt. - - AUTO on `goal`/`strategy`/`lens` lists exactly the agent's legal set in the manifest, with a router rule instructing the agent to choose only from the current manifest. - - `agents/state.ts` accepts every legal tuple, rejects illegal; a `.md` resource and a `state.ts` manifest entry (`{name, description, location}`) exist for each `goals`/`strategies`/`lenses`/`methods` value. - - `` resolves from `agents/methods/*.md`, gated by allow-list ∩ `op_mode` ∩ grade; methods are `read`-on-demand resources, not eager injection (a method may still be backed by a Pi-native skill). - - snapshot RENDER scales by lens-plane + grade-depth; `snapshot-{cwd,graph,nodes}` tools wrap the renderer (markdown in `content`, typed JSON in `details`); PULL stays read-only in `graph/`/`session/`. - - `src/.pi/context/` removed; composition lives in `src/agents/`; `npm run verify` green. -- **Verification:** Inner — `compose()` output assertions (manifest set/filtering/gating per axes; pinned-vs-AUTO router rules; code-owned `{name, description, location}` from `state.ts`); `agents/state.ts` legal/illegal tuple tests (model-based); snapshot render-scaling + pull round-trip. Filed as I38-L in SPEC §Verification Design. Middle — `compose()` legality across projected runtime states/grades; AUTO-choice sensibility and whether the agent reads the selected resource before applying it are tracked as **probe fitness, not gates** (SPEC §Verification Design structural/behavioral split). Outer — manual review of composed prompts + the A14-L behavioral proof (rides on `agent-graph-integration`). No old-vs-new composer equivalence differential: the cutover is a clean delete-and-replace (the thin manifest is intentionally not equivalent to the old eager concatenation; pre-release regenerate-don't-preserve posture). -- **Cross-cutting obligations:** All of goals/strategies/lenses/methods are `read`-on-demand `.md` resources advertised through the per-turn manifest; manifest `{name, description, location}` metadata is code-owned in `agents/state.ts`, never filesystem-discovered (D39-L sealing). `contexts/` is the D60-L render layer only — no `` family. PULL never returns strings; LLM-string RENDER lives only in `agents/contexts/`; reserve "snapshot" for agent-context, keep `workspace.snapshot` for product/UI (D60-L). Workspace posture is workspace-scoped state persisted in `.brunch/workspace.json` and injected into the runtime header when known; it is not spec/session state or graph truth. Honor D52-L dependency direction (`agents/` imports `graph/`+`session/`). -- **Traceability:** D58-L, D59-L, D60-L, D25-L, D40-L, D52-L / I18-L, I33-L, I35-L, I38-L / A14-L (behavioral). Retires: fixed `PROMPT_PACK_ORDER`; empty `agents/contexts/` stubs; the `src/.pi/context/` location; the "Layer-2 eager packs / Layer-3 Pi-skill" framing; `capability` as a synonym for `method`. -- **Design docs:** `memory/SPEC.md` §Active Decisions (D58-L/D59-L/D60-L), §Prompt/runtime profile architecture (concrete `agents/` topology), Lexicon. -- **Current execution pointer:** Not started; **oracle design reconciled, not owed.** The premise change (eager packs → thin manifest, D58-L) already propagated into SPEC §Verification Design: I38-L is the manifest-legality inner gate, the structural/behavioral split (§Verification Stance) makes AUTO-choice and read-compliance fitness metrics rather than gates, and the "manifests before eager injection" design note is filed. Of the prior three grill questions, two are answered by that design (AUTO boundary = manifest-lists-the-legal-set is the inner gate, choice/read quality is fitness; tuple legality = model-based inner test) and the migration differential is resolved as **skip** (clean cutover, pre-release posture). The prompt-injection-automation question is resolved via the push/delegate/pull cut. Likely first slice (after `agent-runtime-vocabulary` lands the enums): `agents/state.ts` + `compose.ts` skeleton over the landed vocabulary, then resource authoring, then snapshots, then migration. - -### sealed-pi-profile-runtime-state - -- **Name:** Sealed Pi profile, transcript-backed runtime state, and graph-model prep (M4 prep envelope) -- **Linear:** [FE-776](https://linear.app/hash/issue/FE-776) -- **Branch:** `ln/fe-776-graph-layer-prep-profile` -- **Kind:** structural hardening (prep envelope before M4 CRUD) -- **Status:** done -- **Objective:** Broader prep envelope before `graph-data-plane` (M4) CRUD work begins. Two strands under one frontier/branch: **(a) Pi harness sealing** — Brunch-owned programmatic settings/resource/tool/prompt/keybinding policy isolates product behavior from ambient user/project `.pi/`; operational mode / role preset / strategy / lens state is appended to Pi JSONL as Brunch custom entries and reconstructed at turn boundaries. **(b) Graph-model lock-and-materialize** — lock the conceptual edge and node contracts in [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md), stub the type/policy surface under `src/graph/`, and prove the A20-L persistence seam now: the Drizzle line, row-schema derivation path, monotonic counter allocation, change-log shape, and Pi `registerTool` round-trip so M4 CRUD lands on settled persistence/schema-derivation foundations. -- **Why now / unlocks:** FE-744 proved multiple Pi extension seams and exposed the exact weak point: ambient resource discovery and settings policy had to be sealed before future `elicit` vs `execute` work could depend on product-owned prompt/tool posture. Once sealing is in code, the cheapest remaining moves before M4 CRUD are conceptual rather than persistence-mechanical: lock the graph model so review-set drafts, reconciliation needs, snapshot bucketing, and CommandExecutor result discriminants share a stable contract; retire the speculative named-relation catalogue and brainstormed edge taxonomy; settle the persistence/schema-derivation toolchain (A20-L). De-risks M5/M6/M7 before graph tools, capture/reviewer jobs, and authority gating depend on the embedded harness or the graph data plane. -- **Acceptance:** - - **Sealing strand:** A `BrunchPiProfile` (or equivalent module boundary) owns settings policy, resource-loader options, extension factories, keybinding/command policy, tool policy, and prompt policy; tests prove ambient context files/extensions/skills/prompt templates/themes do not load while explicit Brunch-owned extension-discovered resources can load intentionally through Pi `resources_discover`; settings that affect product behavior are overridden/sealed or documented as a Pi upstream seam; runtime extension factories load explicitly from `src/.pi/pi-extension-shell.ts` / `src/.pi/extensions/*` and reusable TUI components under `src/.pi/components/*`, with no root project-local Pi discovery path as product runtime. Full selected-state transcript entries under `brunch.agent_runtime_state` can be appended by Brunch helpers and replayed to reconstruct active operational mode, role preset/runtime bundle, strategy, and lens; turn prep composes prompt packs from base Brunch prompt + operational mode + role preset + strategy + lens + spec readiness grade + elicitation posture + current graph/coherence/world state + pending structured-interaction rules; `elicit` suppresses execute/dangerous tools such as raw `bash`/`write` unless explicitly allowed by the active bundle. - - **Graph-model strand:** Phase 1 edge contract locked in `docs/design/GRAPH_MODEL.md` and materialized as type/policy stubs under `src/graph/` (✓ landed at commit `100585a1`). Phase 2 node contract locked (✓ landed at commits `b6ecec1e`–`8346e23d`): 11 intent kinds in 3 derived categories (basic/structural/reasoning), common `GraphNode` shape with `detail` JSON column for `decision`/`term`, `provenance` retired from both nodes and edges, `framing_as` retired, `source` as free-form epistemic attribution, modality-of-claim + source-question agent rubric, context promotion heuristic. Materialized as `src/graph/schema/nodes.ts` and reflected in SPEC via D54-L/D55-L/D56-L/D57-L plus I36-L/I37-L. A20-L spike produces a verdict on the persistence seam over one representative intent-plane slice: Drizzle line choice, row-schema derivation path (`drizzle-zod`, `drizzle-orm/typebox`, or equivalent), monotonic counter allocation, change-log writes, and Pi `registerTool` round-trip. -- **Verification:** Inner — profile/runtimestate unit tests, prompt-composition snapshot tests, tool-policy contract tests, edge/node schema unit tests, category-policy unit tests. Middle — ambient `.pi/` fixture/audit tests proving disabled discovery and sealed settings; explicit Brunch resource-injection test proving extension factories may inject Brunch-owned skills/prompts despite ambient `noSkills`/`noPromptTemplates`; JSONL reload/projection tests for runtime init/switch entries; before-agent-start/tool-call policy tests for `elicit`; persistence spike tests covering one representative table, one insert/select cycle, monotonic counter allocation, change-log append shape, and one Pi `registerTool` parameter binding; I26-L grep-based architectural test wired alongside the first Drizzle import so the single-schema-vocabulary boundary stays enforced. Outer — manual TUI/RPC smoke that active role/lens/strategy changes are inspectable in transcript and reflected in prompt/tool posture rather than hidden UI state. -- **Cross-cutting obligations:** Do not expose Pi's generic extension/skill/prompt/theme configuration to Brunch users; do not make Pi skills the primary authority for core operational prompts; keep raw Pi RPC behind Brunch adapters; keep runtime state linear-transcript-backed and compatible with compaction/session-boundary lifecycle hooks (`session_start`, `resources_discover`, `before_agent_start`, `context`, `tool_call`, `session_before_switch`, `session_before_compact`, `session_shutdown`). Graph-model lock work must trace to `docs/design/GRAPH_MODEL.md`; node lock must preserve the closed-edge-set invariants (immutable accepted-edge identity, `dependency`-only auto-cascade, separate `ReconciliationNeed` substrate with `{kind:'edge'|'node_pair'}` target). The persistence spike is throwaway scope — one representative slice, no broad imports until the verdict lands; if the current beta line blocks (migrations, SQLite fidelity, schema-derivation bugs, or ergonomics), pick the simpler working adapter/line and continue without re-opening M4 design. -- **Traceability:** R25, R26 / D2-L, D16-L, D23-L, D39-L, D40-L, D41-L, D51-L / I24-L, I25-L, I26-L / A19-L, A20-L -- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (canonical graph contract; Phase 1 + Phase 2 locked), [pi-seam-extensions.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md), [pi-ui-extension-patterns.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-ui-extension-patterns.md) -- **Current execution pointer:** - - **Sealing strand:** ✓ Complete. Profile resource/settings/runtime slices landed. Session display names generated from spec title + ordinal, persisted via Pi `session_info`, rendered in TUI chrome. - - **Graph-model strand:** ✓ Complete. Phase 1 (edges) + Phase 2 (nodes) locked. A20-L persistence spike validated (`drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`). - - **Tie-off:** ✓ Both strands at acceptance. `graph-data-plane` (M4 CRUD) is unblocked. - -### graph-data-plane - -- **Name:** Graph data plane (intent-first, workspace-graph-ready) (M4 CRUD) -- **Linear:** [FE-741](https://linear.app/hash/issue/FE-741/graph-data-plane-intent-first-workspace-graph-ready-m4) -- **Branch:** `ln/fe-741-graph-data-plane` (stacked on `ln/fe-737-web-shell`; will re-base above `ln/fe-776-graph-layer-prep-profile` once the prep envelope ties off) -- **Kind:** structural -- **Status:** done (all 6 execution steps complete 2026-06-01) -- **Objective:** Stand up SQLite-backed CRUD over the prep-envelope-locked graph model: durable intent-plane nodes and edges per [`docs/design/GRAPH_MODEL.md`](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md); a single global LSN per commit; the change log; the reconciliation-need substrate; named homes for coherence state (verdicts and violations); and the `commitGraph` atomic batch mutation (D53-L) that accepts `{ nodes, edges }` with intra-batch and existing-node references — all forward-compatible with oracle, design, and plan planes. Source topology follows D52-L: `db/` owns Drizzle schema and migrations; `graph/` owns the CommandExecutor, readers, policy, validators, snapshot bucketing, change-log replay, and recon-need substrate; `graph/` imports from `db/`; no other layer imports `db/` directly. -- **Why now / unlocks:** Pins I1-L, I6-L. Unlocks all agent ↔ graph work (M5+) and lets oracle / design / plan planes be added later without re-foundation. The graph contract and persistence toolchain are settled by the prep envelope, so M4 is pure CRUD/transaction/CommandExecutor work rather than mixed design-and-mechanics. Landing `commitGraph` here (not deferring to M5) means the A14-L proof can run as soon as agent tools are wired. -- **Acceptance:** Graph CRUD + change-log replay tests pass through the `CommandExecutor` public mutation boundary; command results already include success, `needs_human`, `policy_blocked`, `version_conflict`, and `structural_illegal` shapes even if pre-M6 policy classification is minimal; `commitGraph` batch validation is all-or-nothing (I34-L) — if any node or edge fails structural checks, the entire batch is rejected with diagnostics sufficient for agent self-correction; reconciliation-need substrate accepts inserts/updates/resolutions with LSN invariants enforced; oracle-plane stub tables exist (Check, Validation Method, Evidence, Obligation) even if unused; graph snapshot readers support at least two detail levels (cursory full-graph overview, node-neighborhood with hops) per I35-L; the persistence layer proves the one-transaction protocol that couples authority/result classification, version checks, structural validation (including the closed edge-category set, immutable accepted-edge identity per D51-L, and intra-batch reference resolution per D53-L), LSN allocation, change-log append, and any coherence updates. -- **Verification:** Inner gate plus command/result schema/type tests. Middle — property/model-based tests on LSN monotonicity, graph replay, reconciliation invariants (target shape `{kind:'edge'|'node_pair'}`), framing matrix, edge structural legality (closed category set, stance scoping, supersession acyclicity), `commitGraph` batch all-or-nothing property (I34-L), intra-batch reference resolution correctness, existing-node reference validation, and `CommandExecutor` transaction/result behavior; architectural no-bypass tests. Outer — fixture property invariants on reconciliation-substrate begin running. -- **Cross-cutting obligations:** Reuse the Drizzle + `better-sqlite3` persistence shape settled by the prep-envelope A20-L spike; do not re-open the line/adapter choice in M4 unless the spike itself falsifies it. `CommandExecutor` result contract and no-bypass transaction rule become shared infrastructure for later direct-agent, elicitor-capture, deferred observer/auditor, side-task, migration, and UI-attributed writes. Derive row/insert/update runtime schemas from Drizzle table definitions via the schema path chosen during the spike — do not hand-author parallel row schemas. The I26-L grep-based architectural test should already be live from the prep envelope; M4 widens its coverage as new Drizzle imports land. `commitGraph` and `acceptReviewSet` are parallel paths to the same CommandExecutor — both must share the same validation, LSN, and change-log mechanics. -- **Traceability:** R7, R9, R13 / D3-L, D4-L, D6-L, D8-L, D9-L, D16-L, D20-L, D41-L, D51-L, D52-L, D53-L / I1-L, I6-L, I7-L, I11-L, I26-L, I34-L, I35-L / A3-L, A4-L, A14-L -- **Design docs:** [GRAPH_MODEL.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/GRAPH_MODEL.md) (canonical graph contract), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [pi-seam-extensions.md §Graph clock, §Reconciliation-need substrate, §Oracle plane](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) (note: §"Edge types" in pi-seam-extensions.md is retired and superseded by `docs/design/GRAPH_MODEL.md`) -- **Current execution pointer:** **(1)** ✓ `src/db/` Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** ✓ `CommandExecutor` result contract and one-transaction LSN/change-log skeleton with `createNode` proof-of-life, I26-L architectural boundary test, NodeId/EdgeId corrected to `number`. **(3)** skipped — subsumed by (4). **(4)** ✓ `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation (closed category set, stance scoping, self-loop rejection), I34-L all-or-nothing (edge failure rolls back nodes), one LSN per batch, one change_log entry per batch. **(5)** ✓ graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L); **(6)** ✓ reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with target validation, LSN invariants, and change_log; oracle-plane stub acceptance met by existing `createNode` + `ORACLE_KINDS`. **All M4 graph-data-plane steps complete.** - -### agent-graph-integration - -- **Name:** Agent ↔ graph integration through the shared command layer (M5) -- **Linear:** [FE-785](https://linear.app/hash/issue/FE-785) -- **Branch:** `ln/fe-785-agent-graph-integration` (stacked on `ln/fe-776-graph-layer-prep-profile`; includes the folded runtime-vocabulary slice formerly tracked as FE-789) - **Kind:** structural -- **Status:** in-progress -- **Objective:** Brunch installs graph tools through pi's extension seams; agent graph operations — including `commitGraph` batch mutations for the `propose-graph` direct-commit path (D53-L, D26-L) — elicitor post-exchange capture writes, reviewer-attributed advisory writes, review-set batch acceptances for `project-graph`, spec readiness grade/posture updates, and the transcript-native establishment/intent-hint surfaces all route exclusively through the Brunch-owned command layer and shared event substrate; web, TUI, and agent all observe the same changes. **The primary A14-L proof runs here:** test whether the LLM can produce structurally-legal `commitGraph` batches against the real CommandExecutor with bounded retry. +- **Status:** next +- **Objective:** Build the D58-L/D59-L/D60-L `agents/` layer so runtime state changes behavior: `agents/state.ts` legal tuples and resource manifest metadata; `agents/compose.ts` runtime header + gated manifests; Brunch-owned markdown resources for definitions/goals/strategies/lenses/methods; agent-context snapshot renderers; and migration/deletion of the old `src/.pi/context` composer. +- **Why now / unlocks:** Runtime vocabulary has landed, but stored axes are not enough. The POC needs switchable strategies/lenses/goals to change prompt posture and available resources before capture and review-cycle behavior can be judged plausibly. - **Acceptance:** - - ```text - acceptance: - ├── command-routing through CommandExecutor - │ ├── agent CRUD on intent-plane nodes (create / update / link via Brunch tools) - │ ├── elicitor capture (post-exchange, synchronous) - │ ├── reviewer writes (target restricted to `reconciliation_need`) - │ └── acceptReviewSet commits batch atomically (one LSN, one change-log entry) - ├── exchange entries (custom) - │ ├── brunch.establishment_offer [must carry: lens/routing metadata] - │ └── brunch.elicitor_intent_hint [must carry: lens/routing metadata] - ├── capture rules (post-exchange, synchronous) - │ ├── high-confidence extractive facts → commit - │ ├── readiness updates → commit - │ └── low-confidence implications → stay in structured-exchange preface/question material - ├── proposal rules - │ ├── carry explicit support/grounding coverage - │ ├── carry epistemic_status - │ └── only dry-run-valid proposals surface as reviewable review sets - ├── reviewer policy - │ ├── advisory only (writes only `reconciliation_need`) - │ └── initial POC reviewer trigger/scope policy recorded in implementation docs/tests (not implicit) - ├── architectural invariants (lint or test) - │ ├── no direct DB access - │ ├── no caller-side authority bypass outside command layer - │ └── reviewer cannot write anything other than `reconciliation_need` - ├── cross-surface observability - │ └── same change observed across TUI and web client - └── async substrate (conditional) - └── if observer/auditor queues land → backstops only, not primary capture freshness path - ``` -- **Implementation layout:** Per D52-L, graph domain logic lives in `src/graph/` (CommandExecutor, readers, policy, validators, snapshot functions) and persistence in `src/db/`. The Pi-facing adapter goes in one explicit product extension directory, `src/.pi/extensions/graph/`, imported by `src/.pi/pi-extension-shell.ts` as `registerBrunchGraph` rather than discovered dynamically. Use `graph/index.ts` only to register Pi tools, message renderers, and event hooks. Keep tool definitions in `graph/tools/*` (`read-graph`, `commit-graph`, `create-intent-node`, `update-intent-node`, `link-intent-nodes`, `accept-review-set`), boundary schemas in `graph/schemas/*` (`tool-inputs`, `tool-results`, `custom-entries`), transcript helpers in `graph/transcript/*` (`entries`, `projections`, `renderers`), synchronous capture in `graph/capture/post-exchange-capture.ts`, reviewer target enforcement in `graph/reviewer/reviewer-writes.ts`, and the Pi→CommandExecutor translation seam in `graph/command-adapter.ts`. The extension directory must not own SQLite/Drizzle persistence, LSN allocation, structural graph validators, reviewer-agent implementation, or capture model/prompt machinery; those are Brunch product/core modules passed into the extension through explicit shell options such as `{ graph: { commandExecutor, capturePostExchange?, reviewerWrites? } }`. Agent prompts, strategy definitions (including `propose-graph` and `project-graph`), lens definitions, and context builders live in `src/agents/` per D52-L. -- **Verification:** Inner — verify gate plus graph-tool/capture/reviewer command shape tests, review-set payload schema validation for the `present_review_set` structured-exchange flow (`epistemic_status` and support/grounding coverage required), establishment-offer / elicitor-intent-hint schema validation (must declare `lens`), structured-exchange `preface` contract tests, and projection-helper tests for latest-offer lookup. Middle — `CommandExecutor` contract tests including `acceptReviewSet` discriminants and the rule that only dry-run-valid proposal payloads become reviewable review sets, direct-DB no-bypass checks, extension-layout/import-boundary tests proving `src/.pi/extensions/graph/**` reaches graph mutation only through `command-adapter.ts` and never imports Drizzle/SQLite directly, post-exchange capture fixtures distinguishing committed facts from preface-only implications, reviewer-job restart/idempotence tests keyed by batch-acceptance entry id, reviewer-write-target architectural boundary test (rejects non-`reconciliation_need` targets), `acceptReviewSet` batch-atomicity property tests (one LSN / one change-log entry; partial-batch impossible under mid-batch validation failure), `supersedes`-chain acyclicity property tests, lens-routing correctness property tests, differential test comparing dry-run validation at proposal time vs real-run validation at acceptance, and cross-surface projection checks. Outer — targeted batch-proposal probes replay through structured-exchange review cycle + acceptance; A14-L proposal structural-legality rate captured in probe metadata as POC-phase fitness (not merge gate); 1–2 known-bad coherence-problem probe scenarios exercise reviewer precision; side-task / elicitor-capture / reviewer-attributed writes remain indistinguishable from other writes at the command-layer boundary except for attribution and reviewer's narrow target. -- **Cross-cutting obligations:** Preserve the single-authority mutation rule for primary-agent, elicitor-capture, reviewer, side-task, and batch-acceptance flows by making the `CommandExecutor` the only mutation entry; deferred observer/auditor jobs, if introduced, are operational backstops keyed to transcript anchors, not a revived chat/turn store or privileged primary extraction path; reviewer is advisory and writes only to `reconciliation_need`; lens metadata on elicitor-emitted entries routes capture/reviewer/future-auditor consumption; establishment offers remain orientation artifacts for chrome/web surfaces rather than a default exhaustive lens picker. -- **Traceability:** R10, R13, R17, R21, R22, R23 / D4-L, D13-L, D15-L, D18-L, D20-L, D25-L, D26-L, D27-L, D28-L, D29-L, D30-L, D32-L, D45-L, D46-L, D47-L, D50-L / I2-L, I11-L, I14-L, I15-L, I16-L, I17-L, I18-L, I20-L, I30-L, I31-L, I33-L / A3-L, A11-L, A13-L, A14-L, A16-L, A22-L -- **Design docs:** [prd.md §M5, §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md), [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) -- **Current execution pointer:** **(0)** ✓ Runtime vocabulary enabling slice folded into this frontier: `brunch.agent_runtime_state` now has the reconciled goal/strategy/lens/op-mode tuple and legal `propose-graph`/`project-graph` strategies. **(1)** ✓ Source topology move: `src/tui-client/.pi/` → `src/.pi/` per D52-L. **(2)** ✓ `commit_graph` and `read_graph` Pi tools wired through `CommandExecutor` and pre-bound `GraphSnapshotReaders` via `src/.pi/extensions/graph/`; command-adapter translation seam, TypeBox parameter schemas, I26-L-compliant enum re-exports from `graph/index.ts`, extension shell conditional wiring; 9 integration tests. **(3)** ✓ A14-L `propose-graph → commitGraph` product-path proof: the default Brunch runtime factory now opens the workspace graph runtime and registers `read_graph`/`commit_graph`, `src/probes/propose-graph-commit-proof.ts` records retry/diagnostic evidence, and the real run in `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/` persisted 4 nodes + 4 edges on the first attempt. **(4)** ✓ Review-set dry-run gate reconciled to the public RPC contract: `CommandExecutor.dryRunCommitGraph` shares validation with `commitGraph`; `src/.pi/extensions/graph/review-set-proposal.ts` validates structured-exchange review-set payload metadata and translates drafts to graph batches without creating a standalone public review-set proposal entity. Next: wire real `project-graph` agent proposal generation through `present_review_set` / `request_review`, or scope synchronous elicitor capture. - -### subagents-for-proposal-diversity - -- **Name:** Subagents for candidate-proposal diversity (optional enhancement) + - `compose(agentId, sessionState, spec, workspace, snapshots)` emits the agent-control header, runtime-state header, compact context handles, and gated ``, ``, ``, and `` manifests. + - AUTO axes list exactly the legal set for the current agent/op-mode/grade/allow-list; pinned axes point to the pinned resource; illegal tuples are rejected in code. + - At least the P0 behavior resources exist and are readable: `step-wise-disambiguate`, `propose-graph`, `intent` lens, `design` lens, grounding/capture objectives, and structured-exchange/capture/graph-commit methods as needed for following frontiers. + - Snapshot rendering is split correctly: PULL in `graph/`/`session/`, RENDER in `agents/contexts/`, SURFACE through composition or snapshot tools. + - `src/.pi/context/` is removed or reduced to a compatibility-free tombstone; prompt composition lives in `src/agents/`. +- **Verification:** Inner — manifest filtering/gating tests, legal/illegal tuple tests, resource location tests, snapshot render tests. Middle — compose legality across projected runtime states and spec grades; probe/manual prompt review showing two strategies/lenses produce materially different manifests/posture. Behavioral quality remains a fitness signal, not a merge gate. +- **Topology materialization:** Complete `src/agents/{definitions,goals,strategies,lenses,methods,contexts}` as the prompt/control subtree; `.pi/extensions/` only adapts Pi seams; `agents/` may import from `graph/` and `session/`, never the reverse; context string rendering does not leak into `graph/` pull functions. +- **Cross-cutting obligations:** Preserve D39-L sealed resource policy: manifest metadata is code-owned, not filesystem-discovered. Workspace posture is workspace-scoped header input, not spec/session/graph truth. Multi-spec discipline: composition reads the selected spec's grade/graph snapshots only. +- **Traceability:** D25-L, D39-L, D40-L, D52-L, D58-L, D59-L, D60-L / I18-L, I33-L, I35-L, I38-L / A14-L, A22-L. +- **Design docs:** `memory/SPEC.md` §Prompt/runtime profile architecture; `src/agents/README.md`; `src/.pi/README.md`. +- **Current execution pointer:** First slice should be `agents/state.ts` + `compose.ts` skeleton over the landed runtime vocabulary, then minimal P0 resource authoring, then snapshots, then deletion of `src/.pi/context`. + +### capture-response-to-graph + +- **Name:** Structured response capture into selected-spec graph truth - **Linear:** unassigned -- **Kind:** optional enhancement -- **Status:** deferred (lands when `agent-and-graph-integration` is far enough along to benefit; never a blocker for M0–M9) -- **Objective:** Register a single `subagent` Pi tool per D44-L so the main agent can (a) fan out blocking data-gathering calls (scout / researcher / graph-reader) in parallel to ground proposals, then (b) fan out parallel `proposer` invocations to generate diverse candidate variants — the subagent realization of `ln-design`'s "design it twice" pattern and `ln-oracles`'s parallel-fan-out — and finally compose a `present_review_set` structured-exchange payload from those variants via the D31-L meta-rubric. Subagent results return as tool content; no `CommandExecutor` access; no Brunch RPC access; isolated `pi --no-session --no-skills --no-extensions` subprocesses inheriting Brunch Pi Profile sealing. -- **Acceptance:** `subagent` tool registered with `{ agent, task }` and `{ tasks: [] }` parameters; starter agents scout/researcher/graph-reader/proposer land as markdown files with TypeBox-validated frontmatter under `src/.pi/extensions/subagents/agents/`; proposer is system-prompt-only (no tools) and produces exactly one variant per invocation; argv shape per spawned subprocess includes `--no-session --no-skills --no-extensions` plus an explicit per-agent tool allowlist / model / system-prompt path; concurrency cap honored from [src/.pi/extensions/subagents/config.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/subagents/config.json); subagents have no inherited conversation context so the task string must carry everything; result text returns as tool result content with no transcript side-effects; at least one batch-proposal probe exercises a `tasks: []` parallel `proposer` fan-out (≥ 2 variants) feeding a single `present_review_set` payload composed by the main agent via the D31-L meta-rubric. -- **Verification:** Inner — `subagent` tool argv-shape tests; TypeBox schema validation of agent frontmatter and `config.json`; per-starter-agent tool-allowlist conformance (proposer must have an empty tool set). Middle — isolation audit (no ambient `.pi/` resources reachable; parent `CommandExecutor` / Brunch RPC handlers absent from subprocess environment); subprocess streaming / abort propagation tests; parallel-fan-out independence test (two `proposer` invocations with distinct framings produce structurally distinct outputs). Outer — proposal-generation probe invokes scout/researcher/graph-reader to ground, then parallel `proposer` variants, and surfaces the composed review-set proposal with grounding-bundle coverage and `epistemic_status` consistent with the gathered evidence; meta-rubric application visible in the comparison rendering. -- **Cross-cutting obligations:** Preserve the single-authority mutation rule (`CommandExecutor` only — subagents never bypass it) and the sealed Pi Profile (no ambient `.pi/` leakage through the subprocess boundary). Cross-extension agent registration (Amos's `globalThis.__pi_subagents` bridge) is deferred because it conflicts with profile sealing; the POC registry is Brunch-owned only. Worker-style write-capable subagents are deferred until an execute operational mode exists. -- **Traceability:** R20 / D2-L, D26-L, D27-L, D30-L, D31-L, D39-L, D41-L, D44-L / I2-L, I11-L, I24-L, I29-L -- **Design docs:** [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [ELICITATION_LENSES.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/ELICITATION_LENSES.md), [REVIEW_SETS.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/design/REVIEW_SETS.md) - -### authority-model - -- **Name:** Authority model and gated tools (M6) +- **Branch:** to create — `ln/-capture-response-to-graph` +- **Kind:** structural / tracer bullet +- **Status:** next +- **Objective:** Prove the single-exchange path: a typed structured-exchange response is captured synchronously into high-confidence graph mutations through `CommandExecutor`, and the resulting graph change is visible through web/TUI projections. +- **Why now / unlocks:** Structured exchanges and graph commits work separately. This frontier makes elicitation actually graph-native for the POC. It directly attacks A22-L while preserving the single mutation authority. +- **Acceptance:** + - A narrow capture path exists for 2–4 high-confidence intent facts, starting with basic/grounding kinds such as `goal`, `context`, `constraint`, `criterion`, or `assumption`; low-confidence implications remain out of graph truth and can be rendered as preface/disambiguation material. + - Capture targets the spec bound to the session's `brunch.session_binding`; it never writes to a workspace-global graph or an unbound/default spec. + - Captured graph mutations route only through `CommandExecutor` and produce normal LSN/change-log entries. + - The transcript retains the source structured exchange; graph readers expose the committed nodes/edges; the live web observer updates after capture. + - Capture failures are loud and diagnosable (`structural_illegal`, policy/authority result, or explicit no-capture), not silent partial writes. +- **Verification:** Inner — capture classification fixtures; command-input shape tests; no-bypass tests. Middle — replay a structured-exchange response fixture through capture and assert graph/change-log/projection results; negative fixtures for low-confidence material and malformed responses. Outer — manual/probe run: user answers a structured prompt, capture commits a small graph slice, web observer updates. +- **Topology materialization:** `session/` owns transcript/exchange extraction; `graph/capture/` owns capture-to-command translation and structural/domain policy; `.pi/extensions/structured-exchange` remains an adapter; `.pi/extensions/graph` remains a tool adapter; `rpc/` and `web/` observe through projection handlers only. +- **Cross-cutting obligations:** Preserve D4-L/D20-L single-authority mutation; keep capture synchronous and bounded for POC; do not introduce deferred observer/auditor queues or canonical chat/turn tables here. Capture must respect D61-L: claims are node-level truth inside the selected spec. +- **Traceability:** R10, R16, R17, R21, R22 / D4-L, D17-L, D18-L, D20-L, D21-L, D45-L, D52-L, D54-L, D56-L, D57-L, D61-L / I30-L, I31-L / A22-L, A3-L. +- **Design docs:** `docs/design/GRAPH_MODEL.md`; `docs/design/ELICITATION_LENSES.md`; `memory/SPEC.md` D17-L/D18-L/D61-L. + +### graph-tool-resilience + +- **Name:** Broaden direct graph-tool proof beyond the A14 happy path - **Linear:** unassigned -- **Kind:** bounded feature -- **Status:** not-started -- **Objective:** Fill in the policy matrix behind the existing `CommandExecutor` result seam: three-tier policy (autonomous / requires-confirmation / human-only) implemented end-to-end; headless modes fail or delegate cleanly with structured `needs_human`; attribution + optimistic concurrency shared across all callers. -- **Acceptance:** Adversarial briefs requesting human-gated actions in print/RPC produce structured `needs_human` through the command result contract; an authority test matrix passes across all four modes; M6 does not introduce a second policy service or caller-side authority gate. -- **Verification:** Inner gate plus policy classifier/result-shape unit tests. Middle — authority matrix contract tests across TUI/web/print/RPC through the existing `CommandExecutor` result seam. Outer — adversarial probe for structured `needs_human` regression. -- **Traceability:** R5, R6, R12 / D4-L, D20-L -- **Design docs:** [prd.md §Authority Model](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md) - -### turn-boundary-reconciliation - -- **Name:** Detection, relevance, turn-boundary reconciliation (M7) +- **Branch:** to create — `ln/-graph-tool-resilience` +- **Kind:** hardening / tracer bullet +- **Status:** next +- **Objective:** Extend the real `read_graph`/`commit_graph` product-path proof to cover representative failure and complexity cases: existing-node references, structural-illegal diagnostics with bounded retry, and an ambiguity/no-overcommit case. +- **Why now / unlocks:** The A14 commitGraph subclaim is partially validated by one successful run. The POC needs confidence that the direct-commit path is not a handcrafted probe artifact. +- **Acceptance:** + - At least three additional probe scenarios land under `.fixtures/runs/`: existing-node reference, illegal edge/category/stance with retry, and ambiguous prompt where the agent should avoid overcommitting or ask/emit no-op diagnostics according to strategy guidance. + - Probe reports record attempts, retry count, diagnostics seen, final graph counts/LSN, and friction. + - Tool guidance and `structural_illegal` diagnostics are sufficient for at least one corrected retry path; if not, the report names the gap. + - Existing-node refs target the selected spec's graph only. +- **Verification:** Inner — tool schema/adapter tests if guidance changes. Middle/Outer — real model probe runs with transcript/report artifacts; no artificial injection of the module under test that bypasses the default Brunch runtime factory. +- **Topology materialization:** Keep probes in `src/probes/` and `.fixtures/runs/`; keep tool adapter code in `src/.pi/extensions/graph/`; keep validators/diagnostics in `src/graph/`; no probe-only graph runtime wiring that product launch does not use. +- **Cross-cutting obligations:** Avoid harness-as-false-proof: the probe must exercise the same default Brunch runtime factory and registered tools that the product uses. Record fitness, not just pass/fail. +- **Traceability:** D4-L, D20-L, D51-L, D53-L / I34-L, I35-L / A14-L, A5-L. +- **Design docs:** `docs/architecture/probes-and-transcripts.md`; `docs/design/GRAPH_MODEL.md`. + +### project-graph-review-cycle + +- **Name:** Project-graph review-set proposal and atomic acceptance - **Linear:** unassigned -- **Kind:** structural -- **Status:** not-started -- **Objective:** Graph-revision tracking; session interest sets; `worldUpdate` synthesised by `prepareNextTurn`; mention-ledger staleness hints; side-task-result and reviewer-finding drain at the same boundary; session/spec binding transitions — and any lens switches present by then — recompute interest set before next agent turn. -- **Acceptance:** Cross-session paired probe exercises `worldUpdate` filtering; mention-staleness hints synthesise when an entity changed since last snapshot; succeeded side-task results are delivered only at the next turn boundary; reviewer findings from earlier batch acceptances arrive as advisory `reconciliation_need` items at the same boundary, never mid-turn; session/spec binding transitions and any emitted `brunch.lens_switch` entries recompute interest sets. -- **Verification:** Inner gate plus mention-ledger/session-interest unit tests. Middle — generated LSN/change traces and property tests for I4-L, I5-L, I9-L, I12-L, I16-L; subscription/update ordering checks for turn-boundary messages including reviewer findings. Outer — paired-brief adversarial capture passes, including side-task delivery and reviewer-finding delivery when those subsystems are active. -- **Cross-cutting obligations:** This frontier is the rendezvous point for Brunch's shared next-turn event semantics: `worldUpdate`, side-task results, reviewer findings, lens changes, session/spec binding state, and mention staleness must coexist without inventing a second event plane. -- **Traceability:** R11, R13, R14, R18, R21 / D6-L, D11-L, D14-L, D15-L, D17-L, D29-L / I1-L, I4-L, I5-L, I9-L, I12-L, I16-L / A4-L, A9-L, A11-L, A16-L -- **Design docs:** [pi-seam-extensions.md §1 Async side-chain sub-agents](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md#1-async-side-chain-sub-agents), [pi-seam-extensions.md §5 Graph-entity mentions](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) - -### coherence-first-class - -- **Name:** Coherence as a first-class graph property (M8) +- **Branch:** to create — `ln/-project-graph-review-cycle` +- **Kind:** structural / bounded feature +- **Status:** next +- **Objective:** Wire the `project-graph` strategy from real agent proposal generation through `present_review_set` / `request_review`, dry-run gating, approve/request-changes/reject response handling, and atomic `acceptReviewSet` commit. +- **Why now / unlocks:** This is the P1 proposal/review story. It is only P0 if the POC demo requires user-reviewed batch graph commitments rather than direct `propose-graph` and capture paths. +- **Acceptance:** + - The agent can generate a review-set payload with required lens, epistemic status, and grounding/support metadata. + - Only dry-run-valid proposals surface as reviewable; invalid generations remain internal to retry/regeneration. + - Approve commits the entire batch through one `CommandExecutor` call, one LSN, one change-log entry; partial acceptance is not representable. + - Request-changes and reject are transcript-visible outcomes; request-changes can trigger a successor proposal or an explicit deferred path. + - Web/TUI can observe the proposal/decision state enough for the POC; full review UX polish may remain thin. +- **Verification:** Inner — review-set schema tests, dry-run/real-run differential tests, accept atomicity tests. Middle — structured-exchange review-cycle fixture; no-bypass checks. Outer — targeted probe: `project-graph` proposes, user approves, graph updates and web observer sees it. +- **Topology materialization:** Review payload schemas/renderers live under `.pi/extensions/structured-exchange` or `.pi/extensions/graph` only as adapter surfaces; proposal validation/translation lives in `graph/` review modules; agent strategy resource lives in `agents/strategies/project-graph.md`; web observes via RPC projections. +- **Cross-cutting obligations:** Preserve D27-L: review-set proposal is a structured-exchange payload, not a standalone public review-set entity. Reviewer advisory writes remain deferred unless explicitly scoped. +- **Traceability:** R21, R23 / D4-L, D20-L, D26-L, D27-L, D51-L, D53-L / I11-L, I34-L / A14-L, A16-L. +- **Design docs:** `docs/design/REVIEW_SETS.md`; `docs/design/GRAPH_MODEL.md`; `memory/SPEC.md` D27-L. + +### minimal-authority-shell + +- **Name:** Minimal POC authority shell over graph/session actions - **Linear:** unassigned -- **Kind:** structural -- **Status:** not-started -- **Objective:** Structural legality enforced synchronously; semantic coherence stored as explicit product state; UI and agent read the same coherence verdict; before-images available where needed. -- **Acceptance:** "Contradictory requirements" adversarial brief produces an `incoherent` verdict with a backing open reconciliation need; coherence verdict surfaces in the TUI chrome and in `graph.*` reads. -- **Verification:** Inner gate plus structural validator tests. Middle — coherence-emission property tests proving backing reconciliation needs and projection/query visibility. Outer — adversarial probe for contradictory requirements plus manual UI checklist for visible coherence verdict. -- **Cross-cutting obligations:** Coherence verdicts must remain visible through the same transcript/graph authority model that side tasks, elicitation exchanges, deferred audit/reviewer jobs, and reconciliation needs already use; this frontier must not hide coherence behind a private subsystem. -- **Traceability:** R12, R14 / D8-L / I6-L -- **Design docs:** [pi-seam-extensions.md §Reconciliation-need substrate](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) - -### compaction-and-conflict-widening - -- **Name:** Compaction-aware continuity and conflict widening (M9) +- **Branch:** to create — `ln/-minimal-authority-shell` +- **Kind:** hardening +- **Status:** next +- **Objective:** Fill only the authority behavior required for a credible POC: graph writes keep returning structured command results, `elicit` suppresses obvious side-effecting tools, and headless/RPC paths surface structured `needs_human` where the POC actually reaches human-only actions. +- **Why now / unlocks:** Full M6 can remain horizon, but the POC must not look unsafe or mode-specific when graph/capture/review paths are exercised. +- **Acceptance:** + - `CommandExecutor` result discriminants remain the only graph mutation outcome surface for agent, RPC, and capture writes. + - `elicit` operational mode blocks or hides side-effecting Pi tools already identified as unsafe for the POC; remaining strict built-in suppression limits are named as A18-L residue, not ignored. + - Any human-only action encountered by current POC paths returns structured `needs_human` in headless/RPC rather than throwing a TUI-only dialog assumption. + - No new standalone authority service is introduced. +- **Verification:** Inner — policy/result-shape tests for touched actions. Middle — small authority matrix over current POC paths (agent graph tool, capture write, review approve if present, RPC/headless selection). Outer — manual smoke only if a TUI-visible policy path changes. +- **Topology materialization:** Policy lives in `graph/policy` and `.pi/extensions/operational-mode.ts` / command-policy adapters as appropriate; no caller-side policy snippets in `web/`, `rpc/`, or agent resources. +- **Cross-cutting obligations:** This is a minimal shell, not full M6. Do not widen into comprehensive RBAC/permissions unless a current POC path needs it. +- **Traceability:** R5, R6, R10 / D20-L, D34-L, D40-L / A18-L, A3-L. +- **Design docs:** `memory/SPEC.md` D20-L/D34-L/D40-L; `docs/reference/pi-extensions.md`. + +### poc-live-ship-gate + +- **Name:** POC live ship gate and runbook oracle - **Linear:** unassigned -- **Kind:** structural -- **Status:** not-started -- **Objective:** Compaction preserves graph, coherence, and continuity anchors per D43-L; interest sets can widen beyond direct reads when needed; conflict signaling remains intelligible at long horizons. -- **Acceptance:** Long-horizon adversarial brief (50+ turns) replays through compaction with `lastSeenLsn`, interest set, and session binding preserved; spec/session changes across compaction boundaries do not desync; the auto-compaction extension renders the configured preserved-anchor set byte-stable so active spec, in-flight side-task / deferred-auditor-job / reviewer-job bookkeeping, latest `brunch.agent_runtime_state`, latest `brunch.establishment_offer`, latest `brunch.lens_switch`, unresolved staleness hints, and active review-set leaves remain intelligible after compaction; ambient-affordance chrome continues to render the current offer; auto-compaction failure falls through to Pi default compaction rather than dropping anchors silently. -- **Verification:** Inner gate plus continuity-metadata unit tests and TypeBox schema validation of [src/.pi/extensions/auto-compaction-anchors.json](file:///Users/lunelson/Code/hashintel/brunch-next/src/.pi/extensions/auto-compaction-anchors.json). Middle — compaction round-trip/property tests for `lastSeenLsn`, interest set, session binding, graph/coherence anchors, active side-task/deferred-auditor/reviewer bookkeeping, latest-establishment-offer/lens/runtime-state reconstruction; deterministic anchor-rendering tests (same branch + same config → same header bytes); fallback-to-Pi-default behavior under simulated auth failure, empty LLM output, and thrown error. Outer — long-horizon probe passes, including continuity checks for side-task, interest-set, runtime-state, and establishment-offer state when present. -- **Cross-cutting obligations:** Preserve the coherence anchors, session binding, session continuity metadata, and side-task/deferred-auditor/spec state that earlier milestones attached to the shared transcript/event substrate; preserve lens state only if a lens subsystem has landed by then. The auto-compaction extension is the canonical owner of `session_before_compact`; product code paths that touch compaction must compose with it rather than register a parallel hook. -- **Traceability:** R15 / D6-L, D15-L, D43-L / I12-L, I28-L -- **Design docs:** [prd.md §Continuity, Divergence, and Coherence](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/prd.md) +- **Branch:** to create — `ln/-poc-live-ship-gate` +- **Kind:** hardening / release gate +- **Status:** next +- **Objective:** Create and pass the final POC runbook that exercises the real entrypoints together: fresh cwd, multi-spec selection, TUI session, web observer, runtime switch, structured exchange, capture/commit, graph update, and probe artifacts. +- **Why now / unlocks:** This is the harness-as-false-proof guard. If a test path had to inject modules the product never wires, the POC is not shipped. +- **Acceptance:** + - Fresh cwd launches Brunch, creates or resumes an explicit spec/session, and does not implicitly resume stale transcripts. + - A second spec can exist in the same workspace; the runbook confirms the active session/graph target is the selected spec. + - Web attaches as read-only observer over WebSocket RPC and shows the selected spec graph. + - Runtime strategy/lens/goal state is switchable/inspectable and changes composed prompt/resource posture. + - A structured exchange answer or direct graph tool call commits graph truth through `CommandExecutor`; web updates. + - Probe/runbook artifacts record transcript, graph summary, report/friction, and any accepted gaps. +- **Verification:** Middle/Outer — executable where practical, manual where TUI/browser interaction is unavoidable. Pair every visual assertion with a durable artifact or projection query when possible. +- **Topology materialization:** Runbook/probe code lives in `src/probes/` and `.fixtures/runs/`; it must launch product entrypoints rather than import private modules to fake the product path. +- **Cross-cutting obligations:** Keep the gate small and real. Do not turn it into a generic e2e framework or use it to backfill unrelated polish. +- **Traceability:** R4, R7, R10, R11, R12, R16, R19, R24, R28 / D5-L, D11-L, D19-L, D21-L, D33-L, D36-L, D52-L, D61-L / I22-L, I32-L, I35-L, I38-L / A5-L. +- **Design docs:** `docs/architecture/probes-and-transcripts.md`; `docs/architecture/pi-ui-extension-patterns.md`; `memory/SPEC.md` verification stance. ### probes-and-transcripts-evolution - **Name:** Evolve probe/transcript strategy as captures land - **Linear:** unassigned - **Kind:** hardening -- **Status:** not-started -- **Objective:** Keep the current probe/transcript substrate honest as new seams need evidence: report envelopes, Brunch-semantic transcript rendering, artifact layout, targeted probe scenarios, optional brief inputs, agent-as-user evaluator shape (mission/intention, evaluation focus, max-turn budget, blocker/friction report), and per-assumption fitness notes as real probe runs expose gaps. -- **Acceptance:** Each assumption-heavy frontier either lands a transcript-backed probe run under `.fixtures/runs///`, extends the probe/report/transcript contract, or explicitly records "no probe change needed" for the assumptions it touched. Optional brief-shaped inputs may be added only as inputs to concrete probe runs, not as a standalone library obligation. -- **Verification:** PR review on the doc plus cross-check that new/changed probe assertions map to SPEC assumptions/invariants or acknowledged blind spots; downstream probe runs catch regressions and surface assumption fitness rather than only pass/fail. -- **Cross-cutting obligations:** Treat probe/transcript strategy as canonical verification architecture that must stay in sync with SPEC/PLAN, not as optional commentary. If an assumption is not being tested by its assigned frontier, PLAN should say whether it is deferred, accepted as risk, or needs a spike/oracle pass. -- **Traceability:** A5-L -- **Design docs:** [probes-and-transcripts.md](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/probes-and-transcripts.md) - -### flue-pattern-adoption - -- **Name:** Adopt selected Flue patterns post-POC +- **Status:** continuous +- **Objective:** Keep probe/transcript artifacts honest as delivery frontiers land: report envelopes, Brunch-semantic transcript rendering, graph summaries, selected-spec metadata, friction fields, and per-assumption fitness notes. +- **Acceptance:** Each P0/P1 frontier either lands a transcript-backed probe/runbook artifact under `.fixtures/runs///`, extends the report/transcript contract, or explicitly records why no probe change is needed. +- **Verification:** PR review plus cross-check that probe assertions map to SPEC assumptions/invariants or acknowledged blind spots. +- **Topology materialization:** Probe code lives in `src/probes/`; artifacts live in `.fixtures/runs/`; probes exercise public product surfaces unless explicitly marked as source/API spike evidence. +- **Cross-cutting obligations:** Treat probes as product-path evidence, not harness-only green paths. +- **Traceability:** A5-L, I32-L. +- **Design docs:** `docs/architecture/probes-and-transcripts.md`. + +### topology-readmes-and-boundaries + +- **Name:** Source topology README and boundary hardening - **Linear:** unassigned -- **Kind:** structural -- **Status:** horizon -- **Objective:** Bring sandbox abstraction (SessionEnv/SandboxApi style), remote-deployment shape, MCP adapter style, and per-run event-stream style into Brunch via Brunch-side adapters over pi. Not part of POC. -- **Acceptance:** Defer until POC success criteria are met; revisit then. -- **Verification:** Defer. -- **Traceability:** Future Direction Register §Adoption patterns from Flue -- **Design docs:** [pi-seam-extensions.md §Flue framework evaluation](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) - -### oracle-design-plan-graphs - -- **Name:** Lift oracle / design / plan planes from stub to durable -- **Linear:** unassigned -- **Kind:** structural -- **Status:** horizon -- **Objective:** Promote oracle-plane stub to first-class persistence + commands; bring design and plan graphs online behind the same command layer. -- **Acceptance:** Defer until POC success criteria are met. -- **Verification:** Defer. -- **Traceability:** R9, R13 -- **Design docs:** [pi-seam-extensions.md §Oracle plane](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) - -### framework-direction-stubs - -- **Name:** Lightweight stubs for Context layer, capability tiers, candidate artefacts -- **Linear:** unassigned -- **Kind:** bounded feature -- **Status:** horizon -- **Objective:** Add minimal structural stubs (named namespaces, empty tables, or typed placeholders) for the deferred subsystems where a stub is cheaper than leaving a hole. -- **Acceptance:** Discretionary; only land when downstream pressure makes a stub cheaper than a hole. -- **Verification:** Defer. -- **Traceability:** Future Direction Register §Framework alignment & deferred subsystems -- **Design docs:** [pi-seam-extensions.md §Framework alignment & deferred subsystems](file:///Users/lunelson/Code/hashintel/brunch-next/docs/architecture/pi-seam-extensions.md) - -### geolog-and-petri-execution - -- **Name:** Geolog (TA1.2) and petri-net plan execution -- **Linear:** unassigned -- **Kind:** structural -- **Status:** horizon -- **Objective:** Exploratory — Datalog-shaped intent store (Geolog) and petri-net-compiled plan execution. Parallel to Brunch proper; surface here so dependents know it is acknowledged. -- **Acceptance:** Defer; tracked elsewhere. -- **Verification:** Defer. -- **Traceability:** Future Direction Register §Framework alignment & deferred subsystems +- **Kind:** hardening +- **Status:** parallel / attach-to-frontier +- **Objective:** Keep the D52-L source topology legible as delivery work moves files: update local READMEs, add no-bypass/import-boundary checks where a new seam appears, and remove retired compatibility paths. +- **Why now / unlocks:** The topology is itself a delivery asset: future agents and humans need to know where product behavior lives without rediscovering old `src/.pi/context` or root-level scattering. +- **Acceptance:** When a frontier materially changes `src/{.pi, agents, db, graph, session, rpc, web}`, its README/boundary tests reflect the new responsibility split; stale paths are deleted rather than aliased unless the current slice truly needs a transition. +- **Verification:** File-scoped documentation review and existing no-bypass/import-boundary tests; add grep/architecture tests only where they protect a real seam. +- **Topology materialization:** This frontier should usually be implemented as part of the frontier that caused the topology change; keep it separate only for doc/test-only hardening with low conflict. +- **Cross-cutting obligations:** Do not create speculative folders. A directory earns existence by carrying present code/resources or by making an already-used seam legible. +- **Traceability:** D52-L, D39-L, D4-L. +- **Design docs:** `src/README.md`; `src/.pi/README.md`; `src/agents/README.md`; `src/db/README.md`; `src/graph/README.md`; `src/rpc/README.md`; `src/session/README.md`; `src/web/README.md`. ## Recently Completed -- 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. -- 2026-06-01 `graph-data-plane` (FE-741) — Done: all 6 execution steps complete. **(1)** Drizzle schema + `initSchema` DDL push + graph_clock seed. **(2)** `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `createNode` proof-of-life, I26-L architectural boundary test. **(3)** skipped (subsumed by 4). **(4)** `commitGraph` atomic batch mutation with intra-batch + existing-node ref resolution, edge structural validation, I34-L all-or-nothing. **(5)** graph snapshot readers (`getGraphOverview`, `getNodeNeighborhood`) with superseded-predecessor exclusion, configurable hop depth, typed domain returns (I35-L). **(6)** reconciliation-need substrate (`createReconciliationNeed`, `resolveReconciliationNeed`, `getOpenReconciliationNeeds`) with LSN invariants. Verified: `npm run verify`. -- 2026-06-01 `sealed-pi-profile-runtime-state` (FE-776) — Done: prep envelope tied off. Both strands complete: **(a)** Pi harness sealing including sealed profile, runtime-state transcript projection, session display names via Pi `session_info`; **(b)** graph-model lock-and-materialize with Phase 1 (edges) + Phase 2 (nodes) locked in `docs/design/GRAPH_MODEL.md`, code stubs under `src/graph/`, and A20-L persistence spike validating `drizzle-orm@0.45.2` + `drizzle-typebox@0.3.3` + `better-sqlite3@12.8.0`. `graph-data-plane` (M4 CRUD) is now unblocked. Verified: `npm run verify` after each slice. +- 2026-06-02 `agent-graph-integration` enabling slices — Done inside FE-785: runtime vocabulary fixed; source moved from `src/tui-client/.pi` to `src/.pi`; real `read_graph`/`commit_graph` Pi tools route through `CommandExecutor`; default Brunch runtime factory registers graph tools; A14 `propose-graph → commitGraph` probe persisted 4 nodes + 4 edges on first attempt; review-set dry-run gate validates/filters proposal payloads. Verified: targeted tests, `.fixtures/runs/propose-graph-commit/2026-06-02-propose-graph-commit/`, and `npm run verify`. Watch: broad FE-785 bucket is now split into delivery frontiers above. +- 2026-06-02 `spec-persistence-and-startup` — Done: specs are DB rows with integer ids and `readiness_grade`; `createSpec` / `getSpec` / `updateReadinessGrade` route through `CommandExecutor` with change-log audit; startup scaffolds `.brunch/workspace.json` + `.brunch/data.db`; session binding collapsed to `{schemaVersion,specId}` and is fork-portable; inventory resolves spec names from DB. Verified: `npm run verify` and real `brunch --mode print` against a fresh cwd. Watch: richer multi-spec initiative/claim model remains deferred by D61-L. +- 2026-06-01 `graph-data-plane` (FE-741) — Done: Drizzle schema/init, graph clock seed, `CommandExecutor` result contract, one-transaction LSN/change-log skeleton, `commitGraph` atomic batch mutation, graph snapshot readers, and reconciliation-need substrate. Verified: `npm run verify`. Watch: graph is now real but must be surfaced by `live-graph-observer` and exercised by capture/review frontiers. -Older history (including `web-shell`, `graph-data-plane` Phase 1 edge lock, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` +Older history (including `sealed-pi-profile-runtime-state`, `pi-ui-extension-patterns`, `web-shell`, `jsonl-session-viability`, `mode-shell-and-fixture-driver`, `walking-skeleton`): `docs/archive/PLAN_HISTORY.md` ## Dependencies ```text nodes: - sealed-pi-profile-runtime-state [done] (M4 prep envelope: sealing + graph-model lock) - graph-data-plane [done] (M4 CRUD proper) - spec-persistence-and-startup [done] (persistence-model fix + startup regression repair) - agents-composition-layer [next] (agents/ 3-layer composition + snapshots + .pi/context migration) - agent-graph-integration [in-progress] (M5) - subagents-for-proposal-diversity [deferred · optional] - authority-model [not-started] (M6) - turn-boundary-reconciliation [not-started] (M7) - coherence-first-class [not-started] (M8) - compaction-and-conflict-widening [not-started] (M9) - probes-and-transcripts-evolution [continuous, parallel] + live-graph-observer [active · P0] lights up TUI-writer/web-observer graph visibility + agents-composition-layer [next · P0] makes runtime goal/strategy/lens behavior real + capture-response-to-graph [next · P0] structured answer -> graph truth -> observer update + graph-tool-resilience [next · P0] broadens A14 direct graph tool proof + project-graph-review-cycle [next · P1] real project-graph review-set approval loop + minimal-authority-shell [next · P1] thin safety posture for current POC paths + poc-live-ship-gate [next · P1] final fresh-cwd composed product runbook + probes-and-transcripts-evolution [parallel] continuous evidence substrate + topology-readmes-and-boundaries [parallel] attach-to-frontier topology hardening edges: - sealed-pi-profile-runtime-state -[hard]-> graph-data-plane - graph-data-plane -[hard]-> agent-graph-integration - graph-data-plane -[hard]-> spec-persistence-and-startup - spec-persistence-and-startup -[hard]-> agent-graph-integration - agent-graph-integration -[hard]-> agents-composition-layer - agent-graph-integration -[hard]-> authority-model - agent-graph-integration -[hard]-> turn-boundary-reconciliation - agent-graph-integration -[optional]-> subagents-for-proposal-diversity - turn-boundary-reconciliation -[hard]-> coherence-first-class - coherence-first-class -[hard]-> compaction-and-conflict-widening - graph-data-plane -[on promotion]-> oracle-design-plan-graphs - -groups: - unconnected: - flue-pattern-adoption - oracle-design-plan-graphs - framework-direction-stubs - geolog-and-petri-execution + live-graph-observer -[hard]-> capture-response-to-graph + live-graph-observer -[hard]-> poc-live-ship-gate + agents-composition-layer -[hard]-> capture-response-to-graph + agents-composition-layer -[hard]-> graph-tool-resilience + agents-composition-layer -[hard]-> project-graph-review-cycle + capture-response-to-graph -[hard]-> poc-live-ship-gate + graph-tool-resilience -[hard]-> poc-live-ship-gate + project-graph-review-cycle -[optional]-> poc-live-ship-gate + minimal-authority-shell -[hard]-> poc-live-ship-gate + +parallel obligations: + probes-and-transcripts-evolution -[evidence]-> every P0/P1 frontier + topology-readmes-and-boundaries -[boundary]-> every frontier that moves/claims source topology + +horizon: + turn-boundary-reconciliation + coherence-first-class + compaction-and-conflict-widening + subagents-for-proposal-diversity + oracle-design-plan-graphs + flue-pattern-adoption + framework-direction-stubs + geolog-and-petri-execution notes: - - probes-and-transcripts-evolution runs in parallel across all frontiers; not a spine edge. - - unconnected items are horizon work; surfaced for acknowledgment, not active dependency. - - the m5 -> subagents edge is `optional` — subagents is never a blocker for the spine. - - `pi-ui-extension-patterns` (FE-744) tied off 2026-06-01; see docs/archive/PLAN_HISTORY.md. + - `project-graph-review-cycle` is P1 unless the POC demo narrative requires batch proposal/review as a central story; promote it to P0 if so. + - `topology-readmes-and-boundaries` is not a license for abstract cleanup; it rides with concrete delivery seams. + - Multi-spec workspace discipline applies throughout: target the selected/current spec explicitly; no workspace-global graph truth in the POC. ```