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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ jobs:
echo "Run scripts/sync-pi-package.sh against a kimetsu-pi checkout and commit the result there."
exit 1
}
- name: Diff the Pi skill against the published copy
run: |
diff -u crates/kimetsu-chat/assets/pi-skill.md \
.kimetsu-pi/skills/kimetsu-brain/SKILL.md \
|| {
echo "::error::The canonical Pi skill and kimetsu-pi skill have drifted."
echo "Run scripts/sync-pi-package.sh against a kimetsu-pi checkout and commit the result there."
exit 1
}

sdk:
name: TypeScript SDK
Expand Down
80 changes: 76 additions & 4 deletions crates/kimetsu-chat/assets/pi-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@
// crash, unparseable output. Kimetsu is a sidecar — it must never break Pi.

import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

/** Hard cap on any single kimetsu invocation. A hung binary must not stall a turn. */
/** Interactive hooks must not leave a turn waiting on a hung binary. */
const EXEC_TIMEOUT_MS = 10000;

/** Session saving may distill lessons and an episode in two model calls
* (120s each by default). Leave time for both plus local persistence. */
const SESSION_SAVE_TIMEOUT_MS = 300000;

/** Fallback session id when Pi's context does not expose one. Stable per process,
* which is what the brain's per-session dedupe and refractory windows need. */
const FALLBACK_SESSION_ID = `pi-${process.pid}`;
Expand All @@ -30,7 +35,7 @@ const FALLBACK_SESSION_ID = `pi-${process.pid}`;
* stdout is PIPED, not ignored: the context hook communicates entirely through
* it. stderr stays ignored so diagnostics never mix into the parsed payload.
*/
function kimetsuRun(args: string[], input?: string): Promise<string> {
function kimetsuRun(args: string[], input?: string, timeoutMs = EXEC_TIMEOUT_MS): Promise<string> {
return new Promise((resolve) => {
let settled = false;
let timer: ReturnType<typeof setTimeout> | undefined;
Expand All @@ -52,7 +57,7 @@ function kimetsuRun(args: string[], input?: string): Promise<string> {
timer = setTimeout(() => {
child.kill();
done();
}, EXEC_TIMEOUT_MS);
}, timeoutMs);
if (typeof timer.unref === "function") timer.unref();

child.stdout?.setEncoding("utf8");
Expand Down Expand Up @@ -147,11 +152,17 @@ function workspaceArgs(ctx: any): string[] {
}

export default function (pi: ExtensionAPI) {
// Pi persists injected messages, including display:false messages. Only the
// current task's injection belongs in future model calls. A unique marker
// survives Pi's message cloning without trusting content or timestamps.
let activeContext: { id: string; sessionId: string } | undefined;

// session_start fires once when Pi starts up or a new session begins.
// Warming spawns the embedder daemon so the first real retrieval is semantic
// rather than falling back to lexical FTS.
// (`brain warm` takes no --workspace: it resolves the project from its cwd.)
pi.on("session_start", async (_event, _ctx) => {
activeContext = undefined;
await kimetsuRun(["brain", "warm"]);
});

Expand All @@ -161,25 +172,84 @@ export default function (pi: ExtensionAPI) {
// --warm-on-first-prompt folds the repo digest and episodic resume into the
// first turn of each session.
pi.on("before_agent_start", async (event, ctx) => {
const request = { id: randomUUID(), sessionId: sessionIdOf(ctx) };
// Expire the last task immediately, even if retrieval is empty or fails.
activeContext = request;
const payload = JSON.stringify({
session_id: sessionIdOf(ctx),
session_id: request.sessionId,
prompt: typeof event?.prompt === "string" ? event.prompt : "",
});
const stdout = await kimetsuRun(
["brain", "context-hook", "--warm-on-first-prompt", ...workspaceArgs(ctx)],
payload,
);
// A session switch or a newer prompt can supersede an in-flight request.
if (activeContext !== request) return;
const content = parseAdditionalContext(stdout);
if (content === undefined) return; // nothing relevant — zero tokens
return {
message: {
customType: "kimetsu-brain",
content,
display: false,
details: { kimetsuContextId: request.id },
},
};
});

pi.on("context", async (event, ctx) => {
const context = activeContext;
const currentIndex = context && context.sessionId === sessionIdOf(ctx)
? event.messages.findIndex((message) => {
if (message.role !== "custom" || message.customType !== "kimetsu-brain") return false;
const details = message.details as { kimetsuContextId?: unknown } | undefined;
return details?.kimetsuContextId === context.id;
})
: -1;
// Queued steering/follow-up messages bypass before_agent_start. Expire
// the old injection when a newer user message arrives; tool results alone
// do not end the current task's context.
if (currentIndex >= 0 && event.messages.some((message, index) =>
index > currentIndex && message.role === "user"
)) activeContext = undefined;

// Filter the model's copy only; preserve the persisted session history.
return {
messages: event.messages.filter((message, index) =>
message.role !== "custom" || message.customType !== "kimetsu-brain"
|| (activeContext !== undefined && index === currentIndex)
),
};
});

pi.on("session_before_compact", async (event) => {
// Summarization bypasses the context event. Do not turn retrieved evidence
// into a durable summary that could outlive a correction or invalidation.
event.preparation.messagesToSummarize = event.preparation.messagesToSummarize.filter(
(message) => message.role !== "custom" || message.customType !== "kimetsu-brain",
);
event.preparation.turnPrefixMessages = event.preparation.turnPrefixMessages.filter(
(message) => message.role !== "custom" || message.customType !== "kimetsu-brain",
);
});

pi.on("session_before_tree", async (event) => {
// Pi retains a reference to this temporary summary input array, so filter
// in place. The persisted session entries themselves are left untouched.
const entries = event.preparation.entriesToSummarize;
let kept = 0;
for (const entry of entries) {
if (entry.type !== "custom_message" || entry.customType !== "kimetsu-brain") {
entries[kept++] = entry;
}
}
entries.length = kept;
});

pi.on("session_tree", async () => {
activeContext = undefined;
});

// agent_end fires after the LLM turn completes (maps to Kimetsu stop-hook).
pi.on("agent_end", async (event, ctx) => {
await kimetsuRun(
Expand All @@ -190,9 +260,11 @@ export default function (pi: ExtensionAPI) {

// session_shutdown fires on clean session close (maps to session-end-hook).
pi.on("session_shutdown", async (_event, ctx) => {
activeContext = undefined;
await kimetsuRun(
["brain", "session-end-hook", ...workspaceArgs(ctx)],
lifecyclePayload(ctx),
SESSION_SAVE_TIMEOUT_MS,
);
});
}
64 changes: 64 additions & 0 deletions crates/kimetsu-chat/assets/pi-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
name: kimetsu-brain
description: Use when Pi tasks benefit from prior session knowledge, durable lessons, memory corrections, or feedback on helpful memories.
---
Kimetsu is a persistent memory sidecar accessed through the `kimetsu` CLI.
Run commands from the relevant project directory. If the binary is unavailable,
note the absence and continue normally.

## Use the context already provided

The Pi extension retrieves context before each task. Read that injection first.
When it covers the current question, proceed without repeating the same lookup.
Use `kimetsu brain context "<specific question>"` when no useful context was
injected, the task changes, or a missing detail or correction needs fresh evidence.
An empty result is a reason to inspect the repository, not repeat the same query.

Memory is evidence from earlier work. Check conflicts against current files and
the user's instructions. Respect project, environment, and version boundaries;
partial or conflicting evidence does not justify filling gaps with assumptions.

## Record and correct durable lessons

After verifying a reusable lesson, record it with
`kimetsu brain memory add --scope project --kind <kind> "<lesson>"`.
Choose `fact`, `preference`, `convention`, `command`, or `failure_pattern`.
Include the subject and any environment/version limits in the text.

For a correction to an existing claim, update its actual memory ID instead of
adding a contradictory duplicate:

```sh
kimetsu brain memory edit <memory-id> --text "Production gateway port is 4000. Development gateway port remains 3000."
```

Preserve still-valid parts of the claim. Different environments or historical
versions can both be valid; a production correction does not retire development
guidance. When the entire memory is obsolete or false, use
`kimetsu brain memory invalidate <memory-id> --reason "<verified reason>"`.

Find actual IDs with `kimetsu brain context "<specific question>" --json` or
`kimetsu brain memory list --json`; match the text and scope before editing,
invalidating, or citing. For a `memory:<id>` expansion handle, use only `<id>`.
Never invent an ID or treat a file capsule as a memory.
The edit/invalidate commands above operate on the current workspace brain.
Listings may also include portable user-brain memories; those commands cannot
correct them. For a user-brain claim, report the correction and this limitation
instead of claiming the portable memory was updated.

## Credit explicit usefulness

When a particular memory materially helped, record that reliance:

```sh
kimetsu brain cite --memory-id <memory-id> --query "<task it helped with>" --note "<how it helped>"
```

Cite only memories actually used. Being injected, or having tests pass, is not
enough to credit a memory. A citation records usefulness, not proof of truth;
correct wrong claims using the commands above. Do not cite unused memories or
repeat credit for the same use.

`kimetsu brain status` reports initialization, accepted memories, and pending
proposals. Automatic session saving is separate from these deliberate actions;
model-based lesson distillation requires a configured distiller.
24 changes: 2 additions & 22 deletions crates/kimetsu-chat/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,28 +306,8 @@ const PI_EXTENSION_TS: &str = include_str!("../assets/pi-extension.ts");
/// Pi skills are plain Markdown with optional YAML frontmatter. No MCP is
/// available in Pi, so the skill describes the brain commands the agent can
/// shell out to via `pi.exec()` or custom tools if wired.
const PI_SKILL_MD: &str = r#"---
name: kimetsu-brain
description: Use Kimetsu brain shell commands as a persistent memory sidecar across Pi sessions.
---
Kimetsu is a persistent brain sidecar accessible via the `kimetsu` CLI. Use it
when the task may benefit from prior session knowledge, workflow memory, or
durable cross-session context.

Run `kimetsu brain context <query>` when you start a task and read the returned
capsules before deciding on a plan. An empty result means the brain held nothing
relevant and cost nothing — retrieving is cheaper than rediscovering.

Run `kimetsu brain memory add --scope project --kind <kind> "<lesson>"` once
you know something a later session would otherwise have to work out again.
Choose `fact`, `preference`, `convention`, `command`, or `failure_pattern` for
`<kind>`.

`kimetsu brain status` reports whether the brain is initialized, has accepted
memories, or has pending proposals.

If the binary is unavailable, note the absence and continue normally.
"#;
/// The npm package vendors this same asset alongside the extension.
const PI_SKILL_MD: &str = include_str!("../assets/pi-skill.md");

#[cfg(feature = "openclaw")]
/// TypeScript plugin installed at `<oc_dir>/plugins/kimetsu/index.ts`.
Expand Down
1 change: 1 addition & 0 deletions scripts/sync-pi-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ copy() {

echo "syncing $repo_root -> $target"
copy "$repo_root/crates/kimetsu-chat/assets/pi-extension.ts" "$target/extensions/kimetsu.ts"
copy "$repo_root/crates/kimetsu-chat/assets/pi-skill.md" "$target/skills/kimetsu-brain/SKILL.md"
echo "done. Commit the changes in $target."
Loading