portable-agent-layer 0.61.3 → 0.61.4

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.
@@ -7,7 +7,7 @@ description: "Opinion tracker for relationship notes. PROACTIVE: When the user c
7
7
 
8
8
  Tool: `tools/opinion.ts`
9
9
 
10
- Manage confidence-scored opinions about the user. Opinions are promoted from recurring relationship notes (the W/O/B observations captured at session end). This tool is the fast path to grow opinion confidence — relationship notes feed the slow path (+2% per reflect cycle), while explicit confirmations here jump +10%.
10
+ Manage confidence-scored opinions about the user. Opinions are promoted from recurring relationship notes (the W/O/B observations captured at session end). This tool is the fast path to grow opinion confidence — relationship notes feed the slow path (+5% per reflect cycle), while explicit confirmations here jump +10%.
11
11
 
12
12
  Opinions at ≥85% confidence are automatically injected into every session context.
13
13
 
@@ -42,12 +42,12 @@ bun opinion.ts list
42
42
  # Show details for a specific opinion (fuzzy-matched)
43
43
  bun opinion.ts show "keywords"
44
44
 
45
- # Add a new opinion (starts at 50%)
45
+ # Add a new opinion (starts at 60%)
46
46
  bun opinion.ts add "statement" [--category workflow]
47
47
 
48
48
  # Add evidence to an existing opinion
49
- bun opinion.ts evidence "keywords" --supporting "why" # +2%
50
- bun opinion.ts evidence "keywords" --counter "why" # -5%
49
+ bun opinion.ts evidence "keywords" --supporting "why" # +5%
50
+ bun opinion.ts evidence "keywords" --counter "why" # -10%
51
51
  bun opinion.ts evidence "keywords" --confirmation "why" # +10%
52
52
  bun opinion.ts evidence "keywords" --contradiction "why" # -20%
53
53
  ```
@@ -59,9 +59,9 @@ bun opinion.ts evidence "keywords" --contradiction "why" # -20%
59
59
  ## Confidence Lifecycle
60
60
 
61
61
  ```
62
- New opinion (add) → 50%
63
- ├── supporting evidence → +2% each
64
- ├── counter evidence → -5% each
62
+ New opinion (add) → 60%
63
+ ├── supporting evidence → +5% each
64
+ ├── counter evidence → -10% each
65
65
  ├── explicit confirmation → +10% each
66
66
  ├── explicit contradiction → -20% each
67
67
  └── at ≥85% → auto-injected into session context
@@ -147,7 +147,9 @@ switch (command) {
147
147
  const opinion = createOpinion(statement, "manual add");
148
148
  opinion.category = category;
149
149
  saveOpinion(opinion);
150
- console.log(` Added: "${statement}" [${category}] at 50%`);
150
+ console.log(
151
+ ` Added: "${statement}" [${category}] at ${Math.round(opinion.confidence * 100)}%`
152
+ );
151
153
  break;
152
154
  }
153
155
 
