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 +3 -2
- package/assets/skills/fyzz-chat-api/SKILL.md +3 -3
- package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
- 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 +3 -3
- 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/lib/context.ts +7 -41
- package/src/hooks/lib/graduation.ts +1 -1
- package/src/hooks/lib/inference.ts +1 -1
- package/src/hooks/lib/readme-sync.ts +3 -3
- package/src/hooks/lib/security.ts +4 -1
- package/src/hooks/lib/work-tracking.ts +1 -51
- 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/README.md
CHANGED
|
@@ -111,14 +111,15 @@ pal cli install # all available (default)
|
|
|
111
111
|
|
|
112
112
|
| Variable | Description |
|
|
113
113
|
|----------|-------------|
|
|
114
|
-
| `
|
|
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 `
|
|
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 `
|
|
35
|
-
3. Optionally set `
|
|
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
|
|
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.
|
|
17
|
+
const key = process.env.PAL_FYZZ_API_KEY;
|
|
18
18
|
if (!key) {
|
|
19
|
-
console.error("Error:
|
|
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.
|
|
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.
|
|
38
|
+
**3. Capability audit:**
|
|
39
39
|
|
|
40
|
-
Scan
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
**
|
|
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:
|
|
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
|
-
🪞
|
|
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)
|
|
3
|
+
PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured).
|
|
4
4
|
|
|
5
5
|
## Projects
|
|
6
6
|
|
|
7
|
-
|
|
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
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.
|
|
381
|
-
? ok("
|
|
382
|
-
: fail("
|
|
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
|
|
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.
|
|
56
|
-
logDebug("relationship", "Skipped: no
|
|
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.
|
|
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(
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -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
|
|
81
|
+
if (allRecent.length === 0) return null;
|
|
90
82
|
|
|
91
83
|
const lines: string[] = [];
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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 {
|
|
@@ -58,12 +58,12 @@ 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
|
|
|
@@ -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 +
|
|
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();
|
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();
|