portable-agent-layer 0.61.2 → 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.
- package/assets/skills/opinion/SKILL.md +7 -7
- package/assets/skills/opinion/tools/opinion.ts +6 -4
- package/assets/templates/settings.claude.json +0 -2
- package/package.json +1 -1
- package/src/hooks/handlers/failure-principle.ts +1 -1
- package/src/hooks/handlers/rating.ts +1 -1
- package/src/hooks/handlers/session-intelligence.ts +1 -1
- package/src/hooks/handlers/session-name.ts +1 -1
- package/src/hooks/handlers/update-check.ts +8 -5
- package/src/hooks/lib/opinions.ts +15 -4
- package/src/hooks/lib/text-similarity.ts +18 -0
- package/src/targets/lib.ts +18 -1
- package/src/tools/agent/project.ts +18 -1
|
@@ -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 (+
|
|
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
|
|
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" # +
|
|
50
|
-
bun opinion.ts evidence "keywords" --counter "why" # -
|
|
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) →
|
|
63
|
-
├── supporting evidence → +
|
|
64
|
-
├── counter evidence → -
|
|
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(
|
|
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
|
|
222
|
-
evidence "keywords" --supporting "why" Supporting evidence (+
|
|
223
|
-
evidence "keywords" --counter "why" Counter evidence (-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -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:
|
|
63
|
+
timeout: 90000,
|
|
64
64
|
jsonSchema: {
|
|
65
65
|
type: "object" as const,
|
|
66
66
|
properties: {
|
|
@@ -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:
|
|
181
|
+
timeout: 90000,
|
|
182
182
|
jsonSchema: INTELLIGENCE_SCHEMA,
|
|
183
183
|
caller: "session-intelligence",
|
|
184
184
|
sessionId,
|
|
@@ -72,23 +72,26 @@ async function checkRepo(): Promise<UpdateCache> {
|
|
|
72
72
|
});
|
|
73
73
|
await fetch.exited;
|
|
74
74
|
|
|
75
|
-
const
|
|
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
|
|
81
|
+
const remoteHash = (await new Response(remote.stdout).text()).trim();
|
|
82
82
|
|
|
83
|
-
|
|
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
|
|
91
|
+
const behindCount =
|
|
92
|
+
Number.parseInt((await new Response(behind.stdout).text()).trim(), 10) || 0;
|
|
90
93
|
|
|
91
|
-
const available =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/targets/lib.ts
CHANGED
|
@@ -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
|
|
@@ -378,6 +378,23 @@ function removeIscLine(
|
|
|
378
378
|
};
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
// Drops any "### Archived <date>" heading whose block has no content left —
|
|
382
|
+
// e.g. after every ISC filed under that date has been reopened.
|
|
383
|
+
function dropEmptyArchiveHeadings(changelog: string): string {
|
|
384
|
+
const lines = changelog.split("\n");
|
|
385
|
+
const blockHasContent = (headingIdx: number): boolean => {
|
|
386
|
+
for (let j = headingIdx + 1; j < lines.length && !lines[j].startsWith("### "); j++) {
|
|
387
|
+
if (lines[j].trim() !== "") return true;
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
};
|
|
391
|
+
return lines
|
|
392
|
+
.filter((l, i) => !(/^### Archived /.test(l) && !blockHasContent(i)))
|
|
393
|
+
.join("\n")
|
|
394
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
395
|
+
.trim();
|
|
396
|
+
}
|
|
397
|
+
|
|
381
398
|
function archiveLine(changelog: string | undefined, doneLine: string): string {
|
|
382
399
|
const heading = `### Archived ${new Date().toISOString().slice(0, 10)}`;
|
|
383
400
|
const base = (changelog ?? "").trim();
|
|
@@ -439,7 +456,7 @@ function cmdReopenIsc(args: string[]): void {
|
|
|
439
456
|
}
|
|
440
457
|
let removed = removeIscLine(p.changelog ?? "", id);
|
|
441
458
|
if (removed.line) {
|
|
442
|
-
p.changelog = removed.rest;
|
|
459
|
+
p.changelog = dropEmptyArchiveHeadings(removed.rest);
|
|
443
460
|
} else {
|
|
444
461
|
removed = removeIscLine(p.criteria ?? "", id);
|
|
445
462
|
if (removed.line) p.criteria = removed.rest;
|