portable-agent-layer 0.21.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 CHANGED
@@ -111,14 +111,15 @@ pal cli install # all available (default)
111
111
 
112
112
  | Variable | Description |
113
113
  |----------|-------------|
114
- | `ANTHROPIC_API_KEY` | Required for PAL's hook inference (sentiment analysis, session naming). Uses Haiku for low-cost background calls. |
114
+ | `PAL_ANTHROPIC_API_KEY` | Required for PAL's hook inference (sentiment analysis, session naming). Uses Haiku for low-cost background calls. |
115
115
 
116
116
  ### Optional
117
117
 
118
118
  | Variable | Description |
119
119
  |----------|-------------|
120
- | `PAL_GEMINI_API_KEY` | For YouTube video analysis skill |
120
+ | `PAL_GEMINI_API_KEY` | For YouTube video analysis and web search skill |
121
121
  | `PAL_XAI_API_KEY` | For Grok real-time research skill (X/web search) |
122
+ | `PAL_PERPLEXITY_API_KEY` | For Perplexity deep research skill |
122
123
  | `PAL_HOME` | Override user state directory (default: `~/.pal` or repo root) |
123
124
  | `PAL_PKG` | Override package root |
124
125
  | `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
@@ -4,7 +4,7 @@ description: Query Fyzz Chat conversations and projects via the REST API. Use wh
4
4
  argument-hint: <conversations|projects> [options]
5
5
  ---
6
6
 
7
- When you need to access the user's Fyzz Chat conversations or projects, use the `fyzz-api` CLI tool. The tool reads the API key from the `FYZZ_API_KEY` environment variable automatically — never attempt to read, print, or reference the API key or the env var directly.
7
+ When you need to access the user's Fyzz Chat conversations or projects, use the `fyzz-api` CLI tool. The tool reads the API key from the `PAL_FYZZ_API_KEY` environment variable automatically — never attempt to read, print, or reference the API key or the env var directly.
8
8
 
9
9
  ## Available commands
10
10
 
@@ -31,8 +31,8 @@ bun ~/.agents/skills/fyzz-chat-api/tools/fyzz-api.ts -- projects
31
31
  If the tool reports a missing API key:
32
32
 
33
33
  1. Ask the user to create one in Fyzz Chat → Settings → API Keys
34
- 2. They should set `FYZZ_API_KEY` in their shell profile or in PAL's `settings.json` env section
35
- 3. Optionally set `FYZZ_BASE_URL` (defaults to `http://localhost:3000`)
34
+ 2. They should set `PAL_FYZZ_API_KEY` in their shell profile or in PAL's `settings.json` env section
35
+ 3. Optionally set `PAL_FYZZ_BASE_URL` (defaults to `http://localhost:3000`)
36
36
 
37
37
  ## Guidelines
38
38
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Fyzz Chat API — CLI wrapper for programmatic conversation access.
4
4
  *
5
- * Reads the API key from FYZZ_API_KEY env var (never printed to stdout).
5
+ * Reads the API key from PAL_FYZZ_API_KEY env var (never printed to stdout).
6
6
  * Returns JSON responses from the Fyzz Chat REST API.
7
7
  *
8
8
  * Usage:
@@ -14,9 +14,9 @@
14
14
  import { parseArgs } from "node:util";
15
15
 
