@lebronj/pi-suite 0.1.6 → 0.1.8
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 +3 -3
- package/package.json +1 -1
- package/scripts/bootstrap.sh +21 -5
- package/skills/pi-skill/SKILL.md +4 -3
- package/vendor/pi-memory/README.md +4 -4
- package/vendor/pi-memory/index.ts +89 -25
- package/vendor/pi-memory/src/evolution/config.ts +4 -3
- package/vendor/pi-memory/src/evolution/git.ts +22 -7
- package/vendor/pi-memory/src/evolution/index.ts +1 -1
- package/vendor/pi-memory/src/index.ts +1 -0
- package/vendor/pi-memory/test/evolution.test.ts +13 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ pi install npm:pi-subagents
|
|
|
15
15
|
Or use the bootstrap script to install Pi, configure the team OpenAI-compatible endpoint, install this suite, and set up Bun + qmd for memory search:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.
|
|
18
|
+
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.8.tgz | tar -xzO package/scripts/bootstrap.sh | bash
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## What Is Included
|
|
@@ -67,7 +67,7 @@ qmd collection add ~/.pi/agent/memory --name pi-memory
|
|
|
67
67
|
qmd embed
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Memory versioning is enabled by default. It snapshots `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts` into `~/.pi/agent/evolution`, commits local changes automatically, and leaves push manual by default.
|
|
70
|
+
Memory versioning is enabled by default. It snapshots `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts` into `~/.pi/agent/evolution`, commits local changes automatically, and leaves push manual by default. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and the daily file changed since the last scan.
|
|
71
71
|
|
|
72
72
|
Useful commands:
|
|
73
73
|
|
|
@@ -79,7 +79,7 @@ Useful commands:
|
|
|
79
79
|
/memory-version-push
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Memory evolution is local-only by default and does not configure a shared remote. If a user wants backup sync, set `PI_EVOLUTION_REMOTE` to their own private repo before bootstrap/setup, or add a personal remote later with `git -C ~/.pi/agent/evolution remote add origin <url>`. Set `PI_EVOLUTION_AUTO_PUSH=1` only if automatic remote sync is desired.
|
|
83
83
|
|
|
84
84
|
## Goal Mode
|
|
85
85
|
|
package/package.json
CHANGED
package/scripts/bootstrap.sh
CHANGED
|
@@ -113,7 +113,8 @@ WORKSPACE_DIR="${PI_WORKSPACE_DIR:-$PWD}"
|
|
|
113
113
|
WORKSPACE_PI_DIR="$WORKSPACE_DIR/.pi"
|
|
114
114
|
MEMORY_DIR="$AGENT_DIR/memory"
|
|
115
115
|
EVOLUTION_DIR="${PI_EVOLUTION_DIR:-$AGENT_DIR/evolution}"
|
|
116
|
-
EVOLUTION_REMOTE="${PI_EVOLUTION_REMOTE:-
|
|
116
|
+
EVOLUTION_REMOTE="${PI_EVOLUTION_REMOTE:-}"
|
|
117
|
+
LEGACY_SHARED_EVOLUTION_REMOTE="https://github.com/LRM-Teams/pi-evolution.git"
|
|
117
118
|
EVOLUTION_BRANCH="${PI_EVOLUTION_BRANCH:-main}"
|
|
118
119
|
SUITE_SKILLS_DIR="${PI_SUITE_SKILLS_DIR:-$AGENT_DIR/npm/node_modules/@lebronj/pi-suite/skills}"
|
|
119
120
|
|
|
@@ -140,17 +141,32 @@ setup_evolution_repo() {
|
|
|
140
141
|
fi
|
|
141
142
|
if [ ! -e "$EVOLUTION_DIR" ]; then
|
|
142
143
|
mkdir -p "$(dirname "$EVOLUTION_DIR")"
|
|
143
|
-
if
|
|
144
|
+
if [ -n "$EVOLUTION_REMOTE" ]; then
|
|
145
|
+
if ! git clone --branch "$EVOLUTION_BRANCH" "$EVOLUTION_REMOTE" "$EVOLUTION_DIR"; then
|
|
146
|
+
mkdir -p "$EVOLUTION_DIR"
|
|
147
|
+
git -C "$EVOLUTION_DIR" init -b "$EVOLUTION_BRANCH" 2>/dev/null || git -C "$EVOLUTION_DIR" init
|
|
148
|
+
git -C "$EVOLUTION_DIR" checkout -B "$EVOLUTION_BRANCH" >/dev/null 2>&1 || true
|
|
149
|
+
git -C "$EVOLUTION_DIR" remote add origin "$EVOLUTION_REMOTE" 2>/dev/null || true
|
|
150
|
+
fi
|
|
151
|
+
else
|
|
144
152
|
mkdir -p "$EVOLUTION_DIR"
|
|
145
153
|
git -C "$EVOLUTION_DIR" init -b "$EVOLUTION_BRANCH" 2>/dev/null || git -C "$EVOLUTION_DIR" init
|
|
146
154
|
git -C "$EVOLUTION_DIR" checkout -B "$EVOLUTION_BRANCH" >/dev/null 2>&1 || true
|
|
147
|
-
|
|
155
|
+
fi
|
|
156
|
+
elif [ -z "$EVOLUTION_REMOTE" ]; then
|
|
157
|
+
current_remote=$(git -C "$EVOLUTION_DIR" remote get-url origin 2>/dev/null || true)
|
|
158
|
+
if [ "$current_remote" = "$LEGACY_SHARED_EVOLUTION_REMOTE" ]; then
|
|
159
|
+
git -C "$EVOLUTION_DIR" remote remove origin 2>/dev/null || true
|
|
148
160
|
fi
|
|
149
161
|
fi
|
|
150
162
|
mkdir -p "$EVOLUTION_DIR/memory" "$EVOLUTION_DIR/skill-drafts" "$EVOLUTION_DIR/snapshots" "$EVOLUTION_DIR/manifests"
|
|
151
163
|
echo "Memory evolution repo ready: $EVOLUTION_DIR"
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
if [ -n "$EVOLUTION_REMOTE" ]; then
|
|
165
|
+
echo "Remote: $EVOLUTION_REMOTE"
|
|
166
|
+
echo "Auto push remains off by default. Use /memory-version-push or PI_EVOLUTION_AUTO_PUSH=1."
|
|
167
|
+
else
|
|
168
|
+
echo "Remote: none (local-only by default). Set PI_EVOLUTION_REMOTE to a personal private repo if you want backup sync."
|
|
169
|
+
fi
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
setup_evolution_repo
|
package/skills/pi-skill/SKILL.md
CHANGED
|
@@ -46,7 +46,7 @@ Memory tools:
|
|
|
46
46
|
- `memory_edit`: read/add/replace/remove/replace_all/compact structured entries in `MEMORY.md`, `USER.md`, `STATE.md`, and `REVIEW.md`.
|
|
47
47
|
- `scratchpad`: add/done/undo/clear/list checklist items.
|
|
48
48
|
- `memory_search`: qmd-backed keyword, semantic, or deep search across memory files.
|
|
49
|
-
- `memory_curate`: manually run curator lifecycle rules.
|
|
49
|
+
- `memory_curate`: manually run curator lifecycle rules and scan yesterday's daily log into `REVIEW.md` when learning is enabled.
|
|
50
50
|
- `memory_learning_approve`: approve a proposed memory promotion or disabled skill draft by exact id.
|
|
51
51
|
- `memory_learning_reject`: reject or archive a review candidate/proposal without deleting it.
|
|
52
52
|
- `memory_skill_drafts`: list proposed skill drafts.
|
|
@@ -74,6 +74,7 @@ Curator and learning behavior:
|
|
|
74
74
|
- Quotas reset when `month` or `reset` rolls over.
|
|
75
75
|
- Mutations are audited to `audit/curator.jsonl`.
|
|
76
76
|
- Session shutdown may extract conservative learning candidates into `REVIEW.md`; they are not injected as normal memory and are not auto-enabled.
|
|
77
|
+
- `memory_curate` scans yesterday's daily log once per content hash into review candidates, then curator lifecycle and proposal rules process those candidates.
|
|
77
78
|
- Repeated candidates can become proposed memory promotions or proposed disabled skill drafts after `memory_curate`.
|
|
78
79
|
- Approval is explicit by default: memory proposals write to memory stores; skill proposals write disabled drafts under `~/.pi/agent/skill-drafts/`.
|
|
79
80
|
- The curator avoids semantic auto-delete/merge; ambiguous learning stays in review first.
|
|
@@ -82,7 +83,7 @@ Memory versioning:
|
|
|
82
83
|
|
|
83
84
|
- Runtime memory remains authoritative at `~/.pi/agent/memory`; disabled skill drafts remain authoritative at `~/.pi/agent/skill-drafts`.
|
|
84
85
|
- Versioning mirror and snapshots live at `~/.pi/agent/evolution` by default.
|
|
85
|
-
-
|
|
86
|
+
- No remote is configured by default; memory evolution stays local per user/machine unless the user adds a personal private remote.
|
|
86
87
|
- Automatic local snapshot + commit is enabled by default; automatic push is disabled unless `PI_EVOLUTION_AUTO_PUSH=1`.
|
|
87
88
|
- Snapshots run before mutating memory tools, curator runs, learning approve/reject, session summaries/handoffs, compact handoffs, restore, and external curator `run-once`.
|
|
88
89
|
- Slash commands: `/memory-version-status`, `/memory-version-snapshot [reason]`, `/memory-version-list`, `/memory-version-restore <snapshot-id> [memory|skill-drafts|all]`, `/memory-version-push`.
|
|
@@ -121,7 +122,7 @@ Useful memory environment variables:
|
|
|
121
122
|
- `PI_MEMORY_CURATOR_STARTUP_HINT=0`: hide the disabled-curator startup hint.
|
|
122
123
|
- `PI_EVOLUTION_ENABLED=0`: disable snapshot + git versioning.
|
|
123
124
|
- `PI_EVOLUTION_DIR`: override evolution repo directory; default `~/.pi/agent/evolution`.
|
|
124
|
-
- `PI_EVOLUTION_REMOTE`:
|
|
125
|
+
- `PI_EVOLUTION_REMOTE`: optional personal private Git remote; unset by default.
|
|
125
126
|
- `PI_EVOLUTION_BRANCH`: override branch; default `main`.
|
|
126
127
|
- `PI_EVOLUTION_AUTO_COMMIT=0`: disable automatic local commits.
|
|
127
128
|
- `PI_EVOLUTION_AUTO_PUSH=1`: push automatically after commits.
|
|
@@ -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`. 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.
|
|
200
200
|
|
|
201
201
|
Tools and slash commands:
|
|
202
202
|
|
|
@@ -208,7 +208,7 @@ Tools and slash commands:
|
|
|
208
208
|
|
|
209
209
|
Restore always creates a pre-restore snapshot first, then restores the selected target, syncs the mirror, and commits `memory: restore snapshot <id>`.
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
No remote is configured by default. This keeps each user's memory evolution local to their machine and avoids writing personal memory into a shared team repo. If remote backup is desired, set `PI_EVOLUTION_REMOTE` to a personal private repo before setup, or add `origin` manually later. Auto commit is on; auto push is off unless `PI_EVOLUTION_AUTO_PUSH=1` is set. Any remote should be private because memory contents are committed in plaintext, including any secret accidentally written to memory.
|
|
212
212
|
|
|
213
213
|
## External Curator Service
|
|
214
214
|
|
|
@@ -242,14 +242,14 @@ The controller uses a systemd user timer when available and falls back to cron.
|
|
|
242
242
|
| `PI_MEMORY_QMD_UPDATE` | `background`, `manual`, `off` | `background` | Control qmd update after writes |
|
|
243
243
|
| `PI_MEMORY_NO_SEARCH` | `1` | unset | Disable per-turn search injection |
|
|
244
244
|
| `PI_MEMORY_SUMMARIZE_TRANSITIONS` | `1`, `true`, `yes`, `on` | unset | Also summarize lifecycle transitions |
|
|
245
|
-
| `PI_MEMORY_LEARNING` | `off`, `review`, `auto-review` | `review` | Control session learning candidate extraction |
|
|
245
|
+
| `PI_MEMORY_LEARNING` | `off`, `review`, `auto-review` | `review` | Control session and curator daily learning candidate extraction |
|
|
246
246
|
| `PI_MEMORY_LEARNING_MIN_CONFIDENCE` | `low`, `medium`, `high` | `medium` | Minimum extractor confidence to keep |
|
|
247
247
|
| `PI_MEMORY_SKILL_DRAFTS` | `off`, `review` | `review` | Allow curator to propose disabled skill drafts |
|
|
248
248
|
| `PI_MEMORY_AUTO_APPROVE_MEMORY` | `1`, `true`, `yes`, `on` | unset | YOLO mode for approving newly created memory proposals |
|
|
249
249
|
| `PI_MEMORY_AUTO_APPROVE_SKILL_DRAFTS` | `1`, `true`, `yes`, `on` | unset | YOLO mode for creating newly proposed disabled skill drafts |
|
|
250
250
|
| `PI_EVOLUTION_ENABLED` | `0`, `1`, `true`, `false` | `1` | Enable snapshot + git versioning |
|
|
251
251
|
| `PI_EVOLUTION_DIR` | path | `~/.pi/agent/evolution` | Local evolution repo directory |
|
|
252
|
-
| `PI_EVOLUTION_REMOTE` | URL |
|
|
252
|
+
| `PI_EVOLUTION_REMOTE` | URL | unset | Optional personal private Git remote for manual/optional push |
|
|
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 |
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { type ExecFileOptions, execFile } from "node:child_process";
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
29
30
|
import * as fs from "node:fs";
|
|
30
31
|
import * as path from "node:path";
|
|
31
32
|
import { complete, type Message, StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -89,6 +90,7 @@ let USER_FILE = path.join(MEMORY_DIR, "USER.md");
|
|
|
89
90
|
let STATE_FILE = path.join(MEMORY_DIR, "STATE.md");
|
|
90
91
|
let REVIEW_FILE = path.join(MEMORY_DIR, "REVIEW.md");
|
|
91
92
|
let SCRATCHPAD_FILE = path.join(MEMORY_DIR, "SCRATCHPAD.md");
|
|
93
|
+
let LEARNING_STATE_FILE = path.join(MEMORY_DIR, ".learning-state.json");
|
|
92
94
|
let DAILY_DIR = path.join(MEMORY_DIR, "daily");
|
|
93
95
|
let SKILL_DRAFTS_DIR = path.join(path.dirname(MEMORY_DIR), "skill-drafts");
|
|
94
96
|
|
|
@@ -104,6 +106,7 @@ export function _setBaseDir(baseDir: string) {
|
|
|
104
106
|
STATE_FILE = path.join(baseDir, "STATE.md");
|
|
105
107
|
REVIEW_FILE = path.join(baseDir, "REVIEW.md");
|
|
106
108
|
SCRATCHPAD_FILE = path.join(baseDir, "SCRATCHPAD.md");
|
|
109
|
+
LEARNING_STATE_FILE = path.join(baseDir, ".learning-state.json");
|
|
107
110
|
DAILY_DIR = path.join(baseDir, "daily");
|
|
108
111
|
SKILL_DRAFTS_DIR = path.join(path.dirname(baseDir), "skill-drafts");
|
|
109
112
|
}
|
|
@@ -562,9 +565,9 @@ function shouldKeepLearningCandidate(confidence: "low" | "medium" | "high", env:
|
|
|
562
565
|
return confidenceRank(confidence) >= confidenceRank(getMemoryLearningMinConfidence(env));
|
|
563
566
|
}
|
|
564
567
|
|
|
565
|
-
function buildLearningExtractorPrompt(conversationText: string, truncated: boolean, totalChars: number): string {
|
|
568
|
+
function buildLearningExtractorPrompt(conversationText: string, truncated: boolean, totalChars: number, sourceLabel = "session transcript"): string {
|
|
566
569
|
const lines = [
|
|
567
|
-
|
|
570
|
+
`Extract zero or more review candidates from this ${sourceLabel}.`,
|
|
568
571
|
"Return JSON exactly shaped as: {\"candidates\":[{\"kind\":\"bug_fix|skill_candidate|preference|project_fact\",\"confidence\":\"low|medium|high\",\"signature\":\"short stable signature\",\"summary\":\"optional concise summary\",\"targetHints\":[\"memory\",\"skill\"],\"evidence\":\"optional compact evidence\"}]}",
|
|
569
572
|
"Only include verified bug fixes when a failure was followed by an edit/action and successful validation.",
|
|
570
573
|
"Drop one-off trivia, transient status, workflow artifacts, and loop artifacts.",
|
|
@@ -671,31 +674,90 @@ export function parseLearningExtractorResponse(raw: string): ReviewCandidateInpu
|
|
|
671
674
|
return candidates;
|
|
672
675
|
}
|
|
673
676
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
677
|
+
type DailyLearningState = {
|
|
678
|
+
daily?: Record<string, { hash: string; scannedAt: string; candidates: number }>;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
function readLearningState(): DailyLearningState {
|
|
682
|
+
try {
|
|
683
|
+
const parsed = JSON.parse(fs.readFileSync(LEARNING_STATE_FILE, "utf-8"));
|
|
684
|
+
return parsed && typeof parsed === "object" ? parsed as DailyLearningState : {};
|
|
685
|
+
} catch {
|
|
686
|
+
return {};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function writeLearningState(state: DailyLearningState): void {
|
|
691
|
+
fs.writeFileSync(LEARNING_STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function contentHash(content: string): string {
|
|
695
|
+
return createHash("sha256").update(content).digest("hex");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function extractLearningCandidates(
|
|
699
|
+
ctx: ExtensionContext,
|
|
700
|
+
text: string,
|
|
701
|
+
sourceLabel: string,
|
|
702
|
+
source: string,
|
|
703
|
+
date?: string,
|
|
704
|
+
): Promise<ReviewCandidateInput[]> {
|
|
705
|
+
if (getMemoryLearningMode() === "off" || !ctx.model) return [];
|
|
678
706
|
const apiKey = await resolveExitSummaryApiKey(ctx);
|
|
679
|
-
if (!apiKey) return
|
|
680
|
-
const
|
|
681
|
-
if (!
|
|
682
|
-
const truncated = truncateText(
|
|
707
|
+
if (!apiKey) return [];
|
|
708
|
+
const trimmed = text.trim();
|
|
709
|
+
if (!trimmed) return [];
|
|
710
|
+
const truncated = truncateText(trimmed, LEARNING_EXTRACTOR_MAX_CHARS, "end");
|
|
683
711
|
const messages: Message[] = [{
|
|
684
712
|
role: "user",
|
|
685
|
-
content: [{ type: "text", text: buildLearningExtractorPrompt(truncated.text, truncated.truncated,
|
|
713
|
+
content: [{ type: "text", text: buildLearningExtractorPrompt(truncated.text, truncated.truncated, trimmed.length, sourceLabel) }],
|
|
686
714
|
timestamp: Date.now(),
|
|
687
715
|
}];
|
|
716
|
+
const response = await complete(ctx.model, { systemPrompt: LEARNING_EXTRACTOR_SYSTEM_PROMPT, messages }, { apiKey, reasoningEffort: "low" });
|
|
717
|
+
const raw = response.content.filter((part): part is { type: "text"; text: string } => part.type === "text").map((part) => part.text).join("\n");
|
|
718
|
+
return parseLearningExtractorResponse(raw).map((candidate) => ({ ...candidate, source, date }));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function writeLearningCandidates(candidates: ReviewCandidateInput[]): Promise<number> {
|
|
722
|
+
let written = 0;
|
|
723
|
+
const store = new FileMemoryStore(MEMORY_DIR);
|
|
724
|
+
for (const candidate of candidates) {
|
|
725
|
+
const result = await upsertReviewCandidate(store, candidate);
|
|
726
|
+
if (result.changed) written += 1;
|
|
727
|
+
}
|
|
728
|
+
return written;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
type DailyLearningScanResult = { scanned: boolean; changed: number; skipped?: string };
|
|
732
|
+
|
|
733
|
+
async function runYesterdayDailyLearningScan(ctx: ExtensionContext): Promise<DailyLearningScanResult> {
|
|
734
|
+
if (getMemoryLearningMode() === "off") return { scanned: false, changed: 0, skipped: "learning off" };
|
|
735
|
+
const date = yesterdayStr();
|
|
736
|
+
const dailyContent = readFileSafe(dailyPath(date));
|
|
737
|
+
if (!dailyContent?.trim()) return { scanned: false, changed: 0, skipped: `daily/${date}.md empty or missing` };
|
|
738
|
+
const hash = contentHash(dailyContent);
|
|
739
|
+
const state = readLearningState();
|
|
740
|
+
const previous = state.daily?.[date];
|
|
741
|
+
if (previous?.hash === hash) return { scanned: false, changed: 0, skipped: `daily/${date}.md already scanned` };
|
|
688
742
|
try {
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
743
|
+
const candidates = await extractLearningCandidates(ctx, dailyContent, `daily log for ${date}`, `daily/${date}`, date);
|
|
744
|
+
const changed = await writeLearningCandidates(candidates);
|
|
745
|
+
state.daily = { ...(state.daily || {}), [date]: { hash, scannedAt: new Date().toISOString(), candidates: candidates.length } };
|
|
746
|
+
writeLearningState(state);
|
|
747
|
+
return { scanned: true, changed };
|
|
748
|
+
} catch {
|
|
749
|
+
return { scanned: false, changed: 0, skipped: `daily/${date}.md scan failed` };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function runSessionLearningExtractor(ctx: ExtensionContext): Promise<number> {
|
|
754
|
+
const branch = getSessionBranch(ctx);
|
|
755
|
+
if (!branch) return 0;
|
|
756
|
+
const conversation = serializeSessionConversation(branch);
|
|
757
|
+
if (!conversation.hasMessages || !conversation.text.trim()) return 0;
|
|
758
|
+
try {
|
|
759
|
+
const candidates = await extractLearningCandidates(ctx, conversation.text, "session transcript", "session_shutdown");
|
|
760
|
+
return writeLearningCandidates(candidates);
|
|
699
761
|
} catch {
|
|
700
762
|
return 0;
|
|
701
763
|
}
|
|
@@ -1439,7 +1501,7 @@ function getSnapshotMode(): "stable" | "per-turn" {
|
|
|
1439
1501
|
}
|
|
1440
1502
|
|
|
1441
1503
|
|
|
1442
|
-
async function runCurator(reason: string): Promise<string> {
|
|
1504
|
+
async function runCurator(reason: string, ctx?: ExtensionContext): Promise<string> {
|
|
1443
1505
|
ensureDirs();
|
|
1444
1506
|
await evolutionBeforeChange(`curator before ${reason}`, "memory: snapshot before curate", "tool");
|
|
1445
1507
|
const store = new FileMemoryStore(MEMORY_DIR);
|
|
@@ -1448,6 +1510,7 @@ async function runCurator(reason: string): Promise<string> {
|
|
|
1448
1510
|
auditLog: new JsonlAuditLog(MEMORY_DIR),
|
|
1449
1511
|
reason,
|
|
1450
1512
|
});
|
|
1513
|
+
const dailyLearningResult = ctx ? await runYesterdayDailyLearningScan(ctx) : { scanned: false, changed: 0 };
|
|
1451
1514
|
const lifecycleResult = await applyReviewLifecycle(store);
|
|
1452
1515
|
const memoryResult = await proposeMemoryPromotions(store);
|
|
1453
1516
|
const skillResult = getMemorySkillDraftsMode() === "off" ? { created: 0, proposals: [] } : await proposeSkillDrafts(store, { draftsDir: SKILL_DRAFTS_DIR });
|
|
@@ -1465,7 +1528,7 @@ async function runCurator(reason: string): Promise<string> {
|
|
|
1465
1528
|
autoApprovedSkills += 1;
|
|
1466
1529
|
}
|
|
1467
1530
|
}
|
|
1468
|
-
const learningChanges = lifecycleResult.changed + memoryResult.created + skillResult.created + autoApprovedMemory + autoApprovedSkills;
|
|
1531
|
+
const learningChanges = dailyLearningResult.changed + lifecycleResult.changed + memoryResult.created + skillResult.created + autoApprovedMemory + autoApprovedSkills;
|
|
1469
1532
|
if (result.patches.length > 0 || learningChanges > 0) {
|
|
1470
1533
|
snapshotDirty = true;
|
|
1471
1534
|
await ensureQmdAvailableForUpdate();
|
|
@@ -1473,6 +1536,7 @@ async function runCurator(reason: string): Promise<string> {
|
|
|
1473
1536
|
}
|
|
1474
1537
|
await evolutionAfterChange("memory: sync after curate");
|
|
1475
1538
|
const notes = [
|
|
1539
|
+
dailyLearningResult.scanned ? `scanned yesterday daily, wrote ${dailyLearningResult.changed} review candidate change(s)` : dailyLearningResult.skipped ? `daily learning skipped: ${dailyLearningResult.skipped}` : "",
|
|
1476
1540
|
memoryResult.created > 0 ? `proposed ${memoryResult.created} memory promotion(s)` : "",
|
|
1477
1541
|
skillResult.created > 0 ? `proposed ${skillResult.created} skill draft(s)` : "",
|
|
1478
1542
|
autoApprovedMemory > 0 ? `auto-approved ${autoApprovedMemory} memory promotion(s)` : "",
|
|
@@ -2270,11 +2334,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2270
2334
|
pi.registerTool({
|
|
2271
2335
|
name: "memory_curate",
|
|
2272
2336
|
label: "Memory Curate",
|
|
2273
|
-
description: "Run the time-aware memory curator now. It deduplicates exact entries, updates event/quota lifecycle metadata, and appends stale temporary memories to REVIEW.md.",
|
|
2337
|
+
description: "Run the time-aware memory curator now. It scans yesterday's daily log into REVIEW.md, deduplicates exact entries, updates event/quota lifecycle metadata, and appends stale temporary memories to REVIEW.md.",
|
|
2274
2338
|
parameters: Type.Object({}),
|
|
2275
2339
|
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
2276
2340
|
try {
|
|
2277
|
-
const summary = await runCurator("memory_curate tool");
|
|
2341
|
+
const summary = await runCurator("memory_curate tool", _ctx);
|
|
2278
2342
|
return { content: [{ type: "text", text: summary }], details: { summary } };
|
|
2279
2343
|
} catch (error) {
|
|
2280
2344
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -5,7 +5,7 @@ export interface EvolutionConfig {
|
|
|
5
5
|
autoCommit: boolean;
|
|
6
6
|
autoPush: boolean;
|
|
7
7
|
repoDir: string;
|
|
8
|
-
remote: string;
|
|
8
|
+
remote: string | null;
|
|
9
9
|
branch: string;
|
|
10
10
|
memoryDir: string;
|
|
11
11
|
skillDraftsDir: string;
|
|
@@ -27,7 +27,8 @@ type EvolutionEnv = Partial<
|
|
|
27
27
|
>
|
|
28
28
|
>;
|
|
29
29
|
|
|
30
|
-
export const DEFAULT_EVOLUTION_REMOTE = "
|
|
30
|
+
export const DEFAULT_EVOLUTION_REMOTE = "";
|
|
31
|
+
export const LEGACY_SHARED_EVOLUTION_REMOTE = "https://github.com/LRM-Teams/pi-evolution.git";
|
|
31
32
|
export const DEFAULT_EVOLUTION_BRANCH = "main";
|
|
32
33
|
|
|
33
34
|
function homeDir(env: EvolutionEnv): string {
|
|
@@ -55,7 +56,7 @@ export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = pr
|
|
|
55
56
|
autoCommit: truthy(env.PI_EVOLUTION_AUTO_COMMIT, true),
|
|
56
57
|
autoPush: truthy(env.PI_EVOLUTION_AUTO_PUSH, false),
|
|
57
58
|
repoDir: path.resolve(expandHome(env.PI_EVOLUTION_DIR || path.join(agentDir, "evolution"), env)),
|
|
58
|
-
remote: env.PI_EVOLUTION_REMOTE ||
|
|
59
|
+
remote: env.PI_EVOLUTION_REMOTE?.trim() || null,
|
|
59
60
|
branch: env.PI_EVOLUTION_BRANCH || DEFAULT_EVOLUTION_BRANCH,
|
|
60
61
|
memoryDir,
|
|
61
62
|
skillDraftsDir: path.join(agentDir, "skill-drafts"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type
|
|
4
|
+
import { LEGACY_SHARED_EVOLUTION_REMOTE, type EvolutionConfig } from "./config.ts";
|
|
5
5
|
import { pathExists } from "./file-utils.ts";
|
|
6
6
|
|
|
7
7
|
export interface GitStatus {
|
|
@@ -56,14 +56,21 @@ export function ensureEvolutionRepo(config: EvolutionConfig): void {
|
|
|
56
56
|
}
|
|
57
57
|
if (!pathExists(config.repoDir)) {
|
|
58
58
|
fs.mkdirSync(path.dirname(config.repoDir), { recursive: true });
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (config.remote) {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync("git", ["clone", "--branch", config.branch, config.remote, config.repoDir], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
62
|
+
} catch {
|
|
63
|
+
fs.mkdirSync(config.repoDir, { recursive: true });
|
|
64
|
+
runGit(config.repoDir, ["init", "-b", config.branch], { allowFailure: true });
|
|
65
|
+
if (!isGitRepo(config.repoDir)) runGit(config.repoDir, ["init"]);
|
|
66
|
+
runGit(config.repoDir, ["checkout", "-B", config.branch]);
|
|
67
|
+
runGit(config.repoDir, ["remote", "add", "origin", config.remote], { allowFailure: true });
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
62
70
|
fs.mkdirSync(config.repoDir, { recursive: true });
|
|
63
71
|
runGit(config.repoDir, ["init", "-b", config.branch], { allowFailure: true });
|
|
64
72
|
if (!isGitRepo(config.repoDir)) runGit(config.repoDir, ["init"]);
|
|
65
73
|
runGit(config.repoDir, ["checkout", "-B", config.branch]);
|
|
66
|
-
runGit(config.repoDir, ["remote", "add", "origin", config.remote], { allowFailure: true });
|
|
67
74
|
}
|
|
68
75
|
} else if (!isGitRepo(config.repoDir)) {
|
|
69
76
|
fs.mkdirSync(config.repoDir, { recursive: true });
|
|
@@ -73,8 +80,12 @@ export function ensureEvolutionRepo(config: EvolutionConfig): void {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
const remote = runGit(config.repoDir, ["remote", "get-url", "origin"], { allowFailure: true });
|
|
76
|
-
if (
|
|
77
|
-
|
|
83
|
+
if (config.remote) {
|
|
84
|
+
if (!remote) runGit(config.repoDir, ["remote", "add", "origin", config.remote]);
|
|
85
|
+
else if (remote !== config.remote) runGit(config.repoDir, ["remote", "set-url", "origin", config.remote]);
|
|
86
|
+
} else if (remote === LEGACY_SHARED_EVOLUTION_REMOTE) {
|
|
87
|
+
runGit(config.repoDir, ["remote", "remove", "origin"], { allowFailure: true });
|
|
88
|
+
}
|
|
78
89
|
|
|
79
90
|
const branch = runGit(config.repoDir, ["branch", "--show-current"], { allowFailure: true });
|
|
80
91
|
if (!branch) runGit(config.repoDir, ["checkout", "-B", config.branch]);
|
|
@@ -127,6 +138,10 @@ export function commitEvolutionChanges(config: EvolutionConfig, message: string)
|
|
|
127
138
|
|
|
128
139
|
export function pushEvolution(config: EvolutionConfig): string {
|
|
129
140
|
ensureEvolutionRepo(config);
|
|
141
|
+
const remote = runGit(config.repoDir, ["remote", "get-url", "origin"], { allowFailure: true });
|
|
142
|
+
if (!remote) {
|
|
143
|
+
return "No evolution remote configured. Add a personal private remote with `git -C ~/.pi/agent/evolution remote add origin <url>` or set PI_EVOLUTION_REMOTE before setup.";
|
|
144
|
+
}
|
|
130
145
|
const branch = runGit(config.repoDir, ["branch", "--show-current"], { allowFailure: true }) || config.branch;
|
|
131
146
|
return runGit(config.repoDir, ["push", "-u", "origin", branch]);
|
|
132
147
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./config.ts";
|
|
1
|
+
export { DEFAULT_EVOLUTION_BRANCH, 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,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -6,6 +7,7 @@ import { test } from "node:test";
|
|
|
6
7
|
import {
|
|
7
8
|
createEvolutionSnapshot,
|
|
8
9
|
getEvolutionGitStatus,
|
|
10
|
+
LEGACY_SHARED_EVOLUTION_REMOTE,
|
|
9
11
|
listManifests,
|
|
10
12
|
resolveEvolutionConfig,
|
|
11
13
|
restoreEvolutionSnapshot,
|
|
@@ -24,7 +26,6 @@ function testConfig() {
|
|
|
24
26
|
repoDir,
|
|
25
27
|
config: resolveEvolutionConfig(memoryDir, {
|
|
26
28
|
PI_EVOLUTION_DIR: repoDir,
|
|
27
|
-
PI_EVOLUTION_REMOTE: "https://example.invalid/pi-evolution.git",
|
|
28
29
|
PI_EVOLUTION_AUTO_PUSH: "0",
|
|
29
30
|
HOME: root,
|
|
30
31
|
}),
|
|
@@ -50,6 +51,17 @@ test("creates snapshot, manifest, current mirrors, and git commit", () => {
|
|
|
50
51
|
assert.match(getEvolutionGitStatus(config).lastCommit || "", /memory: test snapshot/);
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
test("local-only config removes the legacy shared team remote", () => {
|
|
55
|
+
const { config, repoDir } = testConfig();
|
|
56
|
+
mkdirSync(repoDir, { recursive: true });
|
|
57
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: repoDir, stdio: "ignore" });
|
|
58
|
+
execFileSync("git", ["remote", "add", "origin", LEGACY_SHARED_EVOLUTION_REMOTE], { cwd: repoDir, stdio: "ignore" });
|
|
59
|
+
|
|
60
|
+
createEvolutionSnapshot(config, { reason: "local migration", trigger: "test", commitMessage: "memory: local migration" });
|
|
61
|
+
|
|
62
|
+
assert.equal(getEvolutionGitStatus(config).remote, null);
|
|
63
|
+
});
|
|
64
|
+
|
|
53
65
|
test("sync does not create empty commits when nothing changed", () => {
|
|
54
66
|
const { memoryDir, config } = testConfig();
|
|
55
67
|
mkdirSync(memoryDir, { recursive: true });
|