portable-agent-layer 0.9.0 → 0.11.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 (51) hide show
  1. package/assets/skills/{analyze-pdf.md → analyze-pdf/SKILL.md} +4 -4
  2. package/{src → assets/skills/analyze-pdf}/tools/pdf-download.ts +3 -3
  3. package/assets/skills/{analyze-youtube.md → analyze-youtube/SKILL.md} +4 -4
  4. package/{src → assets/skills/analyze-youtube}/tools/youtube-analyze.ts +2 -2
  5. package/assets/skills/{council.md → council/SKILL.md} +3 -2
  6. package/assets/skills/{create-skill.md → create-skill/SKILL.md} +2 -1
  7. package/assets/skills/{extract-entities.md → extract-entities/SKILL.md} +4 -5
  8. package/{src → assets/skills/extract-entities}/tools/entity-save.ts +3 -3
  9. package/assets/skills/{extract-wisdom.md → extract-wisdom/SKILL.md} +3 -2
  10. package/assets/skills/{first-principles.md → first-principles/SKILL.md} +3 -2
  11. package/assets/skills/{fyzz-chat-api.md → fyzz-chat-api/SKILL.md} +6 -6
  12. package/{src → assets/skills/fyzz-chat-api}/tools/fyzz-api.ts +6 -6
  13. package/assets/skills/{reflect.md → reflect/SKILL.md} +2 -1
  14. package/assets/skills/{research.md → research/SKILL.md} +2 -1
  15. package/assets/skills/{review.md → review/SKILL.md} +2 -1
  16. package/assets/skills/{summarize.md → summarize/SKILL.md} +3 -2
  17. package/assets/skills/telos/SKILL.md +60 -0
  18. package/assets/skills/telos/tools/update-telos.ts +101 -0
  19. package/assets/skills/think/SKILL.md +47 -0
  20. package/assets/templates/AGENTS.md.template +8 -37
  21. package/assets/templates/PAL/CONTEXT_ROUTING.md +12 -0
  22. package/assets/templates/PAL/MEMORY_SYSTEM.md +26 -0
  23. package/assets/templates/PAL/OPINION_TRACKING.md +3 -0
  24. package/assets/templates/PAL/STEERING_RULES.md +23 -0
  25. package/assets/templates/PAL/WORK_TRACKING.md +14 -0
  26. package/assets/templates/settings.claude.json +80 -0
  27. package/package.json +2 -5
  28. package/src/hooks/handlers/rating.ts +4 -47
  29. package/src/hooks/handlers/reflect-trigger.ts +83 -0
  30. package/src/hooks/handlers/relationship.ts +8 -5
  31. package/src/hooks/handlers/session-name.ts +8 -6
  32. package/src/hooks/handlers/work-learning.ts +1 -0
  33. package/src/hooks/handlers/work-session.ts +16 -3
  34. package/src/hooks/lib/claude-md.ts +9 -24
  35. package/src/hooks/lib/context.ts +31 -48
  36. package/src/hooks/lib/graduation.ts +6 -4
  37. package/src/hooks/lib/learning-store.ts +7 -117
  38. package/src/hooks/lib/opinions.ts +191 -0
  39. package/src/hooks/lib/paths.ts +2 -0
  40. package/src/hooks/lib/relationship.ts +5 -4
  41. package/src/hooks/lib/security.ts +2 -0
  42. package/src/hooks/lib/stop.ts +3 -0
  43. package/src/hooks/lib/text-similarity.ts +125 -0
  44. package/src/targets/claude/install.ts +16 -93
  45. package/src/targets/claude/uninstall.ts +22 -47
  46. package/src/targets/lib.ts +190 -48
  47. package/src/targets/opencode/install.ts +13 -2
  48. package/src/targets/opencode/uninstall.ts +4 -1
  49. package/src/tools/analyze.ts +49 -15
  50. package/src/tools/opinion.ts +250 -0
  51. package/src/tools/relationship-reflect.ts +215 -105
@@ -13,6 +13,7 @@ import { parse } from "./frontmatter";
13
13
 
