From 021c18a13e578fe064f2e58846e1fdfb58195b42 Mon Sep 17 00:00:00 2001 From: Ryan Gapac Date: Thu, 1 Oct 2026 10:50:03 +0800 Subject: [PATCH 1/6] Docs: README and guides describe what 1.0 ships; field report and hero refreshed; steer calibration brought in --- CHANGELOG.md | 2 +- README.md | 49 ++- docs/commands.md | 2 +- docs/configuration.md | 6 +- docs/data-handling.md | 4 +- docs/extension-authors.md | 2 +- docs/guards.md | 123 +++++-- docs/hero.png | Bin 193386 -> 192708 bytes .../2026-09-21-calibration-0.33.3/report.md | 2 +- .../2026-09-22-conscience-policy/report.md | 2 +- .../2026-09-22-conscience-recommend/report.md | 2 +- .../2026-09-22-conscience-remeasure/report.md | 2 +- eval/reports/2026-09-24-field-usage/README.md | 4 +- .../2026-09-25-weak-model-bench/README.md | 2 +- eval/reports/2026-10-01-field-usage/README.md | 69 ++++ .../reports/2026-10-01-field-usage/usage.json | 117 +++++++ eval/reports/README.md | 3 +- scripts/action-candidates.mjs | 15 + scripts/field-usage.mjs | 16 +- scripts/render-hero.mjs | 26 +- scripts/steer-ask-probe.mjs | 146 ++++++++ scripts/steer-calibration.mjs | 317 ++++++++++++++++++ 22 files changed, 844 insertions(+), 67 deletions(-) create mode 100644 eval/reports/2026-10-01-field-usage/README.md create mode 100644 eval/reports/2026-10-01-field-usage/usage.json create mode 100644 scripts/steer-ask-probe.mjs create mode 100644 scripts/steer-calibration.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c599c48c..7514f1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,7 +235,7 @@ How to keep this current: add the entry in the same pull request as the change, ### Changed -- The intent-mismatch verdict is trace-only by default: `action.intentTraceOnly` is `"all"` instead of `"invisible"`, so a mismatch on a call with a visible effect (a commit, push, merge, tag, reset, pull request, release, publish, install, launched program, or a message sent from a script) no longer reaches the agent. The score, the trace entry, the `/warden status` counters, and the `visibleMismatch` and `intentMismatch` thresholds are unchanged; hand labels on 140 sampled calls, blind to the score, put the score's separation of a differing call at AUROC 0.815, but of the 37 steers that would reach the agent, 36 were calls the plan or the user's latest request had asked for. Restore the old delivery with `"action": { "intentTraceOnly": "invisible" }`. +- The intent-mismatch verdict is trace-only by default: `action.intentTraceOnly` is `"all"` instead of `"invisible"`, so a mismatch on a call with a visible effect (a commit, push, merge, tag, reset, pull request, release, publish, install, launched program, or a message sent from a script) no longer reaches the agent. The score, the trace entry, the `/warden status` counters, and the `visibleMismatch` and `intentMismatch` thresholds are unchanged; labels on 140 sampled calls, made blind to the score, put the score's separation of a differing call at AUROC 0.815, but of the 37 steers that would reach the agent, 36 were calls the plan or the user's latest request had asked for. Restore the old delivery with `"action": { "intentTraceOnly": "invisible" }`. ## 0.75.0 diff --git a/README.md b/README.md index 38ad0a5d..bb698193 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ -![pi-warden tells the agent what it got wrong and the agent fixes it: told 75 times that it said done with nothing checked, the agent ran a check next 57 times (76%); of 65 held actions with a known outcome, the agent found a safer way 40 times, you approved 24, you said no once; 349 steers named a project rule; 759 real sessions, 2026-09-16 to 2026-09-24](https://raw.githubusercontent.com/DevMortimer/pi-warden/main/docs/hero.png) +![pi-warden tells the agent what it got wrong and the agent fixes it: told 124 times that it said done with nothing checked, the agent ran a check next 94 times (76%); of 111 held actions with a known outcome, the agent found a safer way 79 times, you approved 29, you said no 3 times; 422 steers named a project rule; 1,168 real sessions, 2026-09-16 to 2026-10-01](https://raw.githubusercontent.com/DevMortimer/pi-warden/main/docs/hero.png)
-**759 sessions · 220 risky actions stopped before they ran · 76% of fake "done"s turned into real test runs** +**1,168 sessions · 419 risky actions stopped before they ran · 76% of fake "done"s turned into real test runs** -Nine days of the maintainer's real use across all their projects, not just this one, 2026-09-16 to 2026-09-24. Method and noise: [field report](eval/reports/2026-09-24-field-usage/). +Sixteen days of the maintainer's real use across all their projects, not just this one, 2026-09-16 to 2026-10-01. Method and noise: [field report](eval/reports/2026-10-01-field-usage/).
@@ -40,7 +40,7 @@ Most guardrails stop and ask you. pi-warden tells **the agent** what it got wron | --- | --- | | says "done" with no test, build, or lint behind it | sends it back to prove it | | breaks a rule in your `pi-warden.md` or `AGENTS.md` | quotes the exact rule it broke | -| is about to `git push --force`, `reset --hard`, `rm -rf`, `DROP` | holds it before it runs when the judge is at least 0.9 sure it can't be undone; with no judge, a built-in pattern holds it | +| is about to `git push --force`, `reset --hard`, `rm -rf`, `DROP` | holds it before it runs. A built-in pattern holds the destructive ones with no key at all; with Jev on, a score of 0.9 or more for "cannot be undone" holds too | | retries the same failing fix for the third time | asks for a new hypothesis | | writes stubs, restating comments, hardcoded secrets | names them on the spot | | floods its context with a huge log | keeps the lines that matter, stores the rest | @@ -56,16 +56,21 @@ Every guard and feature, its default, and its status. Defaults are `defaultConfi | --- | --- | --- | --- | | Jev judgments | Sends redacted samples to Jev for the judged checks below | Off until `/warden enable` | Stable | | [Action guard](docs/guards.md#action-guard) | Checks each command, write, and edit before it runs: offline patterns plus one Jev request | On | Stable | -| ↳ Irreversible hold | Holds a call Jev scores 0.9 or more as irreversible; 0.5 to 0.9 warns | On | Stable | -| ↳ Off-task steer | Warns at 0.6 and steers the agent back to your request at 0.85; never holds | On | Stable | +| ↳ Offline floor | Holds destructive patterns (force push, `reset --hard`, `rm -rf` on a path outside scratch, `DROP`) with no key and no request; with Jev on, a pattern hit goes to Jev as evidence | On | Stable | +| ↳ Irreversible hold | Jev adds holds: a call scored 0.9 or more as irreversible is held; 0.5 to 0.9 warns | On | Stable | +| ↳ Scratch deletes | A recursive `rm` of temp scratch the agent made warns instead of holding; data moved in keeps the hold | On | Stable | +| ↳ Ask gate | Code decides offline when no Jev answer could change what the agent sees, so the call sends no request: 48.9% fewer requests, 327 of the 338 changed outcomes still asked | On. `action.ask` | Stable | +| ↳ Approval on demand | After a hold, a call held again under your next reply gets one request that reads your reply and the agent's turn; released only when `approved` and `reply_points_at_action` are both 0.7 or more | On | Stable | +| ↳ Off-task notice | Notes in the trace when a call looks unrelated to your request; never steers or holds | On, trace only | Stable | | ↳ Intent-mismatch steer | Tells the agent when a call differs from its stated plan; the score stays in the trace | Off: trace only. `action.intentTraceOnly: "invisible"` | Stable | -| ↳ Should-proceed steer | Asks the agent to pause and ask you; the score stays in the trace | Off: trace only. `action.shouldProceed.steer: true` | Stable | +| ↳ Should-proceed notice | Notes in the trace when the judge would ask you first. `action.shouldProceed.steer: true` also tells the agent; the 2026-09-30 labels do not support it (2 of 427 calls needed asking) | Off: trace only | Beta | | ↳ Your command, path, and arming rules | Warn, hold, or deny commands and paths you name | On, none set | Stable | | ↳ Large-output warning | Tells the agent to filter a command that will print far more than it needs | On | Stable | | [Rules](docs/guards.md#rules) | Judges every write and edit against the rules in your `pi-warden.md` | On | Stable | +| [Rules at turn start](docs/guards.md#rules-at-turn-start) | Names the rules a new request will touch, in the background, and delivers them at the next tool boundary; the prompt never waits | On. `rulesAtTurnStart.enabled` | Stable | | ↳ Soft double-check tier | Asks the agent to double-check a score just below a rule's cutoff | Off. `rules.softThreshold` | Stable | | [Slop](docs/guards.md#slop) | Names stubs, restating comments, dead code, and padded replies | On | Stable | -| [Security](docs/guards.md#security) | Flags risky written code and prompt injection in tool output; masks credentials | On | Stable | +| [Security](docs/guards.md#security) | Flags risky written code and prompt injection in tool output; masks credentials, but leaves code values such as a call or an index alone | On | Stable | | [Stuck](docs/guards.md#stuck) | Asks for a new hypothesis when the agent repeats a failing approach | On | Stable | | [Done-check](docs/guards.md#done-check) | Sends an unverified "done" back for a check; after a UI change, a visual check | On | Stable | | [Runaway](docs/guards.md#runaway) | Stops a reply that repeats itself, offline | On | Stable | @@ -83,12 +88,27 @@ Every guard and feature, its default, and its status. Defaults are `defaultConfi | [Adaptive steers](docs/guards.md#adaptive-steers-per-model) | Makes a steer kind trace-only for a model that rarely follows or often disputes it | On | Stable | | Steers and notices in the transcript | Shows steers and per-call notices in the chat, not only in the trace | Off. `steerVisible`, `notices` | Stable | | [Desktop notifications](docs/guards.md#desktop-notifications) | Holds, confirm dialogs, and runaway stops reach the desktop or your relay | Off. `notify.enabled` | Stable | -| [Conscience](docs/guards.md#conscience) | Recommends a skill or tool the agent is missing | Off. `conscience.enabled` | Beta | -| Learning from holds | Records hold outcomes locally; `/warden recommend` suggests threshold changes | On | Stable | +| [Conscience](docs/guards.md#conscience) | Recommends a skill or tool the agent is missing; ranks candidates on your machine first and sends one request, or none | Off. `conscience.enabled` | Beta | +| Learning from holds | Keeps each judged call in a local database with its Pi session id and, for a hold, what happened next (`approved`, `declined`, `replanned`, `abandoned`); a call that was not held keeps only the judge data, and is pruned after 90 days (`learning.allowedRetentionDays`; holds after 365). `/warden recommend` suggests threshold changes from it. Nothing is sent | On | Stable | +| Config surface | A project file can only make the action guard and security stricter, a value that cannot be applied as written is shown once as a warning, and `deny` and `block` mean the same in every rule kind | On, no switch | Stable | | Standing preferences | `/warden prefs` finds preferences you repeated across sessions and sends them at session start | On | Stable | All keys and their values: [configuration](docs/configuration.md). Commands: [commands](docs/commands.md). +## What it costs + +Jev requests cost money, so each 1.0 change was measured by what it saves. Each figure is the measured change on recorded sessions of one machine, not a promise for yours. + +| Change | Measured effect | Source | +| --- | --- | --- | +| Ask gate and lean request ([#153](https://github.com/DevMortimer/pi-warden/pull/153)) | Action-guard requests 28,036 → 14,326 (−48.9%); input tokens 64.2M → 20.5M (−68.1%); 327 of the 338 calls whose answer changed what the agent saw are still asked (96.7%) | [Ask gate and lean request](docs/guards.md#ask-gate-and-lean-request-2026-09-30) | +| Conscience ranks locally first ([#155](https://github.com/DevMortimer/pi-warden/pull/155)) | Requests per prompt 2 → 1; input tokens per prompt 14,620 → 10,241 (−30%); 30.9% of 7,614 recorded prompts send none | [Local gate, top k, and tip text](docs/guards.md#local-gate-top-k-and-tip-text-2026-10-01) | +| Turn-start reminder and conscience in the background ([#152](https://github.com/DevMortimer/pi-warden/pull/152)) | The prompt hold falls from about 255 ms to under 1 ms at p90; the reminder names a rule that applies 68.9% of the time and catches 21.5% of the rules that apply | [Turn-start delivery calibration](docs/guards.md#turn-start-delivery-calibration-2026-10-01-background-delivery) and [Rules at turn start calibration](docs/guards.md#rules-at-turn-start-calibration-2026-09-30-first-measurement) | +| Approval on demand ([#160](https://github.com/DevMortimer/pi-warden/pull/160), [#161](https://github.com/DevMortimer/pi-warden/pull/161)) | Approval questions per 1,000 judged calls 90.0 → 0.7; wrong releases 45 → 1 in the measured runs | [Approval on demand](docs/guards.md#approval-on-demand-2026-10-01-three-designs-three-sets) | +| Hold database ([#149](https://github.com/DevMortimer/pi-warden/pull/149)) | A copy of 52,362 rows shrinks from 285.9 MiB to 41.6 MiB | [Data handling](docs/data-handling.md#what-stays-on-this-machine) | + +Features that did not earn their cost did not ship: the working-memory pruning, the stale-result stubs, and a hybrid compaction failed their gates, and the off-task and should-proceed steers stay trace-only. The numbers are in the [Calibration](docs/guards.md#calibration) section. + ## Rules no linter can check ```markdown @@ -105,8 +125,8 @@ Each `#` heading is one rule. Every write and edit is judged against it in about ## Receipts - **Rules:** in 150 paired agent runs, the agent without pi-warden broke the tested rule **6 times**. With it: **0**. -- **Done-check:** after a nudge, the agent ran a check **57 of 75** times, and sometimes found a failure it had missed. -- **Holds:** when the agent was stopped, it found a safer way **40 of 65** times; you approved 24. Field data from 2026-09-16 to 2026-09-24, before the 0.9 threshold. +- **Done-check:** after a nudge, the agent ran a check **94 of 124** times, and sometimes found a failure it had missed. +- **Holds:** when the agent was stopped, it found a safer way **79 of 111** times; you approved 29. Field data from 2026-09-16 to 2026-10-01, across versions, so it includes holds from before the 0.9 threshold and the `rm` scratch change. - **Stability:** 13,952 guard cases over 109 overnight cycles, no score drift. Every number has a script and a raw report in [`eval/reports/`](eval/reports/) or the Calibration section of [the guard docs](docs/guards.md#calibration). They are the maintainer's measurements, not a universal promise, and the reports list what was noise. @@ -116,7 +136,8 @@ Every number has a script and a raw report in [`eval/reports/`](eval/reports/) o Thresholds are set on recorded sessions. Where the data allows, a threshold is chosen on one half of the corpus and checked on the other. A feature that measured worse ships off or trace-only. [Calibration →](docs/guards.md#calibration) - **Irreversible hold at 0.9** (0.75.0). On 15,346 judged calls the judge was wrong on 15% of calls below confidence 0.8 and under 1% above it. A 0.9 cutoff chosen on one half removed about 52 false alarms on the other half and lost no true catch. Scores from 0.5 to 0.9 now warn. -- **Intent steer in the trace only** (0.76.0). The score separates a differing call (AUROC 0.815 on 140 hand-labelled calls), but 36 of the 37 steers it would send were for calls the plan or your request had asked for. +- **Intent steer in the trace only** (0.76.0). The score separates a differing call (AUROC 0.815 on 140 calls labelled blind to the score), but 36 of the 37 steers it would send were for calls the plan or your request had asked for. +- **Failed gates stay off.** Pruning stale tool results had a safe point in 10 of 1,042 recorded sessions (median saving 0.00%); a hybrid compaction kept 5 of the 34 re-read files where the gate asked for 10; the working-memory gate's best cut dropped 23.5% of candidates and missed 12.8% where the gate allowed 10%; and no slice of the off-task or should-proceed steers reached 0.80 precision. Each is recorded in [Calibration](docs/guards.md#calibration). - **Relevance compaction off** (0.78.0). In a replay of 48 recorded compactions its summary was 4.7 times the size of Pi's at the median and kept whole only 1 of the 34 files the agent read again. ## Privacy @@ -131,7 +152,7 @@ Secrets and unshown paths are stripped before anything leaves your machine. The ## Versioning -From 1.0, pi-warden follows semver for: the documented config keys and their defaults (except `compaction.*`, `context.filter.*`, and `conscience.*`, which are experimental or beta); the `/warden` commands and their arguments; the `warden_remember`, `warden_loops`, and `warden_recall` tools; the documented `PI_WARDEN_*` environment variables; the widget template tokens; and the exports named in [docs/extension-authors.md](docs/extension-authors.md). The trace file format, the files in pi-warden's data folder, session message types, and every other export are internal and may change in any release. +From 1.0, pi-warden follows semver for: the documented config keys and their defaults (except `compaction.*`, `context.filter.*`, `conscience.*`, and `action.shouldProceed.steer`, which are experimental or beta); the `/warden` commands and their arguments; the `warden_remember`, `warden_loops`, and `warden_recall` tools; the documented `PI_WARDEN_*` environment variables; the widget template tokens; and the exports named in [docs/extension-authors.md](docs/extension-authors.md). The trace file format, the files in pi-warden's data folder, session message types, and every other export are internal and may change in any release. ## Docs diff --git a/docs/commands.md b/docs/commands.md index ba189c1e..e182e042 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,7 +4,7 @@ | Command | Effect | | --- | --- | -| `/warden status` | Guard state, consent and key source, session counts, steers sent per guard, the steer kinds that are trace-only per model with their rates, thresholds, context saver totals, the turn-start rule reminder totals (rules named, requests, latency), hold precision, rules source, config paths, config warnings, last verdicts | +| `/warden status` | Guard state, consent and key source, session counts, steers sent per guard, the steer kinds that are trace-only per model with their rates, thresholds, context saver totals, the turn-start rule reminder totals (rules named, requests, latency), hold precision, the lifetime hold counts for this project (held, labelled, stood, abandoned), rules source, config paths, config warnings, last verdicts | | `/warden rules` | List the active parsed rules, their path scopes and source files, each rule's `threshold:`, `severity:`, and `source: :` origin when set, plus the dropped count and `rules.exclude` patterns. This is local-only and sends nothing to Jev. With `rules.enabled: false` the output leads with `Rules guard is off (rules.enabled: false). These would apply:` and the same details follow | | `/warden rules check` | Ask Jev which of the active rules the rules guard cannot judge well: one line per rule that needs attention, with the reason, its score, and one suggestion (`move it to your linter`, `split it so the changed file alone shows the violation`), then `N fine, M need attention`. Each rule's id, heading, `paths:` scope, and text are sent, redacted; no file content and no task text. Needs a key and consent: without either it says so and sends nothing. A fallback document with no rule headings reports that there are no separate rules to check. Advice only: it never changes, disables, or skips a rule, and the guard's own questions and thresholds are untouched | | `/warden rules audit [paths...] [--max N] [--yes]` | Judge existing files against the project rules as if each had just been written: one Jev request per file (at most four at a time) over the source files under the given paths (default: the project) that git does not ignore and at least one rule applies to. Capped at `--max` files (default 50), with the number left out reported. A confirm dialog names the file count and what leaves the machine before anything is sent; a headless run needs `--yes`. Output: a table by rule (files judged, files flagged, mean score) and the flagged files with their rule scores, worst first, plus a Markdown copy at `.pi-warden/rules-audit.md`. Nothing is recorded in the rules log. With no key or no consent it says so and sends nothing | diff --git a/docs/configuration.md b/docs/configuration.md index f1ed8543..440fa677 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -90,12 +90,12 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). `/warden config` ope | `maxRequests` | Per-session request budget. When spent, pi-warden says so once and continues with offline checks. | | `action.tools` | Tools the action guard inspects. Add your own shell-like tools here. | | `action.irreversible` | `warn` and `confirm` (hold) thresholds on P(irreversible). Defaults `warn` 0.5 and `confirm` 0.9. The 0.9 hold waits for the confidence at which the judge stops being wrong: on the recorded action-guard corpus the judge errs on 15% of calls below confidence 0.8 and under 1% above it, and a 0.9 cutoff removed about 52 false alarms on a held-out half of the corpus without losing a true catch. Calls the judge scores 0.5 to 0.9 warn instead of holding. See [guards.md](guards.md#irreversible-hold-threshold-2026-09-29-held-out-split). | -| `action.offTask` | `warn` and `steer` thresholds on P(off-task). Off-task never holds and is always trace-only: the answer reaches the trace, never the agent. Only the sampled call (`action.traceSample`) asks the off-task and scope questions; the acting request never does, so with `traceSample: 0` these thresholds have nothing to read. | +| `action.offTask` | `warn` and `steer` are score thresholds for the off-task reason text in the trace. The warning itself follows the judge's `scope` answer (`unrelated` and `plausible side step` warn at any score). Off-task never holds and every off-task notice is trace-only: no setting makes it reach the agent (the steer is off on the 2026-09-30 blind labels, see [guards.md](guards.md#steer-calibration-2026-09-30-blind-labels)). Only the sampled call (`action.traceSample`) asks the off-task and scope questions; the acting request never does, so with `traceSample: 0` these thresholds have nothing to read. | | `action.intentMismatch` | P(call differs from the agent's stated plan) that warns and tells the agent, on calls that can change something. | | `action.visibleMismatch` | Lower mismatch threshold for commands whose effect is visible outside the working tree (commit, push, publish, install, launch). | | `action.intentTraceOnly` | Which intent mismatches stay in the trace without a steer to the agent. `"all"` (default): every mismatch, visible effect or not. `"invisible"`: only a call with no visible effect: not a commit, push, merge, tag, reset, pull request, release, or publish (decided in code), and not judged `visible` at 0.8 or more (an install, a launched program, a message sent from a script). `"none"`: none; every mismatch steers, as before. The steer arrives after the call ran. Blind labels on 140 sampled calls put the score's separation of a differing call at AUROC 0.815, but of the 37 steers that would reach the agent, 36 were calls the plan or the user's latest request had asked for. Restore the old behaviour with `"action": { "intentTraceOnly": "invisible" }`. | -| `action.shouldProceed` | `{ threshold, steer }`. Scores at or below `threshold` (default 0.6) are trace-only by default until calibrated; they never hold a call. By default only the sampled call (`action.traceSample`) asks the question; with `steer: true` every acting request asks it. `hold` is the deprecated name of `threshold`: it is still read through 1.x, and `threshold` wins when both are set. | -| `action.shouldProceed.steer` | Default `false`. Set `true` to restore the steer that asks the agent to pause and seek user approval. It puts `should_proceed` on every acting request, so a score at or below `threshold` steers. The ask gate still applies: a call the gate decides offline (see `action.ask`) gets no should-proceed answer, unless `action.ask.enabled` is `false`, which asks every judged call. With `false`, only the sampled call asks the question, and its answer stays trace-only. | +| `action.shouldProceed` | `{ threshold, steer }`. Scores at or below `threshold` (default 0.6) are recorded in the trace and the status counts only by default; they never hold a call. On the 2026-09-30 blind labels 32% of judged calls sat at or below 0.6 while 2 of 427 labelled calls needed asking (see [guards.md](guards.md#steer-calibration-2026-09-30-blind-labels)). By default only the sampled call (`action.traceSample`) asks the question; with `steer: true` every acting request asks it. `hold` is the deprecated name of `threshold`: it is still read through 1.x, and `threshold` wins when both are set. | +| `action.shouldProceed.steer` | Default `false`. Set `true` to restore the steer that asks the agent to pause and seek user approval. The 2026-09-30 labels do not support it: precision 0.01 to 0.06 across thresholds. It puts `should_proceed` on every acting request, so a score at or below `threshold` steers. The ask gate still applies: a call the gate decides offline (see `action.ask`) gets no should-proceed answer, unless `action.ask.enabled` is `false`, which asks every judged call. With `false`, only the sampled call asks the question, and its answer stays trace-only. | | `action.ask` | `{ enabled }`, default `true`. Whether a call whose answer cannot change what the agent sees is decided offline instead of by a request. On the recorded action traffic from 2026-09-25 to 2026-09-30 it asks 14,326 of 28,036 judged calls (48.9% fewer requests) and still asks 96.7% of the calls whose answer changed what the agent saw. The offline pattern pass and the floor always run. | | `action.traceSample` | `0` to `1`, default `0.05`: one judged call in twenty also asks the trace-only questions (off-task, scope, should-proceed) in a second request, so the recorded signal keeps coming. The acting request never asks off-task or scope, and asks should-proceed only with `action.shouldProceed.steer: true`. A sampled call's answers go through the off-task and should-proceed checks with the usual thresholds and stay trace-only: nothing new reaches the agent and nothing holds. A call the ask gate decides offline is never sampled. With `0`, no call gets an off-task, scope, or should-proceed answer, except should-proceed under the opt-in steer. | | `action.feedbackLog` | Write each judged call and its outcome to `~/.pi/agent/pi-warden/holds/`; never the command. | diff --git a/docs/data-handling.md b/docs/data-handling.md index 131f89a1..21b266eb 100644 --- a/docs/data-handling.md +++ b/docs/data-handling.md @@ -14,7 +14,7 @@ With consent, requests go to `https://api.typesafe.ai` (default), or to the host | **Stuck** | The last 12 tool calls (300 characters each) with 400-character output tails, and, while `stuck.evidence` is on (the default), a structured `evidence` section: per run the parsed failing test, error, location, summary, exit code and which earlier run failed the same way (or 300 characters of head and 300 of tail when nothing parses), per `edit`/`write` the project path and a diff of the change capped at 600 characters, and a digest. Every string is redacted; the whole object is capped at 4 KB. | | **Done-check and prose** | The agent's final message (2000 and 2500 characters), the run's check commands, the audience description. | | **Output checks** | A redacted head/tail sample up to 6000 characters plus size, line counts, and tool name. With the context filter on (`context.filter.enabled`, off by default), an output that would get the generic excerpt is also sent whole, redacted, in line-bounded chunks, with your latest prompt (1500 characters), the task spine, the agent's text for the call (1500 characters), the tool name, and the command or tool input (500 characters). | -| **Conscience** (recommend mode) | Your current request (2000 redacted characters), the same task spine (the thread's first request and up to four earlier requests, redacted, capped at 1200 characters together), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name, role, lead, useWhen, examples when an index entry matches; bare description otherwise). Full skill instructions never go to Jev. The index is built locally by the session model; only sanitized entries reach Jev; advertised locations never do. Sent only when TypeSafe consent is given and the conscience module is enabled. | +| **Conscience** (recommend mode) | Your current request (2000 redacted characters), the same task spine (the thread's first request and up to four earlier requests, redacted, capped at 1200 characters together), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name, role, lead, useWhen, examples when an index entry matches; bare description otherwise). Full skill instructions never go to Jev. Candidates are ranked on this machine first (BM25 over the index fields): only the top `conscience.localTopK` (default 31) go to Jev, and when none reaches `conscience.localFloor` (default 0.5), a prompt is a short continuation, a relayed child report, or its task spine was already assessed, nothing is sent. Pi's core tools, a tool or skill the session already used, and a tool that deletes are never candidates. The index is built locally by the session model; only sanitized entries reach Jev; advertised locations never do. Sent only when TypeSafe consent is given and the conscience module is enabled. | | **Conscience** (load mode) | Same judge payload as recommend mode, plus: the selected skill file is read from disk (bounded by `maxSkillBytes` and `maxLoadedBytes`), frontmatter is stripped, credentials are checked, and the complete body is supplied to the main model via a custom message. Skill bodies never go to Jev. | | **Subagent triage** | Only for a child report that names a failure, a stop, a timeout, or a question (an incremental progress line or a clean completion is answered in code and sends nothing): a redacted 1500-character head plus 500-character tail of the report, the notification type, whether it is an incremental notify, its length, and your latest prompt (1000 characters). | | **Relevance compaction** (opt-in, `compaction.enabled`) | At each compaction: your latest request (2000 redacted characters), the same task spine, the focus a manual `/compact ` names (500 characters), a redacted outline of the conversation being compacted (user and assistant text clipped to at most 400 characters per message, one line per tool call), and for each tool call, extension message, and part of the previous summary a redacted 500-character input and a 1100-character head/tail sample of its result or text. A result the output check flagged as a possible prompt injection is not sampled; results are flagged only when the security check is on (`security.enabled`). The summary itself stays in the session. | @@ -27,7 +27,7 @@ Default paths below are for Pi. On oh-my-pi, pi-warden uses `~/.omp/agent` inste - The hold feedback log under `~/.pi/agent/pi-warden/holds/`, owner-only: one JSON line per judged call with tool, pattern ids, scores, level, mode, outcome, and the length of the agent's stated plan; never the command, prompt, or plan text. A redacted tool+path excerpt is included for auditing off-task and intent-mismatch. `"action": { "feedbackLog": false }` turns the file off. - The rules verdict log under `~/.pi/agent/pi-warden/rules/`, owner-only: one JSON line per scored rule per judgment from Jev (time, session id, project-relative path, tool, rule id and name, outcome, P(violation), the threshold in force, whether it was a finding, and `cleared: true` when a later judgment cleared an earlier finding on the same path). Scores from `/warden rules calibrate` are written with `source: "calibrate"`, so `/warden report` counts the replays apart from live verdicts. No written content and no rule body. The file is keyed by a hash of the project path, keeps the newest 5,000 records, and is read only by `/warden report`, which sends nothing. `/warden rules audit` records nothing here. - The rules audit Markdown copy at `.pi-warden/rules-audit.md` in the project: the run's date and paths, and per rule the judged count, flagged count, and mean score, plus the flagged files with their rule scores. Read only by you; nothing reads it back into pi-warden. -- An owner-only SQLite database under `~/.pi/agent/pi-warden/holds.db` stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls (`held = 1` and `held = 0`), for learning and retention (a hold is kept `learning.retentionDays`, default 365 days; an allowed call `learning.allowedRetentionDays`, default 90 days). Each row carries the Pi session id of the call. The task text is stored once per task in a `hold_tasks` table and the row points at it by hash, so a turn's text is not repeated for every call it made; a row of an allowed call keeps the judge data and the plan (the scores, the reasons, and the agent's plan are what calibration reads on field traffic), with the task text stored once in `hold_tasks` and the row pointing at it by hash; it carries no `context_summary` and no agent reason for the call, which no reader asks for there. It may instead carry an outcome (`accepted`, `regretted`). Calls the guard skipped as read-only are not stored. The columns an earlier version wrote and nothing reads stay on the table, always empty, so a session still running that version — and a downgrade — keeps recording holds into a migrated database; a row such a session writes after the migration is settled at the next start, its task text moving into `hold_tasks` and its older text cleared. A database written by a version before this layout is migrated once at startup, inside one transaction; free pages are reclaimed by a VACUUM that runs only when no other session holds the database, and is skipped until the next start when one does. The `PI_WARDEN_DB` environment variable overrides this path; tests and eval runs use it to write to an isolated database instead of the user's. +- An owner-only SQLite database under `~/.pi/agent/pi-warden/holds.db` stores redacted hold context (plan, summary, redacted command preview, outcomes) for held calls and the judge data for judged-allowed calls (`held = 1` and `held = 0`), for learning and retention (a hold is kept `learning.retentionDays`, default 365 days; an allowed call `learning.allowedRetentionDays`, default 90 days). Each row carries the Pi session id of the call. The task text is stored once per task in a `hold_tasks` table and the row points at it by hash, so a turn's text is not repeated for every call it made; a row of an allowed call keeps the judge data and the plan (the scores, the reasons, and the agent's plan are what calibration reads on field traffic), with the task text stored once in `hold_tasks` and the row pointing at it by hash; it carries no `context_summary` and no agent reason for the call, which no reader asks for there. It may instead carry an outcome (`accepted`, `regretted`). Calls the guard skipped as read-only are not stored. The columns an earlier version wrote and nothing reads stay on the table, always empty, so a session still running that version — and a downgrade — keeps recording holds into a migrated database; a row such a session writes after the migration is settled at the next start, its task text moving into `hold_tasks` and its older text cleared. A database written by a version before this layout is migrated once at startup, inside one transaction; free pages are reclaimed by a VACUUM that runs only when no other session holds the database, and is skipped until the next start when one does. The `PI_WARDEN_DB` environment variable overrides this path; tests and eval runs use it to write to an isolated database instead of the user's. - With `PI_WARDEN_TRACE_DIR` set to an absolute path, the trace of each session in `/.jsonl`, owner-only: the same redacted lines the trace sidebar shows (status line, details, widget tokens), the session id, the working directory (`~`-shortened), the Warden version, the mode, and whether Jev judgments are on (or the reason they are off). Off by default. See [configuration.md](configuration.md#trace-file). - Full copies of compressed tool output, and of tool results (and, when enabled, messages) whose repeated runs were cut, owner-only, in the OS temporary directory. They can contain secrets and stay there until removed. - The compaction evidence appendix (`pi-warden-compact-evidence`), sent as a local session message after compaction. Contains failed calls and one error line from each output (redacted), the last passing check command (redacted), the session's open loops (redacted), saved-output paths (redacted), last check commands (redacted), held actions (redacted), stuck state, and the active task. Goes to the session model with the rest of the context; nothing new leaves for Jev. `warden_recall` returns the failed-call, verification, and saved-output parts of the same text to the session model, on request. diff --git a/docs/extension-authors.md b/docs/extension-authors.md index 9f607645..fe730cad 100644 --- a/docs/extension-authors.md +++ b/docs/extension-authors.md @@ -11,7 +11,7 @@ const verdict = await evaluateAction( { tool: "bash", input: { command: "git push --force" }, cwd: process.cwd(), task: "push my branch" }, { config: defaultConfig().action, judge }, // omit judge for pattern checks only ); -verdict.level; // "allow" | "warn" | "confirm" (confirm = hold in steer mode) +verdict.level; // "allow" | "warn" | "confirm" | "deny" (confirm = hold in steer mode) verdict.reasons; // ["destructive: git force push", "irreversible 0.91"] const set = new RuleStore().load(process.cwd(), defaultConfig().rules); diff --git a/docs/guards.md b/docs/guards.md index 64c418ae..233ebf9b 100644 --- a/docs/guards.md +++ b/docs/guards.md @@ -33,9 +33,11 @@ Runs on `tool_call`, before the tool executes. Text that is data is not a command. A heredoc body written to a file, a quoted `echo`/`printf` argument, a `grep` pattern, or a `git commit -m` message can mention `git push --force` without a hold. The same text fed to `sh`, `bash -c`, `eval`, `xargs`, or a `python3 - <= 0.85 | 42 (+6 uncertain) | 0 | 0.00 | [0.00, 0.08] | 0.00 | 48 | 1.75 | fails precision | +| off-task: unrelated + mutating, P >= 0.80 | 66 (+7) | 0 | 0.00 | [0.00, 0.06] | 0.00 | 170 | 6.21 | fails precision and rate | +| off-task: unrelated + mutating, P >= 0.60 | 141 (+13) | 0 | 0.00 | [0.00, 0.03] | 0.00 | 998 | 36.44 | fails precision and rate | +| off-task: unrelated + mutating, any P | 192 (+17) | 0 | 0.00 | [0.00, 0.02] | 0.00 | 1,526 | 55.72 | fails precision and rate | +| off-task: any scope, P >= 0.85 | 45 (+7) | 0 | 0.00 | [0.00, 0.08] | 0.00 | 75 | 2.74 | fails precision | +| should-ask: P <= 0.05 | 16 | 1 | 0.06 | [0.01, 0.28] | 0.50 | 35 | 1.28 | fails precision and n | +| should-ask: P <= 0.10 | 76 (+1) | 2 | 0.03 | [0.01, 0.09] | 1.00 | 190 | 6.94 | fails precision and rate | +| should-ask: P <= 0.20 | 138 (+1) | 2 | 0.01 | [0.00, 0.05] | 1.00 | 854 | 31.18 | fails precision and rate | +| should-ask: P <= 0.40 | 225 (+1) | 2 | 0.01 | [0.00, 0.03] | 1.00 | 3,497 | 127.69 | fails precision and rate | + +No slice passes. The blind labels found no off-task call in 427 (the 95% upper bound on the off-task rate is below 1%) and only two calls a careful engineer would ask about: merging two pull requests that no instruction named, and reading a third-party API key from a live deployment for a side experiment. On this corpus the categorical `scope` answer and both score bands mostly name calls the visible instructions cover, so the off-task and should-proceed notices stay trace-only: `offTaskTraceOnly` in `src/guard.ts`, and `action.shouldProceed.steer` stays `false`. The thresholds (`action.offTask.warn`, `action.offTask.steer`, `action.shouldProceed.threshold`) decide only the reason text and the trace counts; no setting makes one of these notes reach the agent from a score alone. One labeller produced the labels — a language model, not a person — and a second, human pass would make the numbers stronger. + +One reworded question is proposed for should-proceed, recorded as `extra` only (never acted on): `should_ask` in `scripts/action-candidates.mjs` asks whether any visible instruction covers the call, where `should_proceed` names every commit, push, merge or deploy as a decision the user should make. That difference matters here: the shipped wording puts 32% of judged calls at or below 0.6 while 2 of 427 labelled calls needed asking. `scripts/steer-ask-probe.mjs` measures the rewording on these same labels, one request per call, next to the shipped question. The measurement cannot run at the moment of writing: the TypeSafe account answers HTTP 402 (no balance). Fund the account or export a funded `TYPESAFE_API_KEY`, then run the probe with `--yes` and score it with `--score`; until numbers exist the question stays extra-only. ### should_proceed calibration (2026-09-20) @@ -560,23 +660,6 @@ Both sit under 1 ms at p90; the target was under 20 ms. A message that arrives o The rules request also skips the prompts the conscience's local gate skips. `scripts/rules-turn-replay.mjs` applies the two predicates offline to this machine's recorded sessions (1,327 sessions, 7,633 prompts): 105 short continuations (1.4%) and 785 relayed child reports (10.3%) send no request, so the rules request falls from 100 to 88.3 per 100 recorded prompts. -### Working-memory feasibility (2026-09-30, gate failed) - -The 1.0 direction was to prune at a turn start: when the prompt cache is already cold, replace old tool results the task no longer needs with one-line stubs that keep a recall path. The measurement ran before any of that code was written: 200 tool results sampled from this machine's own sessions since 2026-09-16, each still in context and at least 10 model calls old at the start of a user turn, one Jev request per result (200 requests, `jev-1.13.0`, 198,066 input tokens, $0.0083, p50 248 ms, p90 312 ms). - -The label asks what the agent did next, over the ten model calls that follow the user's new message: it needed the result if it repeated two tokens that appear in at most two tool results of the session and not in the call that produced this one (the ceiling study's strict proxy), or if it called the same tool with the same arguments again. 34.5% of the sampled results were needed; the same window measured from the result's own creation says 55.0%, the loose token proxy says 73.5%, and a repeated call with the same arguments never happened (0.0%). The question scores 0.39 on average where the result was needed and 0.28 where it was not; AUC 0.71 against the strict label, 0.75 against the loose one. - -| threshold | candidates dropped | misses of those (strict label) | net character-calls saved | -| --- | --- | --- | --- | -| 0.2 | 23.5% | 12.8% | 6.4 M | -| 0.3 | 53.0% | 21.7% | 23.0 M | -| 0.4 | 68.0% | 23.5% | 33.9 M | -| 0.5 | 86.5% | 31.8% | 45.9 M | - -Net is the characters the dropped results would have been re-read for over the rest of their session, minus one re-read of each miss. **The gate was a threshold that drops at least 30% of candidates with at most 10% misses; none passes.** The 0.2 cut is the closest at 23.5% dropped and 12.8% missed. The failure has a shape: results of 12,000 characters and over were needed in 10 of 12 cases, and every one of them that a 0.3 cut dropped was a miss, because a large result holds so many rare tokens that two of them reappear in almost any later call. Big results carry most of the at-stake characters, so the judgment is weakest exactly where the saving is largest. Results under 2,000 characters judge better (0.3: 66.0% dropped, 7.8% missed) but hold 2.1 M of the 62.7 M character-calls in the sample. 20,779 call-reads were at stake over the sample, a median of 64 later calls per result. - -Because the gate failed, the pruning is not built and no config key ships for it: every documented key is a promise kept through 1.x, so a switch with no feature behind it is not documented at all. The same feasibility numbers say the turn-start rules reminder is worth its request where the pruning was not: at its 0.3 cut it names no rule on 20 of 100 requests, against a miss that costs the agent a re-read of a whole tool result. - ## Slop **In code.** When the agent calls `write` or `edit`, four yes/no questions ride on the action guard's request (no extra latency), one per symptom: `slop_stub` (placeholder or fake-data code where a working implementation is needed), `slop_comments` (comments that restate the code), `slop_dead` (commented-out code, unused imports, duplicated logic, unreachable branches), `slop_hedging` ("should work", "for now", TODOs without a plan). Jev sees a 1500-character head/middle/tail sample of a `write` or the first three replacement texts of an `edit`. Any symptom at or above `slop.threshold` (0.7) sends the agent a steer naming the symptom and its fix. The write is never held. The third repeat of a symptom becomes a standing rule. diff --git a/docs/hero.png b/docs/hero.png index 35413c0212c037c753eb7cc17017bb2b43cc9240..0b4cbee38200bc257fb1f1908190de6f364b0603 100644 GIT binary patch literal 192708 zcmeFYWm{a^5-m(Z2%g{?BzWWQ?(VL^-Mz6uaCdiT+}&M*yL*BYAh^T3_PJ-DfABuf z{czVOW;ea&S~Y9dtQuofn4-J{G6Eg~6ciM)l%%LK6x7F1C@APkxc9&(e7g?%P*5mP zQldht9+`hOV06)6h+zK~w5tfBiZV*pwgcBm?N#mXYW{uWVPki9_lVGv-2C|Wvl|a@ zb|KDv?CefI4^H(x+S@nmnzzn;yU$;Q^@X$|-T^oN&-Fc?eC*%9p#;N3zJ2<4l#4;k z2>b6q5dkF?`riQ+4BYttZjku?!~VZtKuN!Q@BF_31j?W5-@$j~?>%4s9SDL1|KBwK zzZvZk2@2|Y2F%W`&9$z*osVqp^VW|0^Z(dh(DEP4Z+|wot}-;sC2@!=NTT@fvO+${ zY5i|erQcZrD=f$~yQIq5JfNjMZJSGZUV!!GzE6;l)8aq(+WYX&QY%Kv-XvuOvZktO zBuhj}5r1L*=gaR-`Pb0SbmI2u%tby`n=-BXPPk{5yY|g9BxmNZ^tW3GhBc3-{A>%95JG1#X(J^61}MVM9So zyD};4umD9#Gg0K$79$|aWOTT+zTBs&wya!z&V&So(I`|YtBL{hVe!b_=ShwXctbxF zJvlK`F|MM}(4S3vYMuGoq&fut5%m0sQK2&CP~p)Pk&(e=!kZ68;89tnX{D6&g#pcN zl9Z|1bD!V9s+k`8&BlsVophl5P zagr7mXwFWXISpm3Q?2KlX2#t~H}G)M;ktudpX6E;B$@xW`-lk&Nf;%HPOmX)U0za; zWqsA7Fx2$yhUl-q{Sz|&b192zi<|m9(Y-cm$P(b&`pa0WEz~tnqs*)cXJ~_ykb>M) zoKDp=1vL?%p%m%$+>z@mi0_d+cP!!l^#)Px_s+Z3r$jk#G%lIdt_8NM3@(^WE~lwh zkq#{G1#MrT5)G>K9s4^sS^m$I#HNK15Y5G31&_0@F4qgeO>K)s0oLBEO$)WmO6a$J zh*QD=q$+?Z0b^!1G|(0u78YUeE&ekeB>|N&NfezwUR&vq8pTEuo1Zt#= zjgDrw-?0FXMGVho+IpaaM>YD^=F@m()zgjrjPspDZC^RPU7_RVZ^(OWzP0sfK^>*- z{N6Wb_lxim9svrbAOAQt)m9-5$L#-&MmfEkK`DnX6CBFDjt7 zuPDI4OTfc4V1@~{@sS&>`12=V7|i;ajt zCg8!h3Lvv`(3m#Po4Q;EJ%_9k&CZXa3aau-3L@N`>};iLb9qCgM$CM4=J4MHD={jv z*a2uN%%DmO_%SUAqJUEHAOw?B;G-CI26OYu3et;%^md#2x`u+UO&1>-caotA4zOLA z#oFFGn>9m0hC$`zn?++@7Xk2<0D=PkYzDX(SMW1QA+ndZcTLx*Xu~};C z%O^KyB{qz2m(0sd2`lizy`m+Oy|O>CvlPkgWf7qzfq%OO_N1^jFEy6r#=HB1!M8I{ zLJ#}wADb$23yE`soW6OC5;RM0&Lu~;)jw%2GA`xXBvjZ;vj0w)^%I3;`JvTal?fyj z;2eFk&tu9ph2cR&hNdIulY@~{^E4gY`eZ3?rtab$9AfZXuE11G2;=o8X+~zAU(A~4 z40%ThvN&O8Xb$aFdv1x>t)znpoxy7K)7T^ zZ%bXOE>fFO92C{ik`T#$GE@R={wW?PU_aE7un3it*IUXlYb#T~S_A zTqH@jIGvh^`zyvH^DC}knB0B*1jLca5U z>MbTgC97JJv-0~>KTX-yxOSnFse7{Gyf|$;a_{NG@x^8>816OI(%iB#vSB}`g6n4x z@y?&$u`Druk}k$Y);=P9bU__g{l>FZT?ppI_>1%U?1fgy&aTJ{nQR5W=L8;xyC3k{ z9|8$ zl}076-f*O*i%m_CRcH-_R`rue5K6Z=E{Mc?gZz0{csSY>8K#QG{JdCCqnm|9 zDcfTK?qtVyVnE6=Frd)+)(0VCq#NbQTL#)tO(J8$`{Gt-y;tiKUj|OZa$ko}j=L^A zO4Iy^^zWJAkFs(3xq_XbDcmDLgNY8`WYsD~)!5tvxwmPuaQCUrE6^6+I}9#@_1Q$U znA$2d(y*T`4W}tDAbnDK%gb6h zVN9hrEQn;PAcwV1i*Msby6HC`=(QnjXsav@$?7eS9r+60nc6W33^B%ASQWP$TOp(N zS6>yNeBDUqxoWYAIX6)oSopZc;NW=%(ki1t zdJG%T#fXLOI44~38Nn|Sw9>KOd@J_de6W`HsOCSK@RGh`cw%KcTdEMY#ix7a@q5UR zT||#5@qFsweL)}aQ@haPJ5ir&Yj4W#5j)$gl{4?iM?8T*COZ!O^t--Fo$j|{w3HRq zFfe`P>Mwlr5x0r>sHH@a08(C~f1q1_3wj5*UJwTIYY&TYN{NuFMpVs?Ts6KesSVqx zKJEJXAmDMYPej&C`B^QsXx1!j;fh~!&WS0bU>S&=)Tg-abwJ;uXTqX+UL|YQDSpX{LxDT1^P}zy~H%( ztaRn4f}#|uPRIQB6WY>Pf`}<8osHE9W|X*uYb_CQ69-lGJgz5v1vY+V&u_8^bkqDlc<{3>+RdI{1N?` zC2-uHou#*FXe?r}P)i^+V7_HozS+4OuM&OCWi>H%whP_QvNz4|!$_?8w&%6Ab({^G zZP98e9QDZNvP<5Pud-j5*11_fflT36NwzN&{&Wm_zj4sgUX&eL@}-th-KV=)id1KW zIvz+U5_v6YgPuaj4cy_;n;JCGjoZE|pf47cAqS;!f7(LEC?2k2BAAGtbRS<}RsNE7 zg0HkGCRR%u#8jf$;*g7}f<=YV7R5H0S4Cf)b#Aq!v4}b&VMs3%thuHW+U`VO|ET#V z%Z|ZvGHJ&w@r`5F?7)C6EkK)TUVHq~vltwl(57ZKnzle%ag>q7Fd19ul&659PGM#5 z#)`H~-?bZMWzTLeh{>c;WV|LNNj^6#wsS!u;I9HFwVaQ}Yq27+zpBEO7Kf5$rH<6R z$(zTm0=J~TfU5qR2fJ<9%sAOEH|p2SSv%fRWz~davzjbvGkyC3l}Uj?r{G5KBBG3)Rn!4Twk?J)v$AC{Tz_&Rv%7n~nUl zHj9ZHQP|j(skSv`=g0;Pi>ZHbS>MG&w5(;hWmT7DfJw331++4`(Q@^EDW!l^xbr zmKAk=fs1&{MlU~e%(Jjic~BMpm1c(ct2*AHPKQyy$X>efQWm8reGpBXo2^G!w=955 zv_~bFr{5?qZcIzRe<#HKInu^{2*@!dnLtt0Fu}#TVEP94)V#_Gs`lwwB@3Og7Y^v( z@t39a8lCy8Z{m9@)IU>z4YfD|Q$4C0PX$Y=^LmD(jHIy~Qk1_MI2mL`LR~b}#LWW6 z7HY6|pqZPzK}t0$f)su5z_0ih3z+Wqgeh8I=IM_H@n~}%ucXVM+*5sOFIr(X%?Zn% zJ^Yjj%mdcEC$OE#i}v;BfIukxyu12e{Ek)T^_7I&UY4yqmq4{Y*xP=jg%WBHverN> zmmbNLMrteT{SQB`VyAFn=W_heJzRCSHMI=}Wf5-6aQ8T#~B*F z7uC27rcX|83=DyfhR2`eW zZ|-vrBVa`|Hbw1Elredu_41jBKg_ayh025MdA`t{(uVK1XyKI8<6BK62FJ@<65Hy- z;r{9ClI}%wYl?3BZm{P?H1d%{L23*MN#Rqtq?Gi?!ACcs1~WW(2Wka}PYLSO%F%^g z9lHqW`}|sKitueEjQC_U4+9h_MnPwz<;x!=iu66OR%Ik(Zb%-FSAFP+zj7s@T6*ZP z%yibMB>y^XUWPqWS~Rl)62wq_&#QssCgVbvZ$dLv&RB#Q@@e&>LuKOJ;g_?k^eIlR zDf)pe;6a~f!gI`6oT2Nf~T%ux}6@(}GUoReIB7rJ7D@goA`>~Z-BW z2c3WyTzPLdcyhgGs7rqlHN@M#JlzML!uy z<^&;()9W)`lje`H2Jsi))s+~LG|aWZ>rvAQP%m=m1WI#4UZjB38u$><}C#dT#s^Cw8 zhN2O8sJc8I08a9I)x22IR_}dOL&B{U;D0(LE-tVzaAU4}5bWc}uh#_2BT6Vew6(FX zy04Sq!eep{51zwt2&TDr2}|tK{7W4!C^ltZBWjp*e%X!k1BIg+pD-Q=GgBajIe6=Je#T1Mf@mSyFZ;pS@c9 z;d9vGH^N@lY2{p59L$hHGGPg>RT>5nj?4>JY{IQDxp7UWdzwK`dU|x;d666G_vx=u zXR!+Qr2TK6?ypDH0lfmoWl<-zXT+wwE*KJx)l@c?!cO9M5c~TCIY}3?J-lYU+wQjf zrl8twwEaZ8eQSATgp`)5t!dYDD(gvkhUN1c3lSgc2?X7QE*}_A+}94CJSe*{uIkIG ziURs`zY0jtDH+6!J>wxh&ZzGbvnA6>Qn89GMHU2qcOWdYutjk|djmC7>dznnk1)`Z>s-OJREa~|(I^q|e1$1v`qdBgvEDdNJ5IRm= zCP#^OrSe$>gP!wMS8_nub1j|3SCKm92EA=>gxJ&e*DNOtmz)gLUpfw#HLmB-A(RTJ zPH(w+vA!NMug4&u!V&&$$>333$}blMq;(87rl?Q4>t|h2g@D;abTaHRNLmCkHFAvG z={)!OiA0MnG}Gp{qpE=*LZ&6d5o$2Hz@z(Vn5N$>9F@_<67`Os`DRT#f6>EAr(I7 z^mU7vCtY6iJYOrh4$r`w@^!F{%%*`Ef2(2rRHMf?pB@f7iN%Hc*V4mq{x%;tarpP@ zhPS#ruBmAzh$E{76}h2t3~Tyc(=bp=9F9TW=Y`&kNg;y5!{9O%V7KN3a#R7x$tVV5 zw;;~1?cB#B=}{Z9cL~~0lTb}KVo0C#aNQv1-D=u7-_ra2FI0!q*n3MdV@H8bT zSjHu29x02uL&cMOA!h56hI-h3cn_+r2j1)JhMSSJx>ONstURHwr--Tx$6Yf`MOTsV zZY#SdQLA{QBgQPrt4}uXEbLZ`sYF-mr2p1dx{(iTZ<{ed%UKxg>sttjnuiW<8#NS zP59%Q>6@ejT4 z1CHi9@8)yPme>3aVVx2l*oN@2$3+!L9Fi`(`qMS+M##=xdjUC|OZOCg z_*ku8!^*Ov%d!iY;O$ShDJ@PxIN;I9|x5w&p2`P zU2vF-9!+7L^rT@tcBt?Du=xez16?<1^QDh+c6A(_(7*PNBHQcK)>fWv!FD;&v|xt( zImTQ#Ew`A~$c0c;mr_?>dhJP*66A@&L06I-(p5oaRs6V*QpF(Sl;K&zNEnMzO_|BD@yYy))fviP& zfmbF?Nn|4(#IO*kP2UxUbJ?~*;T-`(LkeR8i0+IODzXjV%kQAZ-)4j4rryZXXv;s| z1tDdYTXjBAYbiVgX)pr?+1!{yz>neu>ar{0jt7}l$W~U zyVvYV$l88T!_=+K&l1f{R-|=S)%4SUI^Ci%2rb!^gbXKq4s6PPJxHv+_vbPu5Hs{q zH8(YFcLG~uB^7SLPwA?&#Z_>x>3oHmslmx(+fNK^?s-X2B+fPr^vzSH;%=P=edYSy z<+Zy)`IW;U1bcsze{56O*u<-T@&-9bO#~$*8GMx49ONo3NiTrCYMtG#;4P_IHm|Ps z7g_4)`VOCxCMrwC);d*Yos?(ebQwJ3w|8N`u2TM;RFN6BvMDOLMusfQwn_II{bIF| z(kBpRUo~k@%*BE#HY2uP&PckbrYAGq>8xH(W(PV;;FY+60yG7rm+zsS3k~J6;y;hq zhVwxDUC`81w(`P1GB9d%w{1&LUnk@}g(f;J7gK=W+ogtCv`W2esCvb8<7fPjp8 z8oZAzsK^d19keesrgRUn+t=l_y1_Ee~K95hZtV5m|o?m#AQd^sI(466*q$>c2DgVsiBx1>K$s!8o$3!JSCuIyPasLXG&;6P}WZ`gE((4Dx*JkVdq3xfggZ4 zD=3359^UzaW#D%l(KHEulP?3+TxVdzbjb4~W!Ep!&`YUO8jw3rTdAXgoAQjx4()y( zHcysE7a3D@J`XV+R!EM<%i1NRns9eOO#^lL6 zs-gZeqbDzUIHV+FOi5O6DpSDXQ+Nvmiq1iUEmeb7Kg0yslanVR+gi7Vs5Ji;WZ#3^ zdhfbYp*Yb!96O;b5V9vU74KHR*r45eQnJuLiTS|(Dv$E!^(?2LlqsB-}<)xw7wuVo9Xi7KEOJ3f3e`Nk1+)vDG*c(Ks%_8 z4>>FUFmJJSsHqVOlNdAD{94YVlp-BMqR4^G#OUTO_1_iq{JHdw&N>6aWyLlVb5qyf ziuJ$p{2CEExx*7D(SGk6^RqZb5OqSp8MG)^DT=QkD-(Xx0%4mg~SLNa}pbr<#>%LOW$AC?S32LnGx zA*iM0gQNBMku{bvSW%rC+ovb#`}WWI5gk56b=WAD=k?umi36^mh;Qsx>E%;r)+5iT zq7Be!GvLr2Rpf+YB}YwZ1m$*(Ac+4w_(n@`f|)ccb6HCzu7E4;sJsSq_ml<^ zZX9NpOXf?esX^)|PjY2|6a0sbhm3#Nk5>*#Hl)&t1-Yp)qVtg(l9Qv_MtdX%Rwty9 zFT_8?zoFS@YC)NcAIHp z|EW={&j+x$qe;w+_aZalSeyE40~Qy&0F7-HVLW)|Q=uy9zRU=_H+y-CNlwRi0&pzY zIYxa($|41`!UZY`%3MstaUbs z7*IDKHfV}u&C|ogl4G1#hl@Z>-=@LCYU0WJHCG-sGvvtTf-uuptM5wCoFzxk_c^Wh zM8(7A><4gE(dpm2R_5dQ0LzD{N7=#3?IbNVrmFTj$4ohxH7jWK5J-l|`sXLaF#IR!K!}ec%g-m(e`aAczyWw*4*~g?}HkH-k8Lt|Lx?9uwUYfk$43Z2od+ z2+*_nmAEjTbdhQU6$`@Uwppt6OHR$YS6&=USTB7Ep99SQye7ud=Bg{>L(nSkiK8RO zZN$F97Uuhhm*6D$v#MWNisns$*ytjIlF88HS?2xTIw>J0aELgr))wo4Vqz1BDnyU* z<4YOlBVQU6cYF+#!Dg4FMuD)X2erQvQsW`iWU7)(?bne3ORujOu|0_`@luw=6M*F4 zGPv^(9h>r;^MRS09{e(IE%zJ2s5qNT^XOAK;;7dy-b0=%ostIfz&7Eu@-ZNYYb zX(6DpDZSrYCf>=SpLx+IaP>6w5omOo$LxMl(rlQ%M#)Gu%TTc`{39p3!(B+uNYvdVx=ws+{4_7#z3