pi-warden 0.40.2 → 0.48.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.
- package/CHANGELOG.md +96 -0
- package/README.md +23 -3
- package/dist/action-guard.d.ts +11 -2
- package/dist/action-guard.js +1 -1
- package/dist/backend.d.ts +4 -0
- package/dist/backend.js +4 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +15 -1
- package/dist/conscience.d.ts +9 -3
- package/dist/conscience.js +13 -4
- package/dist/extension.d.ts +17 -4
- package/dist/extension.js +311 -80
- package/dist/extension.js.bak +263 -76
- package/dist/guard.d.ts +115 -1
- package/dist/guard.js +404 -11
- package/dist/holds.d.ts +2 -0
- package/dist/holds.js +5 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/judge-cooldown.d.ts +44 -0
- package/dist/judge-cooldown.js +92 -0
- package/dist/panel.d.ts +5 -2
- package/dist/panel.js +5 -2
- package/dist/redact.d.ts +5 -3
- package/dist/redact.js +17 -4
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +23 -0
- package/dist/shape.d.ts +46 -0
- package/dist/shape.js +76 -1
- package/dist/trace-file.d.ts +50 -0
- package/dist/trace-file.js +141 -0
- package/dist/trace.d.ts +12 -1
- package/dist/trace.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,102 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
8
8
|
|
|
9
9
|
<!-- Empty. Next release starts here. -->
|
|
10
10
|
|
|
11
|
+
## 0.48.1
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- The intent-mismatch question is asked only when the agent stated a plan for the call: the text of the assistant message that makes the call, or of the text-only message right before it with no tool call in between. Before this, a call with no text of its own was judged against the latest assistant text since the user's prompt, often written before several earlier calls. A shell command with a visible effect (a `git` commit, push, merge, tag, or reset, `gh pr`, `gh release`, or `npm publish` in any segment, decided in code before the request) is still judged against the latest assistant text since the prompt, because an unannounced commit or push after an older plan is the mismatch users object to. On the recorded sessions from 2026-09-21 to 2026-09-24, 151 of 163 intent-mismatch notices were judged against stale text; the question would now be asked for 63 of the 163 (8 against the call's own text, 55 visible actions against stale text). A skipped question is one fewer question in the action request.
|
|
16
|
+
|
|
17
|
+
### Tooling
|
|
18
|
+
|
|
19
|
+
- `tests/steer-delivery.test.ts` pins that a steer sent from `tool_call` or `tool_result` for the last call of a turn joins the request that carries the tool result and costs no extra model request, so the intent-mismatch notice stays a steer.
|
|
20
|
+
- `scripts/intent-cases.mjs` reads Pi session logs and, for every intent-mismatch notice, classifies where the plan came from: the message that made the call, the text-only message right before it, or text from before earlier tool calls (stale). It also runs the built `assistantPlan` on each call's branch and counts the notices whose question would still be asked. Aggregate counts only; offline.
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
|
|
24
|
+
- README leads with a new image of the self-correction loop and its numbers from the first nine days of use; the headline numbers are updated to match.
|
|
25
|
+
|
|
26
|
+
## 0.48.0
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `PI_WARDEN_HOST_PATHS`: a `:`-separated list of absolute directories outside the project where the host lets its agent write. A `write` or `edit` in one of them, after `..` and symlinks are resolved, is not held or warned by the outside-project rule; command rules, path rules, deny rules, sensitive paths, secrets, and Jev still apply. Relative entries, empty entries, and `/` are ignored. Environment only; no config file can set it.
|
|
31
|
+
|
|
32
|
+
## 0.47.1
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- A done-check nudge no longer clears the evidence that caused it. A run that a warden follow-up starts (done-check nudge, runaway recovery, subagent wake) keeps the changes and checks of the run before it, so the agent must still show a passing check. A later "done" with no check in that run is judged and recorded as unverified; the one-nudge-per-prompt bound stays. A user prompt still starts with empty evidence.
|
|
37
|
+
|
|
38
|
+
### Docs
|
|
39
|
+
|
|
40
|
+
- Field usage report for 2026-09-21 to 2026-09-24 (`eval/reports/2026-09-24-field-usage/`) and `scripts/field-usage.mjs`, which reads Pi session logs and the holds database and prints aggregate counts only. README leads with the field numbers, names the task spine under Privacy, and notes the scratch-delete exemption and the judgments-off notice.
|
|
41
|
+
|
|
42
|
+
## 0.47.0
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- A recursive `rm` whose every target is scratch the agent created in this session under the OS temp directory (`mkdir`, `mktemp`, a `write`, or a temp directory a command printed and created) warns as `rm-session-scratch` instead of holding as destructive. Targets are resolved through `..` and symlinks first; a temp root, a wildcard, a variable, a substitution, `sudo` or another privilege prefix, content older than the directory it sits in, or any target that was not created this session (or was replaced since) keeps the hold. The created paths live in memory and reset on `session_start`.
|
|
47
|
+
|
|
48
|
+
## 0.46.0
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- When a guard would ask Jev but cannot, Warden says so once per session and reason, with the fix: `warden: Jev judgments are off (no consent). Run /warden enable.` (`Set PI_WARDEN_ENABLED=1.` headless), `(no key for <backend>). Set <key variable>.` (with ` or run /typesafe login` on the TypeSafe backend, the only one login stores a key for), or for a rejected key `(the key saved by /typesafe login was rejected). Run /typesafe login.` or `(the key in <key variable> was rejected). Check the key, then run /warden status.` A headless session gets a status message. A spent request budget keeps its own warning. Before this, judgments stopped silently.
|
|
53
|
+
- Trace file: the `session` line has `judgments` (`on`, or `off:<reason>` with reason `no_consent`, `no_key`, `key_rejected`, or `budget`), and a `judgments` line records a later change.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- The conscience traces why no judge was available (`no_consent`, `no_key`, `key_rejected`, or `budget`). Before this, it traced `no_consent` whenever the judge was missing, also when consent was given and the saved key was rejected.
|
|
58
|
+
|
|
59
|
+
## 0.45.0
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- A judge cooldown. After three consecutive timeout, network, or other failures, or one auth or configuration failure, pi-warden stops asking Jev for a minute instead of paying a full request timeout on every guarded action. One warning names the failure kind, the duration, and for an auth failure the fix; one info notice says when judgments resume. Guards run as they do with no judge configured, nothing is sent during the window, and `/warden status` counts the checks that ran without Jev. Configure with `judge.failuresBeforeCooldown` and `judge.cooldownMs` (capped at 10 minutes); state is per session. A malformed request that pi-typesafe rejects locally does not count. A request that hits pi-warden's own `timeoutMs` reaches the SDK as an abort, so it is told apart from a user's cancel by the signal's reason and counted as a timeout.
|
|
64
|
+
|
|
65
|
+
## 0.44.1
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- The credential and untrusted-output notice rides the tool result: the banner in the result content and the trace record stay, and the separate steer message is gone, so a notice on the last tool result of a turn no longer costs an extra model turn.
|
|
70
|
+
- Credential detection no longer fires on placeholder values (`<redacted>`, `<...>`, `***`, `REDACTED`, `xxxx`), plain numbers with `_` or `,` separators, or a key name whose value is only an identifier or expression in code (`findSecrets(text)`, `output.secretIds`).
|
|
71
|
+
|
|
72
|
+
## 0.44.0
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- `PI_WARDEN_TRACE_DIR`: with an absolute path, Warden appends its trace to `<path>/<session id>.jsonl`, one owner-only file per Pi session, so a host that runs Pi in RPC mode can read what the status line and the sidebar would show. Each line is a JSON object with `"v": 1` and a `kind`: `session` (session id, `~`-shortened working directory, Warden version, mode, time), `entry` (a numeric `id` unique in the file, time, and the trace entry as stored), or `amend` (the entry `id`, the added line, time). The 100-entry limit of the sidebar does not apply to the file. A write error is reported once and stops the file for the session; guards and tool calls do not change. Unset, empty, or relative: no file. `docs/configuration.md` lists the record format and `docs/data-handling.md` lists the file.
|
|
77
|
+
- `/warden trace` in RPC mode sends the last 20 trace entries as one text notification, newest last, with the trace file path when the file is on. RPC mode settles the overlay request without building the sidebar, so before this the command did nothing there. The terminal sidebar does not change.
|
|
78
|
+
|
|
79
|
+
## 0.43.0
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- The off-task judgment and the conscience recommendation both judge with a task spine in the request state: the thread's first user turn (the goal), the latest user turn, and up to four earlier user turns (newest first). The whole spine is capped at 1200 characters — `task_history` is clipped first, then `goal`; the latest turn is never clipped. A follow-up like "now the tests" is no longer judged without the goal it belongs to. Approval still comes from the latest user turn only; the spine is context, never authorization, and no threshold moved (the conscience beta policy's pinned questionHash still matches). The action approval question now names `spine` beside `context` as text that cannot grant approval. A thread with one user turn sends no spine, so the budget is not spent on a copy of the task. A queued prompt admitted at `message_start` is also assessed with the spine. Both request paths cap a spine they receive at four history entries and the same per-field lengths (goal 1200, each entry 750 characters), whoever built it. After a compaction the goal is still the thread's first user turn: the branch keeps the summarized entries, and a compaction summary never becomes the goal. `docs/data-handling.md` lists the spine for the action guard and the conscience.
|
|
84
|
+
|
|
85
|
+
## 0.42.1
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- A write the action guard holds, denies, or the user declines no longer gets a rules or slop steer. The steer said "the content just written" about content that was never written. A confirm-dialog write gets the steer after the user allows it. An approved retry of a held write is judged again and gets its own steer. The trace still records the rule findings of the held write, marked as not told.
|
|
90
|
+
|
|
91
|
+
## 0.42.0
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- A judged `bash` call now also asks whether the command will print far more output than the agent needs. At or above `context.largeOutput.threshold` (default `0.85`) the agent is told once per command family per session to redirect or filter it, for example to a file with `tail -40`. The call is never held. `context.largeOutput.enabled: false` removes the question. `scripts/context-cases.mjs` has eight labelled commands to calibrate it.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- The rules guard no longer judges writes to files outside the project root or paths ignored by the project's `.gitignore`. Those files are not project code and the rules in `pi-warden.md` do not apply to them.
|
|
100
|
+
|
|
101
|
+
## 0.41.0
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- The A/B eval (`npm run eval:ab`) now scores two more axes per run, shown side by side for the two cells in an "Outcome and waste" section of the report. Outcome: whether every check the task declares passes when the runner re-runs it, the diff violation count, and whether the final reply claimed tests/build success without the agent ever running that check. Waste, read from the saved session log (`eval/waste.mjs`): tool-call count, retries (same tool, same or near-same input, after a failure), reverts (a `git checkout`/`git restore` naming a path, or a `write` restoring a file to earlier content), total tokens, and wall seconds.
|
|
106
|
+
|
|
11
107
|
## 0.40.2
|
|
12
108
|
|
|
13
109
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,11 @@ pi-warden supervises Pi while it works, catching risky actions, ignored rules, s
|
|
|
6
6
|
|
|
7
7
|
Instead of interrupting you for every problem, Warden usually feeds the issue back to the agent so it can correct itself and keep going.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**In nine days of real use: 743 sessions, 193 risky actions held before they ran, 1,292 notes sent to the agent. After a "done" with no test behind it, the agent ran one 78% of the time.**
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
<sub>Numbers from the first nine days of real use (2026-09-16 to 2026-09-24, 743 sessions). The quoted lines are verbatim. Counted with `scripts/field-usage.mjs`; the [field report](eval/reports/2026-09-24-field-usage/) has the method and what was noise.</sub>
|
|
10
14
|
|
|
11
15
|
## Install
|
|
12
16
|
|
|
@@ -39,8 +43,10 @@ Works without any key (offline guards: pattern list, runaway stop, sensitive-pat
|
|
|
39
43
|
|
|
40
44
|
## How intervention works
|
|
41
45
|
|
|
46
|
+

|
|
47
|
+
|
|
42
48
|
1. **Read-only? Skip.** `git status`, `ls`, `read` means no request and no trace entry.
|
|
43
|
-
2. **Known-dangerous pattern? Catch it locally.** Force push, `git reset --hard`, recursive `rm`, SQL `DROP` held instantly.
|
|
49
|
+
2. **Known-dangerous pattern? Catch it locally.** Force push, `git reset --hard`, recursive `rm`, SQL `DROP` held instantly. An agent deleting temp-directory scratch it created in the same session is not held (macOS and Windows).
|
|
44
50
|
3. **Needs judgment?** Warden evaluates the action in context.
|
|
45
51
|
4. **Steer or hold.** Most issues go back to the agent so it can correct itself. Irreversible actions can be stopped before they run.
|
|
46
52
|
|
|
@@ -52,6 +58,7 @@ In the default `steer` mode, Warden talks to the agent rather than interrupting
|
|
|
52
58
|
- **Custom policies**: command, path, and arming rules for your own workflow.
|
|
53
59
|
- **Context saver** trims oversized tool output while keeping the full result retrievable.
|
|
54
60
|
- **Multiple judgment backends**: TypeSafe by default, with OpenRouter support.
|
|
61
|
+
- **Honest about being off**: when judgments cannot run (no consent, no key, a rejected key, budget spent) Warden says so once, with the fix. A failing backend is paused after repeated errors instead of costing a timeout per action.
|
|
55
62
|
- **Full traceability**: inspect what Warden saw, decided, and told the agent.
|
|
56
63
|
- **Desktop alerts** for events that need you.
|
|
57
64
|
|
|
@@ -72,6 +79,19 @@ Jev judges every write and quotes violations back. Rules can be things no linter
|
|
|
72
79
|
|
|
73
80
|
## Does it actually help?
|
|
74
81
|
|
|
82
|
+
### In daily use
|
|
83
|
+
|
|
84
|
+
Four days of the maintainer's own work, 397 sessions ([report](eval/reports/2026-09-24-field-usage/), [raw numbers](eval/reports/2026-09-24-field-usage/usage.json), `scripts/field-usage.mjs` to run it on your own logs):
|
|
85
|
+
|
|
86
|
+
- **Done-check:** 47 nudges; in 35 the agent then ran a test or build.
|
|
87
|
+
- **Holds:** 143 of 19,695 actions. Of the 44 with a recorded outcome, the agent took a safer route 33 times.
|
|
88
|
+
- **Rules:** 195 corrections naming a project rule, including a process wait with no timeout and a home path in a public test fixture.
|
|
89
|
+
- **Context:** about 123,000 tokens of oversized output trimmed in the traced sessions, and no agent needed the full output back.
|
|
90
|
+
|
|
91
|
+
The report also lists what was noise that week (scratch deletes, credential notices on source code, two over-broad rules) and what changed because of it.
|
|
92
|
+
|
|
93
|
+
### In benchmarks
|
|
94
|
+
|
|
75
95
|
In 150 paired agent runs, the control setup violated the tested project rule **6 times**. With Warden: **0**.
|
|
76
96
|
|
|
77
97
|
An overnight stability run covered **13,952 guard cases across 109 cycles** with no score drift.
|
|
@@ -86,7 +106,7 @@ These are project-maintained benchmarks, not universal claims. Raw reports and r
|
|
|
86
106
|
|
|
87
107
|
## Privacy
|
|
88
108
|
|
|
89
|
-
Depending on the configured judgment backend, Warden sends to your provider: a redacted summary of each guarded call, the agent's plan, up to eight redacted prior messages, and redacted tool-output samples. Secrets are stripped before anything leaves the machine. [Full details.](docs/data-handling.md) Security work — pentesting, incident response, CTF — has its own configuration recipe for keeping samples local and for the friction that credentials and lab setups cause: [Recipe: security work](docs/configuration.md#recipe-security-work).
|
|
109
|
+
Depending on the configured judgment backend, Warden sends to your provider: a redacted summary of each guarded call, the agent's plan, up to eight redacted prior messages, the task spine (your first request in the thread and up to four earlier ones, capped at 1,200 characters), and redacted tool-output samples. Secrets are stripped before anything leaves the machine. [Full details.](docs/data-handling.md) Security work — pentesting, incident response, CTF — has its own configuration recipe for keeping samples local and for the friction that credentials and lab setups cause: [Recipe: security work](docs/configuration.md#recipe-security-work).
|
|
90
110
|
|
|
91
111
|
## Documentation
|
|
92
112
|
|
package/dist/action-guard.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ActionGuardConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
2
|
-
import type { EvaluateOptions, PreviousAction, TaskMessage, Verdict } from "./guard.js";
|
|
1
|
+
import type { ActionGuardConfig, LargeOutputConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
2
|
+
import type { EvaluateOptions, PreviousAction, ScratchRecords, TaskMessage, Verdict } from "./guard.js";
|
|
3
|
+
import type { TaskSpine } from "./shape.js";
|
|
3
4
|
import type { Judge } from "pi-typesafe";
|
|
4
5
|
/** One tool call as the agent proposed it. `id` is Pi's tool call id, stable across hooks and retries. */
|
|
5
6
|
export interface ToolCallRef {
|
|
@@ -12,6 +13,8 @@ export interface Conversation {
|
|
|
12
13
|
task: string | undefined;
|
|
13
14
|
/** Scope context only: approval still comes from `task`, never from this history. */
|
|
14
15
|
context?: readonly TaskMessage[] | undefined;
|
|
16
|
+
/** The task spine: the thread's goal and earlier user turns, so follow-ups are judged with the goal they belong to. Scope context only: it never authorizes; approval still comes from `task`. */
|
|
17
|
+
spine?: TaskSpine | undefined;
|
|
15
18
|
/** Tool calls in the same assistant message, this one included; they are judged together. */
|
|
16
19
|
siblings?: readonly ToolCallRef[] | undefined;
|
|
17
20
|
/** The agent's own words in that message (or its latest text under this prompt); shared by the siblings. Explains, never authorizes. */
|
|
@@ -25,10 +28,16 @@ export interface InspectOptions {
|
|
|
25
28
|
signal?: AbortSignal | undefined;
|
|
26
29
|
slop?: SlopGuardConfig | undefined;
|
|
27
30
|
security?: SecurityConfig | undefined;
|
|
31
|
+
/** Adds the large-output question to bash requests. */
|
|
32
|
+
largeOutput?: LargeOutputConfig | undefined;
|
|
28
33
|
/** The rules guard's switch, forwarded to `evaluateAction`: off keeps the rules content out of the request. */
|
|
29
34
|
rules?: EvaluateOptions["rules"];
|
|
30
35
|
/** Calls allowed in the previous turn; the regret question about them rides this call's request, never a sibling's. */
|
|
31
36
|
previousActions?: readonly PreviousAction[] | undefined;
|
|
37
|
+
/** Real paths the agent created under the temp directory in this session, forwarded to `evaluateAction`. */
|
|
38
|
+
scratch?: ScratchRecords | undefined;
|
|
39
|
+
/** Real paths of host directories, forwarded to `evaluateAction`. */
|
|
40
|
+
hostPaths?: readonly string[] | undefined;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* The Action guard for one session. `evaluateAction` judges a single call; this module owns what spans calls:
|
package/dist/action-guard.js
CHANGED
|
@@ -20,7 +20,7 @@ export class ActionGuard {
|
|
|
20
20
|
const { task } = conversation;
|
|
21
21
|
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
22
22
|
const retryAfterHold = this.holdPending && this.lastHoldPrompt !== task;
|
|
23
|
-
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, rules: options.rules, retryAfterHold, previousActions });
|
|
23
|
+
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan, spine: conversation.spine }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, largeOutput: options.largeOutput, rules: options.rules, retryAfterHold, previousActions, scratch: options.scratch, hostPaths: options.hostPaths });
|
|
24
24
|
// A retry after a hold stays sequential because an approval consumed by one sibling changes the question for the next.
|
|
25
25
|
if (options.judge && !retryAfterHold) {
|
|
26
26
|
for (const sibling of conversation.siblings ?? []) {
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { TypeSafeOptions } from "pi-typesafe";
|
|
2
2
|
/** The judgment backend that receives pi-warden's Jev requests. */
|
|
3
3
|
export type JudgmentBackend = "typesafe" | "openrouter";
|
|
4
|
+
/** Why no judge is available: consent not given, no key for the backend, a saved 401 or 403, or the request budget spent. */
|
|
5
|
+
export type JudgmentsOffReason = "no_consent" | "no_key" | "key_rejected" | "budget";
|
|
4
6
|
/** The host the consent disclosure names as the destination, without scheme: `api.typesafe.ai`, `openrouter.ai`. */
|
|
5
7
|
export declare function backendHost(backend: JudgmentBackend): string;
|
|
6
8
|
/** The environment variable that carries the backend's key, for messages that tell the user what to set. */
|
|
7
9
|
export declare function keyEnvFor(backend: JudgmentBackend): string;
|
|
10
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; pi-typesafe's `usesTypesafeKey`, which its package does not export. */
|
|
11
|
+
export declare function loginStoresKey(backend: JudgmentBackend): boolean;
|
|
8
12
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
9
13
|
export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
|
|
10
14
|
/**
|
package/dist/backend.js
CHANGED
|
@@ -7,6 +7,10 @@ export function backendHost(backend) {
|
|
|
7
7
|
export function keyEnvFor(backend) {
|
|
8
8
|
return DECISIONS_BACKENDS[backend].keyEnv ?? DECISIONS_BACKENDS[DEFAULT_BACKEND].keyEnv ?? "TYPESAFE_API_KEY";
|
|
9
9
|
}
|
|
10
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; pi-typesafe's `usesTypesafeKey`, which its package does not export. */
|
|
11
|
+
export function loginStoresKey(backend) {
|
|
12
|
+
return keyEnvFor(backend) === keyEnvFor(DEFAULT_BACKEND);
|
|
13
|
+
}
|
|
10
14
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
11
15
|
export function disclosureFor(backend, disclosure) {
|
|
12
16
|
return backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
package/dist/config.d.ts
CHANGED
|
@@ -193,6 +193,13 @@ export interface ContextConfig {
|
|
|
193
193
|
formatConfidence: number;
|
|
194
194
|
/** Append a compact evidence appendix to the summary during compaction. */
|
|
195
195
|
compactAppendix: boolean;
|
|
196
|
+
/** Prevention before the call: a bash action request asks whether the command will print far more than the agent needs. Never holds. */
|
|
197
|
+
largeOutput: LargeOutputConfig;
|
|
198
|
+
}
|
|
199
|
+
export interface LargeOutputConfig {
|
|
200
|
+
enabled: boolean;
|
|
201
|
+
/** P(the command prints far more than the agent needs) at or above which the agent is steered once per command family per session. */
|
|
202
|
+
threshold: number;
|
|
196
203
|
}
|
|
197
204
|
export interface RunawayConfig {
|
|
198
205
|
enabled: boolean;
|
|
@@ -205,6 +212,12 @@ export interface RunawayConfig {
|
|
|
205
212
|
/** After stopping, start one follow-up turn that names the repeat and asks for the one next step; once per user prompt. */
|
|
206
213
|
recover: boolean;
|
|
207
214
|
}
|
|
215
|
+
export interface JudgeConfig {
|
|
216
|
+
/** Consecutive timeout, network, or other failures before judgments pause; one auth or configuration failure is enough. */
|
|
217
|
+
failuresBeforeCooldown: number;
|
|
218
|
+
/** How long a failing judge is left alone; guards run pattern-only until the next request after it. */
|
|
219
|
+
cooldownMs: number;
|
|
220
|
+
}
|
|
208
221
|
export interface NotifyConfig {
|
|
209
222
|
/** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. Off by default; opt in per user or project. */
|
|
210
223
|
enabled: boolean;
|
|
@@ -304,6 +317,7 @@ export interface WardenConfig {
|
|
|
304
317
|
context: ContextConfig;
|
|
305
318
|
runaway: RunawayConfig;
|
|
306
319
|
notify: NotifyConfig;
|
|
320
|
+
judge: JudgeConfig;
|
|
307
321
|
/** Triage of async subagent reports: Jev separates what needs the agent awake from what is only context. */
|
|
308
322
|
subagent: SubagentConfig;
|
|
309
323
|
/** The status line above the editor and the trace panel. */
|
package/dist/config.js
CHANGED
|
@@ -44,9 +44,10 @@ export function defaultConfig() {
|
|
|
44
44
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
45
45
|
security: { enabled: true, threshold: 0.7 },
|
|
46
46
|
rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
47
|
-
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true },
|
|
47
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, largeOutput: { enabled: true, threshold: 0.85 } },
|
|
48
48
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
49
49
|
notify: { enabled: false, cooldownMs: 10000, command: [] },
|
|
50
|
+
judge: { cooldownMs: 60000, failuresBeforeCooldown: 3 },
|
|
50
51
|
subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
|
|
51
52
|
widget: defaultWidgetConfig(),
|
|
52
53
|
steerVisible: false,
|
|
@@ -322,6 +323,14 @@ function applyNotify(base, raw, allowCommand) {
|
|
|
322
323
|
? [...raw.command] : base.command;
|
|
323
324
|
return { enabled: boolean(raw.enabled, base.enabled), cooldownMs: cooldown, command };
|
|
324
325
|
}
|
|
326
|
+
/** A project config cannot switch the judge off for longer than this by setting a huge cooldown. */
|
|
327
|
+
const MAX_JUDGE_COOLDOWN_MS = 10 * 60 * 1000;
|
|
328
|
+
function applyJudge(base, raw) {
|
|
329
|
+
if (!isObject(raw))
|
|
330
|
+
return base;
|
|
331
|
+
const cooldown = typeof raw.cooldownMs === "number" && Number.isSafeInteger(raw.cooldownMs) && raw.cooldownMs >= 0 ? Math.min(raw.cooldownMs, MAX_JUDGE_COOLDOWN_MS) : base.cooldownMs;
|
|
332
|
+
return { cooldownMs: cooldown, failuresBeforeCooldown: positiveInteger(raw.failuresBeforeCooldown, base.failuresBeforeCooldown) };
|
|
333
|
+
}
|
|
325
334
|
function applySubagent(base, raw) {
|
|
326
335
|
if (!isObject(raw))
|
|
327
336
|
return base;
|
|
@@ -407,6 +416,7 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
407
416
|
subagent: applySubagent(base.subagent, raw.subagent),
|
|
408
417
|
// A project file may switch notifications off or on, but never names a command to run.
|
|
409
418
|
notify: applyNotify(base.notify, raw.notify, source === "user"),
|
|
419
|
+
judge: applyJudge(base.judge, raw.judge),
|
|
410
420
|
action: applyAction(base.action, raw.action, timeoutMs, source),
|
|
411
421
|
stuck: applyStuck(base.stuck, raw.stuck),
|
|
412
422
|
done: applyDone(base.done, raw.done),
|
|
@@ -423,6 +433,10 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
423
433
|
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
424
434
|
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
425
435
|
compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
|
|
436
|
+
largeOutput: isObject(raw.context.largeOutput) ? {
|
|
437
|
+
enabled: boolean(raw.context.largeOutput.enabled, base.context.largeOutput.enabled),
|
|
438
|
+
threshold: probability(raw.context.largeOutput.threshold, base.context.largeOutput.threshold),
|
|
439
|
+
} : base.context.largeOutput,
|
|
426
440
|
} : base.context,
|
|
427
441
|
};
|
|
428
442
|
}
|
package/dist/conscience.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { Questions } from "pi-typesafe";
|
|
9
|
+
import type { JudgmentsOffReason } from "./backend.js";
|
|
9
10
|
import type { ConscienceConfig } from "./config.js";
|
|
11
|
+
import type { TaskSpine } from "./shape.js";
|
|
10
12
|
export type Disposition = "advance" | "awaiting_user" | "no_gap" | "unclear";
|
|
11
13
|
/** Capability roles the session model assigns to index entries. */
|
|
12
14
|
export type CapabilityRole = "research" | "evidence" | "execution" | "delegation" | "review" | "conversation";
|
|
@@ -64,7 +66,7 @@ export interface AssessmentResult {
|
|
|
64
66
|
/** Number of judge requests issued for this assessment. */
|
|
65
67
|
requestCount: number;
|
|
66
68
|
}
|
|
67
|
-
export type SkipReason = "disabled" | "no_consent" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
69
|
+
export type SkipReason = "disabled" | "no_consent" | "no_key" | "key_rejected" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
68
70
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
69
71
|
export declare const SCORE_LEVELS: readonly ["No useful contribution to the current request, or conflicts with the supplied constraints.", "Related to the subject, but already covered, premature, or unable to resolve the current need.", "Addresses a concrete unmet need at the current step without displacing the active workflow.", "Supplies a missing prerequisite or directly applicable documented method needed for the current step."];
|
|
70
72
|
/** Strip absolute paths, URLs, and credentials from a description. */
|
|
@@ -91,11 +93,13 @@ export declare function buildBatchQuestions(batch: Array<{
|
|
|
91
93
|
};
|
|
92
94
|
/**
|
|
93
95
|
* Build the full state object for the judge request (spec §3 state contract).
|
|
96
|
+
* `spine`, when given, carries the task spine — the thread's goal and earlier user turns — beside `task`;
|
|
97
|
+
* scope context only, it never authorizes anything and it is not part of any question.
|
|
94
98
|
*/
|
|
95
99
|
export declare function buildState(task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[], batch: Array<{
|
|
96
100
|
opaqueId: string;
|
|
97
101
|
candidate: Candidate;
|
|
98
|
-
}
|
|
102
|
+
}>, spine?: TaskSpine | undefined): Record<string, unknown>;
|
|
99
103
|
/**
|
|
100
104
|
* Compute a deterministic hash of the assessment questions for deduplication.
|
|
101
105
|
* Recursively sorts all object keys for canonical form.
|
|
@@ -138,6 +142,8 @@ export interface Judge {
|
|
|
138
142
|
}
|
|
139
143
|
export interface ConscienceDeps {
|
|
140
144
|
judge: Judge | undefined;
|
|
145
|
+
/** Why `judge` is undefined, as the extension computed it; the skip reason. `no_consent` when not given. */
|
|
146
|
+
judgmentsOff?: JudgmentsOffReason | undefined;
|
|
141
147
|
config: ConscienceConfig;
|
|
142
148
|
/** Shared timeout from WardenConfig. The effective deadline is min(conscience.timeoutMs, sharedTimeoutMs). */
|
|
143
149
|
sharedTimeoutMs: number;
|
|
@@ -160,4 +166,4 @@ export interface ConscienceDeps {
|
|
|
160
166
|
export declare function assess(prompt: string, recentContext: string, skills: Skill[], tools: {
|
|
161
167
|
name: string;
|
|
162
168
|
description: string;
|
|
163
|
-
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps): Promise<AssessmentResult>;
|
|
169
|
+
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps, spine?: TaskSpine | undefined): Promise<AssessmentResult>;
|
package/dist/conscience.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { choice, noul, score } from "pi-typesafe";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { redact } from "./redact.js";
|
|
4
|
+
import { SPINE_GOAL_LIMIT, SPINE_HISTORY_LIMIT, SPINE_HISTORY_TURNS } from "./shape.js";
|
|
4
5
|
import { fileContentHash, toolSourceHash } from "./hashing.js";
|
|
5
6
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
6
7
|
export const SCORE_LEVELS = [
|
|
@@ -70,8 +71,10 @@ export function buildBatchQuestions(batch, task, recentContext, activeSkills, su
|
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
72
73
|
* Build the full state object for the judge request (spec §3 state contract).
|
|
74
|
+
* `spine`, when given, carries the task spine — the thread's goal and earlier user turns — beside `task`;
|
|
75
|
+
* scope context only, it never authorizes anything and it is not part of any question.
|
|
73
76
|
*/
|
|
74
|
-
export function buildState(task, recentContext, activeSkills, suppliedSkills, batch) {
|
|
77
|
+
export function buildState(task, recentContext, activeSkills, suppliedSkills, batch, spine) {
|
|
75
78
|
const candidates = {};
|
|
76
79
|
for (const { opaqueId, candidate } of batch) {
|
|
77
80
|
const entry = candidate.indexEntry;
|
|
@@ -82,6 +85,12 @@ export function buildState(task, recentContext, activeSkills, suppliedSkills, ba
|
|
|
82
85
|
return {
|
|
83
86
|
task: sanitizeDescription(redact(task)),
|
|
84
87
|
context: sanitizeDescription(redact(recentContext)),
|
|
88
|
+
// The spine's `task` is deliberately not repeated here: the `task` field above already carries it.
|
|
89
|
+
// Same per-field limits as the action request, for a spine built outside taskSpine.
|
|
90
|
+
...(spine ? { spine: {
|
|
91
|
+
goal: sanitizeDescription(redact(spine.goal)).slice(0, SPINE_GOAL_LIMIT),
|
|
92
|
+
task_history: spine.history.slice(0, SPINE_HISTORY_TURNS).map(turn => sanitizeDescription(redact(turn)).slice(0, SPINE_HISTORY_LIMIT)),
|
|
93
|
+
} } : {}),
|
|
85
94
|
active_skills: activeSkills,
|
|
86
95
|
supplied_skills: suppliedSkills,
|
|
87
96
|
candidates,
|
|
@@ -276,14 +285,14 @@ function parseScoreAnswer(answer) {
|
|
|
276
285
|
* Pre-measurement: returns trace-only results. No steers, no auto-loading.
|
|
277
286
|
* Issues concurrent requests (up to four in flight), skills first, shared deadline.
|
|
278
287
|
*/
|
|
279
|
-
export async function assess(prompt, recentContext, skills, tools, activeSkills, suppliedSkills, deps) {
|
|
288
|
+
export async function assess(prompt, recentContext, skills, tools, activeSkills, suppliedSkills, deps, spine) {
|
|
280
289
|
const start = deps.now?.() ?? Date.now();
|
|
281
290
|
const { config, judge, sharedTimeoutMs } = deps;
|
|
282
291
|
if (!config.enabled) {
|
|
283
292
|
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "disabled" };
|
|
284
293
|
}
|
|
285
294
|
if (!judge) {
|
|
286
|
-
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "no_consent" };
|
|
295
|
+
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: deps.judgmentsOff ?? "no_consent" };
|
|
287
296
|
}
|
|
288
297
|
const { candidates, skillOverflow, toolOverflow } = eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills, deps.globalIndex, deps.projectIndex);
|
|
289
298
|
if (candidates.length === 0) {
|
|
@@ -345,7 +354,7 @@ export async function assess(prompt, recentContext, skills, tools, activeSkills,
|
|
|
345
354
|
const opaqueBatch = assignOpaqueIds(work.items.map(e => e.capped.candidate));
|
|
346
355
|
const { questions, dispositionKey } = buildBatchQuestions(opaqueBatch, prompt, recentContext, activeSkills, suppliedSkills);
|
|
347
356
|
state_.hash = questionHash(questions);
|
|
348
|
-
const state = buildState(prompt, recentContext, activeSkills, suppliedSkills, opaqueBatch);
|
|
357
|
+
const state = buildState(prompt, recentContext, activeSkills, suppliedSkills, opaqueBatch, spine);
|
|
349
358
|
const remaining = deadline - (deps.now?.() ?? Date.now());
|
|
350
359
|
if (remaining <= 0) {
|
|
351
360
|
categoryFailed[work.category] = true;
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { JudgmentBackend, JudgmentsOffReason } from "./backend.js";
|
|
2
3
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
4
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
5
|
import type { ShapeResult } from "./shape.js";
|
|
5
|
-
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), 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. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
|
+
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request, the task spine it is judged against (the first request of the thread and up to four redacted earlier requests), and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), the same task spine (the first request of the thread and up to four redacted earlier requests), 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. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
7
|
/** Which guard spent the user's attention. The status line reports one count per guard. */
|
|
7
8
|
export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "prose" | "runaway" | "subagent" | "conscience";
|
|
8
9
|
/**
|
|
@@ -19,11 +20,23 @@ export declare function formatSteers(stats: {
|
|
|
19
20
|
* so judging them one request at a time costs one round trip per call; judging them together costs one round trip.
|
|
20
21
|
*/
|
|
21
22
|
export declare function siblingToolCalls(ctx: ExtensionContext): ToolCallRef[];
|
|
23
|
+
/** Whether the call is a shell command with a visible effect (commit, push, merge, tag, reset, pull request, release, publish). */
|
|
24
|
+
export declare function isVisibleAction(tool: string, input: Record<string, unknown>): boolean;
|
|
22
25
|
/**
|
|
23
|
-
* The agent's own words
|
|
24
|
-
* tool calls only, the
|
|
26
|
+
* The agent's own words for the call: the text of the assistant message that carries it, or, when that message is
|
|
27
|
+
* tool calls only, the text of the assistant message right before it with no tool call in between. Text from before an
|
|
28
|
+
* earlier tool call described that call, so it is no plan and the intent question is not asked. The exception is
|
|
29
|
+
* `visibleAction` (a commit, push, merge, tag, reset, pull request, release, or publish, by `isVisibleCommand`): such a
|
|
30
|
+
* call is still judged against the latest assistant text since the user's prompt, because an unannounced commit or push
|
|
31
|
+
* after an older plan is the mismatch users object to. Sent as `plan`; it explains the step and cannot approve it.
|
|
25
32
|
*/
|
|
26
|
-
export declare function assistantPlan(ctx: ExtensionContext): string | undefined;
|
|
33
|
+
export declare function assistantPlan(ctx: ExtensionContext, visibleAction?: boolean): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* The once-per-session notice: why Jev judgments are off and what turns them on. `rejectedEnv` is the environment
|
|
36
|
+
* variable that holds a rejected key, or undefined when the rejected key is the one `/typesafe login` saved. Never
|
|
37
|
+
* names a key value or a path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">, backend: JudgmentBackend, headless: boolean, rejectedEnv?: string): string;
|
|
27
40
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
28
41
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
29
42
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|