portable-agent-layer 0.20.0 → 0.22.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 +4 -3
- package/assets/agents/gemini-researcher.md +73 -0
- package/assets/agents/grok-researcher.md +10 -1
- package/assets/agents/perplexity-researcher.md +67 -0
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +3 -3
- package/assets/skills/create-pdf/SKILL.md +53 -76
- package/assets/skills/fyzz-chat-api/SKILL.md +3 -3
- package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
- package/assets/skills/research/SKILL.md +9 -9
- package/assets/skills/research/tools/gemini-search.ts +186 -0
- package/assets/skills/research/tools/grok-search.ts +3 -3
- package/assets/skills/research/tools/perplexity-search.ts +150 -0
- package/assets/templates/PAL/ALGORITHM.md +71 -9
- package/assets/templates/PAL/WORK_TRACKING.md +2 -9
- package/package.json +1 -1
- package/src/cli/index.ts +18 -6
- package/src/hooks/handlers/rating.ts +1 -1
- package/src/hooks/handlers/relationship.ts +2 -2
- package/src/hooks/handlers/session-name.ts +1 -1
- package/src/hooks/handlers/work-learning.ts +9 -0
- package/src/hooks/lib/claude-md.ts +17 -5
- package/src/hooks/lib/context.ts +35 -55
- package/src/hooks/lib/export.ts +3 -2
- package/src/hooks/lib/graduation.ts +1 -1
- package/src/hooks/lib/inference.ts +1 -1
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/readme-sync.ts +6 -6
- package/src/hooks/lib/security.ts +5 -1
- package/src/hooks/lib/work-tracking.ts +29 -42
- package/src/targets/claude/install.ts +2 -0
- package/src/targets/cursor/install.ts +2 -0
- package/src/targets/lib.ts +93 -0
- package/src/targets/opencode/install.ts +2 -0
- package/src/tools/agent/algorithm-reflect.ts +120 -0
- package/src/tools/agent/wisdom-frame.ts +0 -2
- package/assets/agents/claude-researcher.md +0 -43
- package/assets/agents/investigative-researcher.md +0 -44
package/src/hooks/lib/context.ts
CHANGED
|
@@ -15,12 +15,7 @@ import { readSessionNames } from "./session-names";
|
|
|
15
15
|
import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
16
16
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
17
17
|
import { readFramePrinciples } from "./wisdom";
|
|
18
|
-
import {
|
|
19
|
-
activeProjects,
|
|
20
|
-
readSessions,
|
|
21
|
-
recentSessions,
|
|
22
|
-
staleProjects,
|
|
23
|
-
} from "./work-tracking";
|
|
18
|
+
import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
|
|
24
19
|
|
|
25
20
|
interface PalSettings {
|
|
26
21
|
loadAtStartup?: { files?: string[] };
|
|
@@ -82,44 +77,16 @@ export function loadActiveWork(): { text: string; summary: string | null } | nul
|
|
|
82
77
|
try {
|
|
83
78
|
const cwd = process.cwd();
|
|
84
79
|
const allRecent = recentSessions(48);
|
|
85
|
-
const projects = activeProjects();
|
|
86
|
-
const stale = staleProjects(7);
|
|
87
80
|
|
|
88
|
-
if (allRecent.length === 0
|
|
81
|
+
if (allRecent.length === 0) return null;
|
|
89
82
|
|
|
90
83
|
const lines: string[] = [];
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (projects.length > 0) {
|
|
102
|
-
lines.push("", "### Active Projects");
|
|
103
|
-
for (const p of projects) {
|
|
104
|
-
const sessionCount = p.sessions.length;
|
|
105
|
-
const ago = formatAgo(p.updated);
|
|
106
|
-
lines.push(`- **${p.name}** (${sessionCount} sessions, last: ${ago})`);
|
|
107
|
-
if (p.nextSteps.length > 0) {
|
|
108
|
-
lines.push(` Next: ${p.nextSteps[0]}`);
|
|
109
|
-
}
|
|
110
|
-
if (p.blockers.length > 0) {
|
|
111
|
-
lines.push(` Blockers: ${p.blockers.join(", ")}`);
|
|
112
|
-
} else {
|
|
113
|
-
lines.push(" Blockers: None");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (stale.length > 0) {
|
|
119
|
-
lines.push("", "### Stale Projects (>7d inactive)");
|
|
120
|
-
for (const p of stale) {
|
|
121
|
-
lines.push(`- **${p.name}** — last active ${formatAgo(p.updated)}`);
|
|
122
|
-
}
|
|
85
|
+
lines.push("## Recent Work (last 48h)");
|
|
86
|
+
for (const s of allRecent.slice(-10).reverse()) {
|
|
87
|
+
const ago = formatAgo(s.ts);
|
|
88
|
+
const here = s.cwd === cwd ? " *" : "";
|
|
89
|
+
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
123
90
|
}
|
|
124
91
|
|
|
125
92
|
// Summary from most recent session
|
|
@@ -240,25 +207,15 @@ export function loadLearningDigest(): string {
|
|
|
240
207
|
const entries = readLearnings(paths.sessionLearning(), 10);
|
|
241
208
|
if (entries.length === 0) return "";
|
|
242
209
|
|
|
243
|
-
|
|
244
|
-
const other = entries.filter((e) => e.cwd !== cwd).slice(0,
|
|
210
|
+
// This-project learnings are now in loadProjectHistoryContext(); only show cross-project here
|
|
211
|
+
const other = entries.filter((e) => e.cwd !== cwd).slice(0, 5);
|
|
245
212
|
|
|
246
|
-
if (
|
|
213
|
+
if (other.length === 0) return "";
|
|
247
214
|
|
|
248
215
|
const lines: string[] = [];
|
|
249
216
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (const e of thisProject) {
|
|
253
|
-
lines.push(`- **${e.title}**`);
|
|
254
|
-
if (e.insights) lines.push(` ${e.insights.split("\n")[0].slice(0, 150)}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (other.length > 0) {
|
|
259
|
-
lines.push(thisProject.length > 0 ? "" : "", "## Other Recent Learnings");
|
|
260
|
-
for (const e of other) lines.push(`- ${e.title}`);
|
|
261
|
-
}
|
|
217
|
+
lines.push("## Other Recent Learnings");
|
|
218
|
+
for (const e of other) lines.push(`- ${e.title}`);
|
|
262
219
|
|
|
263
220
|
return lines.join("\n");
|
|
264
221
|
} catch {
|
|
@@ -346,6 +303,25 @@ export function loadSignalTrends(): string {
|
|
|
346
303
|
}
|
|
347
304
|
}
|
|
348
305
|
|
|
306
|
+
/** Load per-project session history for the current working directory */
|
|
307
|
+
export function loadProjectHistoryContext(): string {
|
|
308
|
+
try {
|
|
309
|
+
const cwd = process.cwd();
|
|
310
|
+
const entries = readProjectHistory(cwd, 15);
|
|
311
|
+
if (entries.length === 0) return "";
|
|
312
|
+
|
|
313
|
+
const lines: string[] = ["## This Project — Session History"];
|
|
314
|
+
for (const e of entries) {
|
|
315
|
+
lines.push(`- **${e.title}** (${e.date})`);
|
|
316
|
+
if (e.summary) lines.push(` ${e.summary.split("\n")[0].slice(0, 150)}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return lines.join("\n");
|
|
320
|
+
} catch {
|
|
321
|
+
return "";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
349
325
|
/** Load recent relationship notes (today + yesterday) */
|
|
350
326
|
export function loadRelationshipContext(): string {
|
|
351
327
|
try {
|
|
@@ -373,6 +349,9 @@ export function buildSystemReminder(): string {
|
|
|
373
349
|
? loadRelationshipContext()
|
|
374
350
|
: "";
|
|
375
351
|
const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
|
|
352
|
+
const projectHistory = isEnabled(settings, "projectHistory")
|
|
353
|
+
? loadProjectHistoryContext()
|
|
354
|
+
: "";
|
|
376
355
|
const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
|
|
377
356
|
const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
|
|
378
357
|
const synthesis = isEnabled(settings, "synthesis")
|
|
@@ -384,6 +363,7 @@ export function buildSystemReminder(): string {
|
|
|
384
363
|
if (wisdom) parts.push(wisdom);
|
|
385
364
|
if (opinions) parts.push(opinions);
|
|
386
365
|
if (relationship) parts.push(relationship);
|
|
366
|
+
if (projectHistory) parts.push(projectHistory);
|
|
387
367
|
if (digest) parts.push(digest);
|
|
388
368
|
if (synthesis) parts.push(synthesis);
|
|
389
369
|
if (trends) parts.push(trends);
|
package/src/hooks/lib/export.ts
CHANGED
|
@@ -15,7 +15,8 @@ const EXPORT_DIRS = ["telos", "memory"];
|
|
|
15
15
|
const SKIP_PATTERNS = ["memory/downloads"];
|
|
16
16
|
|
|
17
17
|
function shouldSkip(relPath: string): boolean {
|
|
18
|
-
|
|
18
|
+
const normalized = relPath.replaceAll("\\", "/");
|
|
19
|
+
return SKIP_PATTERNS.some((p) => normalized.startsWith(p));
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Recursively collect all files under a directory, returning paths relative to root. */
|
|
@@ -32,7 +33,7 @@ function walkDir(dir: string, root: string): string[] {
|
|
|
32
33
|
if (entry.isDirectory()) {
|
|
33
34
|
files.push(...walkDir(fullPath, root));
|
|
34
35
|
} else if (entry.isFile()) {
|
|
35
|
-
files.push(relPath);
|
|
36
|
+
files.push(relPath.replaceAll("\\", "/"));
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
return files;
|
|
@@ -214,7 +214,7 @@ async function generateRecommendations(
|
|
|
214
214
|
ratings: RatingsSummary | null
|
|
215
215
|
): Promise<string[]> {
|
|
216
216
|
if (candidates.length === 0 && !ratings) return [];
|
|
217
|
-
if (!process.env.
|
|
217
|
+
if (!process.env.PAL_ANTHROPIC_API_KEY) {
|
|
218
218
|
return candidates
|
|
219
219
|
.slice(0, 3)
|
|
220
220
|
.map(
|
|
@@ -21,7 +21,7 @@ export interface InferenceResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export async function inference(opts: InferenceOptions): Promise<InferenceResult> {
|
|
24
|
-
const apiKey = process.env.
|
|
24
|
+
const apiKey = process.env.PAL_ANTHROPIC_API_KEY;
|
|
25
25
|
if (!apiKey) return { success: false };
|
|
26
26
|
|
|
27
27
|
const {
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -57,6 +57,7 @@ export const paths = {
|
|
|
57
57
|
relationship: () => ensureDir(home("memory", "relationship")),
|
|
58
58
|
entities: () => ensureDir(home("memory", "entities")),
|
|
59
59
|
failures: () => ensureDir(home("memory", "learning", "failures")),
|
|
60
|
+
projectHistory: () => ensureDir(home("memory", "projects")),
|
|
60
61
|
sessionLearning: () => ensureDir(home("memory", "learning", "session")),
|
|
61
62
|
synthesis: () => ensureDir(home("memory", "learning", "synthesis")),
|
|
62
63
|
backups: () => ensureDir(home("backups")),
|
|
@@ -58,21 +58,21 @@ function extractEnvVars(): string[] {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// PAL_ANTHROPIC_API_KEY from inference.ts
|
|
62
62
|
const inferenceFile = resolve(pkg, "src", "hooks", "lib", "inference.ts");
|
|
63
63
|
if (existsSync(inferenceFile)) {
|
|
64
64
|
const content = readFileSync(inferenceFile, "utf-8");
|
|
65
|
-
if (content.includes("
|
|
66
|
-
vars.add("
|
|
65
|
+
if (content.includes("PAL_ANTHROPIC_API_KEY")) {
|
|
66
|
+
vars.add("PAL_ANTHROPIC_API_KEY");
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// PAL_GEMINI_API_KEY from youtube-analyze.ts
|
|
71
71
|
const youtubeFile = resolve(pkg, "src", "tools", "youtube-analyze.ts");
|
|
72
72
|
if (existsSync(youtubeFile)) {
|
|
73
73
|
const content = readFileSync(youtubeFile, "utf-8");
|
|
74
|
-
if (content.includes("
|
|
75
|
-
vars.add("
|
|
74
|
+
if (content.includes("PAL_GEMINI_API_KEY")) {
|
|
75
|
+
vars.add("PAL_GEMINI_API_KEY");
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -34,6 +34,8 @@ export const HOOK_MANAGED_FILES = [
|
|
|
34
34
|
"debug.log.prev",
|
|
35
35
|
"opinions.json",
|
|
36
36
|
"pal-settings.json",
|
|
37
|
+
"skill-index.json",
|
|
38
|
+
"algorithm-reflections.jsonl",
|
|
37
39
|
];
|
|
38
40
|
|
|
39
41
|
/** Hook-managed directories — AI must not write to or delete from these */
|
|
@@ -44,7 +46,9 @@ export const HOOK_MANAGED_DIRS = [
|
|
|
44
46
|
"memory/learning/synthesis",
|
|
45
47
|
"memory/relationship",
|
|
46
48
|
"memory/wisdom/state",
|
|
47
|
-
"
|
|
49
|
+
"memory/projects",
|
|
50
|
+
".agents/PAL/memory",
|
|
51
|
+
".agents/PAL/telos",
|
|
48
52
|
];
|
|
49
53
|
|
|
50
54
|
/** Escape a string for use in a RegExp */
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured work tracking: session history +
|
|
2
|
+
* Structured work tracking: session history + per-project history.
|
|
3
3
|
* Used by both Claude Code (StopOrchestrator) and opencode (plugin).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { ensureDir, paths } from "./paths";
|
|
9
|
-
import { now } from "./time";
|
|
10
9
|
|
|
11
10
|
// ── Session Records ──────────────────────────────────────────────
|
|
12
11
|
|
|
@@ -143,51 +142,39 @@ export function extractHandoff(lastAssistant: string): string {
|
|
|
143
142
|
return cleaned;
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
// ──
|
|
145
|
+
// ── Per-Project History ──────────────────────────────────────────
|
|
147
146
|
|
|
148
|
-
export interface
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
status: "active" | "paused" | "completed";
|
|
154
|
-
objectives: string[];
|
|
155
|
-
decisions: string[];
|
|
156
|
-
completed: string[];
|
|
157
|
-
blockers: string[];
|
|
158
|
-
nextSteps: string[];
|
|
159
|
-
handoff: string;
|
|
160
|
-
sessions: string[];
|
|
147
|
+
export interface ProjectHistoryEntry {
|
|
148
|
+
date: string;
|
|
149
|
+
title: string;
|
|
150
|
+
summary: string;
|
|
151
|
+
insights: string;
|
|
161
152
|
}
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
/** Convert a cwd path to a filesystem-safe slug (last directory segment) */
|
|
155
|
+
export function cwdToSlug(cwd: string): string {
|
|
156
|
+
const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
157
|
+
return normalized.split("/").pop() || "unknown";
|
|
165
158
|
}
|
|
166
159
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
/** Append a learning entry to the project's history.jsonl */
|
|
161
|
+
export function appendProjectHistory(cwd: string, entry: ProjectHistoryEntry): void {
|
|
162
|
+
const slug = cwdToSlug(cwd);
|
|
163
|
+
const dir = ensureDir(resolve(paths.projectHistory(), slug));
|
|
164
|
+
const historyPath = resolve(dir, "history.jsonl");
|
|
165
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
166
|
+
appendFileSync(historyPath, line, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Read the project history for a given cwd */
|
|
170
|
+
export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry[] {
|
|
171
|
+
const slug = cwdToSlug(cwd);
|
|
172
|
+
const historyPath = resolve(paths.projectHistory(), slug, "history.jsonl");
|
|
173
|
+
if (!existsSync(historyPath)) return [];
|
|
170
174
|
try {
|
|
171
|
-
|
|
175
|
+
const lines = readFileSync(historyPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
176
|
+
return lines.slice(-limit).map((line) => JSON.parse(line) as ProjectHistoryEntry);
|
|
172
177
|
} catch {
|
|
173
|
-
return
|
|
178
|
+
return [];
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
|
-
|
|
177
|
-
export function writeProject(project: Project): void {
|
|
178
|
-
const projects = readProjects();
|
|
179
|
-
project.updated = now();
|
|
180
|
-
projects[project.id] = project;
|
|
181
|
-
writeFileSync(projectsPath(), JSON.stringify(projects, null, 2), "utf-8");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function activeProjects(): Project[] {
|
|
185
|
-
return Object.values(readProjects()).filter((p) => p.status === "active");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function staleProjects(days = 7): Project[] {
|
|
189
|
-
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
190
|
-
return Object.values(readProjects()).filter(
|
|
191
|
-
(p) => p.status === "active" && new Date(p.updated).getTime() < cutoff
|
|
192
|
-
);
|
|
193
|
-
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
countAgents,
|
|
16
16
|
countMd,
|
|
17
17
|
countSkills,
|
|
18
|
+
generateSkillIndex,
|
|
18
19
|
loadSettingsTemplate,
|
|
19
20
|
log,
|
|
20
21
|
mergeSettings,
|
|
@@ -50,6 +51,7 @@ log.success("Merged PAL settings into settings.json");
|
|
|
50
51
|
// --- Copy skills ---
|
|
51
52
|
const skillsDir = resolve(CLAUDE_DIR, "skills");
|
|
52
53
|
copySkills(skillsDir);
|
|
54
|
+
generateSkillIndex();
|
|
53
55
|
|
|
54
56
|
// --- Copy agents ---
|
|
55
57
|
copyAgents();
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
copyPalDocs,
|
|
13
13
|
copySkills,
|
|
14
14
|
countSkills,
|
|
15
|
+
generateSkillIndex,
|
|
15
16
|
loadCursorHooksTemplate,
|
|
16
17
|
log,
|
|
17
18
|
mergeCursorHooks,
|
|
@@ -44,6 +45,7 @@ log.success("Merged PAL hooks into hooks.json");
|
|
|
44
45
|
// --- Symlink skills to ~/.cursor/skills/ ---
|
|
45
46
|
const cursorSkillsDir = resolve(CURSOR_DIR, "skills");
|
|
46
47
|
copySkills(cursorSkillsDir);
|
|
48
|
+
generateSkillIndex();
|
|
47
49
|
|
|
48
50
|
// --- Copy PAL system docs ---
|
|
49
51
|
const palDocsCount = copyPalDocs();
|
package/src/targets/lib.ts
CHANGED
|
@@ -536,6 +536,99 @@ export function removeAgentsFromOpencode(ocAgentsDir: string): string[] {
|
|
|
536
536
|
return removed;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
// --- Skill Index ---
|
|
540
|
+
|
|
541
|
+
interface SkillIndexEntry {
|
|
542
|
+
name: string;
|
|
543
|
+
description: string;
|
|
544
|
+
triggers: string[];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
interface SkillIndex {
|
|
548
|
+
generated: string;
|
|
549
|
+
totalSkills: number;
|
|
550
|
+
skills: Record<string, SkillIndexEntry>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Extract trigger keywords from a skill description */
|
|
554
|
+
function extractTriggers(description: string): string[] {
|
|
555
|
+
// Extract "Use when ..." phrases and key terms
|
|
556
|
+
const triggers = new Set<string>();
|
|
557
|
+
|
|
558
|
+
const useWhen = description.match(/Use when\s+(.+?)(?:\.|$)/i);
|
|
559
|
+
if (useWhen) {
|
|
560
|
+
const words = useWhen[1]
|
|
561
|
+
.toLowerCase()
|
|
562
|
+
.split(/[,\s]+/)
|
|
563
|
+
.filter(
|
|
564
|
+
(w) =>
|
|
565
|
+
w.length > 3 &&
|
|
566
|
+
!["when", "this", "that", "with", "from", "about", "your", "the"].includes(w)
|
|
567
|
+
);
|
|
568
|
+
for (const w of words) triggers.add(w);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Extract domain terms from full description
|
|
572
|
+
const terms = description
|
|
573
|
+
.toLowerCase()
|
|
574
|
+
.match(
|
|
575
|
+
/\b(research|analyze|extract|summarize|review|debug|reflect|council|debate|brainstorm|first.principles|security|pdf|youtube|telos|goals|projects|beliefs|challenges|opinion|skill|create)\b/g
|
|
576
|
+
);
|
|
577
|
+
if (terms) for (const t of terms) triggers.add(t);
|
|
578
|
+
|
|
579
|
+
return [...triggers];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Generate skill-index.json from installed skills in ~/.agents/skills/.
|
|
584
|
+
* Called during install after skills are symlinked.
|
|
585
|
+
*/
|
|
586
|
+
export function generateSkillIndex(): number {
|
|
587
|
+
if (!existsSync(AGENTS_SKILLS_DIR)) return 0;
|
|
588
|
+
|
|
589
|
+
const index: SkillIndex = {
|
|
590
|
+
generated: new Date().toISOString(),
|
|
591
|
+
totalSkills: 0,
|
|
592
|
+
skills: {},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
for (const name of readdirSync(AGENTS_SKILLS_DIR)) {
|
|
596
|
+
const skillMd = resolve(AGENTS_SKILLS_DIR, name, "SKILL.md");
|
|
597
|
+
if (!existsSync(skillMd)) continue;
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
601
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
602
|
+
if (!fmMatch) continue;
|
|
603
|
+
|
|
604
|
+
const fm = fmMatch[1];
|
|
605
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
606
|
+
const descMatch = fm.match(/^description:\s*"?(.+?)"?\s*$/m);
|
|
607
|
+
if (!nameMatch) continue;
|
|
608
|
+
|
|
609
|
+
const skillName = nameMatch[1].trim();
|
|
610
|
+
const description = descMatch?.[1]?.trim() ?? "";
|
|
611
|
+
|
|
612
|
+
index.skills[skillName] = {
|
|
613
|
+
name: skillName,
|
|
614
|
+
description,
|
|
615
|
+
triggers: extractTriggers(description),
|
|
616
|
+
};
|
|
617
|
+
index.totalSkills++;
|
|
618
|
+
} catch {
|
|
619
|
+
/* skip unreadable skills */
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Write to state directory
|
|
624
|
+
const stateDir = resolve(palHome(), "memory", "state");
|
|
625
|
+
mkdirSync(stateDir, { recursive: true });
|
|
626
|
+
writeJson(resolve(stateDir, "skill-index.json"), index);
|
|
627
|
+
log.info(`Skill index: ${index.totalSkills} skills indexed`);
|
|
628
|
+
|
|
629
|
+
return index.totalSkills;
|
|
630
|
+
}
|
|
631
|
+
|
|
539
632
|
/** Count skill subdirectories in ~/.agents/skills/ */
|
|
540
633
|
export function countSkills(): number {
|
|
541
634
|
if (!existsSync(AGENTS_SKILLS_DIR)) return 0;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
copyPalDocs,
|
|
13
13
|
copySkills,
|
|
14
14
|
countSkills,
|
|
15
|
+
generateSkillIndex,
|
|
15
16
|
log,
|
|
16
17
|
writeJson,
|
|
17
18
|
} from "../lib";
|
|
@@ -54,6 +55,7 @@ try {
|
|
|
54
55
|
// --- 3. Install skills into ~/.agents/skills/ ---
|
|
55
56
|
const claudeSkillsDir = resolve(platform.claudeDir(), "skills");
|
|
56
57
|
copySkills(claudeSkillsDir);
|
|
58
|
+
generateSkillIndex();
|
|
57
59
|
log.success("Installed skills to ~/.agents/skills/");
|
|
58
60
|
|
|
59
61
|
// --- 4. Install agents into ~/.config/opencode/agents/ ---
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* AlgorithmReflect — Append structured algorithm reflections to JSONL.
|
|
4
|
+
*
|
|
5
|
+
* Records algorithm performance data after each LEARN phase.
|
|
6
|
+
* Creates a queryable dataset for improving the algorithm over time.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run tool:algorithm-reflect --task "description" --criteria 5 --passed 4 --failed 1 --sentiment 7 \
|
|
10
|
+
* --q1 "Should have read the file before planning" \
|
|
11
|
+
* --q2 "Could have parallelized the explore agents" \
|
|
12
|
+
* --q3 "Missed the implicit constraint about cross-platform"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { parseArgs } from "node:util";
|
|
18
|
+
import { paths } from "../../hooks/lib/paths";
|
|
19
|
+
|
|
20
|
+
// ── Types ──
|
|
21
|
+
|
|
22
|
+
interface AlgorithmReflection {
|
|
23
|
+
timestamp: string;
|
|
24
|
+
task: string;
|
|
25
|
+
criteria_count: number;
|
|
26
|
+
criteria_passed: number;
|
|
27
|
+
criteria_failed: number;
|
|
28
|
+
sentiment: number;
|
|
29
|
+
q1: string;
|
|
30
|
+
q2: string;
|
|
31
|
+
q3: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Core ──
|
|
35
|
+
|
|
36
|
+
function reflectionsPath(): string {
|
|
37
|
+
const dir = resolve(paths.learning(), "reflections");
|
|
38
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
+
return resolve(dir, "algorithm-reflections.jsonl");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function appendReflection(reflection: AlgorithmReflection): {
|
|
43
|
+
success: boolean;
|
|
44
|
+
message: string;
|
|
45
|
+
path: string;
|
|
46
|
+
} {
|
|
47
|
+
const p = reflectionsPath();
|
|
48
|
+
const line = `${JSON.stringify(reflection)}\n`;
|
|
49
|
+
appendFileSync(p, line, "utf-8");
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
message: `Reflection logged: ${reflection.criteria_passed}/${reflection.criteria_count} passed, sentiment ${reflection.sentiment}/10`,
|
|
54
|
+
path: p,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── CLI ──
|
|
59
|
+
|
|
60
|
+
function run() {
|
|
61
|
+
const { values } = parseArgs({
|
|
62
|
+
args: Bun.argv.slice(2),
|
|
63
|
+
options: {
|
|
64
|
+
task: { type: "string" },
|
|
65
|
+
criteria: { type: "string" },
|
|
66
|
+
passed: { type: "string" },
|
|
67
|
+
failed: { type: "string" },
|
|
68
|
+
sentiment: { type: "string" },
|
|
69
|
+
q1: { type: "string" },
|
|
70
|
+
q2: { type: "string" },
|
|
71
|
+
q3: { type: "string" },
|
|
72
|
+
help: { type: "boolean", short: "h" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (values.help) {
|
|
77
|
+
console.log(`
|
|
78
|
+
AlgorithmReflect — Log algorithm performance after LEARN phase
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
bun run tool:algorithm-reflect --task "description" --criteria N --passed N --failed N --sentiment 1-10 \\
|
|
82
|
+
--q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
|
|
83
|
+
|
|
84
|
+
Arguments:
|
|
85
|
+
--task Brief task description
|
|
86
|
+
--criteria Total criteria count
|
|
87
|
+
--passed Criteria passed
|
|
88
|
+
--failed Criteria failed
|
|
89
|
+
--sentiment Implied satisfaction 1-10
|
|
90
|
+
--q1 Q1 — Self: what I'd do differently
|
|
91
|
+
--q2 Q2 — Algorithm: structural improvement
|
|
92
|
+
--q3 Q3 — AI: reasoning blind spot
|
|
93
|
+
|
|
94
|
+
Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
95
|
+
`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!values.task || !values.q1 || !values.q2 || !values.q3) {
|
|
100
|
+
console.error("Required: --task, --q1, --q2, --q3");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reflection: AlgorithmReflection = {
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
task: values.task,
|
|
107
|
+
criteria_count: parseInt(values.criteria || "0", 10),
|
|
108
|
+
criteria_passed: parseInt(values.passed || "0", 10),
|
|
109
|
+
criteria_failed: parseInt(values.failed || "0", 10),
|
|
110
|
+
sentiment: Math.max(1, Math.min(10, parseInt(values.sentiment || "5", 10))),
|
|
111
|
+
q1: values.q1,
|
|
112
|
+
q2: values.q2,
|
|
113
|
+
q3: values.q3,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = appendReflection(reflection);
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (import.meta.main) run();
|
|
@@ -167,8 +167,6 @@ ${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Fre
|
|
|
167
167
|
`- ${date()}: Principle candidate — ${observation}`
|
|
168
168
|
);
|
|
169
169
|
break;
|
|
170
|
-
|
|
171
|
-
case "evolution":
|
|
172
170
|
default:
|
|
173
171
|
content = appendToSection(content, "## Evolution Log", evolutionEntry);
|
|
174
172
|
break;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: claude-researcher
|
|
3
|
-
description: Deep research with academic rigor — query decomposition, multi-source synthesis, scholarly depth. Use for research tasks requiring thorough analysis.
|
|
4
|
-
tools: WebSearch, WebFetch, Read, Grep, Glob
|
|
5
|
-
model: sonnet
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a research specialist focused on **depth and academic rigor**.
|
|
9
|
-
|
|
10
|
-
## Methodology
|
|
11
|
-
|
|
12
|
-
1. **Decompose** the query into 2-3 sub-questions that target the core of the topic
|
|
13
|
-
2. **Search** each sub-question using WebSearch — prioritize authoritative sources (papers, docs, official sites)
|
|
14
|
-
3. **Read** the most promising results with WebFetch to extract detail
|
|
15
|
-
4. **Synthesize** findings into a structured analysis
|
|
16
|
-
|
|
17
|
-
## Guidelines
|
|
18
|
-
|
|
19
|
-
- Prioritize primary sources over summaries
|
|
20
|
-
- Distinguish between established facts, expert consensus, and speculation
|
|
21
|
-
- Note methodology limitations when citing research
|
|
22
|
-
- If a claim has no strong source, say so — do not fabricate citations
|
|
23
|
-
- Keep findings concise but substantive
|
|
24
|
-
|
|
25
|
-
## Output Format
|
|
26
|
-
|
|
27
|
-
```markdown
|
|
28
|
-
## Findings
|
|
29
|
-
|
|
30
|
-
[Numbered list of key discoveries, each with a brief explanation]
|
|
31
|
-
|
|
32
|
-
## Sources
|
|
33
|
-
|
|
34
|
-
[Verified URLs with one-line descriptions — only include URLs you actually visited]
|
|
35
|
-
|
|
36
|
-
## Confidence
|
|
37
|
-
|
|
38
|
-
[High/Medium/Low rating per finding, with brief justification]
|
|
39
|
-
|
|
40
|
-
## Gaps
|
|
41
|
-
|
|
42
|
-
[What couldn't be answered or needs further investigation]
|
|
43
|
-
```
|