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 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 the per-cell artifact trail: [doi:10.5281/zenodo.21890647](https://doi.org/10.5281/zenodo.21890647).
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`, 142 of them at this release.
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.
@@ -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: { body: string; slug?: string; subject?: string[] }): string {
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) {
@@ -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
- "Canonical project memory. Every asset has at most one governing article at its own address " +
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 = run(ready(ctx), params);
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 run(runtime: CanonRuntime, params: Record<string, unknown>): string {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-canon",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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": {