portable-agent-layer 0.61.4 → 0.62.1

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.
@@ -0,0 +1,111 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/kovrichard/portable-agent-layer/main/assets/schema/pal-settings.schema.json",
4
+ "title": "PAL settings",
5
+ "description": "Configuration for the Portable Agent Layer. Lives at ~/.pal/memory/pal-settings.json. All sections are optional; PAL falls back to defaults for anything absent.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "JSON Schema reference — enables editor autocomplete, validation, and hover docs."
12
+ },
13
+ "identity": {
14
+ "type": "object",
15
+ "description": "Who the assistant is and who it serves.",
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "ai": {
19
+ "type": "object",
20
+ "description": "The assistant's persona.",
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "name": { "type": "string", "description": "Short name (e.g. the name you call the assistant)." },
24
+ "fullName": { "type": "string", "description": "Full/formal name." },
25
+ "displayName": { "type": "string", "description": "Uppercase label used in headers." },
26
+ "catchphrase": { "type": "string", "description": "Greeting line. '{name}' is substituted with the ai.name value." }
27
+ }
28
+ },
29
+ "principal": {
30
+ "type": "object",
31
+ "description": "The human the assistant works for.",
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "name": { "type": "string", "description": "The principal's name." },
35
+ "timezone": { "type": "string", "description": "IANA timezone (e.g. Europe/Budapest)." }
36
+ }
37
+ }
38
+ }
39
+ },
40
+ "loadAtStartup": {
41
+ "type": "object",
42
+ "description": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "files": {
46
+ "type": "array",
47
+ "description": "Absolute or PAL-relative file paths to inject at session start.",
48
+ "items": { "type": "string" }
49
+ }
50
+ }
51
+ },
52
+ "dynamicContext": {
53
+ "type": "object",
54
+ "description": "Dynamic context sections injected at session start. Set any key to false to disable it; absent keys default to enabled.",
55
+ "additionalProperties": { "type": "boolean" },
56
+ "properties": {
57
+ "wisdom": { "type": "boolean", "description": "Crystallized wisdom principles." },
58
+ "opinions": { "type": "boolean", "description": "Tracked user opinions." },
59
+ "relationship": { "type": "boolean", "description": "Recent relationship notes." },
60
+ "learningDigest": { "type": "boolean", "description": "Lessons-from-failures digest." },
61
+ "failurePatterns": { "type": "boolean", "description": "Recurring failure patterns." },
62
+ "projectHistory": { "type": "boolean", "description": "Per-project session history." },
63
+ "projects": { "type": "boolean", "description": "Active projects list." },
64
+ "sessionIntelligence": { "type": "boolean", "description": "Rating trends and session signals." },
65
+ "handoff": { "type": "boolean", "description": "Handoff note from the previous session." },
66
+ "selfModel": { "type": "boolean", "description": "The assistant's self-model synthesis." },
67
+ "contextualSteering": { "type": "boolean", "description": "Prompt-time steering self-checks (see the steering section)." },
68
+ "steeringTestReport": { "type": "boolean", "description": "When true, the assistant notes to the user which steering self-check fired. For the dual-live test period; ships false." }
69
+ }
70
+ },
71
+ "attribution": {
72
+ "type": "object",
73
+ "description": "Git co-author attribution opt-in.",
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "enabled": { "type": "boolean", "description": "Add the PAL co-author trailer to commits." },
77
+ "decided": { "type": "boolean", "description": "Gates the one-time attribution prompt." }
78
+ }
79
+ },
80
+ "steering": {
81
+ "type": "object",
82
+ "description": "Contextual steering: PAL injects a short self-check into your prompt when it matches a task type. Toggle the whole feature via dynamicContext.contextualSteering.",
83
+ "additionalProperties": false,
84
+ "properties": {
85
+ "disable": {
86
+ "type": "array",
87
+ "description": "Shipped steering rules to suppress, by tag.",
88
+ "items": {
89
+ "type": "string",
90
+ "enum": ["debugging", "destructive", "refactor", "planning", "testing", "committing", "secrets"]
91
+ },
92
+ "uniqueItems": true
93
+ },
94
+ "rules": {
95
+ "type": "array",
96
+ "description": "Personal steering rules appended to the shipped set. A malformed entry (missing field or bad regex) is skipped, never fatal.",
97
+ "items": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "required": ["tag", "pattern", "snippet"],
101
+ "properties": {
102
+ "tag": { "type": "string", "description": "Short identifier for this rule (also used to dedupe matches)." },
103
+ "pattern": { "type": "string", "description": "JavaScript regex source, matched case-insensitively against the prompt." },
104
+ "snippet": { "type": "string", "description": "The self-check text injected when the pattern matches." }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
@@ -21,6 +21,10 @@
21
21
  {
22
22
  "type": "command",
23
23
  "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
24
+ },
25
+ {
26
+ "type": "command",
27
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
24
28
  }
25
29
  ],
26
30
  "agentStop": [
@@ -21,6 +21,10 @@
21
21
  {
22
22
  "type": "command",
23
23
  "command": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
24
+ },
25
+ {
26
+ "type": "command",
27
+ "command": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
24
28
  }
25
29
  ],
26
30
  "beforeShellExecution": [
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://raw.githubusercontent.com/kovrichard/portable-agent-layer/main/assets/schema/pal-settings.schema.json",
2
3
  "identity": {
3
4
  "ai": {
4
5
  "name": "",
@@ -12,11 +13,9 @@
12
13
  }
13
14
  },
14
15
  "loadAtStartup": {
15
- "_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
16
16
  "files": []
17
17
  },
18
18
  "dynamicContext": {
19
- "_docs": "Dynamic context sections injected at session start. Set to false to disable.",
20
19
  "wisdom": true,
21
20
  "opinions": true,
22
21
  "relationship": true,
@@ -26,6 +25,12 @@
26
25
  "projects": true,
27
26
  "sessionIntelligence": true,
28
27
  "handoff": true,
29
- "selfModel": true
28
+ "selfModel": true,
29
+ "contextualSteering": true,
30
+ "steeringTestReport": false
31
+ },
32
+ "steering": {
33
+ "disable": [],
34
+ "rules": []
30
35
  }
31
36
  }
@@ -18,6 +18,20 @@
18
18
  "Bash(stat //*)",
19
19
  "Bash(readlink //*)",
20
20
  "Bash(lsof *)",
21
+ "Bash(rtk read *)",
22
+ "Bash(rtk ls *)",
23
+ "Bash(rtk tree *)",
24
+ "Bash(rtk find *)",
25
+ "Bash(rtk grep *)",
26
+ "Bash(rtk rg *)",
27
+ "Bash(rtk wc *)",
28
+ "Bash(rtk diff *)",
29
+ "Bash(rtk json *)",
30
+ "Bash(rtk log *)",
31
+ "Bash(rtk deps *)",
32
+ "Bash(rtk env *)",
33
+ "Bash(rtk smart *)",
34
+ "Bash(rtk gain *)",
21
35
  "Bash(bun ~/.pal/skills/*/tools/*.ts *)",
22
36
  "Bash(bun ~/.pal/tools/*.ts *)",
23
37
  "Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
@@ -108,6 +122,15 @@
108
122
  "command": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
109
123
  }
110
124
  ]
125
+ },
126
+ {
127
+ "matcher": "Bash",
128
+ "hooks": [
129
+ {
130
+ "type": "command",
131
+ "command": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
132
+ }
133
+ ]
111
134
  }
