@lebronj/pi-suite 0.1.16 → 0.1.17
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 +11 -3
- package/package.json +1 -1
- package/skills/pi-skill/SKILL.md +29 -5
- package/vendor/pi-memory/README.md +87 -56
- package/vendor/pi-memory/index.ts +522 -310
- package/vendor/pi-memory/package.json +1 -1
- package/vendor/pi-memory/src/cli.ts +56 -32
- package/vendor/pi-memory/src/evolution/config.ts +8 -2
- package/vendor/pi-memory/src/governance/share-candidates.ts +72 -0
- package/vendor/pi-memory/src/index.ts +68 -25
- package/vendor/pi-memory/src/learning/review-compact.ts +36 -0
- package/vendor/pi-memory/src/learning/review-summary.ts +81 -0
- package/vendor/pi-memory/src/manager/local-curator-manager.ts +146 -0
- package/vendor/pi-memory/src/paths/resolve-roots.ts +155 -0
- package/vendor/pi-memory/src/profile/generator.ts +45 -0
- package/vendor/pi-memory/src/service-controller.ts +156 -84
- package/vendor/pi-memory/src/skills/lifecycle.ts +205 -0
- package/vendor/pi-memory/src/sync/connector.ts +146 -0
- package/vendor/pi-memory/src/sync/downflow.ts +54 -0
- package/vendor/pi-memory/src/sync/feedback.ts +30 -0
- package/vendor/pi-memory/src/sync/queue.ts +40 -0
- package/vendor/pi-memory/src/sync/schemas.ts +44 -0
- package/vendor/pi-memory/src/sync/sensitivity.ts +18 -0
- package/vendor/pi-memory/test/manager-service.test.ts +17 -0
- package/vendor/pi-memory/test/resolve-roots.test.ts +63 -0
- package/vendor/pi-memory/test/review-summary.test.ts +36 -0
- package/vendor/pi-memory/test/skill-lifecycle.test.ts +75 -0
- package/vendor/pi-memory/test/sync-local-loop.test.ts +101 -0
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
import { JsonlAuditLog } from "./curator-core/audit.ts";
|
|
3
3
|
import { runMemoryCuratorOnce } from "./curator-core/curate.ts";
|
|
4
4
|
import { FileMemoryStore } from "./curator-store/file-store.ts";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { defaultRegistryPath, markCurrentRootDirty, scanDirtyRoots } from "./manager/local-curator-manager.ts";
|
|
6
|
+
import {
|
|
7
|
+
disableCuratorManagerService,
|
|
8
|
+
disableCuratorService,
|
|
9
|
+
enableCuratorManagerService,
|
|
10
|
+
enableCuratorService,
|
|
11
|
+
getCuratorManagerServiceStatus,
|
|
12
|
+
getCuratorServiceStatus,
|
|
13
|
+
resolveMemoryDir,
|
|
14
|
+
} from "./service-controller.ts";
|
|
7
15
|
|
|
8
16
|
function cliPath(): string {
|
|
9
17
|
return new URL(import.meta.url).pathname;
|
|
@@ -23,11 +31,14 @@ function usage(): string {
|
|
|
23
31
|
return [
|
|
24
32
|
"Usage:",
|
|
25
33
|
" jhp-pi-memory-curator run-once [--memory-dir <path>] [--reason <text>] [--dry-run] [--json]",
|
|
26
|
-
" jhp-pi-memory-curator snapshot [--memory-dir <path>] [--reason <text>] [--json]",
|
|
27
|
-
" jhp-pi-memory-curator push [--memory-dir <path>]",
|
|
28
34
|
" jhp-pi-memory-curator enable [--memory-dir <path>] [--schedule HH:MM]",
|
|
29
35
|
" jhp-pi-memory-curator disable [--memory-dir <path>]",
|
|
30
36
|
" jhp-pi-memory-curator status [--memory-dir <path>]",
|
|
37
|
+
" jhp-pi-memory-curator mark-dirty [--registry <path>]",
|
|
38
|
+
" jhp-pi-memory-curator manager-scan [--registry <path>] [--json]",
|
|
39
|
+
" jhp-pi-memory-curator manager-enable [--registry <path>] [--schedule '0 */6 * * *']",
|
|
40
|
+
" jhp-pi-memory-curator manager-disable [--registry <path>]",
|
|
41
|
+
" jhp-pi-memory-curator manager-status [--registry <path>]",
|
|
31
42
|
].join("\n");
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -41,40 +52,14 @@ async function main(): Promise<void> {
|
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
if (command === "run-once") {
|
|
44
|
-
const reason = readOption(args, "--reason") || "cli";
|
|
45
|
-
const config = resolveEvolutionConfig(memoryDir);
|
|
46
|
-
if (!hasFlag(args, "--dry-run")) {
|
|
47
|
-
createEvolutionSnapshot(config, { reason: `curator before ${reason}`, trigger: "external_curator", commitMessage: "memory: snapshot before curator" });
|
|
48
|
-
}
|
|
49
55
|
const result = await runMemoryCuratorOnce({
|
|
50
56
|
memoryStore: new FileMemoryStore(memoryDir),
|
|
51
57
|
auditLog: new JsonlAuditLog(memoryDir),
|
|
52
|
-
reason,
|
|
58
|
+
reason: readOption(args, "--reason") || "cli",
|
|
53
59
|
dryRun: hasFlag(args, "--dry-run"),
|
|
54
60
|
});
|
|
55
|
-
let evolutionCommit = null;
|
|
56
|
-
if (!hasFlag(args, "--dry-run")) {
|
|
57
|
-
evolutionCommit = syncEvolutionAfterChange(config, "memory: sync after external curator");
|
|
58
|
-
if (config.autoPush) pushEvolution(config);
|
|
59
|
-
}
|
|
60
|
-
if (hasFlag(args, "--json")) console.log(JSON.stringify({ ...result, evolutionCommit }, null, 2));
|
|
61
|
-
else console.log(result.summary);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (command === "snapshot") {
|
|
66
|
-
const result = createEvolutionSnapshot(resolveEvolutionConfig(memoryDir), {
|
|
67
|
-
reason: readOption(args, "--reason") || "cli snapshot",
|
|
68
|
-
trigger: "cli",
|
|
69
|
-
commitMessage: "memory: manual snapshot",
|
|
70
|
-
});
|
|
71
61
|
if (hasFlag(args, "--json")) console.log(JSON.stringify(result, null, 2));
|
|
72
|
-
else console.log(result.
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (command === "push") {
|
|
77
|
-
console.log(pushEvolution(resolveEvolutionConfig(memoryDir)) || "Pushed evolution repo.");
|
|
62
|
+
else console.log(result.summary);
|
|
78
63
|
return;
|
|
79
64
|
}
|
|
80
65
|
|
|
@@ -98,6 +83,45 @@ async function main(): Promise<void> {
|
|
|
98
83
|
return;
|
|
99
84
|
}
|
|
100
85
|
|
|
86
|
+
if (command === "mark-dirty") {
|
|
87
|
+
const registryPath = readOption(args, "--registry") || defaultRegistryPath();
|
|
88
|
+
const record = markCurrentRootDirty(process.env, registryPath);
|
|
89
|
+
if (hasFlag(args, "--json")) console.log(JSON.stringify(record, null, 2));
|
|
90
|
+
else console.log(`Marked dirty: ${record.agent_root}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === "manager-scan") {
|
|
95
|
+
const registryPath = readOption(args, "--registry") || defaultRegistryPath();
|
|
96
|
+
const result = await scanDirtyRoots(registryPath);
|
|
97
|
+
if (hasFlag(args, "--json")) console.log(JSON.stringify(result, null, 2));
|
|
98
|
+
else console.log(`Local curator manager processed ${result.processed} root(s), ${result.failures} failure(s).`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (command === "manager-enable") {
|
|
103
|
+
const registryPath = readOption(args, "--registry") || defaultRegistryPath();
|
|
104
|
+
const result = enableCuratorManagerService({ registryPath, cliPath: cliPath(), schedule: readOption(args, "--schedule") });
|
|
105
|
+
console.log(result.message);
|
|
106
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (command === "manager-disable") {
|
|
111
|
+
const registryPath = readOption(args, "--registry") || defaultRegistryPath();
|
|
112
|
+
const result = disableCuratorManagerService({ registryPath, cliPath: cliPath() });
|
|
113
|
+
console.log(result.message);
|
|
114
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (command === "manager-status") {
|
|
119
|
+
const registryPath = readOption(args, "--registry") || defaultRegistryPath();
|
|
120
|
+
const result = getCuratorManagerServiceStatus({ registryPath, cliPath: cliPath() });
|
|
121
|
+
console.log(result.message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
console.error(`Unknown command: ${command}\n\n${usage()}`);
|
|
102
126
|
process.exitCode = 1;
|
|
103
127
|
}
|
|
@@ -21,6 +21,11 @@ type EvolutionEnv = Partial<
|
|
|
21
21
|
| "PI_EVOLUTION_AUTO_COMMIT"
|
|
22
22
|
| "PI_EVOLUTION_AUTO_PUSH"
|
|
23
23
|
| "PI_EVOLUTION_MAX_SNAPSHOTS"
|
|
24
|
+
| "PI_SKILL_DRAFTS_DIR"
|
|
25
|
+
| "PI_AGENT_ROOT"
|
|
26
|
+
| "MULTICA_WORKSPACES_ROOT"
|
|
27
|
+
| "MULTICA_WORKSPACE_ID"
|
|
28
|
+
| "MULTICA_AGENT_ID"
|
|
24
29
|
| "HOME"
|
|
25
30
|
| "USERPROFILE"
|
|
26
31
|
| "HOMEDRIVE"
|
|
@@ -58,8 +63,9 @@ function expandHome(input: string, env: EvolutionEnv): string {
|
|
|
58
63
|
return input;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = process.env): EvolutionConfig {
|
|
66
|
+
export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = process.env, skillDraftsDir?: string): EvolutionConfig {
|
|
62
67
|
const agentDir = path.dirname(memoryDir);
|
|
68
|
+
const resolvedSkillDraftsDir = skillDraftsDir || env.PI_SKILL_DRAFTS_DIR || path.join(agentDir, "skill-drafts");
|
|
63
69
|
return {
|
|
64
70
|
enabled: truthy(env.PI_EVOLUTION_ENABLED, true),
|
|
65
71
|
autoCommit: truthy(env.PI_EVOLUTION_AUTO_COMMIT, true),
|
|
@@ -69,6 +75,6 @@ export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = pr
|
|
|
69
75
|
remote: env.PI_EVOLUTION_REMOTE?.trim() || null,
|
|
70
76
|
branch: env.PI_EVOLUTION_BRANCH || DEFAULT_EVOLUTION_BRANCH,
|
|
71
77
|
memoryDir,
|
|
72
|
-
skillDraftsDir:
|
|
78
|
+
skillDraftsDir: resolvedSkillDraftsDir,
|
|
73
79
|
};
|
|
74
80
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseEntry } from "../curator-core/metadata.ts";
|
|
2
|
+
import type { MemoryStore } from "../curator-store/types.ts";
|
|
3
|
+
import type { PiAgentEnv } from "../paths/resolve-roots.ts";
|
|
4
|
+
import { appendEvolutionCandidate } from "../sync/queue.ts";
|
|
5
|
+
|
|
6
|
+
export type ShareCandidateGenerationResult = {
|
|
7
|
+
created: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
errors: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function generateShareCandidatesFromReview(memoryStore: MemoryStore, env: PiAgentEnv & Record<string, string | undefined> = process.env): Promise<ShareCandidateGenerationResult> {
|
|
13
|
+
const result: ShareCandidateGenerationResult = { created: 0, skipped: 0, errors: [] };
|
|
14
|
+
const entries = await memoryStore.readEntries("review");
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const parsed = parseEntry(entry);
|
|
17
|
+
if (parsed.metadata.type !== "review") continue;
|
|
18
|
+
if (!isShareable(parsed.metadata)) continue;
|
|
19
|
+
const content = extractShareableContent(parsed.body);
|
|
20
|
+
if (!content) {
|
|
21
|
+
result.skipped += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const type = parsed.metadata.kind === "skill_promotion" || parsed.metadata.target_hints?.includes("skill") ? "skill" : "memory";
|
|
26
|
+
const appended = appendEvolutionCandidate({
|
|
27
|
+
type,
|
|
28
|
+
content,
|
|
29
|
+
tags: tagsFromEntry(parsed.metadata.tags || parsed.metadata.kind || "memory"),
|
|
30
|
+
source: "local_curator",
|
|
31
|
+
suggested_scope: suggestedScope(parsed.metadata.scope),
|
|
32
|
+
status: "candidate",
|
|
33
|
+
sensitivity: parsed.metadata.sensitivity as "none" | "local_path" | "personal" | "secret" | "unknown" | undefined,
|
|
34
|
+
source_candidate_ids: (parsed.metadata.source_candidate_ids || parsed.metadata.id || "").split(",").map((id) => id.trim()).filter(Boolean),
|
|
35
|
+
}, env);
|
|
36
|
+
if (appended.appended) result.created += 1;
|
|
37
|
+
else result.skipped += 1;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isShareable(metadata: Record<string, string>): boolean {
|
|
46
|
+
if (metadata.sensitivity === "secret") return false;
|
|
47
|
+
if (metadata.shareability === "team_candidate" || metadata.shareability === "team_ready") return true;
|
|
48
|
+
if (["workspace", "project", "team", "global"].includes(metadata.scope || "")) return true;
|
|
49
|
+
if (metadata.decision === "promote_share_candidate") return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractShareableContent(body: string): string {
|
|
54
|
+
const lines = body.split("\n");
|
|
55
|
+
const memoryLine = lines.find((line) => line.startsWith("Memory:"));
|
|
56
|
+
if (memoryLine) {
|
|
57
|
+
const start = lines.indexOf(memoryLine);
|
|
58
|
+
return [memoryLine.slice("Memory:".length).trim(), ...lines.slice(start + 1)].join("\n").trim();
|
|
59
|
+
}
|
|
60
|
+
const draftIndex = body.indexOf("Draft content:");
|
|
61
|
+
if (draftIndex >= 0) return body.slice(draftIndex + "Draft content:".length).trim();
|
|
62
|
+
return body.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tagsFromEntry(value: string): string[] {
|
|
66
|
+
return value.split(/[ ,#]+/).map((tag) => tag.trim()).filter(Boolean).slice(0, 8);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function suggestedScope(value: string | undefined): "agent" | "workspace" | "project" | "team" | "global" | "agent_type" {
|
|
70
|
+
if (value === "workspace" || value === "project" || value === "team" || value === "global") return value;
|
|
71
|
+
return "workspace";
|
|
72
|
+
}
|
|
@@ -20,6 +20,29 @@ export {
|
|
|
20
20
|
type MemoryPromotionResult,
|
|
21
21
|
type ReviewLifecycleResult,
|
|
22
22
|
} from "./learning/memory.ts";
|
|
23
|
+
export { generateShareCandidatesFromReview, type ShareCandidateGenerationResult } from "./governance/share-candidates.ts";
|
|
24
|
+
export { compactProcessedReviewEntries, type ReviewCompactResult } from "./learning/review-compact.ts";
|
|
25
|
+
export {
|
|
26
|
+
countPendingReviewItems,
|
|
27
|
+
formatPendingReviewList,
|
|
28
|
+
formatPendingReviewSummary,
|
|
29
|
+
listPendingReviewItems,
|
|
30
|
+
type PendingReviewCounts,
|
|
31
|
+
type PendingReviewItem,
|
|
32
|
+
} from "./learning/review-summary.ts";
|
|
33
|
+
export {
|
|
34
|
+
defaultRegistryPath,
|
|
35
|
+
markCurrentRootDirty,
|
|
36
|
+
scanDirtyRoots,
|
|
37
|
+
type CuratorRegistry,
|
|
38
|
+
type CuratorRootRecord,
|
|
39
|
+
} from "./manager/local-curator-manager.ts";
|
|
40
|
+
export { generateProfiles, type ProfileGenerationResult } from "./profile/generator.ts";
|
|
41
|
+
export { syncPull, syncUpload, type SyncPullResult, type SyncUploadResult } from "./sync/connector.ts";
|
|
42
|
+
export { receiveDelivery, type ReceiveDeliveryResult } from "./sync/downflow.ts";
|
|
43
|
+
export { appendFeedbackEvent, buildFeedbackEvent } from "./sync/feedback.ts";
|
|
44
|
+
export { appendEvolutionCandidate } from "./sync/queue.ts";
|
|
45
|
+
export type { Delivery, EvolutionCandidate, FeedbackEvent, FeedbackEventType, EvolutionUnitType } from "./sync/schemas.ts";
|
|
23
46
|
export {
|
|
24
47
|
approveSkillDraft,
|
|
25
48
|
listSkillDraftProposals,
|
|
@@ -28,32 +51,52 @@ export {
|
|
|
28
51
|
type SkillProposal,
|
|
29
52
|
type SkillProposalResult,
|
|
30
53
|
} from "./learning/skills.ts";
|
|
54
|
+
export {
|
|
55
|
+
disableMemorySkill,
|
|
56
|
+
enableMemorySkill,
|
|
57
|
+
formatEnabledSkillsForPrompt,
|
|
58
|
+
formatSkillList,
|
|
59
|
+
listMemorySkills,
|
|
60
|
+
type SkillDisableResult,
|
|
61
|
+
type SkillEnableResult,
|
|
62
|
+
type SkillLifecycleItem,
|
|
63
|
+
type SkillLifecycleList,
|
|
64
|
+
} from "./skills/lifecycle.ts";
|
|
31
65
|
export { DEFAULT_CURATOR_POLICY, createLifecyclePatches, type CuratorPolicy } from "./curator-core/policy.ts";
|
|
32
66
|
export { validateMemoryPatch, type MemoryPatch } from "./curator-core/patch.ts";
|
|
33
67
|
export { DEFAULT_MEMORY_DIR, FileMemoryStore } from "./curator-store/file-store.ts";
|
|
68
|
+
export {
|
|
69
|
+
ensureAgentRoot,
|
|
70
|
+
resolveAgentRoot,
|
|
71
|
+
resolveAgentRoots,
|
|
72
|
+
resolveFeedbackDir,
|
|
73
|
+
resolveInboxDir,
|
|
74
|
+
resolveMemoryRoot,
|
|
75
|
+
resolveProfileDir,
|
|
76
|
+
resolveSharedCacheDir,
|
|
77
|
+
resolveSkillDraftRoot,
|
|
78
|
+
resolveSyncQueueDir,
|
|
79
|
+
type PiAgentEnv,
|
|
80
|
+
type ResolvedAgentRoots,
|
|
81
|
+
} from "./paths/resolve-roots.ts";
|
|
34
82
|
export { ENTRY_DELIMITER, MEMORY_TARGETS, normalizeMemoryTarget, type CuratorState, type MemoryStore, type MemoryTarget } from "./curator-store/types.ts";
|
|
35
|
-
export {
|
|
36
|
-
export {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
type
|
|
52
|
-
type
|
|
53
|
-
type
|
|
54
|
-
|
|
55
|
-
type RestoreResult,
|
|
56
|
-
type RestoreTarget,
|
|
57
|
-
type SnapshotOptions,
|
|
58
|
-
type SnapshotResult,
|
|
59
|
-
} from "./evolution/index.ts";
|
|
83
|
+
export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_MAX_SNAPSHOTS, DEFAULT_EVOLUTION_REMOTE, LEGACY_SHARED_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./evolution/config.ts";
|
|
84
|
+
export { commitEvolutionChanges, ensureEvolutionRepo, getEvolutionGitStatus, pushEvolution, type GitCommitResult, type GitStatus } from "./evolution/git.ts";
|
|
85
|
+
export { buildManifest, createSnapshotId, listManifests, readManifest, writeManifest, type EvolutionManifest } from "./evolution/manifest.ts";
|
|
86
|
+
export { restoreEvolutionSnapshot, type RestoreResult, type RestoreTarget } from "./evolution/restore.ts";
|
|
87
|
+
export { createEvolutionSnapshot, syncEvolutionAfterChange, type SnapshotOptions, type SnapshotResult } from "./evolution/snapshot.ts";
|
|
88
|
+
export { syncCurrentToEvolution } from "./evolution/sync.ts";
|
|
89
|
+
export {
|
|
90
|
+
disableCuratorManagerService,
|
|
91
|
+
disableCuratorService,
|
|
92
|
+
enableCuratorManagerService,
|
|
93
|
+
enableCuratorService,
|
|
94
|
+
getCuratorManagerServiceStatus,
|
|
95
|
+
getCuratorServiceStatus,
|
|
96
|
+
resolveMemoryDir,
|
|
97
|
+
type CuratorManagerServiceResult,
|
|
98
|
+
type CuratorManagerServiceState,
|
|
99
|
+
type CuratorServiceBackend,
|
|
100
|
+
type CuratorServiceResult,
|
|
101
|
+
type CuratorServiceState,
|
|
102
|
+
} from "./service-controller.ts";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseEntry } from "../curator-core/metadata.ts";
|
|
2
|
+
import { ENTRY_DELIMITER } from "../curator-store/types.ts";
|
|
3
|
+
|
|
4
|
+
export type ReviewCompactResult = {
|
|
5
|
+
activeEntries: string[];
|
|
6
|
+
removedEntries: string[];
|
|
7
|
+
removed: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const COMPACT_STATUSES = new Set(["approved", "rejected", "archived", "merged"]);
|
|
11
|
+
|
|
12
|
+
export function compactProcessedReviewEntries(reviewText: string, options: { now?: Date; compactDays?: number } = {}): ReviewCompactResult {
|
|
13
|
+
const now = options.now ?? new Date();
|
|
14
|
+
const compactDays = options.compactDays ?? 30;
|
|
15
|
+
const activeEntries: string[] = [];
|
|
16
|
+
const removedEntries: string[] = [];
|
|
17
|
+
for (const entry of reviewText.split(ENTRY_DELIMITER).map((item) => item.trim()).filter(Boolean)) {
|
|
18
|
+
if (shouldCompactEntry(entry, now, compactDays)) removedEntries.push(entry);
|
|
19
|
+
else activeEntries.push(entry);
|
|
20
|
+
}
|
|
21
|
+
return { activeEntries, removedEntries, removed: removedEntries.length };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shouldCompactEntry(entry: string, now: Date, compactDays: number): boolean {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = parseEntry(entry);
|
|
27
|
+
if (parsed.metadata.type !== "review" || !COMPACT_STATUSES.has(parsed.metadata.status || "")) return false;
|
|
28
|
+
const timestamp = parsed.metadata.approved_at || parsed.metadata.reviewed_at || parsed.metadata.merged_at || parsed.metadata.last_seen;
|
|
29
|
+
if (!timestamp) return compactDays <= 0;
|
|
30
|
+
const ageMs = now.getTime() - Date.parse(timestamp.includes("T") ? timestamp : `${timestamp}T00:00:00.000Z`);
|
|
31
|
+
if (!Number.isFinite(ageMs)) return false;
|
|
32
|
+
return ageMs >= compactDays * 86_400_000;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parseEntry } from "../curator-core/metadata.ts";
|
|
2
|
+
|
|
3
|
+
export type PendingReviewCounts = {
|
|
4
|
+
memory: number;
|
|
5
|
+
skill: number;
|
|
6
|
+
incoming: number;
|
|
7
|
+
total: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PendingReviewItem = {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
status: string;
|
|
14
|
+
confidence?: string;
|
|
15
|
+
target?: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ENTRY_DELIMITER = "\n§\n";
|
|
20
|
+
|
|
21
|
+
export function countPendingReviewItems(reviewText: string): PendingReviewCounts {
|
|
22
|
+
const items = listPendingReviewItems(reviewText);
|
|
23
|
+
return {
|
|
24
|
+
memory: items.filter((item) => item.kind === "memory_promotion").length,
|
|
25
|
+
skill: items.filter((item) => item.kind === "skill_promotion").length,
|
|
26
|
+
incoming: items.filter((item) => item.kind.startsWith("incoming_")).length,
|
|
27
|
+
total: items.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function listPendingReviewItems(reviewText: string, options: { type?: "memory" | "skill"; limit?: number } = {}): PendingReviewItem[] {
|
|
32
|
+
const limit = Math.max(0, options.limit ?? 20);
|
|
33
|
+
const entries = reviewText.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
|
|
34
|
+
const items: PendingReviewItem[] = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = parseEntry(entry);
|
|
38
|
+
if (parsed.metadata.type !== "review" || parsed.metadata.status !== "proposed") continue;
|
|
39
|
+
const kind = parsed.metadata.kind || "review";
|
|
40
|
+
if (options.type === "memory" && kind !== "memory_promotion") continue;
|
|
41
|
+
if (options.type === "skill" && kind !== "skill_promotion") continue;
|
|
42
|
+
items.push({
|
|
43
|
+
id: parsed.metadata.id || "",
|
|
44
|
+
kind,
|
|
45
|
+
status: parsed.metadata.status || "",
|
|
46
|
+
confidence: parsed.metadata.confidence,
|
|
47
|
+
target: parsed.metadata.promotes_to,
|
|
48
|
+
summary: summarizeReviewBody(parsed.body),
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
// Malformed review entries should never break startup hints or review commands.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return limit > 0 ? items.slice(0, limit) : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatPendingReviewSummary(counts: PendingReviewCounts): string {
|
|
58
|
+
if (counts.total === 0) return "No pending memory/skill proposals.";
|
|
59
|
+
return [
|
|
60
|
+
`Curator completed: ${counts.memory} memory proposal(s) pending, ${counts.skill} skill proposal(s) pending.`,
|
|
61
|
+
"Next: run /memory-review, or approve with memory_learning_approve id=<proposal-id>, reject with memory_learning_reject id=<proposal-id>.",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatPendingReviewList(items: PendingReviewItem[], counts: PendingReviewCounts): string {
|
|
66
|
+
if (items.length === 0) return "No pending memory/skill proposals.";
|
|
67
|
+
const lines = [`Memory review: ${counts.memory} memory / ${counts.skill} skill proposals pending.`];
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const confidence = item.confidence ? ` confidence=${item.confidence}` : "";
|
|
70
|
+
const target = item.target ? ` target=${item.target}` : "";
|
|
71
|
+
lines.push(`- ${item.id || "<missing-id>"}: ${item.kind}${confidence}${target}`);
|
|
72
|
+
if (item.summary) lines.push(` ${item.summary}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("Approve with /memory-review approve <id>, reject with /memory-review reject <id>, or inspect with /memory-review show <id>.");
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function summarizeReviewBody(body: string): string {
|
|
79
|
+
const line = body.split("\n").map((candidate) => candidate.trim()).find(Boolean) || "";
|
|
80
|
+
return line.length > 180 ? `${line.slice(0, 177)}...` : line;
|
|
81
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { JsonlAuditLog } from "../curator-core/audit.ts";
|
|
4
|
+
import { runMemoryCuratorOnce } from "../curator-core/curate.ts";
|
|
5
|
+
import { FileMemoryStore } from "../curator-store/file-store.ts";
|
|
6
|
+
import { applyReviewLifecycle, proposeMemoryPromotions } from "../learning/memory.ts";
|
|
7
|
+
import { proposeSkillDrafts } from "../learning/skills.ts";
|
|
8
|
+
import { ensureAgentRoot, resolveAgentRoots, type PiAgentEnv } from "../paths/resolve-roots.ts";
|
|
9
|
+
import { generateShareCandidatesFromReview } from "../governance/share-candidates.ts";
|
|
10
|
+
import { generateProfiles } from "../profile/generator.ts";
|
|
11
|
+
|
|
12
|
+
export type CuratorRootRecord = {
|
|
13
|
+
workspace_id?: string;
|
|
14
|
+
agent_id?: string;
|
|
15
|
+
agent_root: string;
|
|
16
|
+
memory_dir: string;
|
|
17
|
+
skill_dir: string;
|
|
18
|
+
dirty_since?: string;
|
|
19
|
+
last_curated_at?: string;
|
|
20
|
+
last_synced_at?: string;
|
|
21
|
+
status: "idle" | "dirty" | "running" | "error";
|
|
22
|
+
last_error?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CuratorRegistry = {
|
|
26
|
+
roots: CuratorRootRecord[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function defaultRegistryPath(env: PiAgentEnv = process.env): string {
|
|
30
|
+
const roots = resolveAgentRoots(env);
|
|
31
|
+
if (env.MULTICA_WORKSPACES_ROOT) return join(env.MULTICA_WORKSPACES_ROOT, ".pi-curator", "registry.json");
|
|
32
|
+
if (roots.agentRoot) return join(dirname(dirname(dirname(dirname(roots.agentRoot)))), ".pi-curator", "registry.json");
|
|
33
|
+
return join(env.HOME || process.cwd(), ".pi", "agent", "memory-curator-manager", "registry.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function markCurrentRootDirty(env: PiAgentEnv = process.env, registryPath = defaultRegistryPath(env)): CuratorRootRecord {
|
|
37
|
+
const roots = ensureAgentRoot(env);
|
|
38
|
+
if (!roots.agentRoot) throw new Error("dirty root tracking requires PI_AGENT_ROOT or Multica agent env");
|
|
39
|
+
const registry = readRegistry(registryPath);
|
|
40
|
+
const existing = registry.roots.find((entry) => entry.agent_root === roots.agentRoot);
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const record: CuratorRootRecord = {
|
|
43
|
+
workspace_id: roots.workspaceId,
|
|
44
|
+
agent_id: roots.agentId,
|
|
45
|
+
agent_root: roots.agentRoot,
|
|
46
|
+
memory_dir: roots.memoryDir,
|
|
47
|
+
skill_dir: roots.skillsDir,
|
|
48
|
+
dirty_since: existing?.dirty_since || now,
|
|
49
|
+
last_curated_at: existing?.last_curated_at,
|
|
50
|
+
last_synced_at: existing?.last_synced_at,
|
|
51
|
+
status: "dirty",
|
|
52
|
+
};
|
|
53
|
+
const next = registry.roots.filter((entry) => entry.agent_root !== roots.agentRoot);
|
|
54
|
+
next.push(record);
|
|
55
|
+
writeRegistry(registryPath, { roots: next });
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function scanDirtyRoots(registryPath: string): Promise<{ processed: number; failures: number }> {
|
|
60
|
+
const managerLockPath = join(dirname(registryPath), ".manager-scan.lock");
|
|
61
|
+
if (!acquireLock(managerLockPath, 6 * 60 * 60 * 1000)) throw new Error(`curator manager lock exists for ${registryPath}`);
|
|
62
|
+
try {
|
|
63
|
+
const registry = readRegistry(registryPath);
|
|
64
|
+
let processed = 0;
|
|
65
|
+
let failures = 0;
|
|
66
|
+
for (const root of registry.roots) {
|
|
67
|
+
if (root.status !== "dirty" && !root.dirty_since) continue;
|
|
68
|
+
try {
|
|
69
|
+
await curateRoot(root);
|
|
70
|
+
root.status = "idle";
|
|
71
|
+
root.dirty_since = undefined;
|
|
72
|
+
root.last_curated_at = new Date().toISOString();
|
|
73
|
+
root.last_error = undefined;
|
|
74
|
+
processed += 1;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
root.status = "error";
|
|
77
|
+
root.last_error = error instanceof Error ? error.message : String(error);
|
|
78
|
+
failures += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
writeRegistry(registryPath, registry);
|
|
82
|
+
return { processed, failures };
|
|
83
|
+
} finally {
|
|
84
|
+
releaseLock(managerLockPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function curateRoot(root: CuratorRootRecord): Promise<void> {
|
|
89
|
+
const lockPath = join(root.agent_root, ".curator.lock");
|
|
90
|
+
mkdirSync(root.agent_root, { recursive: true });
|
|
91
|
+
if (!acquireLock(lockPath, 24 * 60 * 60 * 1000)) throw new Error(`curator lock exists for ${root.agent_root}`);
|
|
92
|
+
try {
|
|
93
|
+
const store = new FileMemoryStore(root.memory_dir);
|
|
94
|
+
await runMemoryCuratorOnce({ memoryStore: store, auditLog: new JsonlAuditLog(root.memory_dir), reason: "local-curator-manager" });
|
|
95
|
+
await applyReviewLifecycle(store);
|
|
96
|
+
await proposeMemoryPromotions(store);
|
|
97
|
+
await proposeSkillDrafts(store, { draftsDir: join(root.skill_dir, "drafts") });
|
|
98
|
+
const env = { PI_AGENT_ROOT: root.agent_root, MULTICA_WORKSPACE_ID: root.workspace_id, MULTICA_AGENT_ID: root.agent_id };
|
|
99
|
+
await generateShareCandidatesFromReview(store, env);
|
|
100
|
+
generateProfiles(env);
|
|
101
|
+
} finally {
|
|
102
|
+
try {
|
|
103
|
+
renameSync(lockPath, `${lockPath}.last`);
|
|
104
|
+
} catch {
|
|
105
|
+
releaseLock(lockPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function acquireLock(lockPath: string, staleAfterMs: number): boolean {
|
|
111
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
112
|
+
if (existsSync(lockPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
115
|
+
if (age > staleAfterMs) rmSync(lockPath, { force: true });
|
|
116
|
+
else return false;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(lockPath, `${process.pid}\n${new Date().toISOString()}\n`, { encoding: "utf-8", flag: "wx" });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function releaseLock(lockPath: string): void {
|
|
126
|
+
try {
|
|
127
|
+
rmSync(lockPath, { force: true });
|
|
128
|
+
} catch {
|
|
129
|
+
// best effort cleanup
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readRegistry(registryPath: string): CuratorRegistry {
|
|
134
|
+
if (!existsSync(registryPath)) return { roots: [] };
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(readFileSync(registryPath, "utf-8")) as CuratorRegistry;
|
|
137
|
+
return { roots: Array.isArray(parsed.roots) ? parsed.roots : [] };
|
|
138
|
+
} catch {
|
|
139
|
+
return { roots: [] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writeRegistry(registryPath: string, registry: CuratorRegistry): void {
|
|
144
|
+
mkdirSync(dirname(registryPath), { recursive: true });
|
|
145
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
146
|
+
}
|