@lebronj/pi-suite 0.1.8 → 0.1.9
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/package.json +1 -1
- package/skills/pi-skill/SKILL.md +2 -0
- package/vendor/pi-memory/README.md +2 -1
- package/vendor/pi-memory/index.ts +1 -0
- package/vendor/pi-memory/src/evolution/config.ts +10 -0
- package/vendor/pi-memory/src/evolution/git.ts +3 -0
- package/vendor/pi-memory/src/evolution/index.ts +1 -1
- package/vendor/pi-memory/src/evolution/manifest.ts +20 -5
- package/vendor/pi-memory/src/evolution/snapshot.ts +2 -1
- package/vendor/pi-memory/test/evolution.test.ts +19 -1
package/package.json
CHANGED
package/skills/pi-skill/SKILL.md
CHANGED
|
@@ -85,6 +85,7 @@ Memory versioning:
|
|
|
85
85
|
- Versioning mirror and snapshots live at `~/.pi/agent/evolution` by default.
|
|
86
86
|
- No remote is configured by default; memory evolution stays local per user/machine unless the user adds a personal private remote.
|
|
87
87
|
- Automatic local snapshot + commit is enabled by default; automatic push is disabled unless `PI_EVOLUTION_AUTO_PUSH=1`.
|
|
88
|
+
- Snapshots use a sliding window: keep the latest 100 by default, and delete the oldest snapshot directory and manifest when a new snapshot exceeds the limit.
|
|
88
89
|
- Snapshots run before mutating memory tools, curator runs, learning approve/reject, session summaries/handoffs, compact handoffs, restore, and external curator `run-once`.
|
|
89
90
|
- Slash commands: `/memory-version-status`, `/memory-version-snapshot [reason]`, `/memory-version-list`, `/memory-version-restore <snapshot-id> [memory|skill-drafts|all]`, `/memory-version-push`.
|
|
90
91
|
- Restore always writes a pre-restore snapshot first, then restores selected files and commits the restored state.
|
|
@@ -126,6 +127,7 @@ Useful memory environment variables:
|
|
|
126
127
|
- `PI_EVOLUTION_BRANCH`: override branch; default `main`.
|
|
127
128
|
- `PI_EVOLUTION_AUTO_COMMIT=0`: disable automatic local commits.
|
|
128
129
|
- `PI_EVOLUTION_AUTO_PUSH=1`: push automatically after commits.
|
|
130
|
+
- `PI_EVOLUTION_MAX_SNAPSHOTS`: maximum local snapshots to keep; default `100`.
|
|
129
131
|
|
|
130
132
|
## Web And Research
|
|
131
133
|
|
|
@@ -196,7 +196,7 @@ Pi-memory mirrors the authoritative runtime directories into a local evolution r
|
|
|
196
196
|
|
|
197
197
|
Authoritative runtime data remains `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts`; `~/.pi/agent/evolution` is a versioned mirror and backup repo.
|
|
198
198
|
|
|
199
|
-
Automatic hooks snapshot before and sync/commit after `memory_write`, mutating `memory_edit`, mutating `scratchpad`, `memory_curate`, learning approve/reject, session summary/handoff writes, compaction handoffs, and external `jhp-pi-memory-curator run-once`. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and that daily file changed since the last scan. Read-only operations do not snapshot.
|
|
199
|
+
Automatic hooks snapshot before and sync/commit after `memory_write`, mutating `memory_edit`, mutating `scratchpad`, `memory_curate`, learning approve/reject, session summary/handoff writes, compaction handoffs, and external `jhp-pi-memory-curator run-once`. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and that daily file changed since the last scan. Read-only operations do not snapshot. Snapshots use a sliding window and keep the latest 100 snapshots by default; creating snapshot 101 deletes the oldest snapshot directory and manifest.
|
|
200
200
|
|
|
201
201
|
Tools and slash commands:
|
|
202
202
|
|
|
@@ -253,6 +253,7 @@ The controller uses a systemd user timer when available and falls back to cron.
|
|
|
253
253
|
| `PI_EVOLUTION_BRANCH` | branch | `main` | Local branch used for init/clone |
|
|
254
254
|
| `PI_EVOLUTION_AUTO_COMMIT` | `0`, `1`, `true`, `false` | `1` | Commit sync/snapshot changes automatically |
|
|
255
255
|
| `PI_EVOLUTION_AUTO_PUSH` | `0`, `1`, `true`, `false` | `0` | Push after commits automatically |
|
|
256
|
+
| `PI_EVOLUTION_MAX_SNAPSHOTS` | positive integer | `100` | Maximum snapshots to keep in the local sliding window |
|
|
256
257
|
|
|
257
258
|
## Development
|
|
258
259
|
|
|
@@ -1573,6 +1573,7 @@ function formatEvolutionStatusText(status: ReturnType<typeof getEvolutionGitStat
|
|
|
1573
1573
|
`dirty: ${status.dirty}`,
|
|
1574
1574
|
`autoCommit: ${status.autoCommit}`,
|
|
1575
1575
|
`autoPush: ${status.autoPush}`,
|
|
1576
|
+
`snapshotLimit: ${status.maxSnapshots}`,
|
|
1576
1577
|
`lastCommit: ${status.lastCommit || "n/a"}`,
|
|
1577
1578
|
status.status ? `status:\n${status.status}` : "status: clean",
|
|
1578
1579
|
].join("\n");
|
|
@@ -4,6 +4,7 @@ export interface EvolutionConfig {
|
|
|
4
4
|
enabled: boolean;
|
|
5
5
|
autoCommit: boolean;
|
|
6
6
|
autoPush: boolean;
|
|
7
|
+
maxSnapshots: number;
|
|
7
8
|
repoDir: string;
|
|
8
9
|
remote: string | null;
|
|
9
10
|
branch: string;
|
|
@@ -19,6 +20,7 @@ type EvolutionEnv = Partial<
|
|
|
19
20
|
| "PI_EVOLUTION_ENABLED"
|
|
20
21
|
| "PI_EVOLUTION_AUTO_COMMIT"
|
|
21
22
|
| "PI_EVOLUTION_AUTO_PUSH"
|
|
23
|
+
| "PI_EVOLUTION_MAX_SNAPSHOTS"
|
|
22
24
|
| "HOME"
|
|
23
25
|
| "USERPROFILE"
|
|
24
26
|
| "HOMEDRIVE"
|
|
@@ -30,6 +32,7 @@ type EvolutionEnv = Partial<
|
|
|
30
32
|
export const DEFAULT_EVOLUTION_REMOTE = "";
|
|
31
33
|
export const LEGACY_SHARED_EVOLUTION_REMOTE = "https://github.com/LRM-Teams/pi-evolution.git";
|
|
32
34
|
export const DEFAULT_EVOLUTION_BRANCH = "main";
|
|
35
|
+
export const DEFAULT_EVOLUTION_MAX_SNAPSHOTS = 100;
|
|
33
36
|
|
|
34
37
|
function homeDir(env: EvolutionEnv): string {
|
|
35
38
|
return env.HOME ?? env.USERPROFILE ?? (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined) ?? "~";
|
|
@@ -43,6 +46,12 @@ function truthy(value: string | undefined, fallback: boolean): boolean {
|
|
|
43
46
|
return fallback;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function positiveInteger(value: string | undefined, fallback: number): number {
|
|
50
|
+
if (value === undefined) return fallback;
|
|
51
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
52
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function expandHome(input: string, env: EvolutionEnv): string {
|
|
47
56
|
if (input === "~") return homeDir(env);
|
|
48
57
|
if (input.startsWith("~/")) return path.join(homeDir(env), input.slice(2));
|
|
@@ -55,6 +64,7 @@ export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = pr
|
|
|
55
64
|
enabled: truthy(env.PI_EVOLUTION_ENABLED, true),
|
|
56
65
|
autoCommit: truthy(env.PI_EVOLUTION_AUTO_COMMIT, true),
|
|
57
66
|
autoPush: truthy(env.PI_EVOLUTION_AUTO_PUSH, false),
|
|
67
|
+
maxSnapshots: positiveInteger(env.PI_EVOLUTION_MAX_SNAPSHOTS, DEFAULT_EVOLUTION_MAX_SNAPSHOTS),
|
|
58
68
|
repoDir: path.resolve(expandHome(env.PI_EVOLUTION_DIR || path.join(agentDir, "evolution"), env)),
|
|
59
69
|
remote: env.PI_EVOLUTION_REMOTE?.trim() || null,
|
|
60
70
|
branch: env.PI_EVOLUTION_BRANCH || DEFAULT_EVOLUTION_BRANCH,
|
|
@@ -14,6 +14,7 @@ export interface GitStatus {
|
|
|
14
14
|
lastCommit: string | null;
|
|
15
15
|
autoPush: boolean;
|
|
16
16
|
autoCommit: boolean;
|
|
17
|
+
maxSnapshots: number;
|
|
17
18
|
enabled: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -104,6 +105,7 @@ export function getEvolutionGitStatus(config: EvolutionConfig): GitStatus {
|
|
|
104
105
|
lastCommit: null,
|
|
105
106
|
autoPush: config.autoPush,
|
|
106
107
|
autoCommit: config.autoCommit,
|
|
108
|
+
maxSnapshots: config.maxSnapshots,
|
|
107
109
|
enabled: config.enabled,
|
|
108
110
|
};
|
|
109
111
|
}
|
|
@@ -118,6 +120,7 @@ export function getEvolutionGitStatus(config: EvolutionConfig): GitStatus {
|
|
|
118
120
|
lastCommit: runGit(config.repoDir, ["log", "-1", "--oneline"], { allowFailure: true }) || null,
|
|
119
121
|
autoPush: config.autoPush,
|
|
120
122
|
autoCommit: config.autoCommit,
|
|
123
|
+
maxSnapshots: config.maxSnapshots,
|
|
121
124
|
enabled: config.enabled,
|
|
122
125
|
};
|
|
123
126
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_REMOTE, LEGACY_SHARED_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./config.ts";
|
|
1
|
+
export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_MAX_SNAPSHOTS, DEFAULT_EVOLUTION_REMOTE, LEGACY_SHARED_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./config.ts";
|
|
2
2
|
export { commitEvolutionChanges, ensureEvolutionRepo, getEvolutionGitStatus, pushEvolution, type GitCommitResult, type GitStatus } from "./git.ts";
|
|
3
3
|
export { buildManifest, createSnapshotId, listManifests, readManifest, writeManifest, type EvolutionManifest } from "./manifest.ts";
|
|
4
4
|
export { restoreEvolutionSnapshot, type RestoreResult, type RestoreTarget } from "./restore.ts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { EvolutionConfig } from "./config.ts";
|
|
4
|
-
import { countFiles } from "./file-utils.ts";
|
|
4
|
+
import { countFiles, pathExists } from "./file-utils.ts";
|
|
5
5
|
|
|
6
6
|
export interface EvolutionManifest {
|
|
7
7
|
id: string;
|
|
@@ -57,12 +57,27 @@ export function readManifest(config: EvolutionConfig, id: string): EvolutionMani
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function listManifests(config: EvolutionConfig, limit = 20): EvolutionManifest[] {
|
|
60
|
+
return readAllManifests(config).slice(0, limit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pruneOldSnapshots(config: EvolutionConfig): EvolutionManifest[] {
|
|
64
|
+
const manifests = readAllManifests(config);
|
|
65
|
+
if (manifests.length <= config.maxSnapshots) return [];
|
|
66
|
+
|
|
67
|
+
const removed = manifests.slice(config.maxSnapshots);
|
|
68
|
+
for (const manifest of removed) {
|
|
69
|
+
fs.rmSync(path.join(config.repoDir, "snapshots", manifest.id), { recursive: true, force: true });
|
|
70
|
+
fs.rmSync(path.join(config.repoDir, "manifests", `${manifest.id}.json`), { force: true });
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readAllManifests(config: EvolutionConfig): EvolutionManifest[] {
|
|
60
76
|
const dir = path.join(config.repoDir, "manifests");
|
|
61
77
|
if (!fs.existsSync(dir)) return [];
|
|
62
78
|
return fs.readdirSync(dir)
|
|
63
79
|
.filter((file) => file.endsWith(".json"))
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
.map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")) as EvolutionManifest);
|
|
80
|
+
.map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")) as EvolutionManifest)
|
|
81
|
+
.filter((manifest) => pathExists(path.join(config.repoDir, "snapshots", manifest.id)))
|
|
82
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
|
|
68
83
|
}
|
|
@@ -2,7 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import type { EvolutionConfig } from "./config.ts";
|
|
3
3
|
import { copyDirContents, emptyDir } from "./file-utils.ts";
|
|
4
4
|
import { commitEvolutionChanges, ensureEvolutionRepo, type GitCommitResult } from "./git.ts";
|
|
5
|
-
import { buildManifest, createSnapshotId, writeManifest, type EvolutionManifest } from "./manifest.ts";
|
|
5
|
+
import { buildManifest, createSnapshotId, pruneOldSnapshots, writeManifest, type EvolutionManifest } from "./manifest.ts";
|
|
6
6
|
import { syncCurrentToEvolution } from "./sync.ts";
|
|
7
7
|
|
|
8
8
|
export interface SnapshotOptions {
|
|
@@ -29,6 +29,7 @@ export function createEvolutionSnapshot(config: EvolutionConfig, options: Snapsh
|
|
|
29
29
|
copyDirContents(config.skillDraftsDir, path.join(snapshotDir, "skill-drafts"));
|
|
30
30
|
const manifest = buildManifest(config, id, options.reason, options.trigger || "manual", options.sessionId);
|
|
31
31
|
writeManifest(config, manifest);
|
|
32
|
+
pruneOldSnapshots(config);
|
|
32
33
|
syncCurrentToEvolution(config);
|
|
33
34
|
const commit = commitEvolutionChanges(config, options.commitMessage || `memory: snapshot ${id}`);
|
|
34
35
|
return { manifest, commit };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { test } from "node:test";
|
|
@@ -51,6 +51,24 @@ test("creates snapshot, manifest, current mirrors, and git commit", () => {
|
|
|
51
51
|
assert.match(getEvolutionGitStatus(config).lastCommit || "", /memory: test snapshot/);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("prunes old snapshots beyond the configured window", () => {
|
|
55
|
+
const { memoryDir, repoDir, config } = testConfig();
|
|
56
|
+
config.maxSnapshots = 3;
|
|
57
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
58
|
+
|
|
59
|
+
const ids: string[] = [];
|
|
60
|
+
for (let i = 0; i < 4; i += 1) {
|
|
61
|
+
writeFileSync(join(memoryDir, "MEMORY.md"), `version ${i}\n`, "utf-8");
|
|
62
|
+
const result = createEvolutionSnapshot(config, { reason: `snapshot ${i}`, trigger: "test", commitMessage: `memory: snapshot ${i}` });
|
|
63
|
+
ids.push(result.manifest?.id || "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
assert.equal(existsSync(join(repoDir, "snapshots", ids[0])), false);
|
|
67
|
+
assert.equal(existsSync(join(repoDir, "manifests", `${ids[0]}.json`)), false);
|
|
68
|
+
assert.equal(readdirSync(join(repoDir, "snapshots")).length, 3);
|
|
69
|
+
assert.deepEqual(listManifests(config, 10).map((manifest) => manifest.id), ids.slice(1).reverse());
|
|
70
|
+
});
|
|
71
|
+
|
|
54
72
|
test("local-only config removes the legacy shared team remote", () => {
|
|
55
73
|
const { config, repoDir } = testConfig();
|
|
56
74
|
mkdirSync(repoDir, { recursive: true });
|