112
135
  ],
113
136
  "Stop": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.61.4",
3
+ "version": "0.62.1",
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
@@ -622,6 +622,13 @@ function nodeInstallHint(): string {
622
622
  return "install Node ≥ 22.6 (see https://nodejs.org or your package manager)";
623
623
  }
624
624
 
625
+ function rtkInstallHint(): string {
626
+ if (process.platform === "win32")
627
+ return "download rtk.exe from https://github.com/rtk-ai/rtk/releases and add it to PATH";
628
+ if (process.platform === "darwin") return "`brew install rtk`";
629
+ return "`curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh`";
630
+ }
631
+
625
632
  function checkHookHealth(home: string): HookHealth {
626
633
  const stateDir = resolve(home, "memory", "state");
627
634
  // Read current + all rotated logs (`.1`..`.5`) + legacy `.prev` so a recent
@@ -671,6 +678,7 @@ interface DoctorResult {
671
678
  cursor: ToolCheck;
672
679
  copilot: ToolCheck;
673
680
  codex: ToolCheck;
681
+ rtk: ToolCheck;
674
682
  hasAgent: boolean;
675
683
  }
676
684
 
@@ -684,6 +692,7 @@ function doctor(silent = false): DoctorResult {
684
692
  cursor: { name: "cursor", available: true },
685
693
  copilot: { name: "copilot", available: true },
686
694
  codex: { name: "codex", available: true },
695
+ rtk: { name: "rtk", available: true },
687
696
  hasAgent: true,
688
697
  };
689
698
  }
