portable-agent-layer 0.86.0 → 0.86.2

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 CHANGED
@@ -91,6 +91,7 @@ pal cli status # check your setup
91
91
  | `pal cli timezone [<zone>]` | Show or set your timezone. IANA names only, validated before it is stored |
92
92
  | `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
93
93
  | `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
94
+ | `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `deny <id>`. A draft steers nothing until approved; a denied one is never drafted again |
94
95
  | `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 |
95
96
  | `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
96
97
  | `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) |
@@ -203,7 +204,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
203
204
  | `presentation` | Build branded slide decks from outlines |
204
205
  | `projects` | Look up, resume, register, or manage tracked projects |
205
206
  | `reflect` | Diagnose why a PAL behavior didn't trigger |
206
- | `telos` | Inspect or update goals, beliefs, strategies, narratives |
207
+ | `rule-review` | Approve or deny the rules PAL drafted from your corrections || `telos` | Inspect or update goals, beliefs, strategies, narratives |
207
208
  | `think` | Structured first-pass reasoning on a problem |
208
209
 
209
210
  ---
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: rule-review
3
+ license: MIT
4
+ description: "Review the adaptation rule drafts PAL wrote from the user's corrections and record the user's approve-or-deny decision for each via the rule CLI. Use when the user asks what rule drafts are waiting, wants to review pending rules, or says to approve or deny a rule draft. Not for editing the shipped steering hints or for the opinion tracker."
5
+ argument-hint: "[list | approve <id> | deny <id>]"
6
+ metadata:
7
+ source: portable-agent-layer
8
+ triggers:
9
+ - "rule-review"
10
+ - "rule review"
11
+ - "rule drafts"
12
+ - "pending rules"
13
+ - "review rules"
14
+ - "approve the rule"
15
+ - "deny the rule"
16
+ - "adaptation rules"
17
+ ---
18
+
19
+ # Rule review
20
+
21
+ An adaptation rule is "when this situation happens (trigger), steer the assistant like this (steering)", backed by evidence from the user's past corrections. PAL drafts them; every rule starts as a draft and steers nothing until the user approves it. A denied draft stays on record so it is never drafted again. A draft can only be approved or denied, never edited.
22
+
23
+ Your job: show the user the drafts and record their decision. The decision is the user's alone. Never approve or deny a draft the user has not explicitly decided on.
24
+
25
+ ## Workflow
26
+
27
+ ### 1. List the drafts
28
+
29
+ When the user asks what rule drafts are waiting, wants to review rules, or invokes the skill without an id, run:
30
+
31
+ ```bash
32
+ pal cli rule list
33
+ ```
34
+
35
+ Each draft prints as:
36
+
37
+ ```
38
+ <id> [draft] <situation>
39
+ trigger (<prompt|reply>): <pattern>
40
+ steering: <steering>
41
+ evidence: <line>
42
+ ```
43
+
44
+ - If the output is `No drafts waiting.`, tell the user that and stop.
45
+ - Otherwise present every draft in the format under "Output format", then ask the user to approve or deny each one. Keep the ids from this listing; they are what a follow-up decision refers to.
46
+
47
+ When the user asks about rules already decided (approved, denied, history, all rules), run `pal cli rule list --all` instead and present them the same way, showing each rule's status.
48
+
49
+ ### 2. Resolve which draft the user means
50
+
51
+ Map the user's decision to exactly one draft id before running anything:
52
+
53
+ - The user names an id → use that id.
54
+ - The user says "approve it", "deny it", "approve", "deny", "yes approve", or "reject" right after you presented exactly one draft → use that draft's id.
55
+ - The user says "approve them all" or "deny all" after a listing → one command per listed id, in the order listed.
56
+ - The user refers to a draft by its content ("the one about commit messages") and exactly one presented draft matches → use its id.
57
+ - Anything else, including several drafts presented with no id named, or a decision given before any listing in this conversation → do not guess. Run step 1 if there is no listing yet, then ask which id the user means.
58
+
59
+ A bare "yes", "ok", "fine", or "looks good" is not a decision. Ask "approve or deny <id>?" and wait.
60
+
61
+ ### 3. Record the decision
62
+
63
+ Run the matching command:
64
+
65
+ ```bash
66
+ pal cli rule approve <id>
67
+ pal cli rule deny <id>
68
+ ```
69
+
70
+ Report the CLI's result line verbatim, e.g. `Rule 3f9a1c2e approved: <situation>`.
71
+
72
+ If the command exits 1, report its reason verbatim. The reasons are `No rule with id <id>` and `Rule <id> was already <approved|denied>`. On the first, re-run `pal cli rule list` and show the current ids. On the second, say the draft was already decided and leave it.
73
+
74
+ ### 4. Handle requests to change a draft
75
+
76
+ Editing a draft is not supported, so never offer it and never rewrite a draft's text yourself. When the user wants a different wording, scope, or steering, say so and give the two options: deny the draft, or approve it as is. Then wait for that decision.
77
+
78
+ ## Output format
79
+
80
+ Present each draft as one compact block, nothing else in between:
81
+
82
+ ```
83
+ Draft <id>
84
+ When: <situation>
85
+ Steer: <steering>
86
+ Evidence: <evidence line>
87
+ <evidence line>
88
+ ```
89
+
90
+ Follow the list with one question, e.g. "Approve or deny each? Reply with the id and your decision." After a single draft, "Approve or deny <id>?" is enough.
91
+
92
+ After each decision, output the CLI's result line (or error reason) verbatim and nothing more unless the user asked a question.
93
+
94
+ ## When to use
95
+
96
+ - The user asks what rule drafts are pending, waiting, or need review.
97
+ - The user asks to review, approve, or deny an adaptation rule.
98
+ - The user says "approve it" or "deny it" right after you presented a draft.
99
+ - The user asks which rules have been approved or denied so far.
100
+
101
+ ## Do NOT use
102
+
103
+ - To change the shipped contextual-steering hints. Those live under the `steering` key in `pal-settings.json`, not in rules.
104
+ - For the opinion tracker (confirming or contradicting a tracked preference). That is the `opinion` skill.
105
+ - To edit, reword, or merge a draft. Editing is not supported; the only outcomes are approve or deny.
106
+ - To approve or deny on the user's behalf because a draft "looks reasonable". Wait for the explicit decision.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.86.0",
3
+ "version": "0.86.2",
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
@@ -17,6 +17,7 @@
17
17
  * doctor Check prerequisites and system health
