@lebronj/pi-suite 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/extensions/pet.ts +1033 -0
  4. package/extensions/prompt-url-widget.ts +158 -0
  5. package/extensions/redraws.ts +24 -0
  6. package/extensions/snake.ts +343 -0
  7. package/extensions/tps.ts +47 -0
  8. package/package.json +69 -0
  9. package/prompts/cl.md +54 -0
  10. package/prompts/is.md +25 -0
  11. package/prompts/pr.md +37 -0
  12. package/prompts/wr.md +35 -0
  13. package/scripts/bootstrap.sh +95 -0
  14. package/skills/add-llm-provider.md +57 -0
  15. package/skills/image-to-editable-ppt-slide/SKILL.md +113 -0
  16. package/skills/image-to-editable-ppt-slide/scripts/generate_spec_template.py +91 -0
  17. package/skills/image-to-editable-ppt-slide/scripts/pptx_rebuilder.py +181 -0
  18. package/skills/leetcode-array/SKILL.md +40 -0
  19. package/skills/leetcode-array/problems/best_time_to_buy_and_sell_stock.py +19 -0
  20. package/skills/leetcode-array/problems/product_of_array_except_self.py +22 -0
  21. package/skills/leetcode-array/problems/two_sum.py +19 -0
  22. package/skills/pi-skill/SKILL.md +154 -0
  23. package/skills/weather.md +49 -0
  24. package/vendor/pi-memory/LICENSE +21 -0
  25. package/vendor/pi-memory/README.md +223 -0
  26. package/vendor/pi-memory/index.ts +2367 -0
  27. package/vendor/pi-memory/package.json +68 -0
  28. package/vendor/pi-memory/scripts/postinstall.cjs +44 -0
  29. package/vendor/pi-memory/src/cli.ts +79 -0
  30. package/vendor/pi-memory/src/curator-core/audit.ts +45 -0
  31. package/vendor/pi-memory/src/curator-core/curate.ts +90 -0
  32. package/vendor/pi-memory/src/curator-core/metadata.ts +55 -0
  33. package/vendor/pi-memory/src/curator-core/patch.ts +24 -0
  34. package/vendor/pi-memory/src/curator-core/policy.ts +77 -0
  35. package/vendor/pi-memory/src/curator-store/file-store.ts +51 -0
  36. package/vendor/pi-memory/src/curator-store/types.ts +21 -0
  37. package/vendor/pi-memory/src/index.ts +35 -0
  38. package/vendor/pi-memory/src/learning/candidates.ts +205 -0
  39. package/vendor/pi-memory/src/learning/memory.ts +144 -0
  40. package/vendor/pi-memory/src/learning/skills.ts +200 -0
  41. package/vendor/pi-memory/src/service-controller.ts +248 -0
  42. package/vendor/pi-memory/test/curate.test.ts +68 -0
  43. package/vendor/pi-memory/test/learning-candidates.test.ts +107 -0
  44. package/vendor/pi-memory/test/memory-promotions.test.ts +44 -0
  45. package/vendor/pi-memory/test/metadata.test.ts +17 -0
  46. package/vendor/pi-memory/test/skill-drafts.test.ts +57 -0
  47. package/vendor/pi-memory/test/transition-handoff.test.ts +86 -0