14
14
  export interface FailureEntry {
15
15
  slug: string;
16
+ path: string;
16
17
  rating: number;
17
18
  context: string;
18
19
  principle: string;
@@ -22,11 +23,13 @@ export interface FailureEntry {
22
23
 
23
24
  export interface LearningEntry {
24
25
  filename: string;
26
+ path: string;
25
27
  title: string;
26
28
  category: string;
27
29
  principle: string;
28
30
  date: string;
29
31
  insights: string;
32
+ cwd: string;
30
33
  }
31
34
 
32
35
  // ── Shared Directory Walker ──
@@ -79,6 +82,7 @@ export function readFailures(baseDir: string, limit?: number): FailureEntry[] {
79
82
 
80
83
  entries.push({
81
84
  slug: meta.slug || slug,
85
+ path: capturePath,
82
86
  rating: meta.rating ?? 0,
83
87
  context: meta.context,
84
88
  principle: meta.principle || "",
@@ -119,6 +123,7 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
119
123
  category?: string;
120
124
  principle?: string;
121
125
  date?: string;
126
+ cwd?: string;
122
127
  }>(content);
123
128
 
124
129
  if (!meta.title) continue;
@@ -127,11 +132,13 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
127
132
 
128
133
  entries.push({
129
134
  filename: file,
135
+ path: resolve(monthDir, file),
130
136
  title: meta.title,
131
137
  category: meta.category || "algorithm",
132
138
  principle: meta.principle || "",
133
139
  date: meta.date || "",
134
140
  insights: insightsMatch?.[1]?.trim() || "",
141
+ cwd: meta.cwd || "",
135
142
  });
136
143
 
137
144
  if (limit && entries.length >= limit) return entries;
@@ -146,120 +153,3 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
146
153
 
147
154
  return entries;
148
155
  }
149
-
150
- // ── Text Similarity (Jaccard on keywords) ──
151
-
152
- const STOP_WORDS = new Set([
153
- "the",
154
- "a",
155
- "an",
156
- "is",
157
- "was",
158
- "are",
159
- "were",
160
- "be",
161
- "been",
162
- "being",
163
- "have",
164
- "has",
165
- "had",
166
- "do",
167
- "does",
168
- "did",
169
- "will",
170
- "would",
171
- "could",
172
- "should",
173
- "may",
174
- "might",
175
- "can",
176
- "shall",
177
- "to",
178
- "of",
179
- "in",
180
- "for",
181
- "on",
182
- "with",
183
- "at",
184
- "by",
185
- "from",
186
- "as",
187
- "into",
188
- "through",
189
- "during",
190
- "before",
191
- "after",
192
- "and",
193
- "but",
194
- "or",
195
- "nor",
196
- "not",
197
- "no",
198
- "so",
199
- "if",
200
- "then",
201
- "than",
202
- "that",
203
- "this",
204
- "it",
205
- "its",
206
- "i",
207
- "you",
208
- "he",
209
- "she",
210
- "we",
211
- "they",
212
- "my",
213
- "your",
214
- "his",
215
- "her",
216
- "our",
217
- "their",
218
- "what",
219
- "which",
220
- "who",
221
- "when",
222
- "where",
223
- "how",
224
- "all",
225
- "each",
226
- "both",
227
- "few",
228
- "more",
229
- "most",
230
- "other",
231
- "some",
232
- "such",
233
- "up",
234
- "out",
235
- "about",
236
- "just",
237
- "also",
238
- "very",
239
- "too",
240
- "only",
241
- "own",
242
- ]);
243
-
244
- export function extractKeywords(text: string): Set<string> {
245
- return new Set(
246
- text
247
- .toLowerCase()
248
- .replace(/[^a-z0-9\s-]/g, " ")
249
- .split(/\s+/)
250
- .filter((w) => w.length > 2 && !STOP_WORDS.has(w))
251
- );
252
- }
253
-
254
- export function similarity(a: string, b: string): number {
255
- const ka = extractKeywords(a);
256
- const kb = extractKeywords(b);
257
- if (ka.size === 0 || kb.size === 0) return 0;
258
-
259
- let intersection = 0;
260
- for (const w of ka) {
261
- if (kb.has(w)) intersection++;
262
- }
263
- const union = new Set([...ka, ...kb]).size;
264
- return union > 0 ? intersection / union : 0;
265
- }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Opinion store — persistent confidence-tracked opinions about the user.
3
+ *
4
+ * Opinions are promoted from recurring relationship notes (O/B types) by the
5
+ * reflect tool. Confidence evolves with evidence over time.
6
+ * High-confidence opinions (≥85%) are injected into every session context.
7
+ *
8
+ * Storage: memory/relationship/opinions.json
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { paths } from "./paths";
14
+ import { similarity } from "./text-similarity";
15
+
16
+ // ── Types ──
17
+
18
+ export type EvidenceType = "supporting" | "counter" | "confirmation" | "contradiction";
19
+ export type OpinionCategory = "communication" | "technical" | "workflow" | "general";
20
+
21
+ export interface Evidence {
22
+ date: string;
23
+ type: EvidenceType;
24
+ source: string;
25
+ }
26
+
27
+ export interface Opinion {
28
+ id: string;
29
+ statement: string;
30
+ confidence: number;
31
+ category: OpinionCategory;
32
+ evidence: Evidence[];
33
+ created: string;
34
+ updated: string;
35
+ }
36
+
37
+ // ── Confidence Deltas (matching original PAI) ──
38
+
39
+ const CONFIDENCE_DELTAS: Record<EvidenceType, number> = {
40
+ supporting: 0.02,
41
+ counter: -0.05,
42
+ confirmation: 0.1,
43
+ contradiction: -0.2,
44
+ };
45
+
46
+ const MIN_CONFIDENCE = 0.01;
47
+ const MAX_CONFIDENCE = 0.99;
48
+ const HIGH_CONFIDENCE_THRESHOLD = 0.85;
49
+
50
+ // ── Category Classification ──
51
+
52
+ const CATEGORY_MAP: [RegExp, OpinionCategory][] = [
53
+ [/tone|format|response|verbose|brief|concise|explain|direct|style/i, "communication"],
54
+ [/code|test|build|deploy|function|type|lint|debug|refactor/i, "technical"],
55
+ [/commit|git|release|workflow|process|approach|iterative|step/i, "workflow"],
56
+ ];
57
+
58
+ function classifyCategory(text: string): OpinionCategory {
59
+ for (const [pattern, category] of CATEGORY_MAP) {
60
+ if (pattern.test(text)) return category;
61
+ }
62
+ return "general";
63
+ }
64
+
65
+ // ── File Path ──
66
+
67
+ function opinionsPath(): string {
68
+ return resolve(paths.relationship(), "opinions.json");
69
+ }
70
+
71
+ // ── Store Format ──
72
+
73
+ interface OpinionStore {
74
+ lastReflect: string;
75
+ opinions: Opinion[];
76
+ }
77
+
78
+ // ── CRUD ──
79
+
80
+ function readStore(): OpinionStore {
81
+ const fp = opinionsPath();
82
+ if (!existsSync(fp)) return { lastReflect: "", opinions: [] };
83
+ try {
84
+ const raw = JSON.parse(readFileSync(fp, "utf-8"));
85
+ // Migrate from plain array format
86
+ if (Array.isArray(raw)) return { lastReflect: "", opinions: raw };
87
+ return raw as OpinionStore;
88
+ } catch {
89
+ return { lastReflect: "", opinions: [] };
90
+ }
91
+ }
92
+
93
+ function writeStore(store: OpinionStore): void {
94
+ writeFileSync(opinionsPath(), JSON.stringify(store, null, 2), "utf-8");
95
+ }
96
+
97
+ export function readOpinions(): Opinion[] {
98
+ return readStore().opinions;
99
+ }
100
+
101
+ export function getLastReflectDate(): string {
102
+ return readStore().lastReflect;
103
+ }
104
+
105
+ export function setLastReflectDate(date: string): void {
106
+ const store = readStore();
107
+ store.lastReflect = date;
108
+ writeStore(store);
109
+ }
110
+
111
+ function slugify(text: string): string {
112
+ return text
113
+ .toLowerCase()
114
+ .replace(/[^a-z0-9\s]/g, "")
115
+ .trim()
116
+ .split(/\s+/)
117
+ .slice(0, 6)
118
+ .join("-");
119
+ }
120
+
121
+ /** Find an existing opinion similar to the given text. Returns the opinion or null. */
122
+ export function findSimilarOpinion(
123
+ text: string,
124
+ opinions: Opinion[],
125
+ threshold = 0.3
126
+ ): Opinion | null {
127
+ for (const op of opinions) {
128
+ if (similarity(text, op.statement) >= threshold) return op;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /** Create a new opinion from a recurring note. Starts at confidence 0.50. */
134
+ export function createOpinion(statement: string, source: string): Opinion {
135
+ const now = new Date().toISOString().slice(0, 10);
136
+ return {
137
+ id: slugify(statement),
138
+ statement,
139
+ confidence: 0.5,
140
+ category: classifyCategory(statement),
141
+ evidence: [{ date: now, type: "supporting", source }],
142
+ created: now,
143
+ updated: now,
144
+ };
145
+ }
146
+
147
+ /** Add evidence to an opinion and adjust its confidence. */
148
+ export function addEvidence(
149
+ opinion: Opinion,
150
+ type: EvidenceType,
151
+ source: string
152
+ ): Opinion {
153
+ const now = new Date().toISOString().slice(0, 10);
154
+ const delta = CONFIDENCE_DELTAS[type];
155
+ const newConfidence = Math.min(
156
+ MAX_CONFIDENCE,
157
+ Math.max(MIN_CONFIDENCE, opinion.confidence + delta)
158
+ );
159
+
160
+ return {
161
+ ...opinion,
162
+ confidence: Math.round(newConfidence * 100) / 100,
163
+ evidence: [...opinion.evidence, { date: now, type, source }],
164
+ updated: now,
165
+ };
166
+ }
167
+
168
+ /** Upsert an opinion into the store. */
169
+ export function saveOpinion(opinion: Opinion): void {
170
+ const store = readStore();
171
+ const idx = store.opinions.findIndex((o) => o.id === opinion.id);
172
+ if (idx >= 0) {
173
+ store.opinions[idx] = opinion;
174
+ } else {
175
+ store.opinions.push(opinion);
176
+ }
177
+ writeStore(store);
178
+ }
179
+
180
+ // ── Context Loading ──
181
+
182
+ /** Load high-confidence opinions formatted for system-reminder injection. */
183
+ export function loadOpinionContext(threshold = HIGH_CONFIDENCE_THRESHOLD): string {
184
+ const opinions = readOpinions().filter((o) => o.confidence >= threshold);
185
+ if (opinions.length === 0) return "";
186
+
187
+ const lines = opinions.map(
188
+ (o) => `- [${o.category}] ${o.statement} (${Math.round(o.confidence * 100)}%)`
189
+ );
190
+ return ["## Tracked Opinions", ...lines].join("\n");
191
+ }
@@ -77,4 +77,6 @@ export const assets = {
77
77
  hooks: () => pkg("src", "hooks"),
78
78
  telosTemplates: () => pkg("assets", "templates", "telos"),
79
79
  agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
80
+ claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
81
+ palDocs: () => pkg("assets", "templates", "PAL"),
80
82
  } as const;
@@ -5,6 +5,7 @@
5
5
  * Notes live at memory/relationship/YYYY-MM/YYYY-MM-DD.md
6
6
  * W = world (facts about user's situation)
7
7
  * O = opinion (preference with confidence)
8
+ * B = belief (behavioral pattern with confidence)
8
9
  *
9
10
  * Extraction is handled by the relationship handler via Haiku inference.
10
11
  * This lib provides storage and reading utilities only.
@@ -14,7 +15,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
14
15
  import { resolve } from "node:path";
15
16
  import { ensureDir, paths } from "./paths";
16
17
 
17
- export type NoteType = "W" | "O";
18
+ export type NoteType = "W" | "O" | "B";
18
19
 
19
20
  export interface RelationshipNote {
20
21
  type: NoteType;
@@ -79,8 +80,8 @@ export function appendNotes(notes: RelationshipNote[], sessionId?: string): void
79
80
  if (sessionId) lines.push(`<!-- session:${sessionId} -->`);
80
81
 
81
82
  for (const note of fresh) {
82
- if (note.type === "O" && note.confidence !== undefined) {
83
- lines.push(`- O(c=${note.confidence}): ${note.text}`);
83
+ if ((note.type === "O" || note.type === "B") && note.confidence !== undefined) {
84
+ lines.push(`- ${note.type}(c=${note.confidence}): ${note.text}`);
84
85
  } else {
85
86
  lines.push(`- ${note.type}: ${note.text}`);
86
87
  }
@@ -103,8 +104,8 @@ export function loadRecentNotes(days: number = 2): string {
103
104
  const sections: string[] = [];
104
105
 
105
106
  for (const monthDir of readdirSync(relDir).sort().reverse()) {
107
+ if (!/^\d{4}-\d{2}$/.test(monthDir)) continue;
106
108
  const monthPath = resolve(relDir, monthDir);
107
- if (!existsSync(monthPath)) continue;
108
109
 
109
110
  let files: string[];
110
111
  try {
@@ -32,6 +32,7 @@ export const HOOK_MANAGED_FILES = [
32
32
  "graduated.json",
33
33
  "update-available.json",
34
34
  "debug.log.prev",
35
+ "opinions.json",
35
36
  ];
36
37
 
37
38
  /** Hook-managed directories — AI must not write to or delete from these */
@@ -42,6 +43,7 @@ export const HOOK_MANAGED_DIRS = [
42
43
  "memory/learning/synthesis",
43
44
  "memory/relationship",
44
45
  "memory/wisdom/state",
46
+ ".agents/PAL",
45
47
  ];
46
48
 
47
49
  /** Escape a string for use in a RegExp */
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { autoBackup } from "../handlers/backup";
9
9
  import { captureFailure } from "../handlers/failure";
10
+ import { checkReflectTrigger } from "../handlers/reflect-trigger";
10
11
  import { captureRelationship } from "../handlers/relationship";
11
12
  import { resetTab } from "../handlers/tab";
12
13
  import { updateCounts } from "../handlers/update-counts";
@@ -43,6 +44,7 @@ export async function runStopHandlers(
43
44
  checkPendingFailure(transcript),
44
45
  updateCounts(),
45
46
  autoBackup(),
47
+ checkReflectTrigger(),
46
48
  ]);
47
49
 
48
50
  const handlerNames = [
@@ -53,6 +55,7 @@ export async function runStopHandlers(
53
55
  "pending-failure",
54
56
  "update-counts",
55
57
  "backup",
58
+ "reflect-trigger",
56
59
  ];
57
60
  for (let i = 0; i < results.length; i++) {
58
61
  const r = results[i];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Text similarity using Dice coefficient on keyword sets.
3
+ *
4
+ * Dice is more generous than Jaccard for short, variable-length texts
5
+ * (like chatbot messages and failure contexts) because it divides by
6
+ * average set size instead of union:
7
+ *
8
+ * Dice = 2 * |intersection| / (|A| + |B|)
9
+ */
10
+
11
+ const STOP_WORDS = new Set([
12
+ "the",
13
+ "a",
14
+ "an",
15
+ "is",
16
+ "was",
17
+ "are",
18
+ "were",
19
+ "be",
20
+ "been",
21
+ "being",
22
+ "have",
23
+ "has",
24
+ "had",
25
+ "do",
26
+ "does",
27
+ "did",
28
+ "will",
29
+ "would",
30
+ "could",
31
+ "should",
32
+ "may",
33
+ "might",
34
+ "can",
35
+ "shall",
36
+ "to",
37
+ "of",
38
+ "in",
39
+ "for",
40
+ "on",
41
+ "with",
42
+ "at",
43
+ "by",
44
+ "from",
45
+ "as",
46
+ "into",
47
+ "through",
48
+ "during",
49
+ "before",
50
+ "after",
51
+ "and",
52
+ "but",
53
+ "or",
54
+ "nor",
55
+ "not",
56
+ "no",
57
+ "so",
58
+ "if",
59
+ "then",
60
+ "than",
61
+ "that",
62
+ "this",
63
+ "it",
64
+ "its",
65
+ "i",
66
+ "you",
67
+ "he",
68
+ "she",
69
+ "we",
70
+ "they",
71
+ "my",
72
+ "your",
73
+ "his",
74
+ "her",
75
+ "our",
76
+ "their",
77
+ "what",
78
+ "which",
79
+ "who",
80
+ "when",
81
+ "where",
82
+ "how",
83
+ "all",
84
+ "each",
85
+ "both",
86
+ "few",
87
+ "more",
88
+ "most",
89
+ "other",
90
+ "some",
91
+ "such",
92
+ "up",
93
+ "out",
94
+ "about",
95
+ "just",
96
+ "also",
97
+ "very",
98
+ "too",
99
+ "only",
100
+ "own",
101
+ ]);
102
+
103
+ export function extractKeywords(text: string): Set<string> {
104
+ return new Set(
105
+ text
106
+ .toLowerCase()
107
+ .replace(/[^a-z0-9\s-]/g, " ")
108
+ .split(/\s+/)
109
+ .filter((w) => w.length > 2 && !STOP_WORDS.has(w))
110
+ );
111
+ }
112
+
113
+ /** Dice coefficient on keyword sets. Returns 0-1. */
114
+ export function similarity(a: string, b: string): number {
115
+ const ka = extractKeywords(a);
116
+ const kb = extractKeywords(b);
117
+ if (ka.size === 0 || kb.size === 0) return 0;
118
+
119
+ let intersection = 0;
120
+ for (const w of ka) {
121
+ if (kb.has(w)) intersection++;
122
+ }
123
+
124
+ return (2 * intersection) / (ka.size + kb.size);
125
+ }