pi-canon 0.2.3 → 0.2.4
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 +30 -2
- package/extensions/lib/store.ts +8 -1
- package/extensions/lib/tool.ts +56 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,12 +8,38 @@ Canonical project memory for the [Pi coding agent](https://pi.dev). Every asset
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
### Pi
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
pi install npm:pi-canon
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
Or clone this repo into `~/.pi/agent/extensions/`. Node 22.18 or later, Pi 0.83 or later on the 0.x line. Nothing to configure: the store is created on first write at `<project>/.canon`. The package imports `node:fs` and `node:path` and nothing else, makes no network calls, runs no git, and loads under plain node with no build step.
|
|
16
18
|
|
|
19
|
+
### Codex
|
|
20
|
+
|
|
21
|
+
The repository is a Codex marketplace. Add it once, then install the plugin at user scope:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
codex plugin marketplace add shaneconner/pi-canon
|
|
25
|
+
codex plugin add pi-canon@pi-canon
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a local checkout under development, replace `shaneconner/pi-canon` with its absolute path. Start a new Codex thread after installing or updating it.
|
|
29
|
+
|
|
30
|
+
### Claude Code
|
|
31
|
+
|
|
32
|
+
The same repository is also a Claude Code marketplace:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
claude plugin marketplace add shaneconner/pi-canon --scope user
|
|
36
|
+
claude plugin install pi-canon@pi-canon --scope user
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Again, an absolute checkout path works for local development. Start a new Claude Code session after installing or updating it.
|
|
40
|
+
|
|
41
|
+
Both plugins launch the same dependency-free MCP server and expose the same `pi_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.
|
|
42
|
+
|
|
17
43
|
## The first article
|
|
18
44
|
|
|
19
45
|
A session is not greeted: through 0.2.0 every session opened with an orientation line. A 2x2 experiment with an inert implementation found significant negative main effects for both that line and the tool schema, but their 19/32 versus 21/32 comparison did not resolve which component cost more. Study 3 detected no benefit loss when the line was removed, so 0.2.1 deleted it. The tool description carries the doctrine instead. The first article takes one tool call:
|
|
@@ -171,6 +197,7 @@ Held by the runtime:
|
|
|
171
197
|
- 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.
|
|
172
198
|
- An article surfaces whole, with no character count able to truncate it or hold it back.
|
|
173
199
|
- 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.
|
|
200
|
+
- The Codex and Claude Code hooks cannot inspect that projection. They use an explicit compaction cycle instead: session start, context clear, and compaction discard prior touch state; resume does not. None of those events surfaces an article. A later asset touch does. Codex delivers after each tool result; Claude Code combines the current parallel batch into one packet before the next model request.
|
|
174
201
|
- Reading an article through the tool withdraws its staged capsule before the message goes out.
|
|
175
202
|
|
|
176
203
|
Asked of the agent, and checked by nothing:
|
|
@@ -240,10 +267,11 @@ That is development evidence over two arms of one run and it carries no confirma
|
|
|
240
267
|
|
|
241
268
|
## More
|
|
242
269
|
|
|
243
|
-
- The paper, with
|
|
270
|
+
- The 0.2 paper, with its six-study per-cell artifact trail: [doi:10.5281/zenodo.21960350](https://doi.org/10.5281/zenodo.21960350).
|
|
271
|
+
- The first paper, with its end-to-end per-cell artifact trail: [doi:10.5281/zenodo.21890647](https://doi.org/10.5281/zenodo.21890647).
|
|
244
272
|
- The benchmark, drivers, frozen protocol, and the verifier that recomputes the paper's quantitative claims from the artifacts: [canon-bench](https://github.com/shaneconner/canon-bench).
|
|
245
273
|
- Interactive versions of every figure and the full measurement story: [shaneconner.com/projects/pi-canon](https://shaneconner.com/projects/pi-canon/).
|
|
246
274
|
- The narrative version: [My agents' wiki was written faster than it was read](https://medium.com/@shane.conner/my-agents-wiki-was-written-faster-than-it-was-read-and-what-was-read-sold-me-back-debt-i-had-a8085319c68b).
|
|
247
275
|
- [pi-fold](https://github.com/shaneconner/pi-fold), a separate optional package serving the working tier. pi-canon ships the two persistent tiers of the same four-tier stack: the journal is the episodic tier, the canon the semantic tier. The two compose, neither requires the other, and neither knows what the other has spent.
|
|
248
276
|
|
|
249
|
-
MIT. In a clone of this repo, `node tests/verify.mjs` runs the gate suite: every invariant prints by name and the run must end `all N gates green`,
|
|
277
|
+
MIT. In a clone of this repo, `node tests/verify.mjs` runs the gate suite: every invariant prints by name and the run must end `all N gates green`, 149 of them at this release.
|
package/extensions/lib/store.ts
CHANGED
|
@@ -263,7 +263,12 @@ export class CanonStore {
|
|
|
263
263
|
/* Journal entries are immutable: a fresh dated file per entry, wx so nothing is
|
|
264
264
|
ever overwritten. EEXIST is the retry signal, so concurrent writers each land
|
|
265
265
|
on their own file instead of one losing its entry. */
|
|
266
|
-
journal(entry: {
|
|
266
|
+
journal(entry: {
|
|
267
|
+
body: string;
|
|
268
|
+
slug?: string;
|
|
269
|
+
subject?: string[];
|
|
270
|
+
provenance?: { harness: string; sessionId?: string };
|
|
271
|
+
}): string {
|
|
267
272
|
const slug =
|
|
268
273
|
(entry.slug ?? "entry").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "entry";
|
|
269
274
|
mkdirSync(this.journalDir, { recursive: true });
|
|
@@ -277,6 +282,8 @@ export class CanonStore {
|
|
|
277
282
|
const front = [
|
|
278
283
|
entry.subject?.length ? `subject: [${entry.subject.map(scalar).join(", ")}]` : "",
|
|
279
284
|
`logged: ${stamp}`,
|
|
285
|
+
entry.provenance?.harness ? `harness: ${scalar(entry.provenance.harness)}` : "",
|
|
286
|
+
entry.provenance?.sessionId ? `session: ${scalar(entry.provenance.sessionId)}` : "",
|
|
280
287
|
].filter(Boolean).join("\n");
|
|
281
288
|
const text = `---\n${front}\n---\n${entry.body.trimEnd()}\n`;
|
|
282
289
|
for (let n = 1; ; n += 1) {
|
package/extensions/lib/tool.ts
CHANGED
|
@@ -14,6 +14,57 @@ export interface CanonRuntime {
|
|
|
14
14
|
cwd: string;
|
|
15
15
|
mounts: Mount[];
|
|
16
16
|
retrieval: string;
|
|
17
|
+
provenance?: { harness: string; sessionId?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CANON_TOOL_PARAMETERS = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
action: { type: "string", enum: ["read", "write", "journal", "map", "search"] },
|
|
24
|
+
path: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Article address, e.g. src/core/config. Required for read and write; optional filter for map.",
|
|
27
|
+
},
|
|
28
|
+
body: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description:
|
|
31
|
+
"write: the full article body; specifics beat summaries (who consumes what, exact " +
|
|
32
|
+
"limits, what breaks). journal: the event text, source details intact.",
|
|
33
|
+
},
|
|
34
|
+
capsule: { type: "string", description: "write: one dense line injected when the asset is touched." },
|
|
35
|
+
query: { type: "string", description: "search: words to look for, across articles and the journal." },
|
|
36
|
+
scope: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["rule", "asset"],
|
|
39
|
+
description:
|
|
40
|
+
"write: 'rule' when this article names a cross-cutting rule instead of governing an " +
|
|
41
|
+
"asset, so it is a rule on purpose rather than an article whose asset went missing; " +
|
|
42
|
+
"'asset' to take that back, when the article governs an asset after all.",
|
|
43
|
+
},
|
|
44
|
+
subject: {
|
|
45
|
+
type: "array",
|
|
46
|
+
items: { type: "string" },
|
|
47
|
+
description: "journal: article addresses this event concerns.",
|
|
48
|
+
},
|
|
49
|
+
slug: { type: "string", description: "journal: short name for the entry file." },
|
|
50
|
+
},
|
|
51
|
+
required: ["action"],
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
export function canonToolDescription(retrieval = "none"): string {
|
|
55
|
+
return (
|
|
56
|
+
"Canonical project memory. Every asset has at most one governing article at its own address " +
|
|
57
|
+
"(src/core/config, lake/prices). read the governing article before working on an asset; " +
|
|
58
|
+
"write it after real changes. journal appends an immutable event entry: record the source " +
|
|
59
|
+
"as it happened, names and exact numbers included, because articles distill and only the " +
|
|
60
|
+
"journal keeps the original, so distil the prose but carry exact values through verbatim: " +
|
|
61
|
+
"ids, keys, names, counts, limits and durations, every member of a named set and not " +
|
|
62
|
+
"just the one you are working on. A rule without its values is worth nothing to the " +
|
|
63
|
+
"session that needs it. map lists articles with their capsules. " +
|
|
64
|
+
"Creation is rare: prefer updating the article that already governs. " +
|
|
65
|
+
"File a constraint at the asset it governs, or the shared parent when it spans assets, not " +
|
|
66
|
+
"the asset you happened to edit. " + filingTail(retrieval)
|
|
67
|
+
);
|
|
17
68
|
}
|
|
18
69
|
|
|
19
70
|
/* A path routes to the mount it names (lake:prices), the mount whose directory
|
|
@@ -50,51 +101,8 @@ export function buildCanonTool(ready: (ctx: unknown) => CanonRuntime, retrieval
|
|
|
50
101
|
return {
|
|
51
102
|
name: "pi_canon",
|
|
52
103
|
label: "pi-canon",
|
|
53
|
-
description:
|
|
54
|
-
|
|
55
|
-
"(src/core/config, lake/prices). read the governing article before working on an asset; " +
|
|
56
|
-
"write it after real changes. journal appends an immutable event entry: record the source " +
|
|
57
|
-
"as it happened, names and exact numbers included, because articles distill and only the " +
|
|
58
|
-
"journal keeps the original, so distil the prose but carry exact values through verbatim: " +
|
|
59
|
-
"ids, keys, names, counts, limits and durations, every member of a named set and not " +
|
|
60
|
-
"just the one you are working on. A rule without its values is worth nothing to the " +
|
|
61
|
-
"session that needs it. map lists articles with their capsules. " +
|
|
62
|
-
"Creation is rare: prefer updating the article that already governs. " +
|
|
63
|
-
"File a constraint at the asset it governs, or the shared parent when it spans assets, not " +
|
|
64
|
-
"the asset you happened to edit. " + filingTail(retrieval),
|
|
65
|
-
parameters: {
|
|
66
|
-
type: "object",
|
|
67
|
-
properties: {
|
|
68
|
-
action: { type: "string", enum: ["read", "write", "journal", "map", "search"] },
|
|
69
|
-
path: {
|
|
70
|
-
type: "string",
|
|
71
|
-
description: "Article address, e.g. src/core/config. Required for read and write; optional filter for map.",
|
|
72
|
-
},
|
|
73
|
-
body: {
|
|
74
|
-
type: "string",
|
|
75
|
-
description:
|
|
76
|
-
"write: the full article body; specifics beat summaries (who consumes what, exact " +
|
|
77
|
-
"limits, what breaks). journal: the event text, source details intact.",
|
|
78
|
-
},
|
|
79
|
-
capsule: { type: "string", description: "write: one dense line injected when the asset is touched." },
|
|
80
|
-
query: { type: "string", description: "search: words to look for, across articles and the journal." },
|
|
81
|
-
scope: {
|
|
82
|
-
type: "string",
|
|
83
|
-
enum: ["rule", "asset"],
|
|
84
|
-
description:
|
|
85
|
-
"write: 'rule' when this article names a cross-cutting rule instead of governing an " +
|
|
86
|
-
"asset, so it is a rule on purpose rather than an article whose asset went missing; " +
|
|
87
|
-
"'asset' to take that back, when the article governs an asset after all.",
|
|
88
|
-
},
|
|
89
|
-
subject: {
|
|
90
|
-
type: "array",
|
|
91
|
-
items: { type: "string" },
|
|
92
|
-
description: "journal: article addresses this event concerns.",
|
|
93
|
-
},
|
|
94
|
-
slug: { type: "string", description: "journal: short name for the entry file." },
|
|
95
|
-
},
|
|
96
|
-
required: ["action"],
|
|
97
|
-
},
|
|
104
|
+
description: canonToolDescription(retrieval),
|
|
105
|
+
parameters: CANON_TOOL_PARAMETERS,
|
|
98
106
|
async execute(
|
|
99
107
|
_toolCallId: string,
|
|
100
108
|
params: Record<string, unknown>,
|
|
@@ -102,7 +110,7 @@ export function buildCanonTool(ready: (ctx: unknown) => CanonRuntime, retrieval
|
|
|
102
110
|
_onUpdate: unknown,
|
|
103
111
|
ctx: unknown,
|
|
104
112
|
) {
|
|
105
|
-
const text =
|
|
113
|
+
const text = runCanon(ready(ctx), params);
|
|
106
114
|
return { content: [{ type: "text", text }], details: {} };
|
|
107
115
|
},
|
|
108
116
|
};
|
|
@@ -211,7 +219,7 @@ function excerpt(body: string): string {
|
|
|
211
219
|
return flat.length > 160 ? `${flat.slice(0, 157)}...` : flat;
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
function
|
|
222
|
+
export function runCanon(runtime: CanonRuntime, params: Record<string, unknown>): string {
|
|
215
223
|
const { surfacer } = runtime;
|
|
216
224
|
const action = String(params.action ?? "");
|
|
217
225
|
const { mount, path } = typeof params.path === "string" ? route(runtime, params.path) : { mount: runtime.mounts[0], path: "" };
|
|
@@ -293,6 +301,7 @@ function run(runtime: CanonRuntime, params: Record<string, unknown>): string {
|
|
|
293
301
|
body,
|
|
294
302
|
subject,
|
|
295
303
|
slug: typeof params.slug === "string" ? params.slug : undefined,
|
|
304
|
+
provenance: runtime.provenance,
|
|
296
305
|
});
|
|
297
306
|
/* The article was written before this entry (agents write then journal), so this
|
|
298
307
|
is the first moment both exist. Report what the source kept and the article did
|
package/package.json
CHANGED