@@ -0,0 +1,248 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ export type CuratorServiceBackend = "systemd-user" | "cron" | "none";
7
+
8
+ export type CuratorServiceState = {
9
+ enabled: boolean;
10
+ backend: CuratorServiceBackend;
11
+ schedule: string;
12
+ serviceName: string;
13
+ memoryDir: string;
14
+ cliPath: string;
15
+ installedAt?: string;
16
+ disabledAt?: string;
17
+ lastError?: string;
18
+ };
19
+
20
+ export type CuratorServiceResult = {
21
+ ok: boolean;
22
+ backend: CuratorServiceBackend;
23
+ message: string;
24
+ state: CuratorServiceState;
25
+ };
26
+
27
+ const SERVICE_NAME = "jhp-pi-memory-curator";
28
+ const DEFAULT_SCHEDULE = "03:00";
29
+ const CRON_MARKER = "# jhp-pi-memory-curator";
30
+
31
+ function resolveHome(): string {
32
+ return process.env.HOME ?? process.env.USERPROFILE ?? homedir();
33
+ }
34
+
35
+ export function resolveMemoryDir(env: NodeJS.ProcessEnv = process.env): string {
36
+ if (env.PI_MEMORY_DIR) return env.PI_MEMORY_DIR;
37
+ return join(resolveHome(), ".pi", "agent", "memory");
38
+ }
39
+
40
+ function statePath(memoryDir: string): string {
41
+ return join(memoryDir, ".curator-service.json");
42
+ }
43
+
44
+ function systemdUserDir(): string {
45
+ return join(resolveHome(), ".config", "systemd", "user");
46
+ }
47
+
48
+ function servicePath(): string {
49
+ return join(systemdUserDir(), `${SERVICE_NAME}.service`);
50
+ }
51
+
52
+ function timerPath(): string {
53
+ return join(systemdUserDir(), `${SERVICE_NAME}.timer`);
54
+ }
55
+
56
+ function defaultState(memoryDir: string, cliPath: string): CuratorServiceState {
57
+ return {
58
+ enabled: false,
59
+ backend: "none",
60
+ schedule: DEFAULT_SCHEDULE,
61
+ serviceName: SERVICE_NAME,
62
+ memoryDir,
63
+ cliPath,
64
+ };
65
+ }
66
+
67
+ function readState(memoryDir: string, cliPath: string): CuratorServiceState {
68
+ const path = statePath(memoryDir);
69
+ if (!existsSync(path)) return defaultState(memoryDir, cliPath);
70
+ try {
71
+ return { ...defaultState(memoryDir, cliPath), ...(JSON.parse(readFileSync(path, "utf-8")) as Partial<CuratorServiceState>) };
72
+ } catch {
73
+ return defaultState(memoryDir, cliPath);
74
+ }
75
+ }
76
+
77
+ function writeState(state: CuratorServiceState): void {
78
+ mkdirSync(dirname(statePath(state.memoryDir)), { recursive: true });
79
+ writeFileSync(statePath(state.memoryDir), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
80
+ }
81
+
82
+ function hasCommand(command: string): boolean {
83
+ return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
84
+ }
85
+
86
+ function canUseSystemdUser(): boolean {
87
+ if (!hasCommand("systemctl")) return false;
88
+ const result = spawnSync("systemctl", ["--user", "show-environment"], { stdio: "ignore" });
89
+ return result.status === 0;
90
+ }
91
+
92
+ function shellQuote(value: string): string {
93
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
94
+ }
95
+
96
+ function parseSchedule(schedule: string): { hour: string; minute: string } {
97
+ const match = /^(\d{1,2}):(\d{2})$/.exec(schedule);
98
+ if (!match) throw new Error(`Invalid schedule '${schedule}'. Expected HH:MM.`);
99
+ const hour = Number(match[1]);
100
+ const minute = Number(match[2]);
101
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) throw new Error(`Invalid schedule '${schedule}'. Expected HH:MM.`);
102
+ return { hour: String(hour), minute: String(minute) };
103
+ }
104
+
105
+ function systemdCalendar(schedule: string): string {
106
+ const { hour, minute } = parseSchedule(schedule);
107
+ return `*-*-* ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
108
+ }
109
+
110
+ function writeSystemdUnits(memoryDir: string, cliPath: string, schedule: string): void {
111
+ mkdirSync(systemdUserDir(), { recursive: true });
112
+ const execStart = `${process.execPath} ${cliPath} run-once --memory-dir ${memoryDir} --reason systemd-timer`;
113
+ writeFileSync(
114
+ servicePath(),
115
+ [
116
+ "[Unit]",
117
+ "Description=JHP pi memory curator",
118
+ "",
119
+ "[Service]",
120
+ "Type=oneshot",
121
+ `ExecStart=${execStart}`,
122
+ "",
123
+ ].join("\n"),
124
+ "utf-8",
125
+ );
126
+ writeFileSync(
127
+ timerPath(),
128
+ [
129
+ "[Unit]",
130
+ "Description=Run JHP pi memory curator daily",
131
+ "",
132
+ "[Timer]",
133
+ `OnCalendar=${systemdCalendar(schedule)}`,
134
+ "Persistent=true",
135
+ "Unit=jhp-pi-memory-curator.service",
136
+ "",
137
+ "[Install]",
138
+ "WantedBy=timers.target",
139
+ "",
140
+ ].join("\n"),
141
+ "utf-8",
142
+ );
143
+ }
144
+
145
+ function enableSystemd(memoryDir: string, cliPath: string, schedule: string): void {
146
+ writeSystemdUnits(memoryDir, cliPath, schedule);
147
+ execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
148
+ execFileSync("systemctl", ["--user", "enable", "--now", `${SERVICE_NAME}.timer`], { stdio: "ignore" });
149
+ }
150
+
151
+ function disableSystemd(): void {
152
+ if (!hasCommand("systemctl")) return;
153
+ spawnSync("systemctl", ["--user", "disable", "--now", `${SERVICE_NAME}.timer`], { stdio: "ignore" });
154
+ for (const path of [servicePath(), timerPath()]) {
155
+ try {
156
+ if (existsSync(path)) unlinkSync(path);
157
+ } catch {
158
+ // best effort cleanup
159
+ }
160
+ }
161
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
162
+ }
163
+
164
+ function currentCrontab(): string {
165
+ const result = spawnSync("crontab", ["-l"], { encoding: "utf-8" });
166
+ if (result.status !== 0) return "";
167
+ return result.stdout;
168
+ }
169
+
170
+ function installCrontab(content: string): void {
171
+ const result = spawnSync("crontab", ["-"], { input: content, encoding: "utf-8" });
172
+ if (result.status !== 0) throw new Error(result.stderr || "failed to install crontab");
173
+ }
174
+
175
+ function removeCronLine(): void {
176
+ if (!hasCommand("crontab")) return;
177
+ const existing = currentCrontab();
178
+ const next = existing
179
+ .split(/\r?\n/)
180
+ .filter((line) => !line.includes(CRON_MARKER))
181
+ .join("\n")
182
+ .trim();
183
+ installCrontab(next ? `${next}\n` : "");
184
+ }
185
+
186
+ function enableCron(memoryDir: string, cliPath: string, schedule: string): void {
187
+ if (!hasCommand("crontab")) throw new Error("Neither systemd user timers nor crontab are available.");
188
+ const { hour, minute } = parseSchedule(schedule);
189
+ removeCronLine();
190
+ const command = `${shellQuote(process.execPath)} ${shellQuote(cliPath)} run-once --memory-dir ${shellQuote(memoryDir)} --reason cron ${CRON_MARKER}`;
191
+ const existing = currentCrontab().trim();
192
+ const next = `${existing ? `${existing}\n` : ""}${minute} ${hour} * * * ${command}\n`;
193
+ installCrontab(next);
194
+ }
195
+
196
+ export function enableCuratorService(options: { memoryDir?: string; cliPath: string; schedule?: string }): CuratorServiceResult {
197
+ const memoryDir = options.memoryDir || resolveMemoryDir();
198
+ const schedule = options.schedule || DEFAULT_SCHEDULE;
199
+ const baseState = { ...defaultState(memoryDir, options.cliPath), schedule };
200
+ try {
201
+ if (canUseSystemdUser()) {
202
+ enableSystemd(memoryDir, options.cliPath, schedule);
203
+ const state: CuratorServiceState = { ...baseState, enabled: true, backend: "systemd-user", installedAt: new Date().toISOString() };
204
+ writeState(state);
205
+ return { ok: true, backend: "systemd-user", message: "Enabled systemd user timer for daily 03:00 memory curation.", state };
206
+ }
207
+ enableCron(memoryDir, options.cliPath, schedule);
208
+ const state: CuratorServiceState = { ...baseState, enabled: true, backend: "cron", installedAt: new Date().toISOString() };
209
+ writeState(state);
210
+ return { ok: true, backend: "cron", message: "Enabled cron job for daily memory curation.", state };
211
+ } catch (error) {
212
+ const message = error instanceof Error ? error.message : String(error);
213
+ const state: CuratorServiceState = { ...baseState, enabled: false, backend: "none", lastError: message };
214
+ writeState(state);
215
+ return { ok: false, backend: "none", message, state };
216
+ }
217
+ }
218
+
219
+ export function disableCuratorService(options: { memoryDir?: string; cliPath: string }): CuratorServiceResult {
220
+ const memoryDir = options.memoryDir || resolveMemoryDir();
221
+ const previous = readState(memoryDir, options.cliPath);
222
+ disableSystemd();
223
+ try {
224
+ removeCronLine();
225
+ } catch {
226
+ // best effort cleanup
227
+ }
228
+ const state: CuratorServiceState = { ...previous, enabled: false, backend: "none", disabledAt: new Date().toISOString() };
229
+ writeState(state);
230
+ return { ok: true, backend: previous.backend, message: "Disabled memory curator service.", state };
231
+ }
232
+
233
+ export function getCuratorServiceStatus(options: { memoryDir?: string; cliPath: string }): CuratorServiceResult {
234
+ const memoryDir = options.memoryDir || resolveMemoryDir();
235
+ const state = readState(memoryDir, options.cliPath);
236
+ const parts = [
237
+ `Memory curator service: ${state.enabled ? "enabled" : "disabled"}`,
238
+ `Backend: ${state.backend}`,
239
+ `Schedule: ${state.schedule}`,
240
+ `Memory dir: ${state.memoryDir}`,
241
+ ];
242
+ if (state.lastError) parts.push(`Last error: ${state.lastError}`);
243
+ if (state.backend === "systemd-user" && hasCommand("systemctl")) {
244
+ const active = spawnSync("systemctl", ["--user", "is-active", `${SERVICE_NAME}.timer`], { encoding: "utf-8" });
245
+ parts.push(`systemd timer active: ${active.stdout.trim() || "unknown"}`);
246
+ }
247
+ return { ok: true, backend: state.backend, message: parts.join("\n"), state };
248
+ }
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, readFileSync, statSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { FileMemoryStore, JsonlAuditLog, runMemoryCuratorOnce } from "../src/index.ts";
7
+
8
+ test("curates entries, deduplicates exact duplicates, and writes audit", async () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-curator-"));
10
+ const store = new FileMemoryStore(dir);
11
+ await store.writeEntries("state", [
12
+ "[type:event status:planned date:2026-06-06]\nUser plans to watch NBA.",
13
+ "[type:event status:planned date:2026-06-06]\nUser plans to watch NBA.",
14
+ "Plain note for 2026-06-06.",
15
+ ]);
16
+
17
+ const result = await runMemoryCuratorOnce({
18
+ memoryStore: store,
19
+ auditLog: new JsonlAuditLog(dir),
20
+ now: () => new Date("2026-06-08T03:00:00.000Z"),
21
+ });
22
+
23
+ assert.equal(result.changed, 2);
24
+ assert.deepEqual(await store.readEntries("state"), [
25
+ "[type:event status:past date:2026-06-06]\nUser had planned to watch NBA. Completion status unknown.",
26
+ "Plain note for 2026-06-06.",
27
+ ]);
28
+ assert.equal(existsSync(join(dir, "audit", "curator.jsonl")), true);
29
+ const auditLines = readFileSync(join(dir, "audit", "curator.jsonl"), "utf-8").trim().split("\n");
30
+ assert.equal(auditLines.length, 2);
31
+ });
32
+
33
+ test("dry run returns deterministic patches without writing", async () => {
34
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-curator-"));
35
+ const store = new FileMemoryStore(dir);
36
+ const raw = "[type:event status:planned date:2026-06-06]\nUser plans to watch NBA.";
37
+ await store.writeEntries("state", [raw]);
38
+
39
+ const result = await runMemoryCuratorOnce({
40
+ memoryStore: store,
41
+ now: () => new Date("2026-06-08T03:00:00.000Z"),
42
+ dryRun: true,
43
+ });
44
+
45
+ assert.equal(result.patches.length, 1);
46
+ assert.deepEqual(await store.readEntries("state"), [raw]);
47
+ });
48
+
49
+ test("no-op curator scan does not rewrite memory or review files", async () => {
50
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-curator-"));
51
+ const store = new FileMemoryStore(dir);
52
+ await store.writeEntries("memory", ["[type:fact]\nStable fact."]);
53
+ await store.writeEntries("review", ["[type:review status:candidate id:rev_test kind:preference confidence:medium seen:1 first_seen:2026-06-08 last_seen:2026-06-08]\nSignature: User prefers concise answers."]);
54
+ const memoryPath = join(dir, "MEMORY.md");
55
+ const reviewPath = join(dir, "REVIEW.md");
56
+ const beforeMemory = statSync(memoryPath).mtimeMs;
57
+ const beforeReview = statSync(reviewPath).mtimeMs;
58
+
59
+ const result = await runMemoryCuratorOnce({
60
+ memoryStore: store,
61
+ now: () => new Date("2026-06-08T03:00:00.000Z"),
62
+ });
63
+
64
+ assert.equal(result.patches.length, 0);
65
+ assert.equal(statSync(memoryPath).mtimeMs, beforeMemory);
66
+ assert.equal(statSync(reviewPath).mtimeMs, beforeReview);
67
+ assert.equal(existsSync(join(dir, "audit", "curator.jsonl")), false);
68
+ });
@@ -0,0 +1,107 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import {
7
+ FileMemoryStore,
8
+ createReviewCandidateId,
9
+ parseReviewCandidate,
10
+ renderReviewCandidate,
11
+ upsertReviewCandidate,
12
+ validateReviewCandidateInput,
13
+ } from "../src/index.ts";
14
+
15
+ test("renders and parses review candidate metadata", () => {
16
+ const entry = renderReviewCandidate(
17
+ {
18
+ kind: "bug_fix",
19
+ confidence: "high",
20
+ signature: "npm run check failed on stale metadata order",
21
+ summary: "Keep review metadata stable.",
22
+ targetHints: ["memory", "skill"],
23
+ },
24
+ new Date("2026-06-10T00:00:00.000Z"),
25
+ );
26
+
27
+ const candidate = parseReviewCandidate(entry);
28
+ assert.ok(candidate);
29
+ assert.equal(candidate.id, createReviewCandidateId("bug_fix", "npm run check failed on stale metadata order"));
30
+ assert.equal(candidate.kind, "bug_fix");
31
+ assert.equal(candidate.confidence, "high");
32
+ assert.equal(candidate.seen, 1);
33
+ assert.match(entry, /target_hints:memory,skill/);
34
+ assert.match(entry, /Signature: npm run check failed on stale metadata order/);
35
+ });
36
+
37
+ test("rejects invalid target hints", () => {
38
+ assert.deepEqual(
39
+ validateReviewCandidateInput({
40
+ kind: "skill_candidate",
41
+ confidence: "medium",
42
+ signature: "Reusable flow",
43
+ targetHints: ["workflow" as "memory"],
44
+ }),
45
+ ["invalid target hint 'workflow'"],
46
+ );
47
+ });
48
+
49
+ test("upserts duplicate candidates by normalized signature", async () => {
50
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-learning-"));
51
+ const store = new FileMemoryStore(dir);
52
+
53
+ const first = await upsertReviewCandidate(
54
+ store,
55
+ {
56
+ kind: "bug_fix",
57
+ confidence: "medium",
58
+ signature: "NPM run check failed: stale metadata order.",
59
+ evidence: "first failure",
60
+ },
61
+ new Date("2026-06-10T00:00:00.000Z"),
62
+ );
63
+ const second = await upsertReviewCandidate(
64
+ store,
65
+ {
66
+ kind: "bug_fix",
67
+ confidence: "high",
68
+ signature: "npm run check failed stale metadata order",
69
+ evidence: "second validation",
70
+ },
71
+ new Date("2026-06-11T00:00:00.000Z"),
72
+ );
73
+
74
+ const entries = await store.readEntries("review");
75
+ assert.equal(first.merged, false);
76
+ assert.equal(second.merged, true);
77
+ assert.equal(entries.length, 1);
78
+ const candidate = parseReviewCandidate(entries[0]);
79
+ assert.ok(candidate);
80
+ assert.equal(candidate.id, first.id);
81
+ assert.equal(candidate.seen, 2);
82
+ assert.equal(candidate.confidence, "high");
83
+ assert.equal(candidate.lastSeen, "2026-06-11");
84
+ assert.match(entries[0], /Evidence: first failure/);
85
+ assert.match(entries[0], /Evidence: second validation/);
86
+ });
87
+
88
+ test("does not merge unrelated candidates or malformed review entries", async () => {
89
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-learning-"));
90
+ const store = new FileMemoryStore(dir);
91
+ await store.writeEntries("review", ["[type:review status:candidate]\nSignature: malformed"]);
92
+
93
+ await upsertReviewCandidate(store, {
94
+ kind: "preference",
95
+ confidence: "medium",
96
+ signature: "User prefers concise answers",
97
+ });
98
+ await upsertReviewCandidate(store, {
99
+ kind: "project_fact",
100
+ confidence: "medium",
101
+ signature: "Project stores specs under .pi/docs",
102
+ });
103
+
104
+ const entries = await store.readEntries("review");
105
+ assert.equal(entries.length, 3);
106
+ assert.equal(entries[0], "[type:review status:candidate]\nSignature: malformed");
107
+ });
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import {
7
+ FileMemoryStore,
8
+ applyReviewLifecycle,
9
+ approveMemoryPromotion,
10
+ proposeMemoryPromotions,
11
+ rejectReviewItem,
12
+ upsertReviewCandidate,
13
+ } from "../src/index.ts";
14
+
15
+ test("proposes and approves memory promotion from repeated preference", async () => {
16
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-promotions-"));
17
+ const store = new FileMemoryStore(dir);
18
+ await upsertReviewCandidate(store, { kind: "preference", confidence: "medium", signature: "User prefers direct answers", summary: "User prefers concise direct answers.", targetHints: ["memory"] });
19
+ await upsertReviewCandidate(store, { kind: "preference", confidence: "medium", signature: "User prefers direct answers", summary: "User prefers concise direct answers.", targetHints: ["memory"] });
20
+
21
+ const proposed = await proposeMemoryPromotions(store);
22
+ assert.equal(proposed.created, 1);
23
+ const approved = await approveMemoryPromotion(store, proposed.proposalIds[0]);
24
+ assert.equal(approved.target, "user");
25
+ assert.deepEqual(await store.readEntries("user"), ["[type:preference]\nUser prefers concise direct answers."]);
26
+ assert.match((await store.readEntries("review")).join("\n"), /status:approved/);
27
+ });
28
+
29
+ test("review lifecycle marks stale low-confidence candidates and reject preserves entries", async () => {
30
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-promotions-"));
31
+ const store = new FileMemoryStore(dir);
32
+ await upsertReviewCandidate(store, { kind: "project_fact", confidence: "low", signature: "Old uncertain fact", date: "2026-01-01" });
33
+ const id = (await store.readEntries("review"))[0].match(/id:([^\s]+)/)?.[1];
34
+ assert.ok(id);
35
+
36
+ const lifecycle = await applyReviewLifecycle(store, new Date("2026-05-01T00:00:00.000Z"));
37
+ assert.equal(lifecycle.changed, 1);
38
+ assert.match((await store.readEntries("review")).join("\n"), /status:archived/);
39
+
40
+ await rejectReviewItem(store, id, "rejected");
41
+ const review = await store.readEntries("review");
42
+ assert.equal(review.length, 1);
43
+ assert.match(review[0], /status:rejected/);
44
+ });
@@ -0,0 +1,17 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { parseEntry, parseMetadata, renderEntry } from "../src/index.ts";
4
+
5
+ test("parses metadata", () => {
6
+ assert.deepEqual(parseMetadata("[type:event status:planned date:2026-06-06]"), {
7
+ type: "event",
8
+ status: "planned",
9
+ date: "2026-06-06",
10
+ });
11
+ });
12
+
13
+ test("leaves malformed metadata as plain body", () => {
14
+ const entry = parseEntry("[type:event status:planned\nUser plans to watch NBA.");
15
+ assert.equal(entry.hasMetadata, false);
16
+ assert.equal(renderEntry(entry), "[type:event status:planned\nUser plans to watch NBA.");
17
+ });
@@ -0,0 +1,57 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { FileMemoryStore, approveSkillDraft, listSkillDraftProposals, proposeSkillDrafts, upsertReviewCandidate } from "../src/index.ts";
7
+
8
+ test("proposes skill draft from repeated skill candidate", async () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-skills-"));
10
+ const store = new FileMemoryStore(join(dir, "memory"));
11
+ const draftsDir = join(dir, "skill-drafts");
12
+ for (const evidence of ["first pass", "second pass", "third pass"]) {
13
+ await upsertReviewCandidate(store, {
14
+ kind: "skill_candidate",
15
+ confidence: "medium",
16
+ signature: "fix non erasable TypeScript syntax in pi source",
17
+ summary: "Replace enum and parameter properties with erasable syntax.",
18
+ targetHints: ["skill"],
19
+ evidence,
20
+ });
21
+ }
22
+
23
+ const result = await proposeSkillDrafts(store, { draftsDir });
24
+ assert.equal(result.created, 1);
25
+ assert.equal(result.proposals[0].id.startsWith("skill_"), true);
26
+ assert.match(result.proposals[0].promotesTo, /skill-drafts/);
27
+ const review = await store.readEntries("review");
28
+ assert.equal(review.filter((entry) => entry.includes("kind:skill_promotion")).length, 1);
29
+
30
+ const second = await proposeSkillDrafts(store, { draftsDir });
31
+ assert.equal(second.created, 0);
32
+ });
33
+
34
+ test("approves skill proposal into disabled draft and marks proposal approved", async () => {
35
+ const dir = mkdtempSync(join(tmpdir(), "pi-memory-skills-"));
36
+ const store = new FileMemoryStore(join(dir, "memory"));
37
+ const draftsDir = join(dir, "skill-drafts");
38
+ for (const evidence of ["first pass", "second pass", "third pass"]) {
39
+ await upsertReviewCandidate(store, {
40
+ kind: "skill_candidate",
41
+ confidence: "medium",
42
+ signature: "fix non erasable TypeScript syntax in pi source",
43
+ summary: "Replace enum and parameter properties with erasable syntax.",
44
+ targetHints: ["skill"],
45
+ evidence,
46
+ });
47
+ }
48
+ const proposed = await proposeSkillDrafts(store, { draftsDir });
49
+
50
+ const approved = await approveSkillDraft(store, proposed.proposals[0].id);
51
+ assert.equal(existsSync(approved.path), true);
52
+ assert.match(readFileSync(approved.path, "utf-8"), /description: Use when fix non erasable typescript syntax in pi source\./);
53
+ const proposals = await listSkillDraftProposals(store);
54
+ assert.equal(proposals.length, 1);
55
+ const review = await store.readEntries("review");
56
+ assert.match(review.join("\n"), /status:approved/);
57
+ });
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import {
4
+ buildTransitionHandoff,
5
+ getMemoryLearningMode,
6
+ getMemorySkillDraftsMode,
7
+ parseLearningExtractorResponse,
8
+ shouldSkipExitSummaryForReason,
9
+ shouldWriteTransitionHandoffForReason,
10
+ } from "../index.ts";
11
+
12
+ function message(role: "user" | "assistant", text: string) {
13
+ return {
14
+ type: "message" as const,
15
+ message: {
16
+ role,
17
+ content: [{ type: "text" as const, text }],
18
+ timestamp: 0,
19
+ },
20
+ };
21
+ }
22
+
23
+ function ctx(branch: ReturnType<typeof message>[]) {
24
+ return {
25
+ sessionManager: {
26
+ getBranch: () => branch,
27
+ },
28
+ } as any;
29
+ }
30
+
31
+ test("writes lightweight handoffs only for new and fork transitions", () => {
32
+ assert.equal(shouldSkipExitSummaryForReason("reload"), true);
33
+ assert.equal(shouldSkipExitSummaryForReason("resume"), true);
34
+ assert.equal(shouldSkipExitSummaryForReason("new"), true);
35
+ assert.equal(shouldSkipExitSummaryForReason("fork"), true);
36
+
37
+ assert.equal(shouldWriteTransitionHandoffForReason("reload"), false);
38
+ assert.equal(shouldWriteTransitionHandoffForReason("resume"), false);
39
+ assert.equal(shouldWriteTransitionHandoffForReason("new"), true);
40
+ assert.equal(shouldWriteTransitionHandoffForReason("fork"), true);
41
+ });
42
+
43
+ test("memory learning defaults to review mode", () => {
44
+ assert.equal(getMemoryLearningMode({}), "review");
45
+ assert.equal(getMemoryLearningMode({ PI_MEMORY_LEARNING: "off" }), "off");
46
+ assert.equal(getMemoryLearningMode({ PI_MEMORY_LEARNING: "auto-review" }), "auto-review");
47
+ assert.equal(getMemorySkillDraftsMode({}), "review");
48
+ assert.equal(getMemorySkillDraftsMode({ PI_MEMORY_SKILL_DRAFTS: "off" }), "off");
49
+ });
50
+
51
+ test("learning extractor response accepts only valid review candidates", () => {
52
+ const candidates = parseLearningExtractorResponse(JSON.stringify({
53
+ candidates: [
54
+ { kind: "preference", confidence: "medium", signature: "User prefers concise answers", targetHints: ["memory", "workflow"] },
55
+ { kind: "workflow", confidence: "high", signature: "workflow artifact" },
56
+ { kind: "project_fact", confidence: "low", signature: "low confidence fact" },
57
+ ],
58
+ }));
59
+
60
+ assert.deepEqual(candidates, [{
61
+ kind: "preference",
62
+ confidence: "medium",
63
+ signature: "User prefers concise answers",
64
+ summary: undefined,
65
+ targetHints: ["memory"],
66
+ evidence: undefined,
67
+ source: "session_shutdown",
68
+ }]);
69
+ });
70
+
71
+ test("buildTransitionHandoff captures recent conversation without LLM summary", () => {
72
+ const handoff = buildTransitionHandoff(
73
+ ctx([
74
+ message("user", "Please investigate the memory shutdown behavior."),
75
+ message("assistant", "I found that /reload and /resume should stay silent."),
76
+ ]),
77
+ "new",
78
+ "abcdef12",
79
+ "2026-06-10 02:00:00",
80
+ );
81
+
82
+ assert.ok(handoff);
83
+ assert.match(handoff, /Session Handoff \(auto, transition: \/new\)/);
84
+ assert.match(handoff, /Please investigate the memory shutdown behavior/);
85
+ assert.match(handoff, /reload and \/resume should stay silent/);
86
+ });