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.
Files changed (38) hide show
  1. package/README.md +4 -3
  2. package/assets/agents/gemini-researcher.md +73 -0
  3. package/assets/agents/grok-researcher.md +10 -1
  4. package/assets/agents/perplexity-researcher.md +67 -0
  5. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  6. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +3 -3
  7. package/assets/skills/create-pdf/SKILL.md +53 -76
  8. package/assets/skills/fyzz-chat-api/SKILL.md +3 -3
  9. package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
  10. package/assets/skills/research/SKILL.md +9 -9
  11. package/assets/skills/research/tools/gemini-search.ts +186 -0
  12. package/assets/skills/research/tools/grok-search.ts +3 -3
  13. package/assets/skills/research/tools/perplexity-search.ts +150 -0
  14. package/assets/templates/PAL/ALGORITHM.md +71 -9
  15. package/assets/templates/PAL/WORK_TRACKING.md +2 -9
  16. package/package.json +1 -1
  17. package/src/cli/index.ts +18 -6
  18. package/src/hooks/handlers/rating.ts +1 -1
  19. package/src/hooks/handlers/relationship.ts +2 -2
  20. package/src/hooks/handlers/session-name.ts +1 -1
  21. package/src/hooks/handlers/work-learning.ts +9 -0
  22. package/src/hooks/lib/claude-md.ts +17 -5
  23. package/src/hooks/lib/context.ts +35 -55
  24. package/src/hooks/lib/export.ts +3 -2
  25. package/src/hooks/lib/graduation.ts +1 -1
  26. package/src/hooks/lib/inference.ts +1 -1
  27. package/src/hooks/lib/paths.ts +1 -0
  28. package/src/hooks/lib/readme-sync.ts +6 -6
  29. package/src/hooks/lib/security.ts +5 -1
  30. package/src/hooks/lib/work-tracking.ts +29 -42
  31. package/src/targets/claude/install.ts +2 -0
  32. package/src/targets/cursor/install.ts +2 -0
  33. package/src/targets/lib.ts +93 -0
  34. package/src/targets/opencode/install.ts +2 -0
  35. package/src/tools/agent/algorithm-reflect.ts +120 -0
  36. package/src/tools/agent/wisdom-frame.ts +0 -2
  37. package/assets/agents/claude-researcher.md +0 -43
  38. package/assets/agents/investigative-researcher.md +0 -44
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Gemini Search — CLI tool for grounded search via the Gemini API.
4
+ *
5
+ * Uses Gemini's built-in Google Search grounding to fetch real-time,
6
+ * source-cited information. Optimized for academic and scholarly queries.
7
+ *
8
+ * Requires PAL_GEMINI_API_KEY environment variable.
9
+ *
10
+ * Usage:
11
+ * bun gemini-search.ts -- <query> [--max-tokens 4096]
12
+ * bun gemini-search.ts -- "recent advances in transformer architectures"
13
+ * bun gemini-search.ts -- "CRISPR gene editing clinical trials 2025"
14
+ */
15
+
16
+ import { parseArgs } from "node:util";
17
+
18
+ const API_BASE = "https://generativelanguage.googleapis.com/v1beta";
19
+ const DEFAULT_MODEL = "gemini-3.1-flash-lite-preview";
20
+
21
+ interface GroundingChunk {
22
+ web?: { uri: string; title: string };
23
+ }
24
+
25
+ interface GroundingSupport {
26
+ segment?: { startIndex: number; endIndex: number; text: string };
27
+ groundingChunkIndices?: number[];
28
+ }
29
+
30
+ interface GroundingMetadata {
31
+ webSearchQueries?: string[];
32
+ groundingChunks?: GroundingChunk[];
33
+ groundingSupports?: GroundingSupport[];
34
+ searchEntryPoint?: { renderedContent: string };
35
+ }
36
+
37
+ interface ContentPart {
38
+ text?: string;
39
+ }
40
+
41
+ interface Candidate {
42
+ content?: { parts?: ContentPart[]; role?: string };
43
+ groundingMetadata?: GroundingMetadata;
44
+ }
45
+
46
+ interface GeminiResponse {
47
+ candidates?: Candidate[];
48
+ error?: { message: string; code: number };
49
+ }
50
+
51
+ function loadApiKey(): string {
52
+ const key = process.env.PAL_GEMINI_API_KEY;
53
+ if (!key) {
54
+ console.error("Error: PAL_GEMINI_API_KEY environment variable is not set.");
55
+ console.error("Get an API key at https://aistudio.google.com/apikey");
56
+ process.exit(1);
57
+ }
58
+ return key;
59
+ }
60
+
61
+ const SYSTEM_PROMPT = `You are an academic research assistant. When searching, prioritize:
62
+ - Peer-reviewed papers, preprints (arXiv, bioRxiv, medRxiv)
63
+ - Official documentation and technical specifications
64
+ - University and research institution publications
65
+ - Conference proceedings (NeurIPS, ICML, ACL, CVPR, etc.)
66
+ - Systematic reviews and meta-analyses
67
+
68
+ Always include: author names, publication year, journal/venue when available.
69
+ Distinguish between peer-reviewed findings and preprints/working papers.
70
+ Note methodology limitations and sample sizes when relevant.
71
+ Be thorough but concise.`;
72
+
73
+ export async function geminiSearch(query: string, maxTokens: number): Promise<void> {
74
+ const apiKey = loadApiKey();
75
+
76
+ const body = {
77
+ system_instruction: {
78
+ parts: [{ text: SYSTEM_PROMPT }],
79
+ },
80
+ contents: [
81
+ {
82
+ parts: [{ text: query }],
83
+ },
84
+ ],
85
+ tools: [{ google_search: {} }],
86
+ generationConfig: {
87
+ maxOutputTokens: maxTokens,
88
+ },
89
+ };
90
+
91
+ const url = `${API_BASE}/models/${DEFAULT_MODEL}:generateContent?key=${apiKey}`;
92
+
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(body),
97
+ });
98
+
99
+ if (!response.ok) {
100
+ const err = await response.text().catch(() => "");
101
+ console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
102
+ process.exit(1);
103
+ }
104
+
105
+ const data = (await response.json()) as GeminiResponse;
106
+
107
+ if (data.error) {
108
+ console.error(`Error: ${data.error.message}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ if (!data.candidates || data.candidates.length === 0) {
113
+ console.error("Error: No candidates in Gemini response.");
114
+ process.exit(1);
115
+ }
116
+
117
+ const candidate = data.candidates[0];
118
+
119
+ // Extract text
120
+ const textParts: string[] = [];
121
+ if (candidate.content?.parts) {
122
+ for (const part of candidate.content.parts) {
123
+ if (part.text) textParts.push(part.text);
124
+ }
125
+ }
126
+
127
+ if (textParts.length === 0) {
128
+ console.error("Error: No text content in Gemini response.");
129
+ process.exit(1);
130
+ }
131
+
132
+ console.log(textParts.join("\n\n"));
133
+
134
+ // Extract grounding metadata
135
+ const meta = candidate.groundingMetadata;
136
+ if (meta) {
137
+ if (meta.webSearchQueries && meta.webSearchQueries.length > 0) {
138
+ console.log("\n---\n## Search Queries Used\n");
139
+ for (const q of meta.webSearchQueries) {
140
+ console.log(`- ${q}`);
141
+ }
142
+ }
143
+
144
+ if (meta.groundingChunks && meta.groundingChunks.length > 0) {
145
+ console.log("\n---\n## Sources\n");
146
+ for (const chunk of meta.groundingChunks) {
147
+ if (chunk.web) {
148
+ console.log(`- [${chunk.web.title}](${chunk.web.uri})`);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ async function run() {
156
+ const { positionals, values } = parseArgs({
157
+ allowPositionals: true,
158
+ options: {
159
+ "max-tokens": { type: "string", short: "m", default: "4096" },
160
+ help: { type: "boolean", short: "h" },
161
+ },
162
+ });
163
+
164
+ if (values.help || positionals.length === 0) {
165
+ console.log(`Gemini Search — grounded academic search via Gemini API
166
+
167
+ Usage:
168
+ bun gemini-search.ts -- <query> [options]
169
+
170
+ Options:
171
+ --max-tokens, -m <n> Max response tokens (default: 4096)
172
+ --help, -h Show this help
173
+
174
+ Examples:
175
+ bun gemini-search.ts -- "transformer architecture advances 2025"
176
+ bun gemini-search.ts -- "CRISPR clinical trials"`);
177
+ process.exit(0);
178
+ }
179
+
180
+ const query = positionals.join(" ");
181
+ const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
182
+
183
+ await geminiSearch(query, maxTokens);
184
+ }
185
+
186
+ if (import.meta.main) run();
@@ -5,7 +5,7 @@
5
5
  * Uses the Grok Responses API with web_search and x_search tools
6
6
  * to fetch real-time information from the web and X (Twitter).
7
7
  *
8
- * Requires XAI_API_KEY environment variable.
8
+ * Requires PAL_XAI_API_KEY environment variable.
9
9
  *
10
10
  * Usage:
11
11
  * bun grok-search.ts -- <query> [--sources web,x] [--max-tokens 2048]
@@ -45,9 +45,9 @@ interface GrokResponse {
45
45
  }
46
46
 
47
47
  function loadApiKey(): string {
48
- const key = process.env.XAI_API_KEY;
48
+ const key = process.env.PAL_XAI_API_KEY;
49
49
  if (!key) {
50
- console.error("Error: XAI_API_KEY environment variable is not set.");
50
+ console.error("Error: PAL_XAI_API_KEY environment variable is not set.");
51
51
  console.error("Get an API key at https://console.x.ai/");
52
52
  process.exit(1);
53
53
  }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Perplexity Search — CLI tool for investigative search via the Perplexity Sonar API.
4
+ *
5
+ * Uses Perplexity's Sonar model with built-in web search to fetch
6
+ * source-cited, verified information. Optimized for investigative queries
7
+ * requiring cross-referenced, credible sources.
8
+ *
9
+ * Requires PAL_PERPLEXITY_API_KEY environment variable.
10
+ *
11
+ * Usage:
12
+ * bun perplexity-search.ts -- <query> [--max-tokens 4096]
13
+ * bun perplexity-search.ts -- "corruption allegations against company X"
14
+ * bun perplexity-search.ts -- "timeline of event Y with sources"
15
+ */
16
+
17
+ import { parseArgs } from "node:util";
18
+
19
+ const API_BASE = "https://api.perplexity.ai";
20
+ const DEFAULT_MODEL = "sonar-pro";
21
+
22
+ interface Choice {
23
+ message?: {
24
+ role: string;
25
+ content: string;
26
+ };
27
+ }
28
+
29
+ interface PerplexityResponse {
30
+ choices?: Choice[];
31
+ citations?: string[];
32
+ error?: { message: string; code?: number };
33
+ }
34
+
35
+ function loadApiKey(): string {
36
+ const key = process.env.PAL_PERPLEXITY_API_KEY;
37
+ if (!key) {
38
+ console.error(
39
+ "Error: PAL_PERPLEXITY_API_KEY environment variable is not set.\n" +
40
+ "The Perplexity API could not be reached. The researcher agent should fall back to WebSearch.\n" +
41
+ "To enable Perplexity search, get an API key at https://www.perplexity.ai/settings/api\n" +
42
+ "and set it: export PAL_PERPLEXITY_API_KEY=pplx-..."
43
+ );
44
+ process.exit(1);
45
+ }
46
+ return key;
47
+ }
48
+
49
+ const SYSTEM_PROMPT = `You are an investigative research assistant. When searching, prioritize:
50
+ - Cross-referenced facts verified by 2+ independent sources
51
+ - Primary sources: court filings, official reports, government records, regulatory filings
52
+ - Credible journalism: established outlets with editorial standards
53
+ - Source credibility assessment: note publication reputation, potential bias, date of publication
54
+ - Evidence chains: connect claims to their original sources
55
+
56
+ Always include: source names, publication dates, and direct quotes when available.
57
+ Distinguish between confirmed facts, single-source claims, and unverified allegations.
58
+ Flag contradictions between sources.
59
+ Be thorough but concise.`;
60
+
61
+ export async function perplexitySearch(query: string, maxTokens: number): Promise<void> {
62
+ const apiKey = loadApiKey();
63
+
64
+ const body = {
65
+ model: DEFAULT_MODEL,
66
+ messages: [
67
+ { role: "system", content: SYSTEM_PROMPT },
68
+ { role: "user", content: query },
69
+ ],
70
+ max_tokens: maxTokens,
71
+ return_citations: true,
72
+ search_recency_filter: "week",
73
+ };
74
+
75
+ const response = await fetch(`${API_BASE}/chat/completions`, {
76
+ method: "POST",
77
+ headers: {
78
+ Authorization: `Bearer ${apiKey}`,
79
+ "Content-Type": "application/json",
80
+ },
81
+ body: JSON.stringify(body),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ const err = await response.text().catch(() => "");
86
+ console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ const data = (await response.json()) as PerplexityResponse;
91
+
92
+ if (data.error) {
93
+ console.error(`Error: ${data.error.message}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ if (!data.choices || data.choices.length === 0) {
98
+ console.error("Error: No choices in Perplexity response.");
99
+ process.exit(1);
100
+ }
101
+
102
+ const content = data.choices[0].message?.content;
103
+ if (!content) {
104
+ console.error("Error: No text content in Perplexity response.");
105
+ process.exit(1);
106
+ }
107
+
108
+ console.log(content);
109
+
110
+ // Extract citations
111
+ if (data.citations && data.citations.length > 0) {
112
+ console.log("\n---\n## Sources\n");
113
+ for (let i = 0; i < data.citations.length; i++) {
114
+ console.log(`- [${i + 1}] ${data.citations[i]}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ async function run() {
120
+ const { positionals, values } = parseArgs({
121
+ allowPositionals: true,
122
+ options: {
123
+ "max-tokens": { type: "string", short: "m", default: "4096" },
124
+ help: { type: "boolean", short: "h" },
125
+ },
126
+ });
127
+
128
+ if (values.help || positionals.length === 0) {
129
+ console.log(`Perplexity Search — investigative search via Perplexity Sonar API
130
+
131
+ Usage:
132
+ bun perplexity-search.ts -- <query> [options]
133
+
134
+ Options:
135
+ --max-tokens, -m <n> Max response tokens (default: 4096)
136
+ --help, -h Show this help
137
+
138
+ Examples:
139
+ bun perplexity-search.ts -- "corruption allegations timeline"
140
+ bun perplexity-search.ts -- "regulatory actions against company X"`);
141
+ process.exit(0);
142
+ }
143
+
144
+ const query = positionals.join(" ");
145
+ const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
146
+
147
+ await perplexitySearch(query, maxTokens);
148
+ }
149
+
150
+ if (import.meta.main) run();
@@ -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.20.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,12 +377,12 @@ 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)");
383
- process.env.GEMINI_API_KEY
384
- ? ok("GEMINI_API_KEY is set")
385
- : warn("GEMINI_API_KEY — not set (optional, for YouTube analysis)");
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
+ process.env.PAL_GEMINI_API_KEY
384
+ ? ok("PAL_GEMINI_API_KEY is set")
385
+ : warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
386
386
 
387
387
  // Hook health from debug.log
388
388
  const hookHealth = checkHookHealth(home);
@@ -443,6 +443,18 @@ async function init(args: string[]) {
443
443
  }
444
444
 
445
445
  async function install(targets: { claude: boolean; opencode: boolean; cursor: boolean }) {
446
+ // Ensure dependencies are installed
447
+ const pkg = palPkg();
448
+ log.info("Installing dependencies...");
449
+ const deps = spawnSync("bun", ["install", "--frozen-lockfile"], {
450
+ cwd: pkg,
451
+ stdio: "inherit",
452
+ shell: true,
453
+ });
454
+ if (deps.status !== 0) {
455
+ log.warn("bun install failed — continuing anyway, but hooks may not work");
456
+ }
457
+
446
458
  // Scaffold TELOS + PAL settings, then prompt for missing identity
447
459
  const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
448
460
  const { promptIdentity } = await import("./setup-identity");
@@ -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(
@@ -19,6 +19,7 @@ import {
19
19
  extractLastUser,
20
20
  parseMessages,
21
21
  } from "../lib/transcript";
22
+ import { appendProjectHistory } from "../lib/work-tracking";
22
23
 
23
24
  function slugify(text: string): string {
24
25
  return text
@@ -183,5 +184,13 @@ export async function captureWorkLearning(
183
184
  const filepath = resolve(dir, filename);
184
185
  writeFileSync(filepath, content, "utf-8");
185
186
 
187
+ // Append to per-project history (agent-agnostic recall)
188
+ appendProjectHistory(process.cwd(), {
189
+ date: new Date().toISOString().slice(0, 10),
190
+ title,
191
+ summary,
192
+ insights,
193
+ });
194
+
186
195
  if (sessionId) markCaptured(sessionId, filepath, messages.length);
187
196
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
+ copyFileSync,
10
11
  existsSync,
11
12
  lstatSync,
12
13
  readdirSync,
@@ -48,7 +49,7 @@ function latestMtime(...filePaths: string[]): number {
48
49
  return latest;
49
50
  }
50
51
 
51
- /** Create or verify a symlink pointing to AGENTS.md */
52
+ /** Create or verify a symlink pointing to AGENTS.md (falls back to copy on Windows EPERM) */
52
53
  function ensureOneSymlink(linkPath: string, targetPath: string): void {
53
54
  try {
54
55
  const stat = lstatSync(linkPath);
@@ -58,8 +59,16 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
58
59
  // doesn't exist — create it
59
60
  }
60
61
  ensureDir(dirname(linkPath));
61
- const relTarget = relative(dirname(linkPath), targetPath).replaceAll("\\", "/");
62
- symlinkSync(relTarget, linkPath);
62
+ try {
63
+ const relTarget = relative(dirname(linkPath), targetPath).replaceAll("\\", "/");
64
+ symlinkSync(relTarget, linkPath);
65
+ } catch (e: unknown) {
66
+ if ((e as NodeJS.ErrnoException).code === "EPERM") {
67
+ copyFileSync(targetPath, linkPath);
68
+ } else {
69
+ throw e;
70
+ }
71
+ }
63
72
  }
64
73
 
65
74
  /** Ensure all agent symlinks point to the canonical AGENTS.md */
@@ -156,9 +165,12 @@ export function buildClaudeMd(): string {
156
165
  /** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
157
166
  export function regenerateIfNeeded(): boolean {
158
167
  const { outputPath } = getOutputPaths();
159
- ensureSymlinks();
160
- if (!needsRebuild()) return false;
168
+ if (!needsRebuild()) {
169
+ ensureSymlinks();
170
+ return false;
171
+ }
161
172
  ensureDir(dirname(outputPath));
162
173
  writeFileSync(outputPath, buildClaudeMd(), "utf-8");
174
+ ensureSymlinks();
163
175
  return true;
164
176
  }