portable-agent-layer 0.16.0 → 0.18.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/README.md +13 -3
- package/assets/templates/PAL/ALGORITHM.md +30 -9
- package/assets/templates/PAL/README.md +6 -5
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +7 -0
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +3 -2
- package/src/hooks/lib/claude-md.ts +15 -9
- package/src/hooks/lib/paths.ts +2 -0
- package/src/targets/lib.ts +2 -1
- package/src/tools/agent/analyze.ts +157 -0
- package/src/tools/agent/wisdom-frame.ts +235 -0
- package/src/tools/export.ts +23 -17
- package/src/tools/import.ts +65 -77
- package/src/tools/relationship-reflect.ts +80 -85
- package/src/tools/session-summary.ts +44 -41
- package/src/tools/token-cost.ts +134 -92
- package/src/tools/analyze.ts +0 -152
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* WisdomFrameUpdater — Update wisdom frames with new observations.
|
|
4
|
+
*
|
|
5
|
+
* Takes a domain and observation, updates the appropriate frame file.
|
|
6
|
+
* Creates the frame if it doesn't exist. Tracks observation count and
|
|
7
|
+
* evolution log. Principles are marked [CRYSTAL: N%] manually when
|
|
8
|
+
* confidence is high enough.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bun run tool:wisdom-frame --domain communication --observation "prefers bullet points"
|
|
12
|
+
* bun run tool:wisdom-frame --domain development --observation "refactoring without tests caused regressions" --type anti-pattern
|
|
13
|
+
* bun run tool:wisdom-frame --domain workflow --observation "always run type-check after edits" --type principle
|
|
14
|
+
*
|
|
15
|
+
* Types: principle, contextual-rule, anti-pattern, evolution (default)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { parseArgs } from "node:util";
|
|
21
|
+
import { paths } from "../../hooks/lib/paths";
|
|
22
|
+
|
|
23
|
+
// ── Types ──
|
|
24
|
+
|
|
25
|
+
type ObservationType = "principle" | "contextual-rule" | "anti-pattern" | "evolution";
|
|
26
|
+
|
|
27
|
+
interface UpdateResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
domain: string;
|
|
30
|
+
type: ObservationType;
|
|
31
|
+
message: string;
|
|
32
|
+
framePath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Helpers ──
|
|
36
|
+
|
|
37
|
+
function date(): string {
|
|
38
|
+
return new Date().toISOString().slice(0, 10);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseObservationCount(content: string): number {
|
|
42
|
+
const match = content.match(/\*\*Observation Count:\*\*\s*(\d+)/);
|
|
43
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function incrementCount(content: string): string {
|
|
47
|
+
const current = parseObservationCount(content);
|
|
48
|
+
return content.replace(/(\*\*Observation Count:\*\*\s*)\d+/, `$1${current + 1}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function updateDate(content: string): string {
|
|
52
|
+
return content.replace(/(\*\*Last Updated:\*\*\s*)\S+/, `$1${date()}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function appendToSection(
|
|
56
|
+
content: string,
|
|
57
|
+
sectionHeader: string,
|
|
58
|
+
entry: string,
|
|
59
|
+
fallbackBefore?: string
|
|
60
|
+
): string {
|
|
61
|
+
const idx = content.indexOf(sectionHeader);
|
|
62
|
+
|
|
63
|
+
if (idx === -1) {
|
|
64
|
+
// Section doesn't exist — insert before fallback or at end
|
|
65
|
+
const insertAt = fallbackBefore ? content.indexOf(fallbackBefore) : -1;
|
|
66
|
+
const pos = insertAt !== -1 ? insertAt : content.length;
|
|
67
|
+
return `${content.slice(0, pos)}${sectionHeader}\n\n${entry}\n\n${content.slice(pos)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find end of section (next ## or EOF)
|
|
71
|
+
const afterSection = content.slice(idx + sectionHeader.length);
|
|
72
|
+
const nextSection = afterSection.indexOf("\n## ");
|
|
73
|
+
const insertPoint =
|
|
74
|
+
nextSection === -1 ? content.length : idx + sectionHeader.length + nextSection;
|
|
75
|
+
|
|
76
|
+
return `${content.slice(0, insertPoint)}\n${entry}${content.slice(insertPoint)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Core Update ──
|
|
80
|
+
|
|
81
|
+
export function updateFrame(
|
|
82
|
+
domain: string,
|
|
83
|
+
observation: string,
|
|
84
|
+
type: ObservationType = "evolution"
|
|
85
|
+
): UpdateResult {
|
|
86
|
+
const framesDir = paths.wisdom();
|
|
87
|
+
const framePath = resolve(framesDir, `${domain}.md`);
|
|
88
|
+
|
|
89
|
+
// Create frame if it doesn't exist
|
|
90
|
+
if (!existsSync(framePath)) {
|
|
91
|
+
mkdirSync(framesDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const content = `# Frame: ${domain.charAt(0).toUpperCase() + domain.slice(1)}
|
|
94
|
+
|
|
95
|
+
## Meta
|
|
96
|
+
- **Domain:** ${domain}
|
|
97
|
+
- **Observation Count:** 1
|
|
98
|
+
- **Last Updated:** ${date()}
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Core Principles
|
|
103
|
+
|
|
104
|
+
*No crystallized principles yet. Observations accumulating.*
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Contextual Rules
|
|
109
|
+
|
|
110
|
+
${type === "contextual-rule" ? `- ${observation} (${date()})` : "*None yet.*"}
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Anti-Patterns
|
|
115
|
+
|
|
116
|
+
${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed` : "*None yet.*"}
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Evolution Log
|
|
121
|
+
- ${date()}: Frame created — ${observation}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
writeFileSync(framePath, content);
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
domain,
|
|
128
|
+
type,
|
|
129
|
+
message: `Created new frame "${domain}" with initial observation`,
|
|
130
|
+
framePath,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Update existing frame
|
|
135
|
+
let content = readFileSync(framePath, "utf-8");
|
|
136
|
+
content = incrementCount(content);
|
|
137
|
+
content = updateDate(content);
|
|
138
|
+
|
|
139
|
+
const evolutionEntry = `- ${date()}: ${observation}`;
|
|
140
|
+
|
|
141
|
+
switch (type) {
|
|
142
|
+
case "anti-pattern":
|
|
143
|
+
content = appendToSection(
|
|
144
|
+
content,
|
|
145
|
+
"## Anti-Patterns",
|
|
146
|
+
`\n### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed`,
|
|
147
|
+
"## Evolution Log"
|
|
148
|
+
);
|
|
149
|
+
content = appendToSection(content, "## Evolution Log", evolutionEntry);
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case "contextual-rule":
|
|
153
|
+
content = appendToSection(
|
|
154
|
+
content,
|
|
155
|
+
"## Contextual Rules",
|
|
156
|
+
`- ${observation} (${date()})`,
|
|
157
|
+
"## Anti-Patterns"
|
|
158
|
+
);
|
|
159
|
+
content = appendToSection(content, "## Evolution Log", evolutionEntry);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "principle":
|
|
163
|
+
// Principles logged for manual crystallization — don't auto-add to Core Principles
|
|
164
|
+
content = appendToSection(
|
|
165
|
+
content,
|
|
166
|
+
"## Evolution Log",
|
|
167
|
+
`- ${date()}: Principle candidate — ${observation}`
|
|
168
|
+
);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case "evolution":
|
|
172
|
+
default:
|
|
173
|
+
content = appendToSection(content, "## Evolution Log", evolutionEntry);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
writeFileSync(framePath, content);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
domain,
|
|
182
|
+
type,
|
|
183
|
+
message: `Updated "${domain}" frame with ${type}: ${observation}`,
|
|
184
|
+
framePath,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── CLI ──
|
|
189
|
+
|
|
190
|
+
function run() {
|
|
191
|
+
const { values } = parseArgs({
|
|
192
|
+
args: Bun.argv.slice(2),
|
|
193
|
+
options: {
|
|
194
|
+
domain: { type: "string", short: "d" },
|
|
195
|
+
observation: { type: "string", short: "o" },
|
|
196
|
+
type: { type: "string", short: "t" },
|
|
197
|
+
help: { type: "boolean", short: "h" },
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (values.help) {
|
|
202
|
+
console.log(`
|
|
203
|
+
WisdomFrameUpdater — Update wisdom frames with observations
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
bun run tool:wisdom-frame --domain <domain> --observation "text" [--type <type>]
|
|
207
|
+
|
|
208
|
+
Domains:
|
|
209
|
+
development, workflow, communication, infrastructure, integration, or any custom domain
|
|
210
|
+
|
|
211
|
+
Types:
|
|
212
|
+
principle High-confidence pattern (logged for manual crystallization)
|
|
213
|
+
contextual-rule Context-specific behavioral rule
|
|
214
|
+
anti-pattern Something to avoid
|
|
215
|
+
evolution General observation (default)
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
bun run tool:wisdom-frame -d workflow -o "always run type-check after edits"
|
|
219
|
+
bun run tool:wisdom-frame -d development -o "mocking DB hides migration bugs" -t anti-pattern
|
|
220
|
+
bun run tool:wisdom-frame -d communication -o "user prefers terse summaries" -t principle
|
|
221
|
+
`);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!values.domain || !values.observation) {
|
|
226
|
+
console.error("Required: --domain and --observation");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cliType = (values.type || "evolution") as ObservationType;
|
|
231
|
+
const result = updateFrame(values.domain, values.observation, cliType);
|
|
232
|
+
console.log(JSON.stringify(result, null, 2));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (import.meta.main) run();
|
package/src/tools/export.ts
CHANGED
|
@@ -10,25 +10,31 @@ import { resolve } from "node:path";
|
|
|
10
10
|
import { collectExportFiles, exportZip, timestamp } from "../hooks/lib/export";
|
|
11
11
|
import { palHome } from "../hooks/lib/paths";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const dryRun = args.includes("--dry-run");
|
|
15
|
-
const pathArg = args.find((a) => a !== "--dry-run");
|
|
13
|
+
export { collectExportFiles, exportZip };
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
function run() {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const dryRun = args.includes("--dry-run");
|
|
18
|
+
const pathArg = args.find((a) => a !== "--dry-run");
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (count === 0) {
|
|
30
|
-
console.log("Nothing to export — no gitignored personal files found.");
|
|
20
|
+
const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
|
|
21
|
+
|
|
22
|
+
if (dryRun) {
|
|
23
|
+
const files = collectExportFiles();
|
|
24
|
+
if (files.length === 0) {
|
|
25
|
+
console.log("Nothing to export — no gitignored personal files found.");
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`Would export ${files.length} files → ${outputPath}\n`);
|
|
28
|
+
for (const f of files) console.log(` ${f}`);
|
|
29
|
+
}
|
|
31
30
|
} else {
|
|
32
|
-
|
|
31
|
+
const count = exportZip(outputPath);
|
|
32
|
+
if (count === 0) {
|
|
33
|
+
console.log("Nothing to export — no gitignored personal files found.");
|
|
34
|
+
} else {
|
|
35
|
+
console.log(`Exported ${count} files → ${outputPath}`);
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
|
|
40
|
+
if (import.meta.main) run();
|
package/src/tools/import.ts
CHANGED
|
@@ -13,53 +13,21 @@ import { createInterface } from "node:readline";
|
|
|
13
13
|
import AdmZip from "adm-zip";
|
|
14
14
|
import { palHome } from "../hooks/lib/paths";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const dryRun = args.includes("--dry-run");
|
|
19
|
-
const pathArg = args.find((a) => a !== "--dry-run");
|
|
16
|
+
export function findLatestExport(root: string): string | null {
|
|
17
|
+
const candidates: string[] = [];
|
|
20
18
|
|
|
21
|
-
async function confirm(message: string): Promise<boolean> {
|
|
22
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
-
return new Promise((res) => {
|
|
24
|
-
rl.question(`${message} [y/N] `, (answer) => {
|
|
25
|
-
rl.close();
|
|
26
|
-
res(answer.trim().toLowerCase() === "y");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function findLatestExport(): string | null {
|
|
32
|
-
const files = readdirSync(repoRoot)
|
|
33
|
-
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
34
|
-
.sort()
|
|
35
|
-
.reverse();
|
|
36
|
-
|
|
37
|
-
// Also check backups/
|
|
38
19
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(f) =>
|
|
43
|
-
|
|
44
|
-
f.endsWith(".zip")
|
|
45
|
-
)
|
|
46
|
-
.map((f) => ({ name: f, path: resolve(backupDir, f) }))
|
|
47
|
-
.sort((a, b) => b.name.localeCompare(a.name));
|
|
48
|
-
if (backups.length > 0) files.push(backups[0].name);
|
|
20
|
+
candidates.push(
|
|
21
|
+
...readdirSync(root)
|
|
22
|
+
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
23
|
+
.map((f) => resolve(root, f))
|
|
24
|
+
);
|
|
49
25
|
} catch {
|
|
50
|
-
|
|
26
|
+
/* empty */
|
|
51
27
|
}
|
|
52
28
|
|
|
53
|
-
if (files.length === 0) return null;
|
|
54
|
-
|
|
55
|
-
// Find the most recent by mtime across both locations
|
|
56
|
-
const candidates = [
|
|
57
|
-
...readdirSync(repoRoot)
|
|
58
|
-
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
59
|
-
.map((f) => resolve(repoRoot, f)),
|
|
60
|
-
];
|
|
61
29
|
try {
|
|
62
|
-
const backupDir = resolve(
|
|
30
|
+
const backupDir = resolve(root, "backups");
|
|
63
31
|
candidates.push(
|
|
64
32
|
...readdirSync(backupDir)
|
|
65
33
|
.filter(
|
|
@@ -70,54 +38,74 @@ function findLatestExport(): string | null {
|
|
|
70
38
|
.map((f) => resolve(backupDir, f))
|
|
71
39
|
);
|
|
72
40
|
} catch {
|
|
73
|
-
|
|
41
|
+
/* empty */
|
|
74
42
|
}
|
|
75
43
|
|
|
76
44
|
if (candidates.length === 0) return null;
|
|
77
45
|
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
78
46
|
}
|
|
79
47
|
|
|
80
|
-
|
|
81
|
-
|
|
48
|
+
export function importZip(zipPath: string, targetDir: string, dryRun: boolean): number {
|
|
49
|
+
const zip = new AdmZip(zipPath);
|
|
50
|
+
const entries = zip.getEntries();
|
|
82
51
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const latest = findLatestExport();
|
|
87
|
-
if (!latest) {
|
|
88
|
-
console.error(
|
|
89
|
-
"No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
|
|
90
|
-
);
|
|
91
|
-
process.exit(1);
|
|
52
|
+
if (entries.length === 0) {
|
|
53
|
+
console.log("Archive is empty — nothing to import.");
|
|
54
|
+
return 0;
|
|
92
55
|
}
|
|
93
|
-
console.log(`Found: ${latest}`);
|
|
94
|
-
const zip = new AdmZip(latest);
|
|
95
|
-
const entries = zip.getEntries();
|
|
96
|
-
console.log(
|
|
97
|
-
`Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
|
|
98
|
-
);
|
|
99
56
|
|
|
100
|
-
if (
|
|
101
|
-
console.log(
|
|
102
|
-
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
console.log(`Would import ${entries.length} files → ${targetDir}\n`);
|
|
59
|
+
for (const e of entries) console.log(` ${e.entryName}`);
|
|
60
|
+
return entries.length;
|
|
103
61
|
}
|
|
104
|
-
|
|
62
|
+
|
|
63
|
+
zip.extractAllTo(targetDir, true);
|
|
64
|
+
console.log(`Imported ${entries.length} files → ${targetDir}`);
|
|
65
|
+
console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
|
|
66
|
+
return entries.length;
|
|
105
67
|
}
|
|
106
68
|
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
69
|
+
async function run() {
|
|
70
|
+
const repoRoot = palHome();
|
|
71
|
+
const args = process.argv.slice(2);
|
|
72
|
+
const dryRun = args.includes("--dry-run");
|
|
73
|
+
const pathArg = args.find((a) => a !== "--dry-run");
|
|
110
74
|
|
|
111
|
-
|
|
112
|
-
console.log("Archive is empty — nothing to import.");
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
75
|
+
let zipPath: string;
|
|
115
76
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
77
|
+
if (pathArg) {
|
|
78
|
+
zipPath = resolve(pathArg);
|
|
79
|
+
} else {
|
|
80
|
+
const latest = findLatestExport(repoRoot);
|
|
81
|
+
if (!latest) {
|
|
82
|
+
console.error(
|
|
83
|
+
"No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`Found: ${latest}`);
|
|
88
|
+
const zip = new AdmZip(latest);
|
|
89
|
+
const entries = zip.getEntries();
|
|
90
|
+
console.log(
|
|
91
|
+
`Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
95
|
+
const answer = await new Promise<string>((res) => {
|
|
96
|
+
rl.question("Import this file? [y/N] ", (a) => {
|
|
97
|
+
rl.close();
|
|
98
|
+
res(a);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
102
|
+
console.log("Cancelled.");
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
zipPath = latest;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
importZip(zipPath, repoRoot, dryRun);
|
|
123
109
|
}
|
|
110
|
+
|
|
111
|
+
if (import.meta.main) run();
|