pi-canon 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -14
- package/extensions/canon.ts +1 -1
- package/extensions/lib/surfacing.ts +2 -2
- package/extensions/lib/tool.ts +3 -3
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Canonical project memory for the [Pi coding agent](https://pi.dev). Every asset
|
|
|
4
4
|
|
|
5
5
|
**One setup measured, others welcome.** pi-canon was developed and tested under one configuration: Codex, with GPT 5.6 as the worker model, on an OpenAI subscription. Every number in this README was measured there. Other models, other providers, and API-metered access are untested. If you run it under a different setup, feedback is welcome and so are pull requests.
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
*An illustrative store: 33 articles, 20 journal entries, 40 files. Discs are articles, rings are journal entries hanging under the article each was distilled into, and a square tethered beneath a disc is the asset that article was named for. Six of the articles match no asset and hang untethered, because free knowledge is not a special case here. Selecting a node opens what it holds, what it points at, and what points at it.*
|
|
10
10
|
|
|
@@ -23,24 +23,32 @@ Or clone this repo into `~/.pi/agent/extensions/`. Node 22.18 or later, Pi 0.83
|
|
|
23
23
|
The repository is a Codex marketplace. Add it once, then install the plugin at user scope:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
codex plugin marketplace add shaneconner/
|
|
27
|
-
codex plugin add
|
|
26
|
+
codex plugin marketplace add shaneconner/canon
|
|
27
|
+
codex plugin add canon@canon
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
For a local checkout under development, replace `shaneconner/
|
|
30
|
+
For a local checkout under development, replace `shaneconner/canon` with its absolute path. Start a new Codex thread after installing or updating it.
|
|
31
31
|
|
|
32
32
|
### Claude Code
|
|
33
33
|
|
|
34
34
|
The same repository is also a Claude Code marketplace:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
claude plugin marketplace add shaneconner/
|
|
38
|
-
claude plugin install
|
|
37
|
+
claude plugin marketplace add shaneconner/canon --scope user
|
|
38
|
+
claude plugin install canon@canon --scope user
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Again, an absolute checkout path works for local development. Start a new Claude Code session after installing or updating it.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
### DeepSeek Harness
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
dsh plugin --profile <name> add dsh-canon
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Published separately on npm as `dsh-canon`, built from the same mirrored core. That build ships without the retriever and without the settings screen.
|
|
50
|
+
|
|
51
|
+
The Codex and Claude Code plugins launch the same dependency-free MCP server and expose the same `canon` actions as Pi. Codex surfaces after each tool result. Claude Code deduplicates one capsule packet across each parallel tool batch, immediately before the next model request, which avoids repeated message framing without delaying the agent's next decision. Both give one write-after reminder before the agent stops. An article surfaces at most once per compaction cycle: a compact starts a new cycle, while resuming the same uncompacted session does not. One session may contain several compaction cycles. Compaction discards prior touch state and replays nothing. After it, only a fresh tool-input path can surface that asset's exact or nearest-ancestor article; children and unrelated articles do not ride along. The hooks are inert in projects without `.canon/articles`, and they never create a store merely because a session opened. Review and approve the plugin hooks when the client asks. Journal entries written through the MCP server carry explicit `harness` provenance and a session identifier when the client exposes one.
|
|
44
52
|
|
|
45
53
|
## Defaults
|
|
46
54
|
|
|
@@ -139,13 +147,13 @@ Such an article may say so, with `scope: rule` on the write. Forgetting the decl
|
|
|
139
147
|
|
|
140
148
|
## The tool
|
|
141
149
|
|
|
142
|
-
One tool, `
|
|
150
|
+
One tool, `canon`, five actions: `read`, `write`, `journal`, `map`, and `search`.
|
|
143
151
|
|
|
144
152
|
| action | parameters | does |
|
|
145
153
|
|---|---|---|
|
|
146
154
|
| `read` | `path` | Returns the governing article: title, `capsule`, `updated`, body, and a one-line journal index. A miss returns a sentence naming the address and inviting a write after the task. When an ancestor answers, the title reads `<ancestor> governs <address>`, so the altitude is visible. |
|
|
147
155
|
| `write` | `path`, `capsule`, `body`, `scope` | Creates or updates the article, then returns `Wrote <address>.` and any advisory lint. Never refuses. An empty string means untouched, not erase. A write identical to the stored article is reported as already current and touches nothing, so `updated` keeps meaning the date the content last changed. |
|
|
148
|
-
| `journal` | `body`, `subject`, `slug` | Appends a dated entry as its own file, `<date>-<slug>[-n].md`.
|
|
156
|
+
| `journal` | `body`, `subject`, `slug` | Appends a dated entry as its own file, `<date>-<slug>[-n].md`. canon can never rewrite one. An empty body gets a sentence back asking what happened. |
|
|
149
157
|
| `map` | `path` (optional prefix) | One line per article as `address: capsule`, or a sentence when the store or the filter is empty. Output is unbounded. |
|
|
150
158
|
| `search` | `query`, `journal` | Ranks articles against the words, ten results, each carrying what scopes it: an article its address and capsule. The journal is opt-in via `journal: true`, because events are history rather than current truth, and measured on real stores journal entries about an event crowd out the article carrying it; a default search never reads an entry body and says the journal exists. Opted in, journal entries are first class results scoped by instant and subjects, articles keep half the window, and a short side cedes its slots. Says how many matches the cap dropped. The one action that reaches the journal's content. |
|
|
151
159
|
|
|
@@ -206,7 +214,7 @@ A tool call stages the governing article for whatever it touched and sends nothi
|
|
|
206
214
|
|
|
207
215
|
No character count decides any of this. A capsule is written to fit 1,000 characters, and that is a target handed to the agent at write time, not a gate at read time: an article whose governing asset a turn touched surfaces whole or does not surface. Earlier versions charged capsule text against a session allowance and degraded the overflow to bare pointers. That allowance was removed in 2.0. It was a constant guessing at a policy nobody had measured, and what it decided was how much an agent got to see. What stands in its place is measurement: every surfaced line records what it cost the window, so context taken can be read against relevance afterwards instead of a constant ruling on it in advance. The one remaining reason a line is not capsule text is an article that has no capsule, which surfaces as a pointer naming the address and telling the agent to read it.
|
|
208
216
|
|
|
209
|
-
Reading an article through `
|
|
217
|
+
Reading an article through `canon` withdraws the line staged for it before the message goes out, so pull preempts push. Reading the asset file itself does not, because reading a file is not reading what is known about it, and the capsule may hold exactly the constraint the file does not contain. A read-only session exits quietly. After a successful write, edit, patch, or recognized mutating shell call names a governed asset, settling draws one reminder for its article if the article was not updated, once per batch and re-armed by the next modifying call. Unknown tools still surface knowledge when they name a path, but do not invent an update obligation without positive mutation evidence.
|
|
210
218
|
|
|
211
219
|
Finding a path in a tool call is best effort. Only the input of a tool call is scanned. Results are never scanned, and neither is the model's prose. Inputs are scanned for whole short strings and path-shaped tokens that exist on disk or whose parent directory does, so a file about to be created still surfaces its governing ancestor, and a path with a space inside a longer string is missed. What that feeds, resolution from a path to a governing article, is deterministic. The two claims stay separate on purpose.
|
|
212
220
|
|
|
@@ -230,7 +238,7 @@ Six keys, and any other throws at registration by name, because everything else
|
|
|
230
238
|
The four behavior keys (`surface`, `resurface`, `retrieval`, `standout`) can also come from `~/.config/pi-canon/settings.json`, which the `/canon-settings` command edits from inside the TUI: booleans and retrieval cycle, the standout cutoff steps along its lattice with left/right and takes an exact value on Enter, and every applied change saves immediately through the same validation registration uses. Explicit options win over the file. `root` and `mounts` are per-project topology and stay code-only; they have no row in the editor and no place in the file.
|
|
231
239
|
|
|
232
240
|
- **`root`** places the store. Absolute is used as given, relative joins the project cwd. Default `<project>/.canon`.
|
|
233
|
-
- **`surface: false`** silences the per-turn flush and the settle reminder. The `
|
|
241
|
+
- **`surface: false`** silences the per-turn flush and the settle reminder. The `canon` tool and `/pi-canon` stay registered and working.
|
|
234
242
|
- **`resurface: false`** returns an article to surfacing at most once per session however long ago it left the window. The default is `true`: an article with a presence mark counts as seen only while that mark remains in the context the provider receives, so one folded or compacted away surfaces again the next time its asset is touched. Text shorter than 24 normalized characters has no safe mark and conservatively retains the once-per-session behavior. A fresh touch is what brings a marked article back, so nothing re-surfaces on its own.
|
|
235
243
|
- **`retrieval`** ranks the retrieval corpus against what the agent is doing: every off-spine article, plus any article declared `scope: rule` so a rule stays reachable if an asset later appears at its address. Ordinary asset-scoped articles stay out because the address spine already reaches them. The default is `"none"`, which ranks and surfaces nothing by relevance: the spine alone, exactly as 1.0. `"lexical"` is BM25 over the standard library, no dependency and no model. Anything that needs a model is supplied here as `{ name, score, index? }`, so this package never carries one and never decides which you run. With a retriever configured the tool's filing rule changes with it, because the advice costs knowledge in either direction. On the default it says knowledge filed off the asset path never surfaces, which is true and is why you should not file it there. With a retriever it says the opposite: a constraint governing many assets and owning none belongs at its own address naming the rule, because the only parent unrelated packages share is the root and a root article surfaces on every touch of anything.
|
|
236
244
|
|
|
@@ -251,7 +259,7 @@ An immutable journal, an addressing spine, and recall that arrives unasked could
|
|
|
251
259
|
|
|
252
260
|
Held by the runtime:
|
|
253
261
|
|
|
254
|
-
- A journal entry is created with the exclusive-create flag, so
|
|
262
|
+
- A journal entry is created with the exclusive-create flag, so canon never rewrites or deletes one, and a name collision increments a suffix rather than losing an entry. The files stay ordinary Markdown, so any other tool can still rewrite or delete one: append-only is a property of the tool, not of the filesystem.
|
|
255
263
|
- Once a path is in hand it resolves to exactly one article, walking to the nearest ancestor that has one, or to nothing at all.
|
|
256
264
|
- An article surfaces whole, with no character count able to truncate it or hold it back.
|
|
257
265
|
- An article with a presence mark surfaces at most once while that mark remains in the context the provider receives. Presence is read from that projection rather than remembered, so folding or compaction returns a marked article to surfacing; an untestably short delivery or a harness that reports no projection degrades to at most once per session.
|
|
@@ -295,13 +303,13 @@ trap cells to the floor's 8, and answered recall audits at about a third of the
|
|
|
295
303
|
floor's median token cost. Recall accuracy itself was a wash across arms, and a
|
|
296
304
|
static doctrine file was cheaper on both metered measures while passing three
|
|
297
305
|
fewer trap cells. Full tables, the arms, and the limitations are in
|
|
298
|
-
[RESULTS.md](https://github.com/shaneconner/canon-bench/blob/main/RESULTS.md).
|
|
306
|
+
[RESULTS.md](https://github.com/shaneconner/canon-bench/blob/main/studies/pi-canon/RESULTS.md).
|
|
299
307
|
|
|
300
308
|
That study's forensic pass is what set the current research direction: of
|
|
301
309
|
fourteen recall misses, thirteen first went wrong at the write desk (never
|
|
302
310
|
captured, or captured and later overwritten) and none at retrieval. The
|
|
303
311
|
write-side programme that followed is in
|
|
304
|
-
[write-desk/](https://github.com/shaneconner/canon-bench/tree/main/write-desk),
|
|
312
|
+
[write-desk/](https://github.com/shaneconner/canon-bench/tree/main/studies/pi-canon/write-desk),
|
|
305
313
|
and it is where the growth line documented above comes from: two arms over
|
|
306
314
|
byte-identical eight-session histories, where the arm whose tool names article
|
|
307
315
|
growth ended with fewer superseded values standing in all three captures. The
|
|
@@ -317,6 +325,7 @@ deposit.
|
|
|
317
325
|
- **Mutable Canonical Memory over an Immutable Journal, with Recall by Surfacing**, [doi:10.5281/zenodo.21890647](https://doi.org/10.5281/zenodo.21890647). The first campaign, and the one that asks whether the design holds up at all: one governing article per asset, an append-only journal beneath it, and recall that arrives on a touch, measured against a no-extension floor that received the prior transcripts and is on record reading them.
|
|
318
326
|
- **Pricing Recall in Long-Term Memory for AI Agents**, [doi:10.5281/zenodo.21960350](https://doi.org/10.5281/zenodo.21960350). Six studies on what recall costs and which parts of it earn their keep. It priced the orientation line and the tool schema (both negative, both deleted), set the `standout` cutoff at a measured operating point, and found the store size past which recall that waits to be asked stops working.
|
|
319
327
|
- **The Write Desk**, [doi:10.5281/zenodo.22057257](https://doi.org/10.5281/zenodo.22057257). The first two papers measured recall and took for granted that what the store holds is true. This one tests that and finds it does not hold: writers repeatedly left superseded values in records whose contract is to state what is true now. A condition where the tool speaks at the write boundary ended lower on that endpoint in 20 of 24 capture-lineage comparisons, tied in 2 and higher in 2, but the size did not survive a counterbalanced repeat and is withdrawn rather than qualified. It also freezes the retrieval benchmark that had been reading its corpus live, and reports the cost of two defects found in that freezing by review.
|
|
328
|
+
- **A Durable Fit**, [doi:10.5281/zenodo.22087390](https://doi.org/10.5281/zenodo.22087390). The lifecycle question the line had not yet asked: once a result has entered the agent's live context, how long must it stay there, and who should decide? Six preregistered studies ran the registered ladder of context-withdrawal designs to its end. A transient guidance cue could leave after its one consuming reply; task evidence under an exact-output contract could not be evicted below the task's working set by the fixed window tested; and when the model decided, it kept all 139 consulted results under a disclosed ephemeral default. The shipped durable-by-default design stands validated for this use case, and the line closes with no planned next step.
|
|
320
329
|
|
|
321
330
|
## More
|
|
322
331
|
|
package/extensions/canon.ts
CHANGED
|
@@ -141,7 +141,7 @@ export function registerPiCanon(pi: any, options: CanonOptions = {}): void {
|
|
|
141
141
|
/* Touches stage; turns flush. One steered message per turn rides the provider
|
|
142
142
|
round trip that was happening anyway. */
|
|
143
143
|
pi.on("tool_call", (event: any, ctx: any) => {
|
|
144
|
-
if (!surface || event?.toolName === "
|
|
144
|
+
if (!surface || event?.toolName === "canon") return;
|
|
145
145
|
const { surfacer } = ready(ctx);
|
|
146
146
|
const assets = surfacer.pathsIn(event?.input);
|
|
147
147
|
surfacer.collect(assets);
|
|
@@ -642,7 +642,7 @@ export class Surfacer {
|
|
|
642
642
|
trace("flushed", { lines: lines.length, chars: this.stats.chars });
|
|
643
643
|
return (
|
|
644
644
|
`[pi-canon] Governing article${plural} for what this turn touches. Read the full article with ` +
|
|
645
|
-
`
|
|
645
|
+
`canon before depending on details; update it after real changes.\n${lines.join("\n")}`
|
|
646
646
|
);
|
|
647
647
|
}
|
|
648
648
|
|
|
@@ -674,7 +674,7 @@ export class Surfacer {
|
|
|
674
674
|
trace("settle-nudge", { paths: stale });
|
|
675
675
|
return (
|
|
676
676
|
`[pi-canon] Touched but not updated: ${stale.join(", ")}. If this work changed what is true, ` +
|
|
677
|
-
`update the article with
|
|
677
|
+
`update the article with canon; if nothing durable changed, leave it.`
|
|
678
678
|
);
|
|
679
679
|
}
|
|
680
680
|
}
|
package/extensions/lib/tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* The
|
|
1
|
+
/* The canon tool: one tool, five verbs. Read and update over create; the journal
|
|
2
2
|
for events; map to orient; search when the agent asks. */
|
|
3
3
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -107,8 +107,8 @@ function settle(mount: Mount, raw: string, cwd: string): string {
|
|
|
107
107
|
|
|
108
108
|
export function buildCanonTool(ready: (ctx: unknown) => CanonRuntime, retrieval = "none") {
|
|
109
109
|
return {
|
|
110
|
-
name: "
|
|
111
|
-
label: "
|
|
110
|
+
name: "canon",
|
|
111
|
+
label: "canon",
|
|
112
112
|
description: canonToolDescription(retrieval),
|
|
113
113
|
parameters: CANON_TOOL_PARAMETERS,
|
|
114
114
|
async execute(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-canon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Canonical project memory for the Pi coding agent: one article per asset at a knowable address, an append-only journal beneath it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"node": ">=22.18"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@earendil-works/pi-coding-agent": ">=0.83.0 <
|
|
24
|
+
"@earendil-works/pi-coding-agent": ">=0.83.0 <2",
|
|
25
|
+
"@earendil-works/pi-tui": "*"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"test": "node tests/verify.mjs"
|
|
@@ -43,14 +44,11 @@
|
|
|
43
44
|
"license": "MIT",
|
|
44
45
|
"repository": {
|
|
45
46
|
"type": "git",
|
|
46
|
-
"url": "git+https://github.com/shaneconner/
|
|
47
|
+
"url": "git+https://github.com/shaneconner/canon.git"
|
|
47
48
|
},
|
|
48
|
-
"bugs": "https://github.com/shaneconner/
|
|
49
|
-
"homepage": "https://github.com/shaneconner/
|
|
49
|
+
"bugs": "https://github.com/shaneconner/canon/issues",
|
|
50
|
+
"homepage": "https://github.com/shaneconner/canon#readme",
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"jiti": "^2.7.0"
|
|
52
|
-
},
|
|
53
|
-
"dependencies": {
|
|
54
|
-
"@earendil-works/pi-tui": "^0.84.2"
|
|
55
53
|
}
|
|
56
54
|
}
|