18
18
  * usage Summarize token usage and cost
19
19
  * ledger <sub> [filters] Query the action ledger (log · show · stats)
20
+ * rule list|approve|deny [id] Review rules drafted from your corrections
20
21
  * server start|stop|restart|status The control room, a local page over ~/.pal
21
22
  * <tool> [args] Run a built-in agent tool (project, thread, analyze, …)
22
23
  * skill run <skill> <tool> [-- args] Run a skill's own tool by name, not by path
@@ -202,6 +203,12 @@ async function runCli(command: string | undefined, args: string[]) {
202
203
  if (code !== 0) process.exit(code);
203
204
  break;
204
205
  }
206
+ case "rule": {
207
+ const { runRule } = await import("./rule");
208
+ const code = await runRule(args);
209
+ if (code !== 0) process.exit(code);
210
+ break;
211
+ }
205
212
  case "server": {
206
213
  const { runServer } = await import("./server");
207
214
  const code = await runServer(args);
@@ -298,6 +305,7 @@ function showHelp() {
298
305
  (search · graph · stats · hubs · find · show · add · ls)
299
306
  pal cli ledger <sub> [filters] Query the action ledger (log · show · stats)
300
307
  e.g. ledger log --project X --since 7d
308
+ pal cli rule list|approve|deny [id] Review rules drafted from your corrections
301
309
  pal cli server start|stop|restart|status The control room: a local page to open before a terminal
302
310
  pal cli skill run <skill> <tool> [-- args] Run ~/.pal/skills/<skill>/tools/<tool> by name
303
311
  pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
@@ -879,7 +887,8 @@ async function update() {
879
887
  clearUpdateCache();
880
888
 
881
889
  log.info("Reinstalling...");
882
- process.exit(await install(resolveTargets([]), []));
890
+ const { reinstallInFreshProcess } = await import("./reinstall");
891
+ process.exit(reinstallInFreshProcess());
883
892
  }
884
893
 
885
894
  function cliDebug(args: string[]) {
@@ -0,0 +1,13 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ /**
4
+ * An update replaces PAL's files under a process that already holds the old
5
+ * modules in Bun's cache, so a new installer importing a new export would bind
6
+ * against the stale module and throw. A fresh process loads everything from disk.
7
+ */
8
+ export function reinstallInFreshProcess(entry: string = process.argv[1]): number {
9
+ const run = spawnSync(process.execPath, [entry, "cli", "install"], {
10
+ stdio: "inherit",
11
+ });
12
+ return run.status ?? 1;
13
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * pal cli rule — review the adaptation rules drafted from the user's corrections.
3
+ *
4
+ * Subcommands:
5
+ * list [--all] [--json] Drafts waiting for a decision (--all: every rule)
6
+ * approve <id> Turn a draft into a rule that steers
7
+ * deny <id> Reject a draft; it stays on record so it is not redrafted
8
+ */
9
+
10
+ import {
11
+ type AdaptationRule,
12
+ decideRule,
13
+ type RuleStatus,
14
+ readRules,
15
+ } from "../hooks/lib/adaptation-rules";
16
+
17
+ const DECISIONS: Record<string, Exclude<RuleStatus, "draft">> = {
18
+ approve: "approved",
19
+ deny: "denied",
20
+ };
21
+
22
+ export async function runRule(args: string[]): Promise<number> {
23
+ const [sub, ...rest] = args;
24
+ if (sub === "list") return cmdList(rest);
25
+ if (sub && sub in DECISIONS) return cmdDecide(rest[0], DECISIONS[sub]);
26
+ showHelp();
27
+ return sub === undefined || sub === "help" ? 0 : 1;
28
+ }
29
+
30
+ function cmdList(args: string[]): number {
31
+ const all = args.includes("--all");
32
+ const rules = readRules().filter((r) => all || r.status === "draft");
33
+ if (args.includes("--json")) {
34
+ console.log(JSON.stringify(rules, null, 2));
35
+ return 0;
36
+ }
37
+ if (rules.length === 0) {
38
+ console.log(all ? "No rules yet." : "No drafts waiting.");
39
+ return 0;
40
+ }
41
+ console.log(rules.map(formatRule).join("\n\n"));
42
+ return 0;
43
+ }
44
+
45
+ function formatRule(rule: AdaptationRule): string {
46
+ return [
47
+ `${rule.id} [${rule.status}] ${rule.when}`,
48
+ ` trigger (${rule.trigger.side}): ${rule.trigger.pattern}`,
49
+ ` steering: ${rule.steering}`,
50
+ ...rule.evidence.map((e) => ` evidence: ${e}`),
51
+ ].join("\n");
52
+ }
53
+
54
+ function cmdDecide(
55
+ id: string | undefined,
56
+ decision: Exclude<RuleStatus, "draft">
57
+ ): number {
58
+ if (!id) {
59
+ console.error("Give the rule id: pal cli rule approve|deny <id>");
60
+ return 1;
61
+ }
62
+ const result = decideRule(id, decision);
63
+ if (!result.ok) {
64
+ console.error(result.reason);
65
+ return 1;
66
+ }
67
+ console.log(`Rule ${id} ${decision}: ${result.rule.when}`);
68
+ return 0;
69
+ }
70
+
71
+ function showHelp(): void {
72
+ console.log(`pal cli rule — review adaptation rules drafted from your corrections
73
+
74
+ pal cli rule list [--all] [--json] Drafts waiting for a decision
75
+ pal cli rule approve <id> Approve a draft so it steers
76
+ pal cli rule deny <id> Deny a draft; it is not drafted again`);
77
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Adaptation rules: "when this happens, steer like this", learned from the
3
+ * user's corrections. A rule starts as a draft and only steers once the user
4
+ * approves it. A denied rule stays on record so the same rule is not drafted again.
5
+ */
6
+
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { paths } from "./paths";
11
+
12
+ export type RuleStatus = "draft" | "approved" | "denied";
13
+ type Decision = Exclude<RuleStatus, "draft">;
14
+
15
+ interface RuleTrigger {
16
+ /** Whether the pattern is matched against the user's prompt or the assistant's reply. */
17
+ side: "prompt" | "reply";
18
+ pattern: string;
19
+ }
20
+
21
+ export interface DraftInput {
22
+ when: string;
23
+ trigger: RuleTrigger;
24
+ steering: string;
25
+ evidence: string[];
26
+ }
27
+
28
+ export interface AdaptationRule extends DraftInput {
29
+ id: string;
30
+ status: RuleStatus;
31
+ createdAt: string;
32
+ decidedAt?: string;
33
+ }
34
+
35
+ type DecideResult = { ok: true; rule: AdaptationRule } | { ok: false; reason: string };
36
+
37
+ export function rulesPath(): string {
38
+ return resolve(paths.adaptation(), "rules.json");
39
+ }
40
+
41
+ export function readRules(): AdaptationRule[] {
42
+ const path = rulesPath();
43
+ if (!existsSync(path)) return [];
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
46
+ return Array.isArray(parsed) ? (parsed as AdaptationRule[]) : [];
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ function writeRules(rules: AdaptationRule[]): void {
53
+ const path = rulesPath();
54
+ const pending = `${path}.${process.pid}.tmp`;
55
+ writeFileSync(pending, `${JSON.stringify(rules, null, 2)}\n`, "utf-8");
56
+ renameSync(pending, path);
57
+ }
58
+
59
+ export function addDraft(input: DraftInput): AdaptationRule {
60
+ const rule: AdaptationRule = {
61
+ ...input,
62
+ id: randomUUID().slice(0, 8),
63
+ status: "draft",
64
+ createdAt: new Date().toISOString(),
65
+ };
66
+ writeRules([...readRules(), rule]);
67
+ return rule;
68
+ }
69
+
70
+ export function decideRule(id: string, decision: Decision): DecideResult {
71
+ const rules = readRules();
72
+ const rule = rules.find((r) => r.id === id);
73
+ if (!rule) return { ok: false, reason: `No rule with id ${id}` };
74
+ if (rule.status !== "draft")
75
+ return { ok: false, reason: `Rule ${id} was already ${rule.status}` };
76
+ const decided: AdaptationRule = {
77
+ ...rule,
78
+ status: decision,
79
+ decidedAt: new Date().toISOString(),
80
+ };
81
+ writeRules(rules.map((r) => (r.id === id ? decided : r)));
82
+ return { ok: true, rule: decided };
83
+ }
@@ -98,6 +98,7 @@ export const paths = {
98
98
  wisdom: () => ensureDir(home("memory", "wisdom", "frames")),
99
99
  wisdomState: () => ensureDir(home("memory", "wisdom", "state")),
100
100
  relationship: () => ensureDir(home("memory", "relationship")),
101
+ adaptation: () => ensureDir(home("memory", "adaptation")),
101
102
  knowledge: () => ensureDir(home("memory", "knowledge")),
102
103
  knowledgeDomain: (d: string) => ensureDir(home("memory", "knowledge", d)),
103
104
  failures: () => ensureDir(home("memory", "learning", "failures")),