@@ -218,9 +220,9 @@ switch (command) {
218
220
  Commands:
219
221
  list List all opinions with confidence bars
220
222
  show "keywords" Show opinion details + full evidence history
221
- add "statement" [--category X] Create new opinion (starts at 50%)
222
- evidence "keywords" --supporting "why" Supporting evidence (+2%)
223
- evidence "keywords" --counter "why" Counter evidence (-5%)
223
+ add "statement" [--category X] Create new opinion (starts at 60%)
224
+ evidence "keywords" --supporting "why" Supporting evidence (+5%)
225
+ evidence "keywords" --counter "why" Counter evidence (-10%)
224
226
  evidence "keywords" --confirmation "why" User explicitly confirmed (+10%)
225
227
  evidence "keywords" --contradiction "why" User explicitly contradicted (-20%)
226
228
 
@@ -2,8 +2,6 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Read(//*)",
5
- "Grep(//*)",
6
- "Glob(//*)",
7
5
  "WebFetch",
8
6
  "WebSearch",
9
7
  "Bash(cat //*)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.61.3",
3
+ "version": "0.61.4",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,7 @@ async function processFailurePrinciple(
60
60
  system: `Analyze this failed AI interaction (rated ${pending.rating}/10). Return JSON: {"principle": "<verb-first actionable rule, 10-20 words — write a full sentence, not a fragment>", "detailed_context": "<root cause and what to do differently, 50-150 words>"}.`,
61
61
  user: `User feedback: ${pending.context}\n\nConversation:\n${recent}`,
62
62
  maxTokens: 400,
63
- timeout: 60000,
63
+ timeout: 90000,
64
64
  jsonSchema: {
65
65
  type: "object" as const,
66
66
  properties: {
@@ -310,7 +310,7 @@ async function runSentimentInferenceAndStore(
310
310
  system: SENTIMENT_SYSTEM_PROMPT,
311
311
  user: contextBlock,
312
312
  maxTokens: 500,
313
- timeout: 60000,
313
+ timeout: 90000,
314
314
  jsonSchema: SENTIMENT_SCHEMA,
315
315
  caller: "rating",
316
316
  sessionId,
@@ -178,7 +178,7 @@ async function captureSessionIntelligence(
178
178
  ].join("\n"),
179
179
  user: `User messages:\n${numberedMessages}\n\nLast AI response:\n${assistantWindow}`,
180
180
  maxTokens: 350,
181
- timeout: 60000,
181
+ timeout: 90000,
182
182
  jsonSchema: INTELLIGENCE_SCHEMA,
183
183
  caller: "session-intelligence",
184
184
  sessionId,
@@ -70,7 +70,7 @@ async function upgradeWithInference(
70
70
  system: NAME_PROMPT,
71
71
  user: `Generate a 4-word title for: "${promptText}"`,
72
72
  maxTokens: 20,
73
- timeout: 60000,
73
+ timeout: 90000,
74
74
  caller: "session-name",
75
75
  sessionId,
76
76
  });
@@ -72,23 +72,26 @@ async function checkRepo(): Promise<UpdateCache> {
72
72
  });
73
73
  await fetch.exited;
74
74
 
75
- const local = Bun.spawn(["git", "rev-parse", "HEAD"], {
75
+ const remote = Bun.spawn(["git", "rev-parse", "origin/main"], {
76
76
  cwd: repoDir,
77
77
  stdout: "pipe",
78
78
  stderr: "ignore",
79
79
  windowsHide: true,
80
80
  });
81
- const localHash = (await new Response(local.stdout).text()).trim();
81
+ const remoteHash = (await new Response(remote.stdout).text()).trim();
82
82
 
83
- const remote = Bun.spawn(["git", "rev-parse", "origin/main"], {
83
+ // "Ahead" (local unpushed commits) is not an update — only remote commits we
84
+ // lack are. Count HEAD..origin/main so being ahead or diverged never nags.
85
+ const behind = Bun.spawn(["git", "rev-list", "--count", "HEAD..origin/main"], {
84
86
  cwd: repoDir,
85
87
  stdout: "pipe",
86
88
  stderr: "ignore",
87
89
  windowsHide: true,
88
90
  });
89
- const remoteHash = (await new Response(remote.stdout).text()).trim();
91
+ const behindCount =
92
+ Number.parseInt((await new Response(behind.stdout).text()).trim(), 10) || 0;
90
93
 
91
- const available = localHash !== remoteHash && remoteHash.length > 0;
94
+ const available = behindCount > 0 && remoteHash.length > 0;
92
95
 
93
96
  // Get remote version from package.json on origin/main
94
97
  let latest = current;
@@ -11,7 +11,7 @@
11
11
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  import { paths } from "./paths";
14
- import { similarity } from "./text-similarity";
14
+ import { containment, similarity } from "./text-similarity";
15
15
 
16
16
  // ── Types ──
17
17
 
@@ -118,16 +118,27 @@ function slugify(text: string): string {
118
118
  .join("-");
119
119
  }
120
120
 
121
- /** Find an existing opinion similar to the given text. Returns the opinion or null. */
121
+ /** Find the best-matching opinion for the given text. Returns the opinion or null. */
122
122
  export function findSimilarOpinion(
123
123
  text: string,
124
124
  opinions: Opinion[],
125
125
  threshold = 0.3
126
126
  ): Opinion | null {
127
+ let best: Opinion | null = null;
128
+ let bestScore = 0;
127
129
  for (const op of opinions) {
128
- if (similarity(text, op.statement) >= threshold) return op;
130
+ // Dice handles full-statement comparisons (dedup, reflect promotion);
131
+ // containment lets a short keyword query reach a long statement.
132
+ const score = Math.max(
133
+ similarity(text, op.statement),
134
+ containment(text, op.statement)
135
+ );
136
+ if (score >= threshold && score > bestScore) {
137
+ bestScore = score;
138
+ best = op;
139
+ }
129
140
  }
130
- return null;
141
+ return best;
131
142
  }
132
143
 
133
144
  /** Create a new opinion from a recurring note. Starts at confidence 0.60. */
@@ -155,3 +155,21 @@ export function similarity(a: string, b: string): number {
155
155
 
156
156
  return (2 * intersection) / (ka.size + kb.size);
157
157
  }
158
+
159
+ /**
160
+ * Asymmetric containment: fraction of `query` keywords also present in `target`.
161
+ * Unlike Dice, it is not penalized by `target` being much longer, so a short
162
+ * keyword query can still match a full-sentence statement.
163
+ */
164
+ export function containment(query: string, target: string): number {
165
+ const q = extractKeywords(query);
166
+ const t = extractKeywords(target);
167
+ if (q.size === 0 || t.size === 0) return 0;
168
+
169
+ let intersection = 0;
170
+ for (const w of q) {
171
+ if (t.has(w)) intersection++;
172
+ }
173
+
174
+ return intersection / q.size;
175
+ }
@@ -166,7 +166,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
166
166
  }
167
167
  }
168
168
 
169
- // Merge permissions.allow (deduplicate)
169
+ // Merge permissions.allow (deduplicate), then drop deprecated entries.
170
170
  if (template.permissions?.allow) {
171
171
  result.permissions ??= {};
172
172
  result.permissions.allow ??= [];
@@ -176,6 +176,14 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
176
176
  }
177
177
  }
178
178
  }
179
+ // Strip ineffective Grep(...)/Glob(...) rules left by older templates. Claude Code
180
+ // governs Grep/Glob via Read(...) rules, so a path-scoped Grep()/Glob() entry never
181
+ // matches and Claude Code warns about it on every prompt. Read(//*) already covers them.
182
+ if (result.permissions?.allow) {
183
+ result.permissions.allow = result.permissions.allow.filter(
184
+ (perm) => !isIneffectiveFileToolRule(perm)
185
+ );
186
+ }
179
187
 
180
188
  // Merge skillOverrides (object with skill name keys, add if not present)
181
189
  if (template.skillOverrides && typeof template.skillOverrides === "object") {
@@ -455,6 +463,15 @@ function canonicalPalCmd(cmd: string): string {
455
463
  return withoutEnv;
456
464
  }
457
465
 
466
+ /**
467
+ * True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
468
+ * it resolves Grep/Glob permission through Read(...) rules, so these never match and
469
+ * trigger a warning on every prompt. Bare "Grep"/"Glob" tool allows are left intact.
470
+ */
471
+ function isIneffectiveFileToolRule(perm: string): boolean {
472
+ return /^(?:Grep|Glob)\(/.test(perm);
473
+ }
474
+
458
475
  export function loadCodexHooksTemplate(
459
476
  templatePath: string,
460
477
  pkgRoot: string