diff --git a/package-lock.json b/package-lock.json index 55d92c1..1a0b80a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4356,9 +4356,9 @@ } }, "node_modules/@sting8k/pi-vcc": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@sting8k/pi-vcc/-/pi-vcc-0.3.18.tgz", - "integrity": "sha512-U4wdEm7sxWYzUbn4dAtWqs3SxSzMW8B3aH58djUi/SgppaMdx5BaizX+xaxF+1b6JrlcoGHoFjU1iXakNE7P4A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@sting8k/pi-vcc/-/pi-vcc-0.6.0.tgz", + "integrity": "sha512-wtEcivqZ4ZG6OiW6ae1fbNM8DDtuSqhNiV/Aa6VHWLrTYvHZMkjwgGoGoOD22uHcnAGsKVhGSl52sDgUqSJXfA==", "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.74.0 <1.0.0", "typebox": ">=1.1.24 <2.0.0" @@ -17316,7 +17316,7 @@ "license": "MIT", "dependencies": { "@shikijs/cli": "^4.0.2", - "@sting8k/pi-vcc": "0.3.18", + "@sting8k/pi-vcc": "0.6.0", "@tintinweb/pi-tasks": "0.4.3", "better-sqlite3": "^12.10.0", "diff": "^7.0.0", diff --git a/package.json b/package.json index 73c476f..271670b 100644 --- a/package.json +++ b/package.json @@ -41,13 +41,13 @@ "./packages/pi-ext/extensions/ask-user-question/index.ts", "./packages/pi-ext/extensions/tool-presentation/index.ts", "./packages/pi-ext/extensions/split-fork/index.ts", - "./packages/pi-ext/extensions/superconductor/index.ts", "./packages/pi-ext/extensions/session-store/index.ts", "./packages/pi-ext/extensions/cmux/index.ts", "./packages/pi-ext/extensions/handoff/index.ts", "./packages/pi-ext/extensions/session-snap/index.ts", "./packages/pi-ext/extensions/pi-telescope/index.ts", "./packages/pi-ext/extensions/pi-sem/index.ts", + "./packages/pi-ext/extensions/tool-trim/tool-trim.ts", "./packages/pi-agent-explorer/extensions/agent-explorer.ts", "./packages/pi-cursor-sdk/src/index.ts", "./packages/pi-cache-hit-predictor/index.ts", @@ -64,7 +64,6 @@ "./packages/pi-ext/skills/github", "./packages/pi-ext/skills/visit-webpage", "./packages/pi-ext/skills/web-search", - "./packages/pi-ext/skills/session-query", "./packages/pi-ext/skills/sem", "./packages/pi-ext/skills/pr-review-comments" ], diff --git a/packages/pi-ext/README.md b/packages/pi-ext/README.md index 2d73fc5..9ace871 100644 --- a/packages/pi-ext/README.md +++ b/packages/pi-ext/README.md @@ -38,7 +38,7 @@ Omit a resource type to load all of it. Use an empty array to load none. | [Leader Key](extensions/leader-key) | `Ctrl+X` command palette for sessions, models, thinking levels, labels, and extension commands. | | [Chat to Markdown](extensions/chat-to-md) | Save the last assistant response under `ai-artifacts/chat/` with `/chat-to-md` or `Ctrl+X`, then `w`. | | [Code Review](extensions/review) | `/review` workflows for pull requests, branches, commits, and uncommitted changes. | -| [pi-sem](extensions/pi-sem) | Entity-aware Git diff, context, history, blame, and impact tools powered by `sem`. | +| [pi-sem](extensions/pi-sem) | Focused entity context, impact analysis, and file inventories powered by `sem`. | | [Pi Telescope](extensions/pi-telescope) | Native fuzzy finder for sessions, files, commands, and other providers. | | [Custom Footer](extensions/custom-footer) | Core session line plus one bounded prioritized extension-status line. | | [Startup Screen](extensions/startup-screen) | Centred PI wordmark coloured by the active theme, with a responsive compact fallback. | @@ -52,7 +52,7 @@ Omit a resource type to load all of it. Use an empty array to load none. | [Split Fork](extensions/split-fork) | Fork the current session into a tmux, cmux, or Herdr split or tab. | | [Ask User](extensions/ask-user-question) | Structured single- or multi-select questions with an interactive Pi UI. | | [cmux](extensions/cmux) | Notifications, status, browser, and workspace integration for cmux. | -| [Superconductor](extensions/superconductor) | Worktree status and controls when Pi runs under Superconductor. | +| [Tool Trim](extensions/tool-trim) | Keep `sem_context`, `sem_impact`, and `sem_entities`; deactivate the other semantic tools. | ### Leader Key @@ -128,11 +128,11 @@ Projects can add `REVIEW_GUIDELINES.md`. When `pi-sem` is loaded, review agents can use: -- `sem_diff` for changed entities; - `sem_impact` for dependents and affected tests; -- `sem_context` for focused function or class context; -- `sem_log`, `sem_entities`, and `sem_blame` for history and ownership; and -- `sem_eval` to compare semantic and raw Git diff coverage. +- `sem_context` for focused function or class context; and +- `sem_entities` for structural file inventories. + +Use raw Git diff and file reads for change, history, blame, and line-level evidence. The optional `@ataraxy-labs/sem` dependency installs automatically when the platform supports it. You can also install `sem` with Homebrew or Cargo. @@ -155,7 +155,6 @@ Multiplexer integrations stay quiet when their host application is absent. | [github](skills/github) | Work with issues, pull requests, and CI through `gh`. | | [pr-review-comments](skills/pr-review-comments) | Triage and resolve pull-request review comments. | | [sem](skills/sem) | Apply entity-aware context and impact analysis during code review. | -| [session-query](skills/session-query) | Recover decisions and details from previous Pi sessions. | | [visit-webpage](skills/visit-webpage) | Extract readable Markdown or download an image from a URL. | | [web-search](skills/web-search) | Search the web through Jina without opening a browser. | @@ -169,7 +168,7 @@ on the [Catppuccin](https://github.com/catppuccin/catppuccin) palette. - Pi - Node.js 22.19 or newer - Optional host tools only for their matching integrations: `gh`, `sem`, Herdr, - tmux, cmux, or Superconductor + tmux, or cmux ## License diff --git a/packages/pi-ext/extensions/pi-sem/README.md b/packages/pi-ext/extensions/pi-sem/README.md index e3188d1..19a351d 100644 --- a/packages/pi-ext/extensions/pi-sem/README.md +++ b/packages/pi-ext/extensions/pi-sem/README.md @@ -2,17 +2,15 @@ Semantic Git tooling for Pi, powered by [`sem`](https://github.com/Ataraxy-Labs/sem). -The extension exposes entity-aware tools to the model instead of making it reason over raw line hunks only. +The package keeps three focused entity-aware tools active. Use raw Git and file tools for diffs, history, blame, and line-level evidence. -## Included tools +## Active tools -- `sem_diff` - entity-level diff for working tree, staged changes, commits, or ranges - `sem_impact` - dependency / blast-radius analysis for an entity - `sem_context` - token-budgeted semantic context for a focused entity -- `sem_log` - entity history across git commits - `sem_entities` - structural inventory of a file -- `sem_blame` - entity-level blame for a file -- `sem_eval` - compares `sem diff` against raw `git diff` for coverage and prompt footprint + +[`tool-trim`](../tool-trim/) deactivates `sem_diff`, `sem_eval`, `sem_log`, and `sem_blame` after the extension registers them. ## Installation notes @@ -61,16 +59,15 @@ Important: our first local runs showed that `sem diff --format json` is **not al Good prompts once the extension is loaded: -- “What changed in this branch? Use sem.” -- “Use `sem_eval` to compare semantic diff vs git diff on the current changes.” - “What tests are affected by changes to `buildReport`? Use `sem_impact`.” - “Give me focused context for `buildReport` with a 4000 token budget using `sem_context`." +- “List the entities in this file using `sem_entities`." Recommended default posture: - prefer **`sem_context`** for one suspicious function/class - prefer **`sem_impact`** for blast radius and test selection -- use **`sem_diff`** once for overview / counts / review framing -- keep **raw `git diff` and file reads** for exact line-level evidence +- use **`sem_entities`** to inventory a large file +- use **raw `git diff` and file reads** for exact change evidence If the review extension is loaded too, `/review` now adds this semantic workflow directly into its review prompt. diff --git a/packages/pi-ext/extensions/superconductor/README.md b/packages/pi-ext/extensions/superconductor/README.md deleted file mode 100644 index d6cf8ff..0000000 --- a/packages/pi-ext/extensions/superconductor/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Superconductor - -Native [Superconductor](https://superconductor.dev) integration for pi. - -Superconductor launches pi inside managed git worktrees and terminal sessions, and exposes its state through the `sc` CLI (backed by a local API socket). This extension surfaces that integration directly inside pi so the agent — and you — can work with Superconductor without leaving the session. - -Inspired by the [cmux extension](../cmux/) and projects like [pi-cmux](https://github.com/javiermolinar/pi-cmux). Where cmux speaks a raw Unix-socket protocol, Superconductor is driven entirely through the `sc` CLI, so this extension shells out to `sc ... --json` and parses the structured envelope. - -If pi is not running inside Superconductor, the extension is a silent no-op. - -## What you get - -### Status pill - -A footer status pill shows the Superconductor-owned **target branch** and the current diff size: - -``` -SC →main · 6f +639 -0 -``` - -It refreshes on session start and after each agent run. This is additive — it never overwrites the Superconductor tab title. Set `PI_SC_TAB_TITLE=1` to also drive the tab title with a running/idle indicator. - -### Tools for the model - -| Tool | Actions | -|------|---------| -| `superconductor_worktree` | `status`, `diff_summary`, `set_target_branch`, `rename_branch`, `list_worktrees`, `create` | - -- **Worktree** lets the model read the authoritative target/base branch (it is Superconductor-owned — never inferred from git defaults), inspect the diff, list git worktrees, and **create new worktrees**. - - > **Creating worktrees.** Superconductor's local API (v17) exposes no command to create a managed worktree — SC builds those inside the app. So `create` (and the `/sc-worktree` command) fall back to **raw `git worktree add`** against the root repo (`SUPERCONDUCTOR_ROOT_PATH`), branching off `base` (default `main`). The result is a real git worktree on disk; with `open_in_sc` (the command always tries) it is surfaced via `sc workspace open`, but it is **not** registered as a Superconductor-managed task. - -### Commands - -- `/sc-fork [prompt]` — fork the current session into a new session file and open a fresh Superconductor terminal split. Superconductor terminal panes have no documented "send text" RPC, so the command opens the split and hands you a ready-to-paste `pi --session ` command. -- `/sc-worktree [base]` — create a git worktree off `base` (default `main`) on `branch`, then open it in Superconductor via `sc workspace open`. See the worktree-creation caveat above. - -## Configuration - -| Env var | Effect | -|---------|--------| -| `PI_SC_DISABLE=1` | Disable the extension entirely | -| `PI_SC_TAB_TITLE=1` | Drive the Superconductor tab title with running/idle state | -| `PI_SC_VERBOSE=1` | Log every `sc` invocation to stderr | -| `SUPERCONDUCTOR_SC_BIN` | Override the `sc` binary path (default: `sc` on PATH) | - -Detection uses `SUPERCONDUCTOR_MANAGED_AGENT=1` or the presence of `SUPERCONDUCTOR_LOCAL_API_SOCKET`. - -## Notes - -- All `sc` calls run with the worktree (`SUPERCONDUCTOR_WORKTREE_PATH`) as the working directory. -- `sc` envelopes come in two shapes: most commands wrap the payload in `response`, a few put it at the top level. Errors arrive as `{ "kind": "cli_error", "error": { "code", "message" } }`. The client normalizes all three. diff --git a/packages/pi-ext/extensions/superconductor/commands.ts b/packages/pi-ext/extensions/superconductor/commands.ts deleted file mode 100644 index 63e1a4d..0000000 --- a/packages/pi-ext/extensions/superconductor/commands.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Superconductor slash commands. - * - * /sc-fork [prompt] — fork this pi session and open a new Superconductor - * terminal split. Superconductor terminal panes have no - * documented "send text" RPC, so we open the split and - * hand the user a ready-to-paste `pi --session` command. - * - * /sc-worktree NAME [base] — create a git worktree off base (default main) - * and open it in Superconductor via `sc workspace open`. - */ - -import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; -import { existsSync, promises as fs } from "node:fs"; -import * as path from "node:path"; -import { randomUUID } from "node:crypto"; -import type { ScClient } from "./sc-client.js"; -import { createWorktree } from "./git.js"; - -function shellQuote(value: string): string { - if (value.length === 0) return "''"; - return `'${value.replace(/'/g, `'"'"'`)}'`; -} - -function piInvocation(): string[] { - const script = process.argv[1]; - if (script && existsSync(script)) return [process.execPath, script]; - const name = path.basename(process.execPath).toLowerCase(); - return /^(node|bun)(\.exe)?$/.test(name) ? ["pi"] : [process.execPath]; -} - -async function forkSessionFile(ctx: ExtensionCommandContext): Promise { - const sessionFile = ctx.sessionManager.getSessionFile(); - if (!sessionFile) return undefined; - - const dir = path.dirname(sessionFile); - const branch = ctx.sessionManager.getBranch(); - const header = ctx.sessionManager.getHeader(); - const ts = new Date().toISOString(); - const id = randomUUID(); - const newFile = path.join(dir, `${ts.replace(/[:.]/g, "-")}_${id}.jsonl`); - - const newHeader = { - type: "session", - version: header?.version ?? 3, - id, - timestamp: ts, - cwd: header?.cwd ?? ctx.cwd, - parentSession: sessionFile, - }; - const lines = [JSON.stringify(newHeader), ...branch.map((e) => JSON.stringify(e))].join("\n") + "\n"; - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile(newFile, lines, "utf8"); - return newFile; -} - -export function wireCommands(pi: ExtensionAPI, client: ScClient): void { - pi.registerCommand("sc-fork", { - description: "Fork this session and open a new Superconductor terminal split. Usage: /sc-fork [prompt]", - handler: async (args, ctx) => { - const prompt = args.trim(); - const forked = await forkSessionFile(ctx); - - const split = await client.tabSplit("right", "terminal"); - if (!split.ok) { - ctx.ui.notify(`Could not open split: ${split.error?.message}`, "error"); - if (forked) ctx.ui.notify(`Forked session saved: ${forked}`, "info"); - return; - } - - if (!forked) { - ctx.ui.notify("Opened a terminal split (no persisted session to fork).", "warning"); - return; - } - - const parts = [...piInvocation(), "--session", forked]; - if (prompt) parts.push("--", prompt); - const cmd = parts.map(shellQuote).join(" "); - ctx.ui.notify(`Terminal split opened. Run in it:\n${cmd}`, "info"); - }, - }); - - pi.registerCommand("sc-worktree", { - description: "Create a new git worktree off a base branch. Usage: /sc-worktree [base] (base default: main)", - handler: async (args, ctx) => { - const [branch, base] = args.trim().split(/\s+/); - if (!branch) { - ctx.ui.notify("Usage: /sc-worktree [base]", "warning"); - return; - } - const res = await createWorktree({ branch, base }); - if (!res.ok) { - ctx.ui.notify(`Worktree create failed: ${res.error}`, "error"); - return; - } - const opened = await client.workspaceOpen(res.path); - const tail = opened.ok ? " (opened in SC)" : ` (open in SC failed: ${opened.error?.message})`; - ctx.ui.notify( - `Worktree ready: ${res.path}\nbranch ${res.branch} ← ${res.base}${tail}`, - opened.ok ? "info" : "warning", - ); - }, - }); -} diff --git a/packages/pi-ext/extensions/superconductor/git.ts b/packages/pi-ext/extensions/superconductor/git.ts deleted file mode 100644 index d60b72d..0000000 --- a/packages/pi-ext/extensions/superconductor/git.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Git worktree helper. - * - * Superconductor's local API (v17) does NOT expose a "create worktree" - * command — its worktrees are created inside the app process and never - * surfaced over the `sc` CLI / socket. But SC worktrees are ultimately just - * `git worktree` checkouts, so we create one with raw git and (optionally) - * hand it to SC via `sc workspace open`. - * - * Everything here shells out to `git`. No throws on failure — we return a - * structured result the tool layer can present. - */ - -import { execFile } from "node:child_process"; -import { existsSync } from "node:fs"; -import * as path from "node:path"; - -export interface GitResult { - ok: boolean; - stdout: string; - stderr: string; - code: number | null; -} - -function git(repo: string, args: string[], timeout = 30_000): Promise { - return new Promise((resolve) => { - execFile("git", ["-C", repo, ...args], { timeout, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => { - const code = err ? ((err as NodeJS.ErrnoException).code === "ENOENT" ? null : (err as any).code ?? 1) : 0; - resolve({ - ok: !err, - stdout: (stdout || "").trim(), - stderr: (stderr || "").trim(), - code: typeof code === "number" ? code : null, - }); - }); - }); -} - -/** The primary checkout for the project (the repo SC branched this worktree from). */ -export function rootRepo(): string | undefined { - return process.env.SUPERCONDUCTOR_ROOT_PATH || undefined; -} - -function sanitizeBranchForPath(branch: string): string { - return branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "worktree"; -} - -async function branchExists(repo: string, branch: string): Promise { - const res = await git(repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]); - return res.ok && res.stdout.length > 0; -} - -export interface CreateWorktreeOptions { - /** New (or existing) branch name to check out in the worktree. */ - branch: string; - /** Base ref to branch from when creating a new branch. Default: "main". */ - base?: string; - /** Destination path. Default: sibling of the root repo named "-". */ - dest?: string; - /** Override the root repo. Default: SUPERCONDUCTOR_ROOT_PATH. */ - repo?: string; -} - -export interface CreateWorktreeResult { - ok: boolean; - /** Absolute path of the created worktree (set even on some failures for context). */ - path: string; - branch: string; - base: string; - /** Whether a new branch was created (vs. checking out an existing one). */ - createdBranch: boolean; - error?: string; -} - -/** - * Create a git worktree off `base` (default main) checked out on `branch`. - * - * - If the branch already exists, it is checked out (base is ignored). - * - If it does not exist, it is created from `base`. - */ -export async function createWorktree(opts: CreateWorktreeOptions): Promise { - const repo = opts.repo || rootRepo(); - const base = opts.base || "main"; - const branch = opts.branch; - - const fail = (path: string, createdBranch: boolean, error: string): CreateWorktreeResult => ({ - ok: false, - path, - branch, - base, - createdBranch, - error, - }); - - if (!repo) return fail("", false, "no root repo (SUPERCONDUCTOR_ROOT_PATH unset); pass repo explicitly"); - if (!existsSync(repo)) return fail("", false, `root repo does not exist: ${repo}`); - if (!branch || !branch.trim()) return fail("", false, "branch name is required"); - - // Resolve destination: explicit, else sibling of the repo. - const dest = opts.dest - ? path.resolve(opts.dest) - : path.join(path.dirname(repo), `${path.basename(repo)}-${sanitizeBranchForPath(branch)}`); - - if (existsSync(dest)) return fail(dest, false, `destination already exists: ${dest}`); - - const exists = await branchExists(repo, branch); - - // `git worktree add -b ` for a new branch, - // or `git worktree add ` to attach an existing one. - const args = exists - ? ["worktree", "add", dest, branch] - : ["worktree", "add", dest, "-b", branch, base]; - - const res = await git(repo, args); - if (!res.ok) { - return fail(dest, !exists, res.stderr || res.stdout || `git exited with code ${res.code}`); - } - - return { ok: true, path: dest, branch, base, createdBranch: !exists }; -} - -/** List existing git worktrees of the root repo (porcelain, parsed). */ -export async function listWorktrees(repo?: string): Promise< - { ok: boolean; worktrees: Array<{ path: string; branch?: string; head?: string }>; error?: string } -> { - const r = repo || rootRepo(); - if (!r) return { ok: false, worktrees: [], error: "no root repo (SUPERCONDUCTOR_ROOT_PATH unset)" }; - const res = await git(r, ["worktree", "list", "--porcelain"]); - if (!res.ok) return { ok: false, worktrees: [], error: res.stderr || res.stdout }; - - const worktrees: Array<{ path: string; branch?: string; head?: string }> = []; - let cur: { path: string; branch?: string; head?: string } | null = null; - for (const line of res.stdout.split("\n")) { - if (line.startsWith("worktree ")) { - if (cur) worktrees.push(cur); - cur = { path: line.slice("worktree ".length) }; - } else if (line.startsWith("HEAD ") && cur) { - cur.head = line.slice("HEAD ".length); - } else if (line.startsWith("branch ") && cur) { - cur.branch = line.slice("branch ".length).replace(/^refs\/heads\//, ""); - } - } - if (cur) worktrees.push(cur); - return { ok: true, worktrees }; -} diff --git a/packages/pi-ext/extensions/superconductor/index.ts b/packages/pi-ext/extensions/superconductor/index.ts deleted file mode 100644 index 4f51c0b..0000000 --- a/packages/pi-ext/extensions/superconductor/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * pi-superconductor: native Superconductor integration for pi. - * - * Superconductor launches pi inside managed worktrees + terminal sessions and - * exposes everything through the `sc` CLI (backed by the local API socket). - * This extension surfaces that to pi: - * - * - Status: footer pill with target branch + diff size (+ optional tab title) - * - Tools: superconductor_worktree (status/diff/target branch + create worktree) - * - Commands: /sc-fork (fork into a new terminal split), /sc-worktree (new worktree) - * - * Gracefully degrades: if not running inside Superconductor, it is a no-op. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { ScClient } from "./sc-client.js"; -import { wireStatus } from "./status.js"; -import { wireTools } from "./tools.js"; -import { wireCommands } from "./commands.js"; - -export default function (pi: ExtensionAPI) { - const client = new ScClient(); - - // Not inside Superconductor — do nothing. - if (!client.available) return; - - wireStatus(pi, client); - wireTools(pi, client); - wireCommands(pi, client); -} diff --git a/packages/pi-ext/extensions/superconductor/sc-client.ts b/packages/pi-ext/extensions/superconductor/sc-client.ts deleted file mode 100644 index ff49085..0000000 --- a/packages/pi-ext/extensions/superconductor/sc-client.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Superconductor CLI client — thin wrapper around the `sc` binary. - * - * Unlike cmux (which speaks a raw Unix-socket protocol), Superconductor - * exposes everything through the `sc` CLI, which in turn talks to the - * local API socket (SUPERCONDUCTOR_LOCAL_API_SOCKET). We shell out to `sc` - * with `--json` / `--output json` and parse the structured envelope. - * - * Response envelope shapes observed in the wild: - * { "kind": "worktree_status", "response": { ... } } // most commands - * { "kind": "providers", "providers": [ ... ] } // some commands put data at top level - * { "kind": "cli_error", "error": { "code", "message" } } // any failure - * - * Gracefully degrades: if not running inside Superconductor (or `sc` is - * missing), `available` is false and every call resolves to a failed - * ScResult. No throws, no noise. - */ - -import { execFile } from "node:child_process"; - -export interface ScError { - code: string; - message: string; -} - -export interface ScResult { - /** True when the command ran and `sc` did not return a cli_error. */ - ok: boolean; - /** The "kind" discriminator from the envelope, or "exec_error". */ - kind: string; - /** The payload: `response` field when present, otherwise the whole envelope. */ - response: T | null; - /** Populated when ok is false. */ - error: ScError | null; - /** Full parsed envelope, for callers that need top-level fields. */ - raw: any; -} - -function fail(kind: string, error: ScError): ScResult { - return { ok: false, kind, response: null, error, raw: null }; -} - -export class ScClient { - private readonly bin: string; - private readonly worktree: string | undefined; - private readonly verbose: boolean; - - constructor() { - this.bin = process.env.SUPERCONDUCTOR_SC_BIN || "sc"; - this.worktree = process.env.SUPERCONDUCTOR_WORKTREE_PATH; - this.verbose = process.env.PI_SC_VERBOSE === "1"; - } - - /** True if we appear to be running as a Superconductor-managed agent. */ - get available(): boolean { - if (process.env.PI_SC_DISABLE === "1") return false; - return ( - process.env.SUPERCONDUCTOR_MANAGED_AGENT === "1" || - !!process.env.SUPERCONDUCTOR_LOCAL_API_SOCKET - ); - } - - /** - * Run an `sc` subcommand. `args` should NOT include the json flag — the - * caller picks `--json` or `--output json` via `outputFlag`, because the - * CLI is inconsistent across command families. - */ - async run( - args: string[], - opts: { stdin?: string; timeout?: number; outputFlag?: "json" | "output-json" | "none" } = {}, - ): Promise { - if (!this.available) { - return fail("unavailable", { code: "unavailable", message: "Superconductor is not available" }); - } - - const flagArgs = - opts.outputFlag === "output-json" - ? ["--output", "json"] - : opts.outputFlag === "none" - ? [] - : ["--json"]; - const fullArgs = [...args, ...flagArgs]; - const timeout = opts.timeout ?? 10_000; - - if (this.verbose) console.error("[pi-sc] ->", this.bin, fullArgs.join(" ")); - - return new Promise((resolve) => { - const child = execFile( - this.bin, - fullArgs, - { timeout, cwd: this.worktree, maxBuffer: 8 * 1024 * 1024 }, - (err, stdout, stderr) => { - const out = (stdout || "").trim(); - - // Try to parse JSON first — even on a non-zero exit, `sc` usually - // prints a structured cli_error envelope to stdout. - const parsed = this.parse(out); - if (parsed) { - if (this.verbose) console.error("[pi-sc] <-", parsed.kind, parsed.ok ? "ok" : parsed.error?.code); - return resolve(parsed); - } - - if (err) { - const msg = (stderr || "").trim() || err.message; - const code = (err as NodeJS.ErrnoException).code === "ENOENT" ? "sc_not_found" : "exec_error"; - return resolve(fail("exec_error", { code, message: msg })); - } - - resolve(fail("parse_error", { code: "parse_error", message: out || "empty output" })); - }, - ); - - if (opts.stdin !== undefined) { - child.stdin?.write(opts.stdin); - child.stdin?.end(); - } - }); - } - - /** Parse a single-line JSON envelope into an ScResult, or null if not JSON. */ - private parse(out: string): ScResult | null { - if (!out) return null; - // `sc` may emit several JSON lines (e.g. watch). We only want the last - // complete object for one-shot commands. - const line = out.split("\n").reverse().find((l) => l.trim().startsWith("{")); - if (!line) return null; - let obj: any; - try { - obj = JSON.parse(line); - } catch { - return null; - } - const kind = typeof obj.kind === "string" ? obj.kind : "unknown"; - if (kind === "cli_error" || obj.error) { - const e = obj.error ?? {}; - return fail(kind, { code: e.code ?? "error", message: e.message ?? "unknown error" }); - } - const { kind: _k, response, ...rest } = obj; - return { ok: true, kind, response: response ?? rest, error: null, raw: obj }; - } - - // ── Worktree ────────────────────────────────────────────────────────── - - worktreeStatus(): Promise { - return this.run(["worktree", "status"]); - } - - diffSummary(file?: string): Promise { - return this.run(["worktree", "diff-summary", ...(file ? ["--file", file] : [])]); - } - - setTargetBranch(branch: string): Promise { - return this.run(["worktree", "set-target-branch", branch]); - } - - renameBranch(name: string): Promise { - return this.run(["worktree", "rename-branch", name]); - } - - // ── Workspace ───────────────────────────────────────────────────────── - - /** Open a directory as a Superconductor workspace (project). */ - workspaceOpen(path: string, activate = true): Promise { - return this.run(["workspace", "open", path, ...(activate ? ["--activate"] : [])]); - } - - // ── Tab ─────────────────────────────────────────────────────────────── - - tabTitle(title: string): Promise { - return this.run(["tab", "title", title]); - } - - tabSplit(direction: "up" | "down" | "left" | "right", ui: "auto" | "chat" | "terminal" = "terminal"): Promise { - return this.run(["tab", "split", "--direction", direction, "--ui", ui]); - } -} diff --git a/packages/pi-ext/extensions/superconductor/status.ts b/packages/pi-ext/extensions/superconductor/status.ts deleted file mode 100644 index 97e2253..0000000 --- a/packages/pi-ext/extensions/superconductor/status.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Superconductor status surface. - * - * Two non-destructive signals: - * 1. A pi footer status pill ("SC →main · +12 -3") showing the target - * branch and current diff size. Refreshed on session start and after - * each agent run. This is purely additive — it does not touch the - * Superconductor tab title. - * 2. Optional tab-title state (opt-in via PI_SC_TAB_TITLE=1). Because the - * tab title is user-facing and shared, we only drive it when asked. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import type { ScClient } from "./sc-client.js"; - -const STATUS_KEY = "superconductor"; - -interface WorktreeStatus { - branch?: string; - target_branch?: string; - files_changed?: number; - insertions?: number; - deletions?: number; -} - -export function wireStatus(pi: ExtensionAPI, client: ScClient): void { - const driveTabTitle = process.env.PI_SC_TAB_TITLE === "1"; - let baseLabel = "SC"; - - async function refreshFooter(ctx: { ui: { setStatus(key: string, value?: string): void; hasUI?: boolean } }): Promise { - const res = await client.worktreeStatus(); - if (!res.ok) return; - const s = res.response as WorktreeStatus; - const target = s.target_branch ? `→${s.target_branch}` : ""; - const diff = - s.files_changed && s.files_changed > 0 - ? ` · ${s.files_changed}f +${s.insertions ?? 0} -${s.deletions ?? 0}` - : ""; - baseLabel = `SC ${target}`.trim(); - ctx.ui.setStatus(STATUS_KEY, `${baseLabel}${diff}`); - } - - pi.on("session_start", async (_event, ctx) => { - await refreshFooter(ctx as any); - if (driveTabTitle) await client.tabTitle("● pi idle"); - }); - - pi.on("agent_start", async () => { - if (driveTabTitle) await client.tabTitle("⚡ pi running"); - }); - - pi.on("agent_end", async (_event, ctx) => { - await refreshFooter(ctx as any); - if (driveTabTitle) await client.tabTitle("✓ pi idle"); - }); - - pi.on("session_shutdown", async (_event, ctx) => { - (ctx as any)?.ui?.setStatus?.(STATUS_KEY, undefined); - }); -} diff --git a/packages/pi-ext/extensions/superconductor/tools.ts b/packages/pi-ext/extensions/superconductor/tools.ts deleted file mode 100644 index 7f93cb3..0000000 --- a/packages/pi-ext/extensions/superconductor/tools.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Custom tools exposing Superconductor to the LLM. - * - * - superconductor_worktree: read worktree status / diff, change target branch, - * list git worktrees, and create new ones (raw git, since SC has no create API). - * - * The tool surfaces `sc` cli_error envelopes verbatim so the model can react. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { truncateTail } from "@earendil-works/pi-coding-agent"; -import { StringEnum } from "@earendil-works/pi-ai"; -import { Type } from "typebox"; -import type { ScClient, ScResult } from "./sc-client.js"; -import { createWorktree, listWorktrees, rootRepo } from "./git.js"; - -type ToolResult = { content: Array<{ type: "text"; text: string }>; details: Record }; - -function text(s: string, details: Record = {}): ToolResult { - return { content: [{ type: "text", text: s }], details }; -} - -/** Render an ScResult as a tool result. Throws on failure so pi flags isError. */ -function present(res: ScResult, emptyNote = "ok"): ToolResult { - if (!res.ok) { - throw new Error(`sc error [${res.error?.code}]: ${res.error?.message}`); - } - const payload = res.response; - if (payload === null || payload === undefined) return text(emptyNote); - const json = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2); - const truncated = truncateTail(json, { maxBytes: 50_000, maxLines: 2000 }); - return text(truncated.content, { kind: res.kind }); -} - -export function wireTools(pi: ExtensionAPI, client: ScClient): void { - // ── superconductor_worktree ───────────────────────────────────────────── - pi.registerTool({ - name: "superconductor_worktree", - label: "SC Worktree", - description: [ - "Inspect and control the Superconductor worktree.", - "", - "status — branch, target/base branch, files changed, +/- lines", - "diff_summary — per-file change summary (optional file filter)", - "set_target_branch — change the review/merge target branch", - "rename_branch — rename the current worktree branch", - "list_worktrees — list git worktrees of the root repo", - "create — create a NEW git worktree off a base branch (default main),", - " checked out on a new/existing branch; optionally open it in SC.", - "", - "The target/base branch is Superconductor-owned. Use status to read it; never infer from git defaults.", - "Note: SC has no API to create a managed worktree, so create uses raw git. The new worktree", - "is a real git checkout; with open_in_sc it is surfaced via `sc workspace open` but is not a", - "Superconductor-managed task.", - ].join("\n"), - promptSnippet: "Read Superconductor worktree status, diff, target branch; create new git worktrees off main.", - promptGuidelines: [ - "Use superconductor_worktree status to read the authoritative target/base branch instead of guessing from git.", - "To branch off main into a fresh worktree, use action=create with a branch name (base defaults to main).", - ], - parameters: Type.Object({ - action: StringEnum([ - "status", - "diff_summary", - "set_target_branch", - "rename_branch", - "list_worktrees", - "create", - ] as const), - file: Type.Optional(Type.String({ description: "For diff_summary: limit to a single file path" })), - branch: Type.Optional( - Type.String({ description: "For set_target_branch: new target branch. For create: new/existing branch to check out." }), - ), - name: Type.Optional(Type.String({ description: "For rename_branch: the new branch name" })), - base: Type.Optional(Type.String({ description: "For create: base ref to branch from (default: main)" })), - path: Type.Optional( - Type.String({ description: "For create: destination dir (default: sibling of root repo, '-')" }), - ), - open_in_sc: Type.Optional( - Type.Boolean({ description: "For create: open the new worktree in Superconductor via `sc workspace open`" }), - ), - }), - async execute(_id, params): Promise { - switch (params.action) { - case "status": - return present(await client.worktreeStatus()); - case "diff_summary": - return present(await client.diffSummary(params.file)); - case "set_target_branch": - if (!params.branch) throw new Error("branch is required for set_target_branch"); - return present(await client.setTargetBranch(params.branch), "target branch updated"); - case "rename_branch": - if (!params.name) throw new Error("name is required for rename_branch"); - return present(await client.renameBranch(params.name), "branch renamed"); - case "list_worktrees": { - const res = await listWorktrees(); - if (!res.ok) throw new Error(`git error: ${res.error}`); - return text(JSON.stringify({ root: rootRepo(), worktrees: res.worktrees }, null, 2)); - } - case "create": { - if (!params.branch) throw new Error("branch is required for create"); - const created = await createWorktree({ - branch: params.branch, - base: params.base, - dest: params.path, - }); - if (!created.ok) throw new Error(`worktree create failed: ${created.error}`); - let opened: ScResult | null = null; - if (params.open_in_sc) opened = await client.workspaceOpen(created.path); - return text( - JSON.stringify( - { - path: created.path, - branch: created.branch, - base: created.base, - created_branch: created.createdBranch, - opened_in_sc: opened ? opened.ok : false, - open_error: opened && !opened.ok ? opened.error?.message : undefined, - }, - null, - 2, - ), - { kind: "worktree_created" }, - ); - } - } - }, - }); -} diff --git a/packages/pi-ext/extensions/tool-trim/tool-trim.ts b/packages/pi-ext/extensions/tool-trim/tool-trim.ts new file mode 100644 index 0000000..c8d928a --- /dev/null +++ b/packages/pi-ext/extensions/tool-trim/tool-trim.ts @@ -0,0 +1,22 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +/** + * Keep the useful semantic tools without paying prompt cost for the rest. + * + * Pi has no persistent per-tool disable, so reapply the filter every turn. + * MCP aliases are filtered too in case another extension exposes them. + */ +const DROP = new Set( + ["sem_diff", "sem_eval", "sem_log", "sem_blame"].flatMap((name) => [name, `mcp__pi__${name}`]), +); + +export default function (pi: ExtensionAPI) { + const trim = () => { + const active = pi.getActiveTools(); + const kept = active.filter((name) => !DROP.has(name)); + if (kept.length !== active.length) pi.setActiveTools(kept); + }; + + pi.on("session_start", trim); + pi.on("turn_start", trim); +} diff --git a/packages/pi-ext/package.json b/packages/pi-ext/package.json index 52bcf31..201486b 100644 --- a/packages/pi-ext/package.json +++ b/packages/pi-ext/package.json @@ -22,7 +22,7 @@ ], "dependencies": { "@shikijs/cli": "^4.0.2", - "@sting8k/pi-vcc": "0.3.18", + "@sting8k/pi-vcc": "0.6.0", "@tintinweb/pi-tasks": "0.4.3", "better-sqlite3": "^12.10.0", "diff": "^7.0.0", @@ -61,7 +61,7 @@ "homepage": "https://github.com/iurysza/pi-extensions/tree/main/packages/pi-ext#readme", "scripts": { "sem:evaluate": "node extensions/pi-sem/bin/sem-eval.mjs", - "test": "npm run test:pi-sem && npm run test:tool-presentation && node --test tests/leader-key/favourite-models.test.mjs tests/leader-key/skill-editor.test.mjs tests/chat-to-md/chat-to-md.test.mjs tests/handoff/dependency-contract.test.mjs tests/startup-screen/startup-screen.test.mjs tests/file-search/*.test.mjs", + "test": "npm run test:pi-sem && npm run test:tool-presentation && node --experimental-strip-types --test tests/tool-trim/tool-trim.test.ts && node --test tests/leader-key/favourite-models.test.mjs tests/leader-key/skill-editor.test.mjs tests/chat-to-md/chat-to-md.test.mjs tests/handoff/dependency-contract.test.mjs tests/startup-screen/startup-screen.test.mjs tests/file-search/*.test.mjs", "test:pi-sem": "node --test tests/pi-sem/core.test.mjs tests/review/sem-guidance.test.mjs", "test:tool-presentation": "node scripts/test-tool-presentation.mjs", "typecheck": "tsc --noEmit -p tsconfig.json", @@ -82,20 +82,19 @@ "./extensions/ask-user-question/index.ts", "./extensions/tool-presentation/index.ts", "./extensions/split-fork/index.ts", - "./extensions/superconductor/index.ts", "./extensions/session-store/index.ts", "./extensions/cmux/index.ts", "./extensions/handoff/index.ts", "./extensions/session-snap/index.ts", "./extensions/pi-telescope/index.ts", - "./extensions/pi-sem/index.ts" + "./extensions/pi-sem/index.ts", + "./extensions/tool-trim/tool-trim.ts" ], "skills": [ "./skills/commit", "./skills/github", "./skills/visit-webpage", "./skills/web-search", - "./skills/session-query", "./skills/sem", "./skills/pr-review-comments" ], diff --git a/packages/pi-ext/skills/sem/SKILL.md b/packages/pi-ext/skills/sem/SKILL.md index 84ac822..64147a0 100644 --- a/packages/pi-ext/skills/sem/SKILL.md +++ b/packages/pi-ext/skills/sem/SKILL.md @@ -1,72 +1,36 @@ --- name: sem -description: Entity-aware code change analysis using the pi-sem tools. Use when the user asks what changed, wants blast radius or affected tests, needs focused context for a function/class, wants review help on a commit/branch/PR, or asks to compare semantic diff with raw git diff. Prefer sem_context and sem_impact; use sem_diff selectively for summaries and reviews. +description: Entity-aware code analysis using the focused pi-sem tools. Use for compact context around one function or class, dependency and affected-test analysis, or a structural file inventory. --- # sem -Use the `pi-sem` tools as a **semantic lens**, not as a universal replacement for raw git diff. +Use the focused `pi-sem` tools as a semantic lens. Use raw Git and file tools for diffs, history, blame, and line-level evidence. -## Default decision tree +## Choose the smallest tool -Choose the smallest useful tool first: +1. **Understand one entity** → `sem_context` + - Use for a function, method, class, block, or config section. + - Prefer this before reading a whole large file. -1. **Focused understanding of one entity** → `sem_context` - - Best for a single function, method, class, block, or config section - - Prefer this before reading a whole large file +2. **Find blast radius or affected tests** → `sem_impact` + - Use `scope=tests` for test selection. + - Use `scope=all` for broader dependents and dependencies. -2. **Blast radius / affected tests / hidden dependents** → `sem_impact` - - Use when reasoning about what could break - - Prefer `scope=tests` for test selection - - Prefer `scope=all` when validating broader impact - -3. **Structural inventory of a file** → `sem_entities` - - Use before drilling into a suspicious file - - Good for large files and mixed code/config files - -4. **What changed across a commit/range/working tree** → `sem_diff` - - Use for semantic summaries, entity counts, and review overviews - - Do **not** default to it when you only need exact patch details or a single entity - -5. **History / ownership of an entity** → `sem_log`, `sem_blame` - - Use for regressions, archaeology, and ownership questions +3. **Inventory a file** → `sem_entities` + - Use before drilling into a large or mixed code/config file. ## Review workflow -For commit / branch / PR review: - -1. Run `sem_diff` once for a semantic overview -2. Pick the riskiest changed entities -3. Run `sem_impact` on those entities -4. Run `sem_context` on the suspicious ones you need to understand deeply -5. Confirm final findings with raw `git diff`, `read`, or direct file inspection before citing line numbers - -For snapshot / folder review: - -1. Start with `sem_entities` -2. Use `sem_context` on the most relevant entities -3. Use `sem_impact` only after you identify something suspicious - -## Important caveats - -- `sem diff --format json` is **not always smaller** than raw `git diff` -- `sem` may under-cover tests, assets, generated files, or non-semantic glue code -- Do not cite `sem` output alone as final evidence for line-level review comments -- If semantic coverage looks incomplete, fall back to raw `git diff`, `read`, `grep`, and file inspection - -## Good prompts / tool choices - -- “What changed in this commit?” → `sem_diff` -- “What tests are affected by this function?” → `sem_impact` with `scope=tests` -- “Give me focused context for this class without reading the whole file.” → `sem_context` -- “What is in this Terraform file?” → `sem_entities` -- “How did this function evolve?” → `sem_log` - -## Anti-patterns +1. Inspect the actual change with raw Git diff and file reads. +2. Identify the riskiest changed entities. +3. Run `sem_impact` on entities with possible cross-file effects. +4. Run `sem_context` where focused dependency-aware context helps. +5. Use `sem_entities` when a file's structure is unclear. +6. Confirm every finding against the changed code before citing lines. -Avoid these habits: +## Caveats -- Running `sem_diff` repeatedly when `sem_context` would answer the question faster -- Using only raw `git diff` for entity counts or blast radius questions -- Treating `sem_impact` as infallible; verify surprising results with file reads and grep -- Writing review findings from semantic summaries without checking the actual changed code +- `sem` may under-cover tests, assets, generated files, or non-semantic glue code. +- Treat surprising impact results as leads, then verify them with file reads and search. +- Do not write review findings from semantic output alone. diff --git a/packages/pi-ext/skills/session-query/SKILL.md b/packages/pi-ext/skills/session-query/SKILL.md deleted file mode 100644 index 8fa1319..0000000 --- a/packages/pi-ext/skills/session-query/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: session-query -description: Query previous pi sessions to retrieve context, decisions, code changes, or other information. Use when you need to look up what happened in a parent session or any other session file. -disable-model-invocation: true ---- - -# Session Query - -Query pi session files to retrieve context from past conversations. - -This skill is automatically invoked in handed-off sessions when you need to look up details from the parent session. - -## Usage - -Use the `session_query` tool: - -``` -session_query(sessionPath, question) -``` - -- `sessionPath`: Full path to the session file (provided in the "Parent session:" line) -- `question`: Specific question about that session (e.g., "What files were modified?" or "What approach was chosen?") - -## Examples - -``` -session_query("/path/to/session.jsonl", "What files were modified?") -session_query("/path/to/session.jsonl", "What approach was chosen for authentication?") -session_query("/path/to/session.jsonl", "Summarize the key decisions made") -``` - -The tool loads the session and uses an LLM to answer your question based on its contents. Ask specific questions for best results. diff --git a/packages/pi-ext/tests/handoff/dependency-contract.test.mjs b/packages/pi-ext/tests/handoff/dependency-contract.test.mjs index a6b9698..4a74338 100644 --- a/packages/pi-ext/tests/handoff/dependency-contract.test.mjs +++ b/packages/pi-ext/tests/handoff/dependency-contract.test.mjs @@ -13,7 +13,7 @@ const handoffEntry = resolve(packageRoot, "extensions/handoff/index.ts"); describe("handoff dependency contract", () => { it("pins and loads the internal modules shipped by its dependencies", async () => { const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8")); - assert.equal(manifest.dependencies["@sting8k/pi-vcc"], "0.3.18"); + assert.equal(manifest.dependencies["@sting8k/pi-vcc"], "0.6.0"); assert.equal(manifest.dependencies["@tintinweb/pi-tasks"], "0.4.3"); const agentDir = await mkdtemp(join(tmpdir(), "pi-ext-handoff-loader-")); diff --git a/packages/pi-ext/tests/review/sem-guidance.test.mjs b/packages/pi-ext/tests/review/sem-guidance.test.mjs index 757d134..c7d1258 100644 --- a/packages/pi-ext/tests/review/sem-guidance.test.mjs +++ b/packages/pi-ext/tests/review/sem-guidance.test.mjs @@ -11,17 +11,18 @@ test("getSemToolAvailability detects active sem review tools", () => { assert.equal(availability.any, true); }); -test("buildSemReviewGuidance for commit review includes selective sem workflow", () => { +test("buildSemReviewGuidance for commit review uses only retained sem tools", () => { const guidance = buildSemReviewGuidance( { type: "commit", sha: "abc1234" }, - getSemToolAvailability(["sem_diff", "sem_context", "sem_impact", "sem_entities"]), + getSemToolAvailability(["sem_context", "sem_impact", "sem_entities"]), ); assert.ok(guidance); assert.match(guidance, /Semantic tooling for this diff review/); - assert.match(guidance, /sem_diff.*abc1234/); + assert.doesNotMatch(guidance, /sem_diff/); assert.match(guidance, /sem_impact/); assert.match(guidance, /sem_context/); + assert.match(guidance, /sem_entities/); assert.match(guidance, /confirm final findings with raw `git diff`, `read`, or direct file inspection/); }); diff --git a/packages/pi-ext/tests/tool-trim/tool-trim.test.ts b/packages/pi-ext/tests/tool-trim/tool-trim.test.ts new file mode 100644 index 0000000..5f6e346 --- /dev/null +++ b/packages/pi-ext/tests/tool-trim/tool-trim.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import toolTrim from "../../extensions/tool-trim/tool-trim.ts"; + +test("removes rejected sem tools and aliases on session and turn start", () => { + const handlers = new Map void>(); + let active = [ + "read", + "sem_diff", + "sem_context", + "mcp__pi__sem_eval", + "sem_impact", + "sem_entities", + ]; + const writes: string[][] = []; + + toolTrim({ + getActiveTools: () => active, + setActiveTools: (tools: string[]) => { + active = tools; + writes.push(tools); + }, + on: (event: string, handler: () => void) => handlers.set(event, handler), + } as never); + + handlers.get("session_start")?.(); + assert.deepEqual(active, ["read", "sem_context", "sem_impact", "sem_entities"]); + + active = ["read", "sem_log", "sem_blame"]; + handlers.get("turn_start")?.(); + assert.deepEqual(active, ["read"]); + assert.equal(writes.length, 2); +}); + +test("does not rewrite an already-clean active tool list", () => { + const handlers = new Map void>(); + let writes = 0; + + toolTrim({ + getActiveTools: () => ["read", "sem_context", "sem_impact", "sem_entities"], + setActiveTools: () => writes++, + on: (event: string, handler: () => void) => handlers.set(event, handler), + } as never); + + handlers.get("session_start")?.(); + assert.equal(writes, 0); +});