portable-agent-layer 0.108.0 → 0.110.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/scientific-research/SKILL.md +58 -12
- package/package.json +6 -2
- package/src/cli/correction.ts +115 -0
- package/src/cli/infer.ts +1 -1
- package/src/cli/rule.ts +121 -14
- package/src/cli/tree.ts +2 -0
- package/src/hooks/handlers/rating.ts +21 -2
- package/src/hooks/handlers/update-check.ts +6 -1
- package/src/hooks/lib/adaptation-candidates.ts +41 -13
- package/src/hooks/lib/adaptation-rules.ts +138 -9
- package/src/hooks/lib/adaptation-steering.ts +7 -2
- package/src/hooks/lib/adaptation-turns.ts +43 -1
- package/src/hooks/lib/context.ts +2 -2
- package/src/hooks/lib/correction-backfill.ts +56 -0
- package/src/hooks/lib/correction-kind.ts +165 -0
- package/src/hooks/lib/correction-rates.ts +151 -0
- package/src/hooks/lib/inference.ts +43 -18
- package/src/hooks/lib/models.ts +76 -28
- package/src/hooks/lib/opinion-gate.ts +73 -0
- package/src/hooks/lib/rule-drafter.ts +57 -24
- package/src/hooks/lib/semi-static.ts +2 -2
- package/src/tools/control-room/relationship.ts +43 -18
- package/src/tools/control-room/server.ts +12 -5
- package/src/tools/control-room/ui/dist/assets/{index-mFpXpiBj.js → index-D0pEd7Ln.js} +1 -1
- package/src/tools/control-room/ui/dist/index.html +1 -1
- package/src/tools/control-room/ui/lib/write.ts +3 -1
- package/src/tools/control-room/ui/screens/relationship.tsx +101 -29
package/README.md
CHANGED
|
@@ -90,14 +90,15 @@ pal cli status # check your setup
|
|
|
90
90
|
| `pal cli patterns` | How you work across projects, read from the event log |
|
|
91
91
|
| `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
|
|
92
92
|
| `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
|
|
93
|
-
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium
|
|
93
|
+
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium\|large\|flagship` (on Claude: Haiku, Sonnet, Opus, Fable; on Codex: Luna, Sol, Sol at high effort, Astra; on Antigravity: Flash at low, medium and high effort, and no flagship; an agent without the tier asked refuses it), `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
|
94
94
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
95
95
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
96
96
|
| `pal cli telos` | Which TELOS topics are answered, in interview order, and which one comes next |
|
|
97
97
|
| `pal cli timezone [<zone>]` | Show or set your timezone. IANA names only, validated before it is stored |
|
|
98
98
|
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
99
99
|
| `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
|
|
100
|
-
| `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `deny <id
|
|
100
|
+
| `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `opinion <id>`, `deny <id>`, `retire <id>`, `add-opinion --kind --when --steering`. A draft steers nothing until approved; a denied one is never drafted again. An opinion has no trigger and steers every session, at most 8 of them and one per correction kind; `list --all` shows each one's correction-kind rate before and after approval |
|
|
101
|
+
| `pal cli correction` | The kinds of mistakes you corrected — `kinds [--days N] [--split DATE] [--json]` shows each kind per 100 turns, before and after a date with `--split`; `backfill` labels confirmed corrections without a kind or with one from an older prompt, and `--relabel` labels every one again |
|
|
101
102
|
| `pal cli server` | The morning screen: a local page to open before a terminal — three moves for today, an urgent/important grid over every project *and* every stated goal, where you left off, and a drawer holding the signal and the action ledger — `start [--port <n>]`, `stop`, `status`. Loopback only, default port 7250. No model runs on page load |
|
|
102
103
|
| `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
|
|
103
104
|
| `pal cli skill doctor <name>` | Evaluate a skill against the authoring best practices (folder/file-name match, name, description, body length, point-of-view, reference depth) |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scientific-research
|
|
3
3
|
license: MIT
|
|
4
|
-
description: "Answers scientific questions from trustworthy primary sources only (PubMed-indexed peer-reviewed literature, clinical guidelines from ERC, ESC, AHA, CDC and specialty societies, NEJM, The Lancet, JAMA, BMJ, UpToDate when accessible), with every claim cited to a source that was actually read, graded by evidence level and year, and cross-checked by a verifier agent. Use for EVERY health, medical, nutrition, drug, supplement, symptom, disease, treatment, diagnosis, exercise-physiology or other empirical scientific question, including 'is it safe to', 'does X work', 'what does the evidence say', 'side effects of', 'how much should I take' and 'what do the guidelines recommend'. Never answer such questions from memory; route them here."
|
|
4
|
+
description: "Answers scientific questions from trustworthy primary sources only (PubMed-indexed peer-reviewed literature, clinical guidelines from ERC, ESC, AHA, CDC and specialty societies, NEJM, The Lancet, JAMA, BMJ, UpToDate when accessible), with every claim cited to a source that was actually read, graded by evidence level and year, its funding and any vendor ties named, and cross-checked by a verifier agent. Excludes preprints and MDPI journals and prefers SJR Q1 journals. Use for EVERY health, medical, nutrition, drug, supplement, symptom, disease, treatment, diagnosis, exercise-physiology or other empirical scientific question, including 'is it safe to', 'does X work', 'what does the evidence say', 'side effects of', 'how much should I take' and 'what do the guidelines recommend'. Never answer such questions from memory; route them here."
|
|
5
5
|
argument-hint: <scientific or health question>
|
|
6
6
|
metadata:
|
|
7
7
|
source: portable-agent-layer
|
|
@@ -57,9 +57,26 @@ specialty society (WHO, NICE, EMA, FDA and their peers). Label each such
|
|
|
57
57
|
source in the answer as "outside the core list".
|
|
58
58
|
|
|
59
59
|
Not sources, under any circumstances: blogs, news sites, forums, supplement or
|
|
60
|
-
vendor sites, encyclopedias, press releases, lecture slides, model memory.
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
vendor sites, encyclopedias, press releases, lecture slides, model memory.
|
|
61
|
+
|
|
62
|
+
Excluded even when they look peer-reviewed:
|
|
63
|
+
|
|
64
|
+
- **Preprints and repositories**: bioRxiv, medRxiv, PsyArXiv, arXiv, SSRN, Research
|
|
65
|
+
Square, OSF Preprints, Preprints.org and any other server of unreviewed
|
|
66
|
+
manuscripts. Do not search them. PubMed and Europe PMC also index preprints, so
|
|
67
|
+
add `NOT preprint[pt]` to PubMed queries and drop any record whose publication type
|
|
68
|
+
is Preprint.
|
|
69
|
+
- **MDPI journals** (Nutrients, Medicina, IJMS, Healthcare and the rest), identified
|
|
70
|
+
by publisher, not by title: a DOI starting with `10.3390` or a page on `mdpi.com`.
|
|
71
|
+
Add `-site:mdpi.com` to web searches.
|
|
72
|
+
|
|
73
|
+
### Journal rank
|
|
74
|
+
|
|
75
|
+
Prefer journals ranked Q1 by SCImago (SJR, `scimagojr.com`) in the subject category
|
|
76
|
+
that fits the question. Rank breaks ties within one evidence level and never
|
|
77
|
+
outranks study design: a Q2 RCT beats a Q1 observational study. Cite a journal below
|
|
78
|
+
Q1 only when no Q1 source covers the claim, and label it with its quartile, for
|
|
79
|
+
example "SJR Q2". Guidelines and official bodies are not ranked.
|
|
63
80
|
|
|
64
81
|
## Citation and evidence rules
|
|
65
82
|
|
|
@@ -74,12 +91,23 @@ For every claim record and later print:
|
|
|
74
91
|
review.
|
|
75
92
|
- **Year** of publication or of the guideline edition, so stale evidence is visible.
|
|
76
93
|
When a newer edition of a guideline exists, cite the newer one.
|
|
94
|
+
- **Journal quartile**: the SJR quartile of the journal, for every journal article.
|
|
95
|
+
- **Funding and conflicts of interest**, read from the source's Funding, Acknowledgements
|
|
96
|
+
and Competing Interests sections, never inferred. Name the funders, then add one label:
|
|
97
|
+
- **vendor-funded**: a funder sells the product or intervention studied, or is a
|
|
98
|
+
trade group of its sellers (manufacturer, supplement brand, food or drug industry
|
|
99
|
+
association).
|
|
100
|
+
- **vendor-linked authors**: the funding is independent, but an author is employed,
|
|
101
|
+
paid or holds shares in such a company.
|
|
102
|
+
- **funding not stated**: the opened source has no funding statement.
|
|
103
|
+
- **funding not accessible**: the statement sits behind a paywall that did not open.
|
|
77
104
|
|
|
78
105
|
Apply these tests before keeping a claim:
|
|
79
106
|
|
|
80
107
|
1. Does the opened source state this claim, in this population, at this dose or in
|
|
81
108
|
this setting? Paraphrase the source, do not extrapolate from it.
|
|
82
|
-
2. Does the source belong to the allowed set
|
|
109
|
+
2. Does the source belong to the allowed set, and is it neither a preprint nor an MDPI
|
|
110
|
+
journal? If not, discard the claim.
|
|
83
111
|
3. Is the number or direction of effect quoted exactly as the source gives it
|
|
84
112
|
(effect size, confidence interval, absolute versus relative risk)?
|
|
85
113
|
|
|
@@ -144,12 +172,20 @@ Rules:
|
|
|
144
172
|
- Search only your lane's domains (use site: restricted queries). Open each
|
|
145
173
|
candidate and read the relevant section before using it.
|
|
146
174
|
- Prefer the newest guideline edition, then systematic reviews and meta-analyses,
|
|
147
|
-
then RCTs, then observational work.
|
|
175
|
+
then RCTs, then observational work. Within one level, prefer journals ranked SJR
|
|
176
|
+
Q1 in the question's subject category.
|
|
177
|
+
- Never search or cite preprint servers (bioRxiv, medRxiv, PsyArXiv, arXiv, SSRN,
|
|
178
|
+
Research Square, OSF Preprints, Preprints.org). Add NOT preprint[pt] to PubMed
|
|
179
|
+
queries and drop any record typed Preprint.
|
|
180
|
+
- Never cite an MDPI journal: a DOI starting with 10.3390 or a page on mdpi.com. Add
|
|
181
|
+
-site:mdpi.com to web searches.
|
|
148
182
|
- For each claim return: the claim in one sentence, quoted as the source states it;
|
|
149
183
|
population and setting; effect size with confidence interval when given; evidence
|
|
150
184
|
level (guideline recommendation with class/level or GRADE, meta-analysis, RCT,
|
|
151
185
|
observational, expert opinion); year; full citation (authors or body, title,
|
|
152
|
-
journal or body, year, PMID or DOI or URL)
|
|
186
|
+
journal or body, year, PMID or DOI or URL); the journal's SJR quartile; funding:
|
|
187
|
+
the funders named in the source and one label, vendor-funded, vendor-linked
|
|
188
|
+
authors, funding not stated or funding not accessible.
|
|
153
189
|
- Lane D only: if UpToDate does not open or shows a paywall, return the single line
|
|
154
190
|
"UpToDate inaccessible" and nothing else.
|
|
155
191
|
- Do not fill gaps from memory. If your lane has nothing on a sub-question, say
|
|
@@ -180,8 +216,15 @@ For every claim:
|
|
|
180
216
|
numbers. Mark "confirmed" or "could not verify: <what differs>".
|
|
181
217
|
3. Confirm the source is in the allowed set: PubMed-indexed peer-reviewed journal,
|
|
182
218
|
ERC/ESC/AHA/CDC or another official health body or specialty society, NEJM, The
|
|
183
|
-
Lancet, JAMA, BMJ, or UpToDate.
|
|
184
|
-
allowed
|
|
219
|
+
Lancet, JAMA, BMJ, or UpToDate. A preprint, a record typed Preprint in PubMed, or
|
|
220
|
+
an MDPI journal (DOI starting with 10.3390) is never allowed. Otherwise mark
|
|
221
|
+
"could not verify: source not allowed".
|
|
222
|
+
4. Check the journal's SJR quartile on scimagojr.com and correct it when the lane
|
|
223
|
+
got it wrong.
|
|
224
|
+
5. Open the source's Funding and Competing Interests sections and confirm the
|
|
225
|
+
funders and the label (vendor-funded, vendor-linked authors, funding not stated,
|
|
226
|
+
funding not accessible). Correct the label when it differs, and flag every
|
|
227
|
+
vendor-funded or vendor-linked study.
|
|
185
228
|
|
|
186
229
|
Then return three sections:
|
|
187
230
|
- Confirmed matches: claims supported by two or more independent sources, with
|
|
@@ -203,8 +246,10 @@ Then:
|
|
|
203
246
|
1. **Bottom line** — two to four sentences answering the question directly, with
|
|
204
247
|
citation numbers.
|
|
205
248
|
2. **Key findings** — one bullet per claim: the claim, inline citation number(s)
|
|
206
|
-
`[n]`, evidence level, year.
|
|
207
|
-
|
|
249
|
+
`[n]`, evidence level, year, funding label. Write **vendor-funded** or
|
|
250
|
+
**vendor-linked authors** in bold and name the company. Group by sub-question
|
|
251
|
+
when there are several. Mark any source outside the core list and any journal
|
|
252
|
+
below SJR Q1 with its quartile.
|
|
208
253
|
3. **Agreement and contradictions** — the verifier's confirmed matches and
|
|
209
254
|
contradictions, each contradiction with who says what and the likely reason.
|
|
210
255
|
4. **What is uncertain / not found** — sub-questions with no trustworthy evidence
|
|
@@ -214,7 +259,8 @@ Then:
|
|
|
214
259
|
5. **Method** — one line: parallel subagents or sequential lanes; UpToDate read or
|
|
215
260
|
inaccessible; number of sources opened.
|
|
216
261
|
6. **References** — numbered list matching the inline numbers: authors or body,
|
|
217
|
-
title, journal or body, year, PMID or DOI or URL
|
|
262
|
+
title, journal or body, year, PMID or DOI or URL, SJR quartile, and funding (the
|
|
263
|
+
funders and the label).
|
|
218
264
|
|
|
219
265
|
For a question about the user's own health, add exactly one plain sentence after the
|
|
220
266
|
Bottom line: this is a summary of published evidence, not individual medical advice,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,11 @@
|
|
|
70
70
|
"eval:waiting-on": "bun eval/run.ts waiting-on",
|
|
71
71
|
"eval:reaction": "bun eval/run.ts reaction",
|
|
72
72
|
"eval:correction-check": "bun eval/run.ts correction-check",
|
|
73
|
-
"eval:entity-extraction": "bun eval/entity-extraction/build-prompt.ts && bun eval/run.ts entity-extraction"
|
|
73
|
+
"eval:entity-extraction": "bun eval/entity-extraction/build-prompt.ts && bun eval/run.ts entity-extraction",
|
|
74
|
+
"eval:correction-kind": "bun eval/correction-kind/build-prompt.ts && bun eval/run.ts correction-kind",
|
|
75
|
+
"eval:correction-kind:test": "bun eval/correction-kind/test-set.ts",
|
|
76
|
+
"eval:rule-drafter": "bun eval/rule-drafter/build-prompt.ts && bun eval/run.ts rule-drafter",
|
|
77
|
+
"eval:rule-drafter:test": "bun eval/rule-drafter/test-set.ts"
|
|
74
78
|
},
|
|
75
79
|
"lint-staged": {
|
|
76
80
|
"*.ts": [
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli correction — the kinds of mistakes the user corrected, and how often.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* kinds [--days N] [--split DATE] [--json] Corrections per 100 turns, by kind
|
|
6
|
+
* backfill [--relabel] Label the confirmed corrections without a current kind
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readTurns } from "../hooks/lib/adaptation-turns";
|
|
10
|
+
import {
|
|
11
|
+
backfillCorrectionKinds,
|
|
12
|
+
type KindLabeller,
|
|
13
|
+
labelWithInference,
|
|
14
|
+
} from "../hooks/lib/correction-backfill";
|
|
15
|
+
import { type KindRates, kindRates, splitRates } from "../hooks/lib/correction-rates";
|
|
16
|
+
import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
|
|
17
|
+
|
|
18
|
+
function correctionCommandFor(label: KindLabeller) {
|
|
19
|
+
return group({
|
|
20
|
+
summary: "The kinds of mistakes you corrected, and how often",
|
|
21
|
+
commands: {
|
|
22
|
+
kinds: leaf({
|
|
23
|
+
summary: "Corrections per 100 turns, by kind",
|
|
24
|
+
options: {
|
|
25
|
+
days: { type: "string", description: "Window in days (default 30)" },
|
|
26
|
+
split: { type: "string", description: "Compare before and after this date" },
|
|
27
|
+
json: { type: "boolean", description: "Machine-readable output" },
|
|
28
|
+
},
|
|
29
|
+
run: ({ values }) => cmdKinds(values.days, values.split, values.json === true),
|
|
30
|
+
}),
|
|
31
|
+
backfill: leaf({
|
|
32
|
+
summary: "Label the confirmed corrections without a kind of the current version",
|
|
33
|
+
options: {
|
|
34
|
+
relabel: {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Label every confirmed correction again, after a prompt change",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
run: ({ values }) => cmdBackfill(label, values.relabel === true),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const correctionCommand = correctionCommandFor(labelWithInference);
|
|
46
|
+
|
|
47
|
+
export function runCorrection(
|
|
48
|
+
args: string[],
|
|
49
|
+
label: KindLabeller = labelWithInference
|
|
50
|
+
): Promise<number> {
|
|
51
|
+
return runCommand(correctionCommandFor(label), args, ["pal", "cli", "correction"]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function windowDays(days: string | undefined): number {
|
|
55
|
+
if (days === undefined) return 30;
|
|
56
|
+
const parsed = Number(days);
|
|
57
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
58
|
+
throw new UsageError(`--days must be a whole number of days, got "${days}"`);
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function splitDate(split: string): Date {
|
|
63
|
+
const at = new Date(split);
|
|
64
|
+
if (Number.isNaN(at.getTime()))
|
|
65
|
+
throw new UsageError(`--split must be a date such as 2026-10-20, got "${split}"`);
|
|
66
|
+
return at;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function kindLine(name: string, count: string, rate: string, extra = ""): string {
|
|
70
|
+
return ` ${name.padEnd(22)}${count.padStart(4)} ${rate.padStart(5)}${extra}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatRates(rates: KindRates, title: string): string {
|
|
74
|
+
const stale = rates.stale ? `, ${rates.stale} labelled by an older prompt` : "";
|
|
75
|
+
const header = `${title}: ${rates.turns} turns, ${rates.corrections} confirmed corrections, ${rates.unlabelled} without a kind${stale}`;
|
|
76
|
+
const rows = rates.kinds.map((rate) => {
|
|
77
|
+
const names = rate.suggestions.length
|
|
78
|
+
? ` (${[...new Set(rate.suggestions)].join(", ")})`
|
|
79
|
+
: "";
|
|
80
|
+
return kindLine(rate.kind, String(rate.count), rate.per100.toFixed(1), names);
|
|
81
|
+
});
|
|
82
|
+
return [header, kindLine("kind", "n", "/100"), ...rows].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cmdKinds(days: string | undefined, split: string | undefined, json: boolean) {
|
|
86
|
+
const turns = readTurns(windowDays(days));
|
|
87
|
+
const result = split ? splitRates(turns, splitDate(split)) : kindRates(turns);
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const sections =
|
|
93
|
+
"before" in result
|
|
94
|
+
? [
|
|
95
|
+
formatRates(result.before, "Before ".concat(split ?? "")),
|
|
96
|
+
formatRates(result.after, "From ".concat(split ?? "")),
|
|
97
|
+
]
|
|
98
|
+
: [formatRates(result, "Last ".concat(String(windowDays(days)), " days"))];
|
|
99
|
+
console.log(sections.join("\n\n"));
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function labelledLine(turn: { ts: string; issue: string }, kind: string): string {
|
|
104
|
+
return `${turn.ts.slice(0, 16)} ${kind} ${turn.issue}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cmdBackfill(label: KindLabeller, relabel: boolean) {
|
|
108
|
+
const result = await backfillCorrectionKinds({
|
|
109
|
+
label,
|
|
110
|
+
relabel,
|
|
111
|
+
onLabelled: (turn, kind) => console.log(labelledLine(turn, kind?.kind ?? "failed")),
|
|
112
|
+
});
|
|
113
|
+
console.log(`Labelled ${result.labelled}, failed ${result.failed}.`);
|
|
114
|
+
return result.failed > 0 ? 1 : 0;
|
|
115
|
+
}
|
package/src/cli/infer.ts
CHANGED
|
@@ -136,7 +136,7 @@ export const inferCommand = leaf({
|
|
|
136
136
|
tier: {
|
|
137
137
|
type: "string",
|
|
138
138
|
value: "<tier>",
|
|
139
|
-
description: `Model size: ${INFERENCE_TIERS.join("
|
|
139
|
+
description: `Model size: ${INFERENCE_TIERS.join(", ")} (default small)`,
|
|
140
140
|
},
|
|
141
141
|
system: { type: "string", value: "<file>", description: "System prompt file" },
|
|
142
142
|
schema: {
|
package/src/cli/rule.ts
CHANGED
|
@@ -4,17 +4,32 @@
|
|
|
4
4
|
* Subcommands:
|
|
5
5
|
* list [--all] [--json] Drafts waiting for a decision (--all: every rule)
|
|
6
6
|
* approve <id> Turn a draft into a rule that steers
|
|
7
|
+
* opinion <id> Keep a draft as an opinion, steering every session
|
|
7
8
|
* deny <id> Reject a draft; it stays on record so it is not redrafted
|
|
9
|
+
* retire <id> End an approved rule or opinion
|
|
10
|
+
* add-opinion --kind K --when W --steering S [--evidence E]
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import {
|
|
11
14
|
type AdaptationRule,
|
|
15
|
+
addOpinion,
|
|
12
16
|
decideRule,
|
|
13
|
-
|
|
17
|
+
isOpinion,
|
|
14
18
|
readRules,
|
|
19
|
+
retireRule,
|
|
15
20
|
type TriggerProof,
|
|
16
21
|
} from "../hooks/lib/adaptation-rules";
|
|
17
|
-
import {
|
|
22
|
+
import { readTurns, type Turn } from "../hooks/lib/adaptation-turns";
|
|
23
|
+
import { CORRECTION_KINDS, type CorrectionKindName } from "../hooks/lib/correction-kind";
|
|
24
|
+
import {
|
|
25
|
+
EFFECT_WINDOW_DAYS,
|
|
26
|
+
type OpinionEffect,
|
|
27
|
+
opinionEffectOf,
|
|
28
|
+
} from "../hooks/lib/correction-rates";
|
|
29
|
+
import { gateAdvice } from "../hooks/lib/opinion-gate";
|
|
30
|
+
import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
|
|
31
|
+
|
|
32
|
+
type DecideResult = ReturnType<typeof decideRule>;
|
|
18
33
|
|
|
19
34
|
export const ruleCommand = group({
|
|
20
35
|
summary: "Review the rules PAL drafted from your corrections",
|
|
@@ -30,12 +45,36 @@ export const ruleCommand = group({
|
|
|
30
45
|
approve: leaf({
|
|
31
46
|
summary: "Approve a draft so it steers",
|
|
32
47
|
args: "<id>",
|
|
33
|
-
run: ({ positionals }) =>
|
|
48
|
+
run: ({ positionals }) =>
|
|
49
|
+
report(positionals[0], decideRule(positionals[0], "approved")),
|
|
50
|
+
}),
|
|
51
|
+
opinion: leaf({
|
|
52
|
+
summary: "Keep a draft as an opinion, steering every session without a trigger",
|
|
53
|
+
args: "<id>",
|
|
54
|
+
run: ({ positionals }) =>
|
|
55
|
+
report(positionals[0], decideRule(positionals[0], "opinion")),
|
|
34
56
|
}),
|
|
35
57
|
deny: leaf({
|
|
36
58
|
summary: "Deny a draft; it stays on record so it is not drafted again",
|
|
37
59
|
args: "<id>",
|
|
38
|
-
run: ({ positionals }) =>
|
|
60
|
+
run: ({ positionals }) =>
|
|
61
|
+
report(positionals[0], decideRule(positionals[0], "denied")),
|
|
62
|
+
}),
|
|
63
|
+
retire: leaf({
|
|
64
|
+
summary: "End an approved rule or opinion",
|
|
65
|
+
args: "<id>",
|
|
66
|
+
run: ({ positionals }) => report(positionals[0], retireRule(positionals[0])),
|
|
67
|
+
}),
|
|
68
|
+
"add-opinion": leaf({
|
|
69
|
+
summary: "Write an opinion by hand; it is approved at once",
|
|
70
|
+
options: {
|
|
71
|
+
kind: { type: "string", description: "The correction kind it prevents" },
|
|
72
|
+
when: { type: "string", description: "The situation, in one sentence" },
|
|
73
|
+
steering: { type: "string", description: "The instruction, imperative" },
|
|
74
|
+
evidence: { type: "string", description: "Why it is needed" },
|
|
75
|
+
},
|
|
76
|
+
run: ({ values }) =>
|
|
77
|
+
cmdAddOpinion(values.kind, values.when, values.steering, values.evidence),
|
|
39
78
|
}),
|
|
40
79
|
},
|
|
41
80
|
});
|
|
@@ -54,37 +93,105 @@ function cmdList(all: boolean, json: boolean): number {
|
|
|
54
93
|
console.log(all ? "No rules yet." : "No drafts waiting.");
|
|
55
94
|
return 0;
|
|
56
95
|
}
|
|
57
|
-
|
|
96
|
+
const measure: Measure = { turns: readTurns(EFFECT_WINDOW_DAYS), rules: readRules() };
|
|
97
|
+
console.log(rules.map((rule) => formatRule(rule, measure)).join("\n\n"));
|
|
58
98
|
return 0;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
|
-
|
|
101
|
+
interface Measure {
|
|
102
|
+
turns: Turn[];
|
|
103
|
+
rules: AdaptationRule[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatRule(rule: AdaptationRule, measure: Measure): string {
|
|
107
|
+
const label = isOpinion(rule) ? "opinion" : rule.status;
|
|
62
108
|
return [
|
|
63
|
-
`${rule.id} [${
|
|
109
|
+
`${rule.id} [${label}] ${rule.when}`,
|
|
64
110
|
...(rule.widens ? [` widens: ${rule.widens}`] : []),
|
|
65
|
-
|
|
111
|
+
...(rule.trigger
|
|
112
|
+
? [` trigger (${rule.trigger.side}): ${rule.trigger.pattern}`]
|
|
113
|
+
: []),
|
|
66
114
|
` steering: ${rule.steering}`,
|
|
115
|
+
...(rule.kind ? [` kind: ${rule.kind}`] : []),
|
|
67
116
|
...(rule.check ? [` check: ${rule.check}`] : []),
|
|
68
117
|
...(rule.proof ? [` proof: ${formatProof(rule.proof)}`] : []),
|
|
118
|
+
...offerLines(rule),
|
|
119
|
+
...gateLines(rule, measure),
|
|
120
|
+
...effectLines(rule, measure),
|
|
69
121
|
...rule.evidence.map((e) => ` evidence: ${e}`),
|
|
70
122
|
].join("\n");
|
|
71
123
|
}
|
|
72
124
|
|
|
125
|
+
function offerLines(rule: AdaptationRule): string[] {
|
|
126
|
+
if (rule.offer !== "opinion-only" || rule.status !== "draft" || rule.opinionGate)
|
|
127
|
+
return [];
|
|
128
|
+
const why = rule.trigger ? "the trigger did not prove" : "no trigger";
|
|
129
|
+
return [` offer: ${why}; keep it as an opinion or deny it`];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function gateLines(rule: AdaptationRule, { turns, rules }: Measure): string[] {
|
|
133
|
+
const advice = gateAdvice(rule, rules, turns);
|
|
134
|
+
return advice ? [` no opinion: ${advice}`] : [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function effectLines(rule: AdaptationRule, { turns, rules }: Measure): string[] {
|
|
138
|
+
const effect = opinionEffectOf(rule, rules, turns);
|
|
139
|
+
if (!effect) return [];
|
|
140
|
+
return [
|
|
141
|
+
` effect: ${formatWindows(effect)} (${effect.verdict})`,
|
|
142
|
+
` other kinds: ${formatWindows(effect.control)}`,
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatWindows({
|
|
147
|
+
before,
|
|
148
|
+
after,
|
|
149
|
+
}: Pick<OpinionEffect, "before" | "after">): string {
|
|
150
|
+
return `${before.count} in ${before.turns} turns before (${before.per100.toFixed(1)}/100), ${after.count} in ${after.turns} after (${after.per100.toFixed(1)}/100)`;
|
|
151
|
+
}
|
|
152
|
+
|
|
73
153
|
function formatProof(proof: TriggerProof): string {
|
|
74
154
|
return `fired on ${proof.firedCorrections}/${proof.corrections} corrections, ${proof.firedOrdinary}/${proof.ordinary} ordinary turns`;
|
|
75
155
|
}
|
|
76
156
|
|
|
77
|
-
function
|
|
78
|
-
if (rule.id
|
|
79
|
-
|
|
157
|
+
function resultLine(id: string, rule: AdaptationRule): string {
|
|
158
|
+
if (rule.id !== id)
|
|
159
|
+
return `Rule ${rule.id} now triggers on: ${rule.trigger?.pattern} (widened by ${id})`;
|
|
160
|
+
const label = isOpinion(rule) ? "kept as an opinion" : rule.status;
|
|
161
|
+
return `Rule ${id} ${label}: ${rule.when}`;
|
|
80
162
|
}
|
|
81
163
|
|
|
82
|
-
function
|
|
83
|
-
const result = decideRule(id, decision);
|
|
164
|
+
function report(id: string, result: DecideResult): number {
|
|
84
165
|
if (!result.ok) {
|
|
85
166
|
console.error(result.reason);
|
|
86
167
|
return 1;
|
|
87
168
|
}
|
|
88
|
-
console.log(
|
|
169
|
+
console.log(resultLine(id, result.rule));
|
|
89
170
|
return 0;
|
|
90
171
|
}
|
|
172
|
+
|
|
173
|
+
function correctionKind(kind: string | undefined): CorrectionKindName {
|
|
174
|
+
if (CORRECTION_KINDS.includes(kind as CorrectionKindName))
|
|
175
|
+
return kind as CorrectionKindName;
|
|
176
|
+
throw new UsageError(`--kind must be one of ${CORRECTION_KINDS.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function required(name: string, value: string | undefined): string {
|
|
180
|
+
if (!value?.trim()) throw new UsageError(`--${name} is required`);
|
|
181
|
+
return value.trim();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cmdAddOpinion(
|
|
185
|
+
kind: string | undefined,
|
|
186
|
+
when: string | undefined,
|
|
187
|
+
steering: string | undefined,
|
|
188
|
+
evidence: string | undefined
|
|
189
|
+
): number {
|
|
190
|
+
const result = addOpinion({
|
|
191
|
+
kind: correctionKind(kind),
|
|
192
|
+
when: required("when", when),
|
|
193
|
+
steering: required("steering", steering),
|
|
194
|
+
evidence: evidence?.trim() ? [evidence.trim()] : [],
|
|
195
|
+
});
|
|
196
|
+
return report(result.ok ? result.rule.id : "", result);
|
|
197
|
+
}
|
package/src/cli/tree.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../tools/lib/command";
|
|
16
16
|
import { builtinTools, builtinToolVerbs } from "./builtin-tools";
|
|
17
17
|
import { contentCommand } from "./content";
|
|
18
|
+
import { correctionCommand } from "./correction";
|
|
18
19
|
import { costCommand } from "./cost";
|
|
19
20
|
import { eventsCommand } from "./events";
|
|
20
21
|
import { identityCommand } from "./identity";
|
|
@@ -169,6 +170,7 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
169
170
|
knowledge: knowledgeCommand,
|
|
170
171
|
ledger: ledgerCommand,
|
|
171
172
|
rule: ruleCommand,
|
|
173
|
+
correction: correctionCommand,
|
|
172
174
|
server: serverCommand,
|
|
173
175
|
skill: skillCommand,
|
|
174
176
|
subagent: subagentCommand,
|
|
@@ -17,6 +17,12 @@ import {
|
|
|
17
17
|
} from "../lib/adaptation-candidates";
|
|
18
18
|
import { readRules } from "../lib/adaptation-rules";
|
|
19
19
|
import { appendTurn, readTurns, withRequests } from "../lib/adaptation-turns";
|
|
20
|
+
import {
|
|
21
|
+
correctionKindRequest,
|
|
22
|
+
needsCorrectionKind,
|
|
23
|
+
parseCorrectionKind,
|
|
24
|
+
withCorrectionKind,
|
|
25
|
+
} from "../lib/correction-kind";
|
|
20
26
|
import { spawnDetachedInference } from "../lib/detached-inference";
|
|
21
27
|
import { canInfer, inference } from "../lib/inference";
|
|
22
28
|
import { replyEnd } from "../lib/interaction-samples";
|
|
@@ -231,6 +237,16 @@ async function checkCorrection(reply: string, message: string, sessionId?: strin
|
|
|
231
237
|
return result.success ? parseCorrectionCheck(result.output) : null;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
async function labelCorrectionKind(
|
|
241
|
+
reply: string,
|
|
242
|
+
message: string,
|
|
243
|
+
issue: string,
|
|
244
|
+
sessionId?: string
|
|
245
|
+
) {
|
|
246
|
+
const result = await inference(correctionKindRequest(reply, message, issue, sessionId));
|
|
247
|
+
return result.success ? parseCorrectionKind(result.output) : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
function knownRules() {
|
|
235
251
|
const waiting = readCandidates().filter((candidate) => candidate.verdict === "waiting");
|
|
236
252
|
return [
|
|
@@ -251,7 +267,7 @@ async function draftRuleCandidate(sessionId?: string): Promise<void> {
|
|
|
251
267
|
else logDebug("rule-drafter", `no candidate: ${result.output ?? result.error ?? ""}`);
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
/** Background mode: label the reaction, check a possible correction, store the rating. */
|
|
270
|
+
/** Background mode: label the reaction, check a possible correction, name a confirmed one's kind, store the rating. */
|
|
255
271
|
async function runReactionRatingAndStore(
|
|
256
272
|
message: string,
|
|
257
273
|
reply: string,
|
|
@@ -273,7 +289,10 @@ async function runReactionRatingAndStore(
|
|
|
273
289
|
label,
|
|
274
290
|
check
|
|
275
291
|
);
|
|
276
|
-
|
|
292
|
+
const kind = needsCorrectionKind(turn)
|
|
293
|
+
? await labelCorrectionKind(reply, message, turn.issue, sessionId)
|
|
294
|
+
: null;
|
|
295
|
+
if (turn) appendTurn(withCorrectionKind(turn, kind), sentAt);
|
|
277
296
|
if (turn?.confirmed) await draftRuleCandidate(sessionId);
|
|
278
297
|
} catch (err) {
|
|
279
298
|
const { logError } = await import("../lib/log");
|
|
@@ -134,6 +134,11 @@ async function checkRepo(): Promise<UpdateCache> {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** npm's latest tag lags a fresh release by seconds; an older latest is never an update. */
|
|
138
|
+
export function isNewerRelease(latest: string, current: string): boolean {
|
|
139
|
+
return Bun.semver.order(latest, current) === 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
async function checkNpm(): Promise<UpdateCache> {
|
|
138
143
|
const current = getInstalledVersion();
|
|
139
144
|
|
|
@@ -149,7 +154,7 @@ async function checkNpm(): Promise<UpdateCache> {
|
|
|
149
154
|
|
|
150
155
|
return {
|
|
151
156
|
checkedAt: new Date().toISOString(),
|
|
152
|
-
available: latest
|
|
157
|
+
available: isNewerRelease(latest, current),
|
|
153
158
|
current,
|
|
154
159
|
latest,
|
|
155
160
|
mode: "package",
|