16
16
  function loadApiKey(): string {
17
- const key = process.env.FYZZ_API_KEY;
17
+ const key = process.env.PAL_FYZZ_API_KEY;
18
18
  if (!key) {
19
- console.error("Error: FYZZ_API_KEY environment variable is not set.");
19
+ console.error("Error: PAL_FYZZ_API_KEY environment variable is not set.");
20
20
  console.error("Set it in your shell profile or PAL settings.json env section.");
21
21
  process.exit(1);
22
22
  }
@@ -25,7 +25,7 @@ function loadApiKey(): string {
25
25
 
26
26
  async function apiFetch(path: string, params?: Record<string, string>): Promise<unknown> {
27
27
  const apiKey = loadApiKey();
28
- const baseUrl = process.env.FYZZ_BASE_URL ?? "http://localhost:3000";
28
+ const baseUrl = process.env.PAL_FYZZ_BASE_URL ?? "http://localhost:3000";
29
29
 
30
30
  const url = new URL(`/api/v1${path}`, baseUrl);
31
31
  if (params) {
@@ -35,13 +35,55 @@ Format:
35
35
 
36
36
  Include at least one anti-criterion (C-A prefix).
37
37
 
38
- **3. Select capabilities:**
38
+ **3. Capability audit:**
39
39
 
40
- Scan the available skills listing. Select skills and tools you'll invoke during EXECUTE. Selecting a capability = commitment to invoke it via tool call. Don't select what you won't use.
40
+ Scan ALL 14 capabilities below. For each, assign exactly one disposition:
41
+ - **USE** — will invoke during a specific phase. State which.
42
+ - **DECLINE** — would help but not worth it for this task's scope.
43
+ - **N/A** — genuinely irrelevant to this task.
44
+
45
+ **A: Foundation**
46
+
47
+ | # | Capability | Invocation |
48
+ |---|-----------|------------|
49
+ | 1 | Task Tool | TaskCreate, TaskUpdate, TaskList |
50
+ | 2 | AskUserQuestion | Built-in tool |
51
+ | 3 | Skills (ACTIVE SCAN) | Read `skill-index.json`, match triggers against task |
52
+
53
+ **B: Thinking & Analysis**
54
+
55
+ | # | Capability | Invocation |
56
+ |---|-----------|------------|
57
+ | 4 | Think (analysis router) | `think` skill |
58
+ | 5 | First Principles | `first-principles` skill |
59
+ | 6 | Council (multi-perspective) | `council` skill |
60
+ | 7 | Plan Mode | EnterPlanMode tool |
61
+
62
+ **C: Agents & Research**
63
+
64
+ | # | Capability | Invocation |
65
+ |---|-----------|------------|
66
+ | 8 | Research (multi-agent) | `research` skill |
67
+ | 9 | Subagents | Agent tool (Explore, Plan, general-purpose) |
68
+ | 10 | Background agents | Agent tool with `run_in_background: true` |
69
+
70
+ **D: Execution & Verification**
71
+
72
+ | # | Capability | Invocation |
73
+ |---|-----------|------------|
74
+ | 11 | Git worktree isolation | `isolation: "worktree"` on Agent |
75
+ | 12 | Test runner | `bun test`, vitest, jest, pytest |
76
+ | 13 | Static analysis | `tsc --noEmit`, biome, eslint |
77
+ | 14 | CLI probes | curl, diff, jq, exit codes |
78
+
79
+ **Capability #3 (Skills) requires active scanning.** Read `skill-index.json` and match the task against skill triggers. "Skills — N/A" without evidence of scanning is an error.
41
80
 
42
81
  Output:
43
82
  ```
44
- 🏹 CAPABILITIES: [list each selected skill/tool and why]
83
+ 🏹 CAPABILITIES (14/14):
84
+ USE: [#, #, #] — [reason (phase: WHICH)]
85
+ DECLINE: [#, #] — [reason]
86
+ N/A: [rest]
45
87
  ```
46
88
 
47
89
  ### ━━━ 🧠 PLAN ━━━ 2/5
@@ -92,11 +134,25 @@ If any criteria failed, fix and re-verify before completing.
92
134
 
93
135
  Reflect on the work and capture reusable knowledge. Skip this phase when the work was trivial or purely mechanical.
94
136
 
95
- **1. Reflection** (one sentence each):
96
- - What would I do differently next time?
97
- - What would a better algorithm have done differently?
137
+ **1. Algorithm Reflection** (one sentence each — reflect on ALGORITHM PERFORMANCE, not task subject matter):
138
+
139
+ **Q1 — Self:** "What would I have done differently in this Algorithm run?"
140
+ Focus: phase execution, criteria quality, capability selection decisions.
141
+
142
+ **Q2 — Algorithm:** "What would a smarter algorithm have done differently?"
143
+ Focus: structural improvements — missing phases, better gating, capability triggers, ISC patterns.
144
+
145
+ **Q3 — AI:** "What would a fundamentally smarter AI have done differently?"
146
+ Focus: reasoning approach, problem decomposition, anticipation, blind spots.
147
+
148
+ **2. Reflection Log** — record algorithm performance:
149
+
150
+ ```bash
151
+ bun ~/.agents/PAL/tools/algorithm-reflect.ts --task "description" --criteria N --passed N --failed N --sentiment 1-10 \
152
+ --q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
153
+ ```
98
154
 
99
- **2. Wisdom Frame** — if the session produced a genuine, reusable insight:
155
+ **3. Wisdom Frame** — if the session produced a genuine, reusable insight:
100
156
 
101
157
  ```bash
102
158
  bun ~/.agents/PAL/tools/wisdom-frame.ts --domain <domain> --observation "insight" [--type principle|contextual-rule|anti-pattern|evolution]
@@ -118,7 +174,10 @@ Only write if the insight is **genuine and reusable** — not every session prod
118
174
  📋 CRITERIA:
119
175
  [criteria checklist]
120
176
 
121
- 🏹 CAPABILITIES: [selected capabilities]
177
+ 🏹 CAPABILITIES (14/14):
178
+ USE: [#, #] — [reason]
179
+ DECLINE: [#] — [reason]
180
+ N/A: [rest]
122
181
 
123
182
  ━━━ 🧠 PLAN ━━━ 2/5
124
183
  🧠 RISKS: [risks]
@@ -136,6 +195,9 @@ Only write if the insight is **genuine and reusable** — not every session prod
136
195
  🗣️ {{IDENTITY_NAME}}: [summary]
137
196
 
138
197
  ━━━ 📚 LEARN ━━━ 5/5
139
- 🪞 REFLECT: [what I'd do differently]
198
+ 🪞 Q1 — Self: [what I'd do differently]
199
+ 🪞 Q2 — Algorithm: [structural improvement]
200
+ 🪞 Q3 — AI: [reasoning blind spot]
201
+ 📊 REFLECTION LOG: [appended to algorithm-reflections.jsonl]
140
202
  📝 WISDOM: [frame update if genuine insight, or "No new insight"]
141
203
  ```
@@ -1,14 +1,7 @@
1
1
  # Work Tracking
2
2
 
3
- PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured) and `memory/state/projects.json` (AI-managed).
3
+ PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured).
4
4
 
5
5
  ## Projects
6
6
 
7
- Update `projects.json` via the work-tracking library when:
8
- - **Starting sustained multi-session work** → create a project with objectives and an id (slugified, e.g. "pdf-template-engine")
9
- - **Making a key decision** → add to the project's `decisions` array
10
- - **Completing a milestone** → add to `completed`, remove from `nextSteps`
11
- - **Session ends with open work** → update `nextSteps` and `handoff`
12
- - **Work is done** → set status to "completed"
13
-
14
- Do not create projects for one-off questions or quick fixes.
7
+ Projects are managed in `telos/PROJECTS.md` and force-loaded at session startup via `pal-settings.json loadAtStartup.files`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -377,9 +377,9 @@ function doctor(silent = false): DoctorResult {
377
377
  telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
378
378
 
379
379
  // API key checks
380
- process.env.ANTHROPIC_API_KEY
381
- ? ok("ANTHROPIC_API_KEY is set")
382
- : fail("ANTHROPIC_API_KEY — not set (hooks need it for inference)");
380
+ process.env.PAL_ANTHROPIC_API_KEY
381
+ ? ok("PAL_ANTHROPIC_API_KEY is set")
382
+ : fail("PAL_ANTHROPIC_API_KEY — not set (hooks need it for inference)");
383
383
  process.env.PAL_GEMINI_API_KEY
384
384
  ? ok("PAL_GEMINI_API_KEY is set")
385
385
  : warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
@@ -358,6 +358,6 @@ export async function captureRating(message: string, sessionId?: string): Promis
358
358
  return;
359
359
  }
360
360
 
361
- // Path 2: Implicit sentiment (requires ANTHROPIC_API_KEY — inference silently no-ops without it)
361
+ // Path 2: Implicit sentiment (requires PAL_ANTHROPIC_API_KEY — inference silently no-ops without it)
362
362
  await handleImplicitSentiment(cleaned, sessionId);
363
363
  }
@@ -52,8 +52,8 @@ export async function captureRelationship(
52
52
  return;
53
53
  }
54
54
 
55
- if (!process.env.ANTHROPIC_API_KEY) {
56
- logDebug("relationship", "Skipped: no ANTHROPIC_API_KEY");
55
+ if (!process.env.PAL_ANTHROPIC_API_KEY) {
56
+ logDebug("relationship", "Skipped: no PAL_ANTHROPIC_API_KEY");
57
57
  return;
58
58
  }
59
59
 
@@ -42,7 +42,7 @@ export async function captureSessionName(
42
42
  logDebug("session-name", `Named from prompt: "${name}"`);
43
43
 
44
44
  // Spawn detached background process to upgrade with Haiku inference
45
- if (!process.env.ANTHROPIC_API_KEY) return;
45
+ if (!process.env.PAL_ANTHROPIC_API_KEY) return;
46
46
  try {
47
47
  const promptB64 = Buffer.from(message.slice(0, 800)).toString("base64");
48
48
  const child = spawn(
@@ -15,13 +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
- readProjectHistory,
21
- readSessions,
22
- recentSessions,
23
- staleProjects,
24
- } from "./work-tracking";
18
+ import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
25
19
 
26
20
  interface PalSettings {
27
21
  loadAtStartup?: { files?: string[] };
@@ -83,44 +77,16 @@ export function loadActiveWork(): { text: string; summary: string | null } | nul
83
77
  try {
84
78
  const cwd = process.cwd();
85
79
  const allRecent = recentSessions(48);
86
- const projects = activeProjects();
87
- const stale = staleProjects(7);
88
80
 
89
- if (allRecent.length === 0 && projects.length === 0) return null;
81
+ if (allRecent.length === 0) return null;
90
82
 
91
83
  const lines: string[] = [];
92
84
 
93
- if (allRecent.length > 0) {
94
- lines.push("## Recent Work (last 48h)");
95
- for (const s of allRecent.slice(-10).reverse()) {
96
- const ago = formatAgo(s.ts);
97
- const here = s.cwd === cwd ? " *" : "";
98
- lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
99
- }
100
- }
101
-
102
- if (projects.length > 0) {
103
- lines.push("", "### Active Projects");
104
- for (const p of projects) {
105
- const sessionCount = p.sessions.length;
106
- const ago = formatAgo(p.updated);
107
- lines.push(`- **${p.name}** (${sessionCount} sessions, last: ${ago})`);
108
- if (p.nextSteps.length > 0) {
109
- lines.push(` Next: ${p.nextSteps[0]}`);
110
- }
111
- if (p.blockers.length > 0) {
112
- lines.push(` Blockers: ${p.blockers.join(", ")}`);
113
- } else {
114
- lines.push(" Blockers: None");
115
- }
116
- }
117
- }
118
-
119
- if (stale.length > 0) {
120
- lines.push("", "### Stale Projects (>7d inactive)");
121
- for (const p of stale) {
122
- lines.push(`- **${p.name}** — last active ${formatAgo(p.updated)}`);
123
- }
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}`);
124
90
  }
125
91
 
126
92
  // Summary from most recent session
@@ -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.ANTHROPIC_API_KEY) {
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.ANTHROPIC_API_KEY;
24
+ const apiKey = process.env.PAL_ANTHROPIC_API_KEY;
25
25
  if (!apiKey) return { success: false };
26
26
 
27
27
  const {
@@ -58,12 +58,12 @@ function extractEnvVars(): string[] {
58
58
  }
59
59
  }
60
60
 
61
- // ANTHROPIC_API_KEY from inference.ts
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("ANTHROPIC_API_KEY")) {
66
- vars.add("ANTHROPIC_API_KEY");
65
+ if (content.includes("PAL_ANTHROPIC_API_KEY")) {
66
+ vars.add("PAL_ANTHROPIC_API_KEY");
67
67
  }
68
68
  }
69
69
 
@@ -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 */
@@ -45,7 +47,8 @@ export const HOOK_MANAGED_DIRS = [
45
47
  "memory/relationship",
46
48
  "memory/wisdom/state",
47
49
  "memory/projects",
48
- ".agents/PAL",
50
+ ".agents/PAL/memory",
51
+ ".agents/PAL/telos",
49
52
  ];
50
53
 
51
54
  /** Escape a string for use in a RegExp */
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Structured work tracking: session history + persistent projects.
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
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
 
@@ -179,52 +178,3 @@ export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry
179
178
  return [];
180
179
  }
181
180
  }
182
-
183
- // ── Persistent Projects ──────────────────────────────────────────
184
-
185
- export interface Project {
186
- id: string;
187
- name: string;
188
- created: string;
189
- updated: string;
190
- status: "active" | "paused" | "completed";
191
- objectives: string[];
192
- decisions: string[];
193
- completed: string[];
194
- blockers: string[];
195
- nextSteps: string[];
196
- handoff: string;
197
- sessions: string[];
198
- }
199
-
200
- function projectsPath(): string {
201
- return resolve(ensureDir(paths.state()), "projects.json");
202
- }
203
-
204
- export function readProjects(): Record<string, Project> {
205
- const p = projectsPath();
206
- if (!existsSync(p)) return {};
207
- try {
208
- return JSON.parse(readFileSync(p, "utf-8"));
209
- } catch {
210
- return {};
211
- }
212
- }
213
-
214
- export function writeProject(project: Project): void {
215
- const projects = readProjects();
216
- project.updated = now();
217
- projects[project.id] = project;
218
- writeFileSync(projectsPath(), JSON.stringify(projects, null, 2), "utf-8");
219
- }
220
-
221
- export function activeProjects(): Project[] {
222
- return Object.values(readProjects()).filter((p) => p.status === "active");
223
- }
224
-
225
- export function staleProjects(days = 7): Project[] {
226
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
227
- return Object.values(readProjects()).filter(
228
- (p) => p.status === "active" && new Date(p.updated).getTime() < cutoff
229
- );
230
- }
@@ -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();
@@ -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();