@@ -694,6 +703,7 @@ function doctor(silent = false): DoctorResult {
694
703
  const cursor = checkTool("cursor");
695
704
  const copilot = checkTool("copilot", ["version"]);
696
705
  const codex = checkTool("codex");
706
+ const rtk = checkTool("rtk");
697
707
  const hasAgent =
698
708
  claude.available ||
699
709
  opencode.available ||
@@ -751,6 +761,11 @@ function doctor(silent = false): DoctorResult {
751
761
  : fail(
752
762
  "Playwright Chromium — not found (run 'pal cli install' or 'bunx playwright install chromium')"
753
763
  );
764
+ rtk.available
765
+ ? ok(rtk.version || "rtk")
766
+ : info(
767
+ `rtk — not installed (optional; enables Bash output compression — ${rtkInstallHint()})`
768
+ );
754
769
 
755
770
  console.log("");
756
771
  log.info("PAL state");
@@ -1023,7 +1038,7 @@ function doctor(silent = false): DoctorResult {
1023
1038
  console.log("");
1024
1039
  }
1025
1040
 
1026
- return { bun, claude, opencode, cursor, copilot, codex, hasAgent };
1041
+ return { bun, claude, opencode, cursor, copilot, codex, rtk, hasAgent };
1027
1042
  }
1028
1043
 
1029
1044
  // ── Commands ──
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Hook: PreToolUse — transparently wraps Bash commands in `rtk` to compress
3
+ * tool output before it reaches the model's context.
4
+ *
5
+ * PAL owns the wiring + presence-gating; rtk owns the rewrite. We do NOT
6
+ * reimplement rtk's selectivity or per-agent JSON shape — we forward the hook's
7
+ * stdin to `rtk hook <agent>` and pass its stdout straight back. rtk emits the
8
+ * correct rewrite object for each agent (Claude/Cursor/Copilot all differ), or
9
+ * nothing when a command isn't worth rewriting.
10
+ *
11
+ * Fail-open by construction: if rtk isn't installed, the agent can't rewrite,
12
+ * or anything throws, we emit nothing and exit 0 — the original command runs
13
+ * unchanged. rtk absent must never block or alter a Bash call.
14
+ *
15
+ * Codex (allow/deny only) and opencode (plugin path) have no `rtk hook`
16
+ * subcommand and are handled elsewhere — this hook no-ops for them.
17
+ */
18
+
19
+ import { getActiveAgent } from "./lib/agent";
20
+ import { findBinaryOnPath } from "./lib/which";
21
+
22
+ const RTK_HOOK_SUBCOMMAND: Partial<Record<ReturnType<typeof getActiveAgent>, string>> = {
23
+ claude: "claude",
24
+ cursor: "cursor",
25
+ copilot: "copilot",
26
+ };
27
+
28
+ async function run(): Promise<void> {
29
+ const subcommand = RTK_HOOK_SUBCOMMAND[getActiveAgent()];
30
+ if (!subcommand) return; // codex/opencode — no rtk hook processor
31
+
32
+ const rtk = findBinaryOnPath("rtk");
33
+ if (!rtk) return; // fail-open: rtk not installed → command runs unchanged
34
+
35
+ const stdin = await Bun.stdin.text();
36
+ const proc = Bun.spawn([rtk, "hook", subcommand], {
37
+ stdin: Buffer.from(stdin),
38
+ stdout: "pipe",
39
+ stderr: "ignore", // drop rtk's "no hook installed" stderr nag
40
+ });
41
+ const out = await new Response(proc.stdout).text();
42
+ await proc.exited;
43
+ if (proc.exitCode === 0 && out) {
44
+ process.stdout.write(out);
45
+ }
46
+ }
47
+
48
+ try {
49
+ await run();
50
+ } catch {
51
+ // Fail open — emit nothing, allow the original command.
52
+ }
@@ -7,7 +7,7 @@
7
7
  * - session-name: generate 4-word session headline on first prompt
8
8
  */
9
9
 
10
- import { injectRetrieval } from "./handlers/inject-retrieval";
10
+ import { injectPromptContext } from "./handlers/inject-retrieval";
11
11
  import { captureRating } from "./handlers/rating";
12
12
  import { captureSessionName } from "./handlers/session-name";
13
13
  import { logDebug, logError, logPromptSnapshot } from "./lib/log";
@@ -30,8 +30,8 @@ logDebug("UserPromptOrchestrator", `Input: ${JSON.stringify(input).slice(0, 200)
30
30
  if (!input?.prompt) process.exit(0);
31
31
 
32
32
  const sessionId = input.session_id ?? input.sessionId ?? input.conversation_id;
33
- const retrieval = await injectRetrieval(input.prompt);
34
- logPromptSnapshot(input.prompt, retrieval);
33
+ const injected = await injectPromptContext(input.prompt);
34
+ logPromptSnapshot(input.prompt, injected);
35
35
 
36
36
  const results = await Promise.allSettled([
37
37
  captureRating(input.prompt, sessionId),
@@ -12,6 +12,7 @@ import { logDebug, logError } from "../lib/log";
12
12
  import { runRetrieval } from "../lib/retrieval";
13
13
  import { ensureIndex } from "../lib/retrieval-index";
14
14
  import { isEnabled } from "../lib/settings";
15
+ import { getSteeringReminder } from "../lib/steering";
15
16
 
16
17
  const TIMEOUT_MS = 250;
17
18
 
@@ -51,12 +52,11 @@ export async function getRetrievalReminder(prompt: string): Promise<string | nul
51
52
  return result.reminder;
52
53
  }
53
54
 
54
- /** Write retrieval reminder to stdout in the correct format for the current agent.
55
+ /** Write a reminder to stdout in the correct format for the current agent.
55
56
  * Claude Code: plain text. Cursor: { additional_context }. Codex: hookSpecificOutput JSON.
56
- * Returns the reminder string that was injected, or null if nothing was injected. */
57
- export async function injectRetrieval(prompt: string): Promise<string | null> {
58
- const reminder = await getRetrievalReminder(prompt);
59
- if (!reminder) return null;
57
+ * MUST be called at most once per hook run — Cursor/Codex expect a single JSON
58
+ * object on stdout, so all prompt-time context is merged before this call. */
59
+ function writeForAgent(reminder: string): void {
60
60
  if (isCursor()) {
61
61
  process.stdout.write(JSON.stringify({ additional_context: reminder }));
62
62
  } else if (isCodex()) {
@@ -71,5 +71,17 @@ export async function injectRetrieval(prompt: string): Promise<string | null> {
71
71
  } else {
72
72
  process.stdout.write(`${reminder}\n`);
73
73
  }
74
- return reminder;
74
+ }
75
+
76
+ /** Gather all prompt-time context — prior-lesson retrieval + contextual steering —
77
+ * merge into a single payload, and do the one per-agent write. Returns the combined
78
+ * reminder that was injected, or null if there was nothing to inject. */
79
+ export async function injectPromptContext(prompt: string): Promise<string | null> {
80
+ const retrieval = await getRetrievalReminder(prompt);
81
+ const steering = getSteeringReminder(prompt);
82
+ const parts = [steering, retrieval].filter((p): p is string => Boolean(p));
83
+ if (parts.length === 0) return null;
84
+ const combined = parts.join("\n\n");
85
+ writeForAgent(combined);
86
+ return combined;
75
87
  }
@@ -20,8 +20,7 @@
20
20
  * yet wired and currently fall through to the API path.
21
21
  */
22
22
 
23
- import { accessSync, constants, existsSync } from "node:fs";
24
- import { basename, delimiter, resolve as resolvePath } from "node:path";
23
+ import { basename } from "node:path";
25
24
  import {
26
25
  getActiveAgent,
27
26
  isClaude,
@@ -33,6 +32,7 @@ import {
33
32
  import { logDebug } from "./log";
34
33
  import { HAIKU_MODEL } from "./models";
35
34
  import { buildSpawnGuardEnv, getInferenceDepth, SPAWN_GUARD_ENV } from "./spawn-guard";
35
+ import { findBinaryOnPath } from "./which";
36
36
 
37
37
  export function hasApiKey(): boolean {
38
38
  return !!process.env.PAL_ANTHROPIC_API_KEY;
@@ -213,45 +213,6 @@ let opencodeBinaryCache: string | null | undefined;
213
213
  let copilotBinaryCache: string | null | undefined;
214
214
  let cursorBinaryCache: string | null | undefined;
215
215
 
216
- /**
217
- * Resolve a binary on PATH to its full absolute path.
218
- *
219
- * Manual PATH walk (instead of Bun.which / `which` subprocess) because:
220
- * 1. Ubuntu 24.04 dropped the `which` binary entirely.
221
- * 2. Windows has no `which` at all.
222
- * 3. Bun.which snapshots PATH at startup and ignores mid-test mutations.
223
- * 4. Bun.spawn on Windows is inconsistent at resolving PATHEXT for bare
224
- * names — passing the full `.cmd`/`.exe` path bypasses that fragility.
225
- *
226
- * Returns the resolved absolute path or null.
227
- */
228
- function findBinaryOnPath(name: string): string | null {
229
- const PATH = process.env.PATH;
230
- if (!PATH) return null;
231
- const exts =
232
- process.platform === "win32"
233
- ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")
234
- : [""];
235
- for (const dir of PATH.split(delimiter)) {
236
- if (!dir) continue;
237
- for (const ext of exts) {
238
- const candidate = resolvePath(dir, name + ext);
239
- try {
240
- if (process.platform === "win32") {
241
- // Windows has no executable bit — existence in PATHEXT is enough.
242
- if (existsSync(candidate)) return candidate;
243
- } else {
244
- accessSync(candidate, constants.X_OK);
245
- return candidate;
246
- }
247
- } catch {
248
- /* not here — try next */
249
- }
250
- }
251
- }
252
- return null;
253
- }
254
-
255
216
  function getClaudeBinary(): string | null {
256
217
  if (claudeBinaryCache !== undefined) return claudeBinaryCache;
257
218
  claudeBinaryCache = findBinaryOnPath("claude");
@@ -26,17 +26,24 @@ export function flagshipAuthorModel(agent: AgentType): string | undefined {
26
26
  return FLAGSHIP_AUTHOR_MODEL[agent];
27
27
  }
28
28
 
29
+ export interface ModelPricing {
30
+ input: number;
31
+ output: number;
32
+ cacheWrite5m: number;
33
+ cacheWrite1h: number;
34
+ cacheRead: number;
35
+ }
36
+
37
+ export interface TokenUsage {
38
+ input: number;
39
+ output: number;
40
+ cacheWrite5m: number;
41
+ cacheWrite1h: number;
42
+ cacheRead: number;
43
+ }
44
+
29
45
  /** Pricing per million tokens (USD) — from https://platform.claude.com/docs/en/about-claude/pricing */
30
- export const MODEL_PRICING: Record<
31
- string,
32
- {
33
- input: number;
34
- output: number;
35
- cacheWrite5m: number;
36
- cacheWrite1h: number;
37
- cacheRead: number;
38
- }
39
- > = {
46
+ export const MODEL_PRICING: Record<string, ModelPricing> = {
40
47
  [HAIKU_MODEL]: {
41
48
  input: 1,
42
49
  output: 5,
@@ -51,6 +58,13 @@ export const MODEL_PRICING: Record<
51
58
  cacheWrite1h: 20,
52
59
  cacheRead: 1,
53
60
  },
61
+ "claude-opus-5": {
62
+ input: 5,
63
+ output: 25,
64
+ cacheWrite5m: 6.25,
65
+ cacheWrite1h: 10,
66
+ cacheRead: 0.5,
67
+ },
54
68
  "claude-opus-4-8": {
55
69
  input: 5,
56
70
  output: 25,
@@ -72,6 +86,13 @@ export const MODEL_PRICING: Record<
72
86
  cacheWrite1h: 10,
73
87
  cacheRead: 0.5,
74
88
  },
89
+ "claude-opus-4-5": {
90
+ input: 5,
91
+ output: 25,
92
+ cacheWrite5m: 6.25,
93
+ cacheWrite1h: 10,
94
+ cacheRead: 0.5,
95
+ },
75
96
  // Claude Sonnet 5 — introductory pricing through 2026-08-31; standard (3/15/3.75/6/0.30) applies from 2026-09-01.
76
97
  "claude-sonnet-5": {
77
98
  input: 2,
@@ -95,3 +116,37 @@ export const MODEL_PRICING: Record<
95
116
  cacheRead: 0.3,
96
117
  },
97
118
  };
119
+
120
+ function longestPrefixKey(model: string): string | null {
121
+ let best: string | null = null;
122
+ for (const key of Object.keys(MODEL_PRICING)) {
123
+ if (model.startsWith(key) && (best === null || key.length > best.length)) best = key;
124
+ }
125
+ return best;
126
+ }
127
+
128
+ /**
129
+ * Rates for a model ID. Transcripts carry variants of the same model — dated
130
+ * (`claude-opus-5-20260115`) and context-tagged (`claude-opus-5[1m]`) — so an
131
+ * exact miss falls back to the longest table key the ID starts with.
132
+ */
133
+ export function pricingFor(model: string): ModelPricing | null {
134
+ const exact = MODEL_PRICING[model];
135
+ if (exact) return exact;
136
+ const prefix = longestPrefixKey(model);
137
+ return prefix ? MODEL_PRICING[prefix] : null;
138
+ }
139
+
140
+ /** USD cost of a token usage record. Unpriced models bill as 0. */
141
+ export function costOfUsage(model: string, usage: TokenUsage): number {
142
+ const p = pricingFor(model);
143
+ if (!p) return 0;
144
+ return (
145
+ (usage.input * p.input +
146
+ usage.output * p.output +
147
+ usage.cacheWrite5m * p.cacheWrite5m +
148
+ usage.cacheWrite1h * p.cacheWrite1h +
149
+ usage.cacheRead * p.cacheRead) /
150
+ 1_000_000
151
+ );
152
+ }
@@ -25,6 +25,11 @@ export interface PalSettingsData {
25
25
  dynamicContext?: Record<string, boolean>;
26
26
  /** Git co-author attribution opt-in. `decided` gates the one-time prompt. */
27
27
  attribution?: { enabled?: boolean; decided?: boolean };
28
+ /** Contextual-steering user extension: personal rules + shipped rules to suppress by tag. */
29
+ steering?: {
30
+ disable?: string[];
31
+ rules?: Array<{ tag: string; pattern: string; snippet: string }>;
32
+ };
28
33
  [key: string]: unknown;
29
34
  }
30
35