Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,12 @@ Once set up, Capacitor runs silently in the background. Every Claude Code (and C
- **Repository context** — git repo, branch, and PR linkage
- **In-agent upgrade prompts** — in Claude Code sessions, when the server is running a newer kcap release than the local CLI, additional context is injected into the session so the agent can offer the user an upgrade via `kcap update`. The stderr `kcap` update hint continues to fire for direct command-line use.
- **SessionStart context injection** — at every session start the server injects top evaluation-derived fact clusters for the current repo into Claude's `additionalContext`. The injected block is split into two sections: `## Known patterns` (repo/project facts relevant to any reader) and `## Guidance from past sessions` (agent-targeted action items derived from prior eval suggestions with `audience: "agent"`). Opt out by setting `disable_session_guidelines: true` in `~/.config/kcap/config.json` or via `kcap config set disable_session_guidelines true`.
- **SessionStart team-memory index** — at every session start (Claude Code, Codex CLI, GitHub Copilot CLI, Gemini CLI, AWS Kiro CLI, Google Antigravity, and Cursor CLI's `cursor-agent` — see the capability matrix below for the full per-harness rollout) `kcap` also fetches a compact index of durable [team memories](#memory-mcp-server-for-agents) visible for the current repo/machine and appends a `## Team memory` block to the session's injected context (`additionalContext` for Claude, Codex, Copilot, and Gemini, `additional_context` for Cursor, raw stdout for Kiro, `injectSteps`/`userMessage` for Antigravity): one `slug: description` line per memory, grouped **Org / Team / Yours**, with a nudge to call `get_memory` / `search_memories` for full content. Only the index is injected — never the bodies — so the cost stays roughly flat as the pool grows (mirrors a local `MEMORY.md`). Best-effort and fail-open (a slow or failed fetch injects nothing, never blocking the hook), and only ever injected once per conversation. Opt out with `disable_memory_index: true` in `~/.config/kcap/config.json` or `kcap config set disable_memory_index true`.
- **SessionStart team-memory index** — at every session start (Claude Code, Codex CLI, GitHub Copilot CLI, Gemini CLI, AWS Kiro CLI, Google Antigravity, Pi, OpenCode, and Cursor CLI's `cursor-agent` — see the capability matrix below for the full per-harness rollout) `kcap` also fetches a compact index of durable [team memories](#memory-mcp-server-for-agents) visible for the current repo/machine and appends a `## Team memory` block to the session's injected context (`additionalContext` for Claude, Codex, Copilot, and Gemini, `additional_context` for Cursor, raw stdout for Kiro, `injectSteps`/`userMessage` for Antigravity, system-prompt append via the kcap Pi extension for Pi, and system-entry append via the kcap OpenCode plugin for OpenCode): one `slug: description` line per memory, grouped **Org / Team / Yours**, with a nudge to call `get_memory` / `search_memories` for full content. Only the index is injected — never the bodies — so the cost stays roughly flat as the pool grows (mirrors a local `MEMORY.md`). Best-effort and fail-open (a slow or failed fetch injects nothing, never blocking the hook), and only ever injected once per conversation. Opt out with `disable_memory_index: true` in `~/.config/kcap/config.json` or `kcap config set disable_memory_index true`.
- **Crash resilience** — if a `kcap` command hits an unexpected error it records the exception (with stack trace) to `~/.config/kcap/crash.log` (honours `KCAP_CONFIG_DIR`; size-capped) and exits cleanly instead of aborting. Hook and detached-generator commands the coding agent spawns **fail open** (exit 0, nothing surfaced to the agent); other commands exit non-zero with a one-line stderr message pointing at the log.

The SessionStart memory foundation is deliberately separate from harness activation. Every row uses
the same typed fetch/render, lifecycle, fenced lease, and golden output contracts. Claude, Cursor,
Codex, Copilot, Gemini, Kiro, and Antigravity are wired; each remaining adapter is activated and
Codex, Copilot, Gemini, Kiro, Pi, OpenCode, and Antigravity are wired; each remaining adapter is activated and
live-certified by its own AI-1456 child issue.

| Harness | Shared foundation | Hook/extension wired | Live receipt | Upstream status |
Expand All @@ -224,7 +224,7 @@ live-certified by its own AI-1456 child issue.
| GitHub Copilot CLI | yes | yes (`sessionStart`'s top-level `additionalContext`; silent when there is nothing to inject) | **certified** — gated live cert, `copilot 1.0.75` | available |
| Gemini CLI | yes | yes (`sessionStart`'s top-level `additionalContext`; falls back to the plain `{"continue":true}` allow payload when there is nothing to inject) | pending | available |
| Kiro CLI | yes | yes (`agentSpawn` raw stdout — no envelope; Kiro appends hook stdout to agent context verbatim) | **certified** — gated live cert, `kiro-cli 2.12.1`. The once-per-session dedupe is unit-covered only: a resumed `kiro-cli` invocation carries a different hook session id, so it cannot certify it | available — injects **once per session** despite `agentSpawn` firing every prompt |
| Pi | yes | no | pending | extension bridge required |
| Pi | yes | yes (extension: the session-start hook prints the fragment raw on stdout — zero bytes when there is nothing to inject — and `kcap.ts` appends it to each turn's chained system prompt in `before_agent_start`; the fetch stays once per session behind the durable lease, keyed on the session **file**, so resume dedupes and fork re-fetches) | pending — gated cert shipped (`KCAP_PI_MEMORY_LIVE=1` + `KCAP_URL`, positive + negative control, echo-detector guard), not yet run live | available |
| OpenCode | yes | yes (plugin `experimental.chat.system.transform` — the fragment is appended as a new system entry, never replacing one; the CLI writes it raw on the start hook's stdout and zero bytes when there is nothing to inject) | **certified** — gated live cert, `opencode 1.18.9`, positive case plus negative control. Injection rides an **experimental** OpenCode API, so this cert is the only thing that would notice an upstream change silently ceasing delivery — it already earned that: it caught a start/first-request race no unit test could see, where a one-turn session got no index at all. Needs the plugin AND the `kcap` on `PATH` to be the same build; the positive case asks a model to echo 32 random hex characters, so a small free model flakes on transcription — the failure message distinguishes that from a delivery failure | available; transform contract measured on `opencode` 1.18.9. Appends on **every request** (OpenCode rebuilds the system array per request), while the fetch stays **once per session** behind the durable lease |
| Antigravity | yes | yes (`PreInvocation` → `{"injectSteps":[{"userMessage":…}]}`; zero bytes when there is no index) | pending — **IDE only**. The gated cert (`KCAP_ANTIGRAVITY_MEMORY_LIVE=1` + `KCAP_URL`) was run against `agy` 1.1.10 and the model did **not** receive the index: the CLI's print mode fires the hook but ignores the returned `injectSteps`. Our side is verified (hook fires; a correct 516–2518 byte payload is emitted; run completes well inside the hook budget), so the cert is retained as the regression test for when upstream honours it. Certification therefore needs the **manual IDE procedure** | available on the **IDE**; on the **CLI** the hook fires and captures, but the injection is not surfaced to the model (`agy` 1.1.10). Injects **once per conversation** despite `PreInvocation` firing every invocation |

Expand Down
64 changes: 55 additions & 9 deletions src/Capacitor.Cli.Core/Pi/PiExtensionInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,76 @@ public static class PiExtensionInstaller {
// Pi has no shell hooks, so this extension bridges Pi's in-process lifecycle
// events to the kcap CLI: on session start/shutdown it invokes
// `kcap hook --pi`, which POSTs /hooks/session-{start,end}/pi and runs the
// transcript watcher (vendor=pi). Safe-by-default — every handler swallows
// errors so a kcap or server hiccup never disrupts the pi session.
// transcript watcher (vendor=pi). On session-start, kcap's stdout is a DATA
// channel carrying the team-memory index fragment, which this extension
// appends to each turn's system prompt (before_agent_start). Safe-by-default —
// every handler swallows errors so a kcap or server hiccup never disrupts the
// pi session.

export default function (pi: any) {
async function notify(event: string, ctx: any, reason?: string) {
// Team-memory fragment for the CURRENT session file, or null. Keyed by file
// because that is Pi's stable session identity: resume reuses the file,
// fork/switch mint a different one (which must never inherit this fragment).
let memFile: string | null = null;
let memFragment: string | null = null;

// Only stdout that opens with the marker is a memory fragment. Anything else
// (a future diagnostic, an error string) must not reach the system prompt.
const MEMORY_MARKER = "<!-- kcap-memory-index:v1 -->";

async function notify(event: string, ctx: any, reason?: string): Promise<any> {
try {
const file = ctx?.sessionManager?.getSessionFile?.();
if (!file) return; // ephemeral (--no-session): nothing to record
if (!file) return null; // ephemeral (--no-session): nothing to record
const args = ["hook", "--pi", "--event", event, "--file", String(file)];
if (ctx?.cwd) args.push("--cwd", String(ctx.cwd));
if (reason) args.push("--reason", String(reason));
// kcap spawns a detached watcher and returns fast; bound it so a hung
// kcap can never stall pi's startup or shutdown.
await pi.exec("kcap", args, { timeout: 10000 });
// --memory-contract declares that THIS extension captures and delivers
// the command's stdout; an older kcap ignores unknown args (fail-open).
if (event === "session-start") args.push("--memory-contract", "1");
// kcap spawns a detached watcher and returns fast — except on session-start, where it
// also blocks briefly (bounded to a ~3.5s hook budget) awaiting the memory-index fetch
// before returning. Either way this exec timeout sits well outside that ceiling, so a
// hung kcap can never stall pi's startup or shutdown.
const res = await pi.exec("kcap", args, { timeout: 10000 });
return { file: String(file), stdout: res?.stdout ?? "" };
} catch {
// never disrupt the pi session
return null; // never disrupt the pi session
}
}

pi.on("session_start", async (event: any, ctx: any) => {
await notify("session-start", ctx, event?.reason);
const res = await notify("session-start", ctx, event?.reason);
if (!res) return;
if (res.file !== memFile) {
// A different session file never inherits the previous fragment.
memFile = res.file;
memFragment = null;
}
const out = String(res.stdout ?? "").trim();
// A later empty result never erases a cached ready fragment (repeat
// session_start with a spent lease legitimately returns nothing).
if (out.startsWith(MEMORY_MARKER)) memFragment = out;
});

pi.on("before_agent_start", async (_event: any, ctx: any) => {
try {
if (!memFragment) return;
const current = ctx?.sessionManager?.getSessionFile?.();
if (!current || String(current) !== memFile) return;
// Chained system prompt is rebuilt fresh each turn; append exactly once
// per turn (the includes() guard is belt-and-braces against re-entry).
if (_event?.systemPrompt && !String(_event.systemPrompt).includes(memFragment)) {
return { systemPrompt: _event.systemPrompt + "\n\n" + memFragment };
}
} catch {
// never disrupt the pi session
}
});

pi.on("session_shutdown", async (event: any, ctx: any) => {
memFile = null;
memFragment = null;
await notify("session-end", ctx, event?.reason);
});
}
Expand Down
Loading
Loading