@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/extensions/pet.ts +1033 -0
- package/extensions/prompt-url-widget.ts +158 -0
- package/extensions/redraws.ts +24 -0
- package/extensions/snake.ts +343 -0
- package/extensions/tps.ts +47 -0
- package/package.json +69 -0
- package/prompts/cl.md +54 -0
- package/prompts/is.md +25 -0
- package/prompts/pr.md +37 -0
- package/prompts/wr.md +35 -0
- package/scripts/bootstrap.sh +95 -0
- package/skills/add-llm-provider.md +57 -0
- package/skills/image-to-editable-ppt-slide/SKILL.md +113 -0
- package/skills/image-to-editable-ppt-slide/scripts/generate_spec_template.py +91 -0
- package/skills/image-to-editable-ppt-slide/scripts/pptx_rebuilder.py +181 -0
- package/skills/leetcode-array/SKILL.md +40 -0
- package/skills/leetcode-array/problems/best_time_to_buy_and_sell_stock.py +19 -0
- package/skills/leetcode-array/problems/product_of_array_except_self.py +22 -0
- package/skills/leetcode-array/problems/two_sum.py +19 -0
- package/skills/pi-skill/SKILL.md +154 -0
- package/skills/weather.md +49 -0
- package/vendor/pi-memory/LICENSE +21 -0
- package/vendor/pi-memory/README.md +223 -0
- package/vendor/pi-memory/index.ts +2367 -0
- package/vendor/pi-memory/package.json +68 -0
- package/vendor/pi-memory/scripts/postinstall.cjs +44 -0
- package/vendor/pi-memory/src/cli.ts +79 -0
- package/vendor/pi-memory/src/curator-core/audit.ts +45 -0
- package/vendor/pi-memory/src/curator-core/curate.ts +90 -0
- package/vendor/pi-memory/src/curator-core/metadata.ts +55 -0
- package/vendor/pi-memory/src/curator-core/patch.ts +24 -0
- package/vendor/pi-memory/src/curator-core/policy.ts +77 -0
- package/vendor/pi-memory/src/curator-store/file-store.ts +51 -0
- package/vendor/pi-memory/src/curator-store/types.ts +21 -0
- package/vendor/pi-memory/src/index.ts +35 -0
- package/vendor/pi-memory/src/learning/candidates.ts +205 -0
- package/vendor/pi-memory/src/learning/memory.ts +144 -0
- package/vendor/pi-memory/src/learning/skills.ts +200 -0
- package/vendor/pi-memory/src/service-controller.ts +248 -0
- package/vendor/pi-memory/test/curate.test.ts +68 -0
- package/vendor/pi-memory/test/learning-candidates.test.ts +107 -0
- package/vendor/pi-memory/test/memory-promotions.test.ts +44 -0
- package/vendor/pi-memory/test/metadata.test.ts +17 -0
- package/vendor/pi-memory/test/skill-drafts.test.ts +57 -0
- package/vendor/pi-memory/test/transition-handoff.test.ts +86 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseEntry, renderEntry, type ParsedEntry } from "../curator-core/metadata.ts";
|
|
3
|
+
import type { MemoryStore } from "../curator-store/types.ts";
|
|
4
|
+
|
|
5
|
+
export const REVIEW_CANDIDATE_KINDS = ["bug_fix", "skill_candidate", "preference", "project_fact"] as const;
|
|
6
|
+
export const REVIEW_PROMOTION_KINDS = ["memory_promotion", "memory_merge", "skill_promotion"] as const;
|
|
7
|
+
export const REVIEW_STATUSES = [
|
|
8
|
+
"candidate",
|
|
9
|
+
"merged",
|
|
10
|
+
"proposed",
|
|
11
|
+
"approved",
|
|
12
|
+
"rejected",
|
|
13
|
+
"archived",
|
|
14
|
+
"stale",
|
|
15
|
+
"needs_review",
|
|
16
|
+
] as const;
|
|
17
|
+
export const REVIEW_CONFIDENCES = ["low", "medium", "high"] as const;
|
|
18
|
+
export const REVIEW_TARGET_HINTS = ["memory", "skill"] as const;
|
|
19
|
+
|
|
20
|
+
export type ReviewCandidateKind = (typeof REVIEW_CANDIDATE_KINDS)[number];
|
|
21
|
+
export type ReviewStatus = (typeof REVIEW_STATUSES)[number];
|
|
22
|
+
export type ReviewConfidence = (typeof REVIEW_CONFIDENCES)[number];
|
|
23
|
+
export type ReviewTargetHint = (typeof REVIEW_TARGET_HINTS)[number];
|
|
24
|
+
|
|
25
|
+
export type ReviewCandidateInput = {
|
|
26
|
+
kind: ReviewCandidateKind;
|
|
27
|
+
confidence: ReviewConfidence;
|
|
28
|
+
signature: string;
|
|
29
|
+
summary?: string;
|
|
30
|
+
source?: string;
|
|
31
|
+
targetHints?: ReviewTargetHint[];
|
|
32
|
+
evidence?: string;
|
|
33
|
+
date?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ParsedReviewCandidate = {
|
|
37
|
+
entry: ParsedEntry;
|
|
38
|
+
id: string;
|
|
39
|
+
kind: ReviewCandidateKind;
|
|
40
|
+
status: ReviewStatus;
|
|
41
|
+
confidence: ReviewConfidence;
|
|
42
|
+
seen: number;
|
|
43
|
+
firstSeen: string;
|
|
44
|
+
lastSeen: string;
|
|
45
|
+
signature: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
evidence: string[];
|
|
48
|
+
targetHints: ReviewTargetHint[];
|
|
49
|
+
normalizedSignature: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type UpsertReviewCandidateResult = {
|
|
53
|
+
changed: boolean;
|
|
54
|
+
merged: boolean;
|
|
55
|
+
id: string;
|
|
56
|
+
entry: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const CONFIDENCE_RANK: Record<ReviewConfidence, number> = {
|
|
60
|
+
low: 0,
|
|
61
|
+
medium: 1,
|
|
62
|
+
high: 2,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function todayUtc(now: Date): string {
|
|
66
|
+
return now.toISOString().slice(0, 10);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeCandidateSignature(kind: string, signature: string): string {
|
|
70
|
+
return `${kind}:${signature.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ")}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createReviewCandidateId(kind: string, signature: string): string {
|
|
74
|
+
const digest = createHash("sha256").update(normalizeCandidateSignature(kind, signature)).digest("hex").slice(0, 12);
|
|
75
|
+
return `rev_${digest}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validateReviewCandidateInput(input: ReviewCandidateInput): string[] {
|
|
79
|
+
const errors: string[] = [];
|
|
80
|
+
if (!REVIEW_CANDIDATE_KINDS.includes(input.kind)) errors.push(`invalid kind '${input.kind}'`);
|
|
81
|
+
if (!REVIEW_CONFIDENCES.includes(input.confidence)) errors.push(`invalid confidence '${input.confidence}'`);
|
|
82
|
+
if (!input.signature.trim()) errors.push("signature is required");
|
|
83
|
+
for (const hint of input.targetHints ?? []) {
|
|
84
|
+
if (!REVIEW_TARGET_HINTS.includes(hint)) errors.push(`invalid target hint '${hint}'`);
|
|
85
|
+
}
|
|
86
|
+
return errors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderReviewCandidate(input: ReviewCandidateInput, now = new Date()): string {
|
|
90
|
+
const errors = validateReviewCandidateInput(input);
|
|
91
|
+
if (errors.length) throw new Error(`Invalid review candidate: ${errors.join("; ")}`);
|
|
92
|
+
|
|
93
|
+
const signature = input.signature.trim();
|
|
94
|
+
const date = input.date || todayUtc(now);
|
|
95
|
+
const metadata: Record<string, string> = {
|
|
96
|
+
type: "review",
|
|
97
|
+
status: "candidate",
|
|
98
|
+
id: createReviewCandidateId(input.kind, signature),
|
|
99
|
+
kind: input.kind,
|
|
100
|
+
confidence: input.confidence,
|
|
101
|
+
seen: "1",
|
|
102
|
+
first_seen: date,
|
|
103
|
+
last_seen: date,
|
|
104
|
+
};
|
|
105
|
+
if (input.source?.trim()) metadata.source = input.source.trim().replace(/[\s\]]+/g, "-");
|
|
106
|
+
if (input.targetHints?.length) metadata.target_hints = [...new Set(input.targetHints)].join(",");
|
|
107
|
+
|
|
108
|
+
const body = [`Signature: ${signature}`];
|
|
109
|
+
if (input.summary?.trim()) body.push(`Summary: ${input.summary.trim()}`);
|
|
110
|
+
if (input.evidence?.trim()) body.push(`Evidence: ${input.evidence.trim()}`);
|
|
111
|
+
return renderEntry({ metadata, body: body.join("\n"), raw: "", hasMetadata: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parseReviewCandidate(raw: string): ParsedReviewCandidate | null {
|
|
115
|
+
const entry = parseEntry(raw);
|
|
116
|
+
const metadata = entry.metadata;
|
|
117
|
+
if (metadata.type !== "review" || metadata.status !== "candidate") return null;
|
|
118
|
+
if (!metadata.id || !REVIEW_CANDIDATE_KINDS.includes(metadata.kind as ReviewCandidateKind)) return null;
|
|
119
|
+
if (!REVIEW_STATUSES.includes(metadata.status as ReviewStatus)) return null;
|
|
120
|
+
if (!REVIEW_CONFIDENCES.includes(metadata.confidence as ReviewConfidence)) return null;
|
|
121
|
+
const signature = extractBodyField(entry.body, "Signature");
|
|
122
|
+
if (!signature) return null;
|
|
123
|
+
const seen = Number.parseInt(metadata.seen || "1", 10);
|
|
124
|
+
const targetHints = (metadata.target_hints || "")
|
|
125
|
+
.split(",")
|
|
126
|
+
.map((hint) => hint.trim())
|
|
127
|
+
.filter((hint): hint is ReviewTargetHint => REVIEW_TARGET_HINTS.includes(hint as ReviewTargetHint));
|
|
128
|
+
return {
|
|
129
|
+
entry,
|
|
130
|
+
id: metadata.id,
|
|
131
|
+
kind: metadata.kind as ReviewCandidateKind,
|
|
132
|
+
status: metadata.status as ReviewStatus,
|
|
133
|
+
confidence: metadata.confidence as ReviewConfidence,
|
|
134
|
+
seen: Number.isFinite(seen) && seen > 0 ? seen : 1,
|
|
135
|
+
firstSeen: metadata.first_seen || metadata.last_seen || todayUtc(new Date()),
|
|
136
|
+
lastSeen: metadata.last_seen || metadata.first_seen || todayUtc(new Date()),
|
|
137
|
+
signature,
|
|
138
|
+
summary: extractBodyField(entry.body, "Summary") || undefined,
|
|
139
|
+
evidence: extractBodyFields(entry.body, "Evidence"),
|
|
140
|
+
targetHints,
|
|
141
|
+
normalizedSignature: normalizeCandidateSignature(metadata.kind, signature),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function upsertReviewCandidate(
|
|
146
|
+
memoryStore: MemoryStore,
|
|
147
|
+
input: ReviewCandidateInput,
|
|
148
|
+
now = new Date(),
|
|
149
|
+
): Promise<UpsertReviewCandidateResult> {
|
|
150
|
+
const newEntry = renderReviewCandidate(input, now);
|
|
151
|
+
const newCandidate = parseReviewCandidate(newEntry);
|
|
152
|
+
if (!newCandidate) throw new Error("Rendered review candidate could not be parsed.");
|
|
153
|
+
|
|
154
|
+
const entries = await memoryStore.readEntries("review");
|
|
155
|
+
const existingIndex = entries.findIndex((entry) => {
|
|
156
|
+
const candidate = parseReviewCandidate(entry);
|
|
157
|
+
return candidate?.normalizedSignature === newCandidate.normalizedSignature;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (existingIndex < 0) {
|
|
161
|
+
await memoryStore.writeEntries("review", [...entries, newEntry]);
|
|
162
|
+
return { changed: true, merged: false, id: newCandidate.id, entry: newEntry };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existing = parseReviewCandidate(entries[existingIndex]);
|
|
166
|
+
if (!existing) throw new Error("Matched review candidate could not be parsed.");
|
|
167
|
+
const updatedEntry = mergeReviewCandidate(existing, input, now);
|
|
168
|
+
if (updatedEntry === entries[existingIndex]) return { changed: false, merged: true, id: existing.id, entry: updatedEntry };
|
|
169
|
+
const updatedEntries = [...entries];
|
|
170
|
+
updatedEntries[existingIndex] = updatedEntry;
|
|
171
|
+
await memoryStore.writeEntries("review", updatedEntries);
|
|
172
|
+
return { changed: true, merged: true, id: existing.id, entry: updatedEntry };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function mergeReviewCandidate(existing: ParsedReviewCandidate, input: ReviewCandidateInput, now: Date): string {
|
|
176
|
+
const metadata = { ...existing.entry.metadata };
|
|
177
|
+
metadata.seen = String(existing.seen + 1);
|
|
178
|
+
metadata.last_seen = input.date || todayUtc(now);
|
|
179
|
+
if (CONFIDENCE_RANK[input.confidence] > CONFIDENCE_RANK[existing.confidence]) metadata.confidence = input.confidence;
|
|
180
|
+
if (input.targetHints?.length) {
|
|
181
|
+
const current = (metadata.target_hints || "").split(",").map((hint) => hint.trim()).filter(Boolean);
|
|
182
|
+
metadata.target_hints = [...new Set([...current, ...input.targetHints])].join(",");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let body = existing.entry.body;
|
|
186
|
+
if (input.evidence?.trim() && !body.includes(input.evidence.trim())) {
|
|
187
|
+
body = `${body.trim()}\nEvidence: ${input.evidence.trim()}`;
|
|
188
|
+
}
|
|
189
|
+
return renderEntry({ ...existing.entry, metadata, body });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function extractBodyField(body: string, field: string): string | null {
|
|
193
|
+
const prefix = `${field}:`;
|
|
194
|
+
const line = body.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
195
|
+
return line?.slice(prefix.length).trim() || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function extractBodyFields(body: string, field: string): string[] {
|
|
199
|
+
const prefix = `${field}:`;
|
|
200
|
+
return body
|
|
201
|
+
.split("\n")
|
|
202
|
+
.filter((candidate) => candidate.startsWith(prefix))
|
|
203
|
+
.map((candidate) => candidate.slice(prefix.length).trim())
|
|
204
|
+
.filter(Boolean);
|
|
205
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { parseEntry, renderEntry } from "../curator-core/metadata.ts";
|
|
2
|
+
import type { MemoryStore, MemoryTarget } from "../curator-store/types.ts";
|
|
3
|
+
import { parseReviewCandidate, type ParsedReviewCandidate } from "./candidates.ts";
|
|
4
|
+
|
|
5
|
+
export type MemoryPromotionResult = {
|
|
6
|
+
created: number;
|
|
7
|
+
proposalIds: string[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type MemoryApprovalResult = {
|
|
11
|
+
proposalId: string;
|
|
12
|
+
target: MemoryTarget;
|
|
13
|
+
content: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ReviewLifecycleResult = {
|
|
17
|
+
changed: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function proposeMemoryPromotions(memoryStore: MemoryStore, seenThreshold = 2): Promise<MemoryPromotionResult> {
|
|
21
|
+
const entries = await memoryStore.readEntries("review");
|
|
22
|
+
const existingProposalSources = new Set(
|
|
23
|
+
entries
|
|
24
|
+
.map((entry) => parseEntry(entry).metadata)
|
|
25
|
+
.filter((metadata) => metadata.type === "review" && (metadata.kind === "memory_promotion" || metadata.kind === "memory_merge"))
|
|
26
|
+
.flatMap((metadata) => (metadata.source_candidate_ids || "").split(",").map((id) => id.trim()).filter(Boolean)),
|
|
27
|
+
);
|
|
28
|
+
const proposals = entries
|
|
29
|
+
.map(parseReviewCandidate)
|
|
30
|
+
.filter((candidate): candidate is ParsedReviewCandidate => candidate !== null)
|
|
31
|
+
.filter((candidate) => isMemoryCandidateReady(candidate, seenThreshold) && !existingProposalSources.has(candidate.id))
|
|
32
|
+
.map(renderMemoryProposalEntry);
|
|
33
|
+
if (proposals.length === 0) return { created: 0, proposalIds: [] };
|
|
34
|
+
await memoryStore.writeEntries("review", [...entries, ...proposals]);
|
|
35
|
+
return { created: proposals.length, proposalIds: proposals.map((entry) => parseEntry(entry).metadata.id || "").filter(Boolean) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function approveMemoryPromotion(memoryStore: MemoryStore, proposalId: string): Promise<MemoryApprovalResult> {
|
|
39
|
+
const entries = await memoryStore.readEntries("review");
|
|
40
|
+
const index = entries.findIndex((entry) => parseEntry(entry).metadata.id === proposalId);
|
|
41
|
+
if (index < 0) throw new Error(`No review proposal found for id '${proposalId}'.`);
|
|
42
|
+
const parsed = parseEntry(entries[index]);
|
|
43
|
+
if (parsed.metadata.type !== "review" || parsed.metadata.kind !== "memory_promotion" || parsed.metadata.status !== "proposed") {
|
|
44
|
+
throw new Error(`Review entry '${proposalId}' is not a proposed memory promotion.`);
|
|
45
|
+
}
|
|
46
|
+
const target = normalizePromotionTarget(parsed.metadata.promotes_to);
|
|
47
|
+
const content = bodyBlock(parsed.body, "Memory") || bodyField(parsed.body, "Proposal");
|
|
48
|
+
if (!content) throw new Error(`Memory proposal '${proposalId}' has no memory content.`);
|
|
49
|
+
const targetEntries = await memoryStore.readEntries(target);
|
|
50
|
+
if (!targetEntries.includes(content)) await memoryStore.writeEntries(target, [...targetEntries, content]);
|
|
51
|
+
const approved = renderEntry({
|
|
52
|
+
...parsed,
|
|
53
|
+
metadata: { ...parsed.metadata, status: "approved", approved_at: new Date().toISOString() },
|
|
54
|
+
body: `${parsed.body.trim()}\nApplied: ${target}`,
|
|
55
|
+
});
|
|
56
|
+
const updated = [...entries];
|
|
57
|
+
updated[index] = approved;
|
|
58
|
+
await memoryStore.writeEntries("review", updated);
|
|
59
|
+
return { proposalId, target, content };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function rejectReviewItem(memoryStore: MemoryStore, id: string, status: "rejected" | "archived" = "rejected"): Promise<void> {
|
|
63
|
+
const entries = await memoryStore.readEntries("review");
|
|
64
|
+
const index = entries.findIndex((entry) => parseEntry(entry).metadata.id === id);
|
|
65
|
+
if (index < 0) throw new Error(`No review entry found for id '${id}'.`);
|
|
66
|
+
const parsed = parseEntry(entries[index]);
|
|
67
|
+
if (parsed.metadata.type !== "review") throw new Error(`Entry '${id}' is not a review entry.`);
|
|
68
|
+
const updated = [...entries];
|
|
69
|
+
updated[index] = renderEntry({ ...parsed, metadata: { ...parsed.metadata, status, reviewed_at: new Date().toISOString() } });
|
|
70
|
+
await memoryStore.writeEntries("review", updated);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function applyReviewLifecycle(memoryStore: MemoryStore, now = new Date(), staleDays = 30, archiveDays = 90): Promise<ReviewLifecycleResult> {
|
|
74
|
+
const entries = await memoryStore.readEntries("review");
|
|
75
|
+
let changed = 0;
|
|
76
|
+
const updated = entries.map((entry) => {
|
|
77
|
+
const parsed = parseEntry(entry);
|
|
78
|
+
if (parsed.metadata.type !== "review" || parsed.metadata.status !== "candidate") return entry;
|
|
79
|
+
const lastSeen = parsed.metadata.last_seen || parsed.metadata.first_seen;
|
|
80
|
+
if (!lastSeen) return entry;
|
|
81
|
+
const ageDays = Math.floor((now.getTime() - Date.parse(`${lastSeen}T00:00:00.000Z`)) / 86_400_000);
|
|
82
|
+
if (!Number.isFinite(ageDays) || ageDays < staleDays) return entry;
|
|
83
|
+
const nextStatus = ageDays >= archiveDays && parsed.metadata.confidence === "low" ? "archived" : "needs_review";
|
|
84
|
+
changed += 1;
|
|
85
|
+
return renderEntry({ ...parsed, metadata: { ...parsed.metadata, status: nextStatus, reviewed_at: now.toISOString() } });
|
|
86
|
+
});
|
|
87
|
+
if (changed > 0) await memoryStore.writeEntries("review", updated);
|
|
88
|
+
return { changed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isMemoryCandidateReady(candidate: ParsedReviewCandidate, threshold: number): boolean {
|
|
92
|
+
if (!["preference", "project_fact", "bug_fix"].includes(candidate.kind) && !candidate.targetHints.includes("memory")) return false;
|
|
93
|
+
return candidate.seen >= threshold || candidate.confidence === "high";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderMemoryProposalEntry(candidate: ParsedReviewCandidate): string {
|
|
97
|
+
const target = candidate.kind === "preference" ? "user" : "memory";
|
|
98
|
+
const content = formatMemoryContent(candidate);
|
|
99
|
+
return renderEntry({
|
|
100
|
+
metadata: {
|
|
101
|
+
type: "review",
|
|
102
|
+
status: "proposed",
|
|
103
|
+
id: `mem_${candidate.id.slice(4)}`,
|
|
104
|
+
kind: "memory_promotion",
|
|
105
|
+
confidence: candidate.confidence,
|
|
106
|
+
source_candidate_ids: candidate.id,
|
|
107
|
+
promotes_to: target,
|
|
108
|
+
},
|
|
109
|
+
body: [`Proposal: Promote reviewed candidate to ${target}.`, `Memory: ${content}`].join("\n"),
|
|
110
|
+
raw: "",
|
|
111
|
+
hasMetadata: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatMemoryContent(candidate: ParsedReviewCandidate): string {
|
|
116
|
+
const type = candidate.kind === "preference" ? "preference" : "fact";
|
|
117
|
+
const summary = candidate.summary || candidate.signature;
|
|
118
|
+
return `[type:${type}]\n${summary}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePromotionTarget(value: string | undefined): MemoryTarget {
|
|
122
|
+
if (value === "user" || value === "state" || value === "review") return value;
|
|
123
|
+
return "memory";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bodyField(body: string, field: string): string | null {
|
|
127
|
+
const prefix = `${field}:`;
|
|
128
|
+
const line = body.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
129
|
+
return line?.slice(prefix.length).trim() || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bodyBlock(body: string, field: string): string | null {
|
|
133
|
+
const prefix = `${field}:`;
|
|
134
|
+
const lines = body.split("\n");
|
|
135
|
+
const start = lines.findIndex((line) => line.startsWith(prefix));
|
|
136
|
+
if (start < 0) return null;
|
|
137
|
+
const first = lines[start].slice(prefix.length).trim();
|
|
138
|
+
const rest: string[] = [];
|
|
139
|
+
for (const line of lines.slice(start + 1)) {
|
|
140
|
+
if (/^[A-Z][A-Za-z ]+:/.test(line)) break;
|
|
141
|
+
rest.push(line);
|
|
142
|
+
}
|
|
143
|
+
return [first, ...rest].join("\n").trim() || null;
|
|
144
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { parseEntry, renderEntry } from "../curator-core/metadata.ts";
|
|
4
|
+
import type { MemoryStore } from "../curator-store/types.ts";
|
|
5
|
+
import { parseReviewCandidate, type ParsedReviewCandidate } from "./candidates.ts";
|
|
6
|
+
|
|
7
|
+
export type SkillProposal = {
|
|
8
|
+
id: string;
|
|
9
|
+
sourceCandidateIds: string[];
|
|
10
|
+
slug: string;
|
|
11
|
+
description: string;
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
promotesTo: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SkillProposalOptions = {
|
|
18
|
+
draftsDir: string;
|
|
19
|
+
seenThreshold?: number;
|
|
20
|
+
now?: Date;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SkillProposalResult = {
|
|
24
|
+
created: number;
|
|
25
|
+
proposals: SkillProposal[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SkillApprovalResult = {
|
|
29
|
+
proposalId: string;
|
|
30
|
+
path: string;
|
|
31
|
+
content: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function proposeSkillDrafts(
|
|
35
|
+
memoryStore: MemoryStore,
|
|
36
|
+
options: SkillProposalOptions,
|
|
37
|
+
): Promise<SkillProposalResult> {
|
|
38
|
+
const entries = await memoryStore.readEntries("review");
|
|
39
|
+
const candidates = entries.map(parseReviewCandidate).filter((candidate): candidate is ParsedReviewCandidate => candidate !== null);
|
|
40
|
+
const threshold = options.seenThreshold ?? 3;
|
|
41
|
+
const proposals: SkillProposal[] = [];
|
|
42
|
+
const existingProposalSources = new Set(
|
|
43
|
+
entries
|
|
44
|
+
.map((entry) => parseEntry(entry).metadata)
|
|
45
|
+
.filter((metadata) => metadata.type === "review" && metadata.kind === "skill_promotion")
|
|
46
|
+
.flatMap((metadata) => (metadata.source_candidate_ids || "").split(",").map((id) => id.trim()).filter(Boolean)),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (!isSkillCandidateReady(candidate, threshold) || existingProposalSources.has(candidate.id)) continue;
|
|
51
|
+
proposals.push(createSkillProposal(candidate, options.draftsDir));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (proposals.length === 0) return { created: 0, proposals: [] };
|
|
55
|
+
await memoryStore.writeEntries("review", [...entries, ...proposals.map(renderSkillProposalEntry)]);
|
|
56
|
+
return { created: proposals.length, proposals };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function approveSkillDraft(memoryStore: MemoryStore, proposalId: string): Promise<SkillApprovalResult> {
|
|
60
|
+
const entries = await memoryStore.readEntries("review");
|
|
61
|
+
const index = entries.findIndex((entry) => parseEntry(entry).metadata.id === proposalId);
|
|
62
|
+
if (index < 0) throw new Error(`No review proposal found for id '${proposalId}'.`);
|
|
63
|
+
const parsed = parseEntry(entries[index]);
|
|
64
|
+
if (parsed.metadata.type !== "review" || parsed.metadata.kind !== "skill_promotion" || parsed.metadata.status !== "proposed") {
|
|
65
|
+
throw new Error(`Review entry '${proposalId}' is not a proposed skill promotion.`);
|
|
66
|
+
}
|
|
67
|
+
const path = parsed.metadata.promotes_to;
|
|
68
|
+
if (!path) throw new Error(`Skill proposal '${proposalId}' has no promotes_to path.`);
|
|
69
|
+
const content = buildSkillDraftFromProposal(parsed.body);
|
|
70
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
71
|
+
writeFileSync(path, content, { encoding: "utf-8", flag: "wx" });
|
|
72
|
+
const approved = renderEntry({
|
|
73
|
+
...parsed,
|
|
74
|
+
metadata: { ...parsed.metadata, status: "approved", approved_at: new Date().toISOString() },
|
|
75
|
+
body: `${parsed.body.trim()}\nApplied: ${path}`,
|
|
76
|
+
});
|
|
77
|
+
const updated = [...entries];
|
|
78
|
+
updated[index] = approved;
|
|
79
|
+
await memoryStore.writeEntries("review", updated);
|
|
80
|
+
return { proposalId, path, content };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function listSkillDraftProposals(memoryStore: MemoryStore): Promise<SkillProposal[]> {
|
|
84
|
+
const entries = await memoryStore.readEntries("review");
|
|
85
|
+
return entries
|
|
86
|
+
.map((entry) => parseEntry(entry))
|
|
87
|
+
.filter((entry) => entry.metadata.type === "review" && entry.metadata.kind === "skill_promotion")
|
|
88
|
+
.map((entry) => ({
|
|
89
|
+
id: entry.metadata.id || "",
|
|
90
|
+
sourceCandidateIds: (entry.metadata.source_candidate_ids || "").split(",").map((id) => id.trim()).filter(Boolean),
|
|
91
|
+
slug: slugFromPath(entry.metadata.promotes_to || ""),
|
|
92
|
+
description: bodyField(entry.body, "Description") || "",
|
|
93
|
+
title: bodyField(entry.body, "Title") || "Skill Draft",
|
|
94
|
+
body: entry.body,
|
|
95
|
+
promotesTo: entry.metadata.promotes_to || "",
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isSkillCandidateReady(candidate: ParsedReviewCandidate, threshold: number): boolean {
|
|
100
|
+
if (candidate.kind !== "skill_candidate" && !candidate.targetHints.includes("skill")) return false;
|
|
101
|
+
if (candidate.confidence !== "high" && candidate.seen < threshold) return false;
|
|
102
|
+
return candidate.seen >= threshold || candidate.confidence === "high";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createSkillProposal(candidate: ParsedReviewCandidate, draftsDir: string): SkillProposal {
|
|
106
|
+
const slug = slugify(candidate.signature);
|
|
107
|
+
const title = titleFromSignature(candidate.signature);
|
|
108
|
+
const description = `Use when ${candidate.signature.toLowerCase()}.`;
|
|
109
|
+
const promotesTo = join(draftsDir, slug, "SKILL.md");
|
|
110
|
+
const evidence = candidate.evidence.length ? candidate.evidence.map((item) => `- ${item}`).join("\n") : "- Source candidate evidence is in REVIEW.md.";
|
|
111
|
+
const body = [
|
|
112
|
+
`Title: ${title}`,
|
|
113
|
+
`Description: ${description}`,
|
|
114
|
+
`Source candidates: ${candidate.id}`,
|
|
115
|
+
"Proposal: Draft a reusable skill from repeated reviewed evidence.",
|
|
116
|
+
"",
|
|
117
|
+
"Draft content:",
|
|
118
|
+
"```md",
|
|
119
|
+
"---",
|
|
120
|
+
`name: ${slug}`,
|
|
121
|
+
`description: ${description}`,
|
|
122
|
+
"---",
|
|
123
|
+
"",
|
|
124
|
+
`# ${title}`,
|
|
125
|
+
"",
|
|
126
|
+
"## When to use",
|
|
127
|
+
description,
|
|
128
|
+
"",
|
|
129
|
+
"## Method",
|
|
130
|
+
candidate.summary || candidate.signature,
|
|
131
|
+
"",
|
|
132
|
+
"## Evidence",
|
|
133
|
+
evidence,
|
|
134
|
+
"",
|
|
135
|
+
"## Stop condition",
|
|
136
|
+
"Stop after the validation signal from the source evidence passes, or report the remaining blocker.",
|
|
137
|
+
"```",
|
|
138
|
+
].join("\n");
|
|
139
|
+
return {
|
|
140
|
+
id: `skill_${candidate.id.slice(4)}`,
|
|
141
|
+
sourceCandidateIds: [candidate.id],
|
|
142
|
+
slug,
|
|
143
|
+
description,
|
|
144
|
+
title,
|
|
145
|
+
body,
|
|
146
|
+
promotesTo,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderSkillProposalEntry(proposal: SkillProposal): string {
|
|
151
|
+
return renderEntry({
|
|
152
|
+
metadata: {
|
|
153
|
+
type: "review",
|
|
154
|
+
status: "proposed",
|
|
155
|
+
id: proposal.id,
|
|
156
|
+
kind: "skill_promotion",
|
|
157
|
+
confidence: "high",
|
|
158
|
+
source_candidate_ids: proposal.sourceCandidateIds.join(","),
|
|
159
|
+
promotes_to: proposal.promotesTo,
|
|
160
|
+
},
|
|
161
|
+
body: proposal.body,
|
|
162
|
+
raw: "",
|
|
163
|
+
hasMetadata: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildSkillDraftFromProposal(body: string): string {
|
|
168
|
+
const marker = "Draft content:";
|
|
169
|
+
const markerIndex = body.indexOf(marker);
|
|
170
|
+
if (markerIndex < 0) throw new Error("Skill proposal does not include draft content.");
|
|
171
|
+
const fenced = body.slice(markerIndex + marker.length).trim();
|
|
172
|
+
if (!fenced.startsWith("```md") || !fenced.endsWith("```")) throw new Error("Skill proposal draft content must be a markdown fence.");
|
|
173
|
+
return `${fenced.slice(5, -3).trim()}\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function bodyField(body: string, field: string): string | null {
|
|
177
|
+
const prefix = `${field}:`;
|
|
178
|
+
const line = body.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
179
|
+
return line?.slice(prefix.length).trim() || null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function slugify(value: string): string {
|
|
183
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
184
|
+
return slug || "reviewed-skill";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function titleFromSignature(signature: string): string {
|
|
188
|
+
return signature
|
|
189
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
190
|
+
.trim()
|
|
191
|
+
.split(/\s+/)
|
|
192
|
+
.slice(0, 8)
|
|
193
|
+
.map((word) => word[0]?.toUpperCase() + word.slice(1).toLowerCase())
|
|
194
|
+
.join(" ") || "Reviewed Skill";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function slugFromPath(value: string): string {
|
|
198
|
+
const parts = value.split(/[\\/]+/).filter(Boolean);
|
|
199
|
+
return parts.length > 1 ? parts[parts.length - 2] : "";
|
|
200
|
+
}
|