pi-warden 0.5.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/action-guard.d.ts +50 -0
- package/dist/action-guard.js +63 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -1
- package/dist/done.d.ts +3 -1
- package/dist/done.js +22 -25
- package/dist/excerpt.d.ts +25 -0
- package/dist/excerpt.js +171 -0
- package/dist/extension.d.ts +3 -6
- package/dist/extension.js +63 -65
- package/dist/guard.d.ts +3 -3
- package/dist/guard.js +73 -80
- package/dist/index.d.ts +11 -5
- package/dist/index.js +6 -3
- package/dist/jev.d.ts +23 -0
- package/dist/jev.js +19 -0
- package/dist/output.d.ts +28 -5
- package/dist/output.js +54 -30
- package/dist/prose.d.ts +3 -1
- package/dist/prose.js +8 -11
- package/dist/recall.d.ts +13 -0
- package/dist/recall.js +68 -0
- package/dist/saver.d.ts +23 -6
- package/dist/saver.js +44 -6
- package/dist/shape.js +6 -1
- package/dist/stuck.d.ts +3 -1
- package/dist/stuck.js +17 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-warden
|
|
2
2
|
|
|
3
|
-
A second pair of eyes for [Pi](https://pi.dev) that makes the agent smarter instead of interrupting you. Before the agent runs a `bash`, `write`, or `edit` call (or a shell command through [context-mode](https://www.npmjs.com/package/context-mode)'s `ctx_execute`), pi-warden asks [Jev](https://typesafe.ai) two questions about it in ~250 ms: *would this destroy something that cannot be recovered?* and *is this what the user actually asked for?* The answers are probabilities, so `rm -rf dist` after "rebuild from scratch" sails through while `npm run db:reset` after "add a column" is held, and the agent is told why so it re-plans or asks you in chat. More guards watch the run: a **stuck-loop** detector, a **done-check** for completion claims that no test backed up, **slop** notes for stubs, filler, and padded replies, a check for **injected instructions** in tool output, and **
|
|
3
|
+
A second pair of eyes for [Pi](https://pi.dev) that makes the agent smarter instead of interrupting you. Before the agent runs a `bash`, `write`, or `edit` call (or a shell command through [context-mode](https://www.npmjs.com/package/context-mode)'s `ctx_execute`), pi-warden asks [Jev](https://typesafe.ai) two questions about it in ~250 ms: *would this destroy something that cannot be recovered?* and *is this what the user actually asked for?* The answers are probabilities, so `rm -rf dist` after "rebuild from scratch" sails through while `npm run db:reset` after "add a column" is held, and the agent is told why so it re-plans or asks you in chat. More guards watch the run: a **stuck-loop** detector, a **done-check** for completion claims that no test backed up, **slop** notes for stubs, filler, and padded replies, a check for **injected instructions** in tool output, and a **context saver** that replaces repeated tool results with a note, keeps exact failing tests and errors from large runner output, and points recalls at a search instead of a whole-file read. Every verdict lands on a status line above the editor; **click it** (fullscreen mode), press `ctrl+shift+w`, or run `/warden trace` to open a live trace sidebar with the scores, the reasons, and exactly what the agent was told. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ Three jobs, in order of maturity:
|
|
|
16
16
|
| --- | --- | --- |
|
|
17
17
|
| **Security** | Action holds and offline patterns protect risky operations. Tool-output checks flag possible injected instructions and credential shapes; insecure written code gets a targeted steer. Output notices are advisory, not a sandbox or a guarantee that an attack is contained. | built |
|
|
18
18
|
| **Deslopify** | Written code is scored per symptom — stubs, restating comments, dead code, hedging — and the agent gets a nudge naming the symptom and its fix; repeats become a standing rule. Replies are scored for wordiness, assistant clichés, and jargon against an `audience` setting, and a trend across replies shapes the next one. | built |
|
|
19
|
-
| **Context saving** | Jev selects a retention policy
|
|
19
|
+
| **Context saving** | Only the newest tool result is ever touched, so the prompt cache prefix stays byte-identical. An identical repeat of an earlier result becomes a short note (code only). For large new results Jev selects a retention policy and names the output format; code keeps exact lines through a format parser (failing tests, errors with file:line, changed files, package notices) or a deterministic head/diagnostic/tail excerpt, stores an exact copy, and tells the agent how to search it instead of re-reading it. Task-aware segment selection and compaction support remain planned. | built |
|
|
20
20
|
|
|
21
21
|
## Why a coding agent needs this
|
|
22
22
|
|
|
@@ -88,17 +88,21 @@ Nudges from the stuck, done, slop, and prose guards are custom messages in the a
|
|
|
88
88
|
|
|
89
89
|
If TypeSafe cannot answer (timeout after 5 s, outage, budget), an action-guard call is allowed with a warning (`failOpen: true`; set it to `false` to hold instead), and the other guards simply skip. When the per-session request budget is spent, pi-warden says so once and continues with offline checks. Consent in headless runs comes from `PI_WARDEN_ENABLED=1`; `PI_WARDEN_MODE` overrides the mode.
|
|
90
90
|
|
|
91
|
-
### Tool-output security and
|
|
91
|
+
### Tool-output security and context saving
|
|
92
92
|
|
|
93
93
|
With consent, content-bearing tools (`read`, fetch/search tools and named MCP equivalents) are checked for instructions that redirect the assistant or request private data. Other tools, including shell/context-mode tools, are checked from 2048 characters. Jev receives a redacted 6000-character head/tail sample, so attacks in an unsampled middle can be missed. A score at or above `security.threshold` adds an untrusted-data notice around the text and a hidden steer. Local credential-shape checks work without network access and warn not to echo or commit possible secrets. Images, result details, error flags, and usage are preserved.
|
|
94
94
|
|
|
95
95
|
Written code gets a `security_risk` question in the existing action request: hardcoded credentials, disabled TLS checks, unsafe shell/SQL interpolation, broad permissions, or bypassed verification. A threshold crossing warns and steers; it does not authorize an action or automatically block otherwise safe edits.
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
**Duplicates (code only).** A single-text-block result of at least `context.duplicateMinChars` (2000) whose content, ignoring colour codes and trailing whitespace, is identical to an earlier result of this session is replaced by a note naming the earlier tool and the size, plus a recall footer. The exact text is stored once; later copies reuse the file. No Jev request is made. Re-running the same failing test command is the typical case: nothing changed, and the note says so.
|
|
98
98
|
|
|
99
|
-
**
|
|
99
|
+
**Retention and format (Jev decides, code applies).** For a single text block of at least `context.tailMinChars`, the same output request asks Jev to choose `all`, `errors_and_summary`, or `summary_only`, and which tool format produced the output: `vitest_jest`, `node_test`, `tsc`, `eslint`, `pytest`, `git_diff`, `git_log`, `npm_install`, or `other`. The full result stays when P(keep all) exceeds `1 - context.confidence` (default: keep when Jev gives `all` more than 0.2), when the probability is missing, or on any request error. When a format has at least `context.formatConfidence` (0.7) and its markers are present, a deterministic parser keeps the exact lines that matter for that format: failing tests with their assertion lines, compiler and linter errors with file and line, changed files with +/- counts, package manager notices, and the final summary line. Otherwise code keeps bounded head/diagnostic/tail excerpts. Nothing is paraphrased. An exact copy is written to an owner-only temporary directory before replacement; the excerpt links to it. If storage fails, the original stays. Multiple text blocks keep their layout and are not compressed. Trace and widget show the actual bytes saved. No extra steer is persisted for compression alone.
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
**Recall through search.** The footer under every excerpt or duplicate note names the file and a search command that exists on this machine, probed once per session in this order: `rg`, `ag`, `ugrep`, `git grep --no-index`, `grep`, PowerShell `Select-String`, `findstr`. It asks for a scoped search or a ranged `read` and says not to read the whole file. `context.recallTool` pins one of those names or `none`; `auto` probes. Recalls are counted by kind: a whole-file read undoes the saving, a scoped access keeps it.
|
|
102
|
+
|
|
103
|
+
**Measuring it.** `/warden status` keeps a session ledger so the saver's value is a number, not a claim: how many outputs were large enough to consider, how many Jev compressed, how many duplicates were dropped, the bytes removed (≈ tokens at 4 bytes each), the *token-turns spared* (removed tokens × every later turn, which is what a prompt cache would otherwise have re-read), and **recalls** — how often the agent went back to a stored full output, split into whole-file reads and scoped accesses. A recall means the excerpt was not enough; a recall rate above about 10% says `context.confidence` is too low for your work, near 0% says it could go lower. Whole-file recalls also give back the saving; scoped ones do not. Every compression and recall also appears in the trace with the running ledger. The quality effect (less context rot) does not show in a counter; compare a scripted task with `context.enabled` on and off if you want that number.
|
|
104
|
+
|
|
105
|
+
Set `context.enabled: false` to stop all of it. The saver never prunes a warm prefix, changes the system prompt, changes the tool list after load, or edits historical session entries: every change is to the newest tool result before it enters the session, so the prompt cache prefix is unaffected. Full-output files can contain secrets; they remain in the OS temporary directory until you or the OS remove them. Excerpts can omit relevant evidence; read the linked file before making a decision that needs it.
|
|
102
106
|
|
|
103
107
|
### Task continuity
|
|
104
108
|
|
|
@@ -164,7 +168,7 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
164
168
|
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
|
|
165
169
|
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
|
|
166
170
|
"security": { "enabled": true, "threshold": 0.7 },
|
|
167
|
-
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8 },
|
|
171
|
+
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
|
|
168
172
|
"slop": {
|
|
169
173
|
"enabled": true,
|
|
170
174
|
"threshold": 0.7,
|
|
@@ -179,7 +183,7 @@ A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (f
|
|
|
179
183
|
|
|
180
184
|
## Data handling
|
|
181
185
|
|
|
182
|
-
- With consent, each guarded call sends to `https://api.typesafe.ai`: your latest prompt (truncated to 1500 characters), the tool name, the command (truncated to 2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character head/middle/tail sample for `write`, and the first three edit pairs (400 characters each) for `edit`. The stuck detector sends the last 12 tool calls (300 characters each) with 400-character output tails; the done-check and the prose check send the agent's final message (2000 and 2500 characters) with the run's check commands and the audience description. Scope and retention requests also carry up to eight prior user/assistant text messages (750 redacted characters each). Output checks send a redacted head/tail sample up to 6000 characters plus size and tool name. No unrelated files or telemetry.
|
|
186
|
+
- With consent, each guarded call sends to `https://api.typesafe.ai`: your latest prompt (truncated to 1500 characters), the tool name, the command (truncated to 2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character head/middle/tail sample for `write`, and the first three edit pairs (400 characters each) for `edit`. The stuck detector sends the last 12 tool calls (300 characters each) with 400-character output tails; the done-check and the prose check send the agent's final message (2000 and 2500 characters) with the run's check commands and the audience description. Scope and retention requests also carry up to eight prior user/assistant text messages (750 redacted characters each). Output checks send a redacted head/tail sample up to 6000 characters plus size, line counts, and tool name; the format question uses that same sample. Duplicate detection sends nothing. No unrelated files or telemetry.
|
|
183
187
|
- Obvious credentials in the action (`Authorization` headers, `TOKEN=`/`SECRET=` assignments, `sk-`, `ghp_`, `AKIA`, JWTs, URL passwords, PEM blocks) are replaced with `[redacted]` before sending. This is best-effort; do not rely on it for prompts that contain secrets.
|
|
184
188
|
- Text returned or steered to the agent names the tool, the reasons, and the scores, not the command text. UI errors never include upstream response bodies or keys.
|
|
185
189
|
- Judgments are model output. Thresholds are yours to tune; a hold is information for the agent and for you, not a verdict on either.
|
|
@@ -206,6 +210,8 @@ verdict.judgment; // { irreversible, offTask, scope, scopeConfidence, approved
|
|
|
206
210
|
steerReason(verdict, { canApprove: true }); // the text the agent receives for a hold
|
|
207
211
|
```
|
|
208
212
|
|
|
213
|
+
`evaluateAction` judges one call. What spans calls in a session, the hold → reply → retry approval and the batched judging of sibling tool calls, is `ActionGuard`: `inspect(call, conversation, options)` returns the same `Verdict`, `hold(task)` records a hold, `turnEnd()` and `reset()` follow Pi's turn and session.
|
|
214
|
+
|
|
209
215
|
Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`, `formatVerdict`, the question sets, the stuck detector (`AttemptWindow`, `makeAttempt`, `evaluateStuck`, `stuckNudge`), the done-check (`classifyToolResult`, `recordOutcome`, `needsDoneCheck`, `evaluateDone`, `doneNudge`), and the config helpers.
|
|
210
216
|
|
|
211
217
|
## Development
|
|
@@ -213,8 +219,9 @@ Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`,
|
|
|
213
219
|
```bash
|
|
214
220
|
npm install
|
|
215
221
|
npm run check # typecheck, offline tests (mocked transport), build
|
|
216
|
-
npm run test:live #
|
|
217
|
-
node scripts/security-cases.mjs
|
|
222
|
+
npm run test:live # 55 billable synthetic judgments across all guards (key from .env or the stored login); pass action|slop|approval|stuck|done|security|context for one group
|
|
223
|
+
node scripts/security-cases.mjs # the 9-case output-security and task-continuity set on its own
|
|
224
|
+
node scripts/context-cases.mjs # 15 labelled outputs that tune retention and format for the context saver
|
|
218
225
|
npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
|
|
219
226
|
node scripts/slop-cases.mjs # 22 billable cases that tune the slop and prose questions
|
|
220
227
|
npm run preview # re-render docs/preview.png from the recorded live verdicts (needs a Chrome binary)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ActionGuardConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
2
|
+
import type { TaskMessage, Verdict } from "./guard.js";
|
|
3
|
+
import type { Judge } from "./jev.js";
|
|
4
|
+
/** One tool call as the agent proposed it. `id` is Pi's tool call id, stable across hooks and retries. */
|
|
5
|
+
export interface ToolCallRef {
|
|
6
|
+
id: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
input: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** What the session says about the call: the latest user prompt, recent messages for scope, and the sibling calls of the same assistant message. */
|
|
11
|
+
export interface Conversation {
|
|
12
|
+
task: string | undefined;
|
|
13
|
+
/** Scope context only: approval still comes from `task`, never from this history. */
|
|
14
|
+
context?: readonly TaskMessage[] | undefined;
|
|
15
|
+
/** Tool calls in the same assistant message, this one included; they are judged together. */
|
|
16
|
+
siblings?: readonly ToolCallRef[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface InspectOptions {
|
|
19
|
+
config: ActionGuardConfig;
|
|
20
|
+
cwd: string;
|
|
21
|
+
/** Omit to run offline pattern checks only (no consent, no network). */
|
|
22
|
+
judge?: Judge | undefined;
|
|
23
|
+
signal?: AbortSignal | undefined;
|
|
24
|
+
slop?: SlopGuardConfig | undefined;
|
|
25
|
+
security?: SecurityConfig | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The Action guard for one session. `evaluateAction` judges a single call; this module owns what spans calls:
|
|
29
|
+
*
|
|
30
|
+
* - **Hold and approval.** After a hold in steer mode, the next guarded call that runs under a *new* user prompt asks Jev
|
|
31
|
+
* whether that prompt approves it. The retry rarely repeats the held string byte for byte (a `command -v` dropped, a
|
|
32
|
+
* different timeout), so approval is judged against the action itself, never matched against the earlier command text.
|
|
33
|
+
* A re-hold under the reply keeps the original reference prompt; otherwise the reply could never approve anything.
|
|
34
|
+
* Without a judge, a reply that reads as approval stands in for the question.
|
|
35
|
+
* - **Sibling prejudging.** Calls of one assistant message are judged as soon as the first of them is inspected, so their
|
|
36
|
+
* requests go out together. A judgment is used once and only for the input it was made for; an earlier hook may have
|
|
37
|
+
* changed the call's input, and a stale judgment is discarded, not reused. Prejudgments do not outlive their turn.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ActionGuard {
|
|
40
|
+
private readonly prejudged;
|
|
41
|
+
private lastHoldPrompt;
|
|
42
|
+
private holdPending;
|
|
43
|
+
/** Judges one call. The verdict's `approvedByUser` means a pending hold was released by the user's reply. */
|
|
44
|
+
inspect(call: ToolCallRef, conversation: Conversation, options: InspectOptions): Promise<Verdict>;
|
|
45
|
+
/** The call inspected under `task` was held: the next inspection under a different prompt asks whether that prompt approves it. */
|
|
46
|
+
hold(task: string | undefined): void;
|
|
47
|
+
/** Siblings that were never inspected (an earlier one terminated the batch, or Esc) do not outlive their turn. */
|
|
48
|
+
turnEnd(): void;
|
|
49
|
+
reset(): void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { evaluateAction, textApproves } from "./guard.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Action guard for one session. `evaluateAction` judges a single call; this module owns what spans calls:
|
|
4
|
+
*
|
|
5
|
+
* - **Hold and approval.** After a hold in steer mode, the next guarded call that runs under a *new* user prompt asks Jev
|
|
6
|
+
* whether that prompt approves it. The retry rarely repeats the held string byte for byte (a `command -v` dropped, a
|
|
7
|
+
* different timeout), so approval is judged against the action itself, never matched against the earlier command text.
|
|
8
|
+
* A re-hold under the reply keeps the original reference prompt; otherwise the reply could never approve anything.
|
|
9
|
+
* Without a judge, a reply that reads as approval stands in for the question.
|
|
10
|
+
* - **Sibling prejudging.** Calls of one assistant message are judged as soon as the first of them is inspected, so their
|
|
11
|
+
* requests go out together. A judgment is used once and only for the input it was made for; an earlier hook may have
|
|
12
|
+
* changed the call's input, and a stale judgment is discarded, not reused. Prejudgments do not outlive their turn.
|
|
13
|
+
*/
|
|
14
|
+
export class ActionGuard {
|
|
15
|
+
prejudged = new Map();
|
|
16
|
+
lastHoldPrompt;
|
|
17
|
+
holdPending = false;
|
|
18
|
+
/** Judges one call. The verdict's `approvedByUser` means a pending hold was released by the user's reply. */
|
|
19
|
+
async inspect(call, conversation, options) {
|
|
20
|
+
const { task } = conversation;
|
|
21
|
+
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
22
|
+
const retryAfterHold = this.holdPending && this.lastHoldPrompt !== task;
|
|
23
|
+
const judgeCall = (tool, input) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, retryAfterHold });
|
|
24
|
+
// A retry after a hold stays sequential because an approval consumed by one sibling changes the question for the next.
|
|
25
|
+
if (options.judge && !retryAfterHold) {
|
|
26
|
+
for (const sibling of conversation.siblings ?? []) {
|
|
27
|
+
if (sibling.id === call.id || this.prejudged.has(sibling.id) || !options.config.tools.includes(sibling.tool))
|
|
28
|
+
continue;
|
|
29
|
+
const verdict = judgeCall(sibling.tool, sibling.input);
|
|
30
|
+
verdict.catch(() => undefined);
|
|
31
|
+
this.prejudged.set(sibling.id, { key: JSON.stringify(sibling.input), verdict, used: false });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const key = JSON.stringify(call.input);
|
|
35
|
+
const ready = this.prejudged.get(call.id);
|
|
36
|
+
const pending = ready && !ready.used && ready.key === key && !retryAfterHold ? ready.verdict : judgeCall(call.tool, call.input);
|
|
37
|
+
this.prejudged.set(call.id, { key, verdict: pending, used: true });
|
|
38
|
+
const verdict = await pending;
|
|
39
|
+
if (retryAfterHold && !options.judge && verdict.level === "confirm" && textApproves(task)) {
|
|
40
|
+
verdict.level = "allow";
|
|
41
|
+
verdict.approvedByUser = true;
|
|
42
|
+
verdict.reasons = ["user approved in the latest message", ...verdict.reasons];
|
|
43
|
+
}
|
|
44
|
+
if (verdict.approvedByUser)
|
|
45
|
+
this.holdPending = false;
|
|
46
|
+
return verdict;
|
|
47
|
+
}
|
|
48
|
+
/** The call inspected under `task` was held: the next inspection under a different prompt asks whether that prompt approves it. */
|
|
49
|
+
hold(task) {
|
|
50
|
+
if (!this.holdPending)
|
|
51
|
+
this.lastHoldPrompt = task;
|
|
52
|
+
this.holdPending = true;
|
|
53
|
+
}
|
|
54
|
+
/** Siblings that were never inspected (an earlier one terminated the batch, or Esc) do not outlive their turn. */
|
|
55
|
+
turnEnd() {
|
|
56
|
+
this.prejudged.clear();
|
|
57
|
+
}
|
|
58
|
+
reset() {
|
|
59
|
+
this.prejudged.clear();
|
|
60
|
+
this.lastHoldPrompt = undefined;
|
|
61
|
+
this.holdPending = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -64,7 +64,15 @@ export interface ContextConfig {
|
|
|
64
64
|
tailMinChars: number;
|
|
65
65
|
/** Minimum P(the full output is not needed), 1 - P(all), before code removes output. */
|
|
66
66
|
confidence: number;
|
|
67
|
+
/** A text result at least this long that repeats an earlier result of this session is replaced by a short note (code only). */
|
|
68
|
+
duplicateMinChars: number;
|
|
69
|
+
/** Search command named in the recall footer; `auto` detects one at load. */
|
|
70
|
+
recallTool: RecallTool;
|
|
71
|
+
/** Minimum P(format) before a format-specific parser builds the excerpt instead of the generic head/tail one. */
|
|
72
|
+
formatConfidence: number;
|
|
67
73
|
}
|
|
74
|
+
export type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
|
|
75
|
+
export declare function isRecallTool(value: unknown): value is RecallTool;
|
|
68
76
|
export type WardenMode = "steer" | "confirm" | "advise";
|
|
69
77
|
export interface WardenConfig {
|
|
70
78
|
/** Master switch. false disables every guard, including offline pattern checks. */
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,10 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
5
5
|
import { defaultWidgetConfig } from "./widget.js";
|
|
6
|
+
const RECALL_TOOLS = ["auto", "rg", "ag", "ugrep", "git-grep", "grep", "select-string", "findstr", "none"];
|
|
7
|
+
export function isRecallTool(value) {
|
|
8
|
+
return typeof value === "string" && RECALL_TOOLS.includes(value);
|
|
9
|
+
}
|
|
6
10
|
export const PACKAGE_NAME = "pi-warden";
|
|
7
11
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
8
12
|
export const CONFIG_SCHEMA = 3;
|
|
@@ -26,7 +30,7 @@ export function defaultConfig() {
|
|
|
26
30
|
done: { enabled: true, claimsDone: 0.7, nudge: true },
|
|
27
31
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
28
32
|
security: { enabled: true, threshold: 0.7 },
|
|
29
|
-
context: { enabled: true, tailMinChars: 12000, confidence: 0.8 },
|
|
33
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7 },
|
|
30
34
|
widget: defaultWidgetConfig(),
|
|
31
35
|
steerVisible: false,
|
|
32
36
|
};
|
|
@@ -162,6 +166,9 @@ function applyGuards(base, raw, timeoutMs) {
|
|
|
162
166
|
enabled: boolean(raw.context.enabled, base.context.enabled),
|
|
163
167
|
tailMinChars: positiveInteger(raw.context.tailMinChars, base.context.tailMinChars),
|
|
164
168
|
confidence: probability(raw.context.confidence, base.context.confidence),
|
|
169
|
+
duplicateMinChars: positiveInteger(raw.context.duplicateMinChars, base.context.duplicateMinChars),
|
|
170
|
+
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
171
|
+
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
165
172
|
} : base.context,
|
|
166
173
|
};
|
|
167
174
|
}
|
package/dist/done.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { IntegrationErrorCode } from "pi-typesafe";
|
|
1
2
|
import type { DoneGuardConfig } from "./config.js";
|
|
2
|
-
import type { Judge } from "./
|
|
3
|
+
import type { Judge } from "./jev.js";
|
|
3
4
|
export type ToolOutcome = "read" | "mutation" | "check-pass" | "check-fail" | "unknown";
|
|
4
5
|
/**
|
|
5
6
|
* What a finished tool call contributes to the run's evidence. Only write/edit count as code changes: shell side effects
|
|
@@ -56,6 +57,7 @@ export interface DoneVerdict {
|
|
|
56
57
|
evidence: RunEvidence;
|
|
57
58
|
judgment?: DoneJudgment;
|
|
58
59
|
error?: string;
|
|
60
|
+
errorCode?: IntegrationErrorCode;
|
|
59
61
|
}
|
|
60
62
|
export declare function buildDoneRequest(task: string | undefined, finalMessage: string, evidence: RunEvidence): {
|
|
61
63
|
state: {
|
package/dist/done.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { choice, noul
|
|
1
|
+
import { choice, noul } from "pi-typesafe";
|
|
2
2
|
import { isReadOnlyCommand } from "./guard.js";
|
|
3
|
+
import { askJev } from "./jev.js";
|
|
3
4
|
import { redact } from "./redact.js";
|
|
4
5
|
import { commandOf } from "./tools.js";
|
|
5
6
|
import { DEFAULT_TEMPLATES, doneTokens, renderTemplate } from "./widget.js";
|
|
@@ -109,31 +110,27 @@ export function buildDoneRequest(task, finalMessage, evidence) {
|
|
|
109
110
|
}
|
|
110
111
|
const APPLIES_THRESHOLD = 0.5;
|
|
111
112
|
export async function evaluateDone(task, finalMessage, evidence, options) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (falseClaim)
|
|
131
|
-
reasons.push(`claims checks passed (${judgment.claimsVerified.toFixed(2)}) but none ran`);
|
|
132
|
-
return { unverified, falseClaim, reasons, evidence, judgment };
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
return { unverified: false, falseClaim: false, reasons: [], evidence, error: error instanceof TypeSafeIntegrationError ? error.message : "TypeSafe request failed." };
|
|
113
|
+
const result = await askJev(options.judge, buildDoneRequest(task, finalMessage, evidence), { timeoutMs: options.timeoutMs, signal: options.signal });
|
|
114
|
+
if (!result.ok)
|
|
115
|
+
return { unverified: false, falseClaim: false, reasons: [], evidence, error: result.error, ...(result.errorCode ? { errorCode: result.errorCode } : {}) };
|
|
116
|
+
const judgment = {
|
|
117
|
+
claimsDone: result.answers.claims_done.noul,
|
|
118
|
+
claimsVerified: result.answers.claims_verified.noul,
|
|
119
|
+
verificationApplies: result.answers.verification_applies.noul,
|
|
120
|
+
outcome: result.answers.outcome.choice,
|
|
121
|
+
model: result.model,
|
|
122
|
+
elapsedMs: result.elapsedMs,
|
|
123
|
+
};
|
|
124
|
+
const unverified = judgment.claimsDone >= options.config.claimsDone && judgment.outcome !== "blocked" && judgment.verificationApplies >= APPLIES_THRESHOLD;
|
|
125
|
+
const falseClaim = unverified && judgment.claimsVerified >= 0.7 && evidence.checks.length === 0;
|
|
126
|
+
const reasons = [];
|
|
127
|
+
if (unverified) {
|
|
128
|
+
const failed = evidence.checks.filter(check => !check.passed).length;
|
|
129
|
+
reasons.push(`reports completion (${judgment.claimsDone.toFixed(2)}) after ${evidence.mutations} file change${evidence.mutations === 1 ? "" : "s"} with ${failed ? `${failed} failed check${failed === 1 ? "" : "s"} and no passing one` : "no test, build, or lint run"}`);
|
|
136
130
|
}
|
|
131
|
+
if (falseClaim)
|
|
132
|
+
reasons.push(`claims checks passed (${judgment.claimsVerified.toFixed(2)}) but none ran`);
|
|
133
|
+
return { unverified, falseClaim, reasons, evidence, judgment };
|
|
137
134
|
}
|
|
138
135
|
/** Follow-up for the agent: verify or say plainly that nothing was verified. */
|
|
139
136
|
export function doneNudge(verdict) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format-aware excerpts. Jev selects which tool produced the output; code keeps the exact lines that matter for that
|
|
3
|
+
* format: failing tests with their assertion lines, compiler and linter errors with file:line, changed files with
|
|
4
|
+
* counts, package manager warnings, and the final summary. Nothing is paraphrased. A parser returns undefined when
|
|
5
|
+
* it does not recognise its markers, and the caller falls back to the generic head/diagnostic/tail excerpt.
|
|
6
|
+
*/
|
|
7
|
+
export type OutputFormat = "vitest_jest" | "node_test" | "tsc" | "eslint" | "pytest" | "git_diff" | "git_log" | "npm_install" | "other";
|
|
8
|
+
export declare const formatQuestion: {
|
|
9
|
+
format: import("pi-typesafe").ChoiceQuestion<{
|
|
10
|
+
readonly vitest_jest: "A Vitest or Jest run: ✓/✗/× or PASS/FAIL per test, a 'Test Files' or 'Tests:' summary.";
|
|
11
|
+
readonly node_test: "Node's built-in test runner: TAP lines 'ok'/'not ok' with '# tests', or the spec reporter with ✔/✖ and 'ℹ tests'.";
|
|
12
|
+
readonly tsc: "TypeScript compiler diagnostics: 'file(line,col): error TSnnnn:' lines, 'Found N errors'.";
|
|
13
|
+
readonly eslint: "ESLint report: a file path line followed by 'line:col error|warning message rule', then '✖ N problems'.";
|
|
14
|
+
readonly pytest: "pytest: dots/F per test, '___ test_name ___' failure headers, 'E ' assertion lines, 'FAILED'/'PASSED' lines, '=== N passed/failed ===' summary.";
|
|
15
|
+
readonly git_diff: "A unified diff: 'diff --git', '---'/'+++', '@@' hunks with +/- lines.";
|
|
16
|
+
readonly git_log: "git log or git status output: 'commit <hash>' blocks or one-line hashes with subjects; 'modified:'/'new file:' status lines.";
|
|
17
|
+
readonly npm_install: "npm, pnpm, or yarn install: 'added N packages', 'npm warn', 'deprecated', 'vulnerabilities', 'up to date'.";
|
|
18
|
+
readonly other: "Anything else: source code, data, documentation, a shell listing, a mixed or unknown log.";
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Exact lines selected by the parser for `format`, joined and capped, with the last non-empty lines as a tail so the
|
|
23
|
+
* final status is always present. Undefined when the format is `other` or its markers are absent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatExcerpt(text: string, format: OutputFormat): string | undefined;
|
package/dist/excerpt.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { choice } from "pi-typesafe";
|
|
2
|
+
export const formatQuestion = {
|
|
3
|
+
format: choice("Which kind of tool produced `output`? Judge from the markers in the sample; `tool` names the calling tool, not the program. Select other when unsure or when the output is source code, data, or prose.", {
|
|
4
|
+
vitest_jest: "A Vitest or Jest run: ✓/✗/× or PASS/FAIL per test, a 'Test Files' or 'Tests:' summary.",
|
|
5
|
+
node_test: "Node's built-in test runner: TAP lines 'ok'/'not ok' with '# tests', or the spec reporter with ✔/✖ and 'ℹ tests'.",
|
|
6
|
+
tsc: "TypeScript compiler diagnostics: 'file(line,col): error TSnnnn:' lines, 'Found N errors'.",
|
|
7
|
+
eslint: "ESLint report: a file path line followed by 'line:col error|warning message rule', then '✖ N problems'.",
|
|
8
|
+
pytest: "pytest: dots/F per test, '___ test_name ___' failure headers, 'E ' assertion lines, 'FAILED'/'PASSED' lines, '=== N passed/failed ===' summary.",
|
|
9
|
+
git_diff: "A unified diff: 'diff --git', '---'/'+++', '@@' hunks with +/- lines.",
|
|
10
|
+
git_log: "git log or git status output: 'commit <hash>' blocks or one-line hashes with subjects; 'modified:'/'new file:' status lines.",
|
|
11
|
+
npm_install: "npm, pnpm, or yarn install: 'added N packages', 'npm warn', 'deprecated', 'vulnerabilities', 'up to date'.",
|
|
12
|
+
other: "Anything else: source code, data, documentation, a shell listing, a mixed or unknown log.",
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
const LIMIT = 6000;
|
|
16
|
+
/** Header lines start a block; the following non-empty lines up to `depth` are kept with it. */
|
|
17
|
+
function capture(lines, header, keep, depth, summary) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let headers = 0;
|
|
20
|
+
let remaining = 0;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (header.test(line)) {
|
|
23
|
+
headers++;
|
|
24
|
+
remaining = depth;
|
|
25
|
+
out.push(line);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (summary.test(line)) {
|
|
29
|
+
out.push(line);
|
|
30
|
+
remaining = 0;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (remaining > 0 && line.trim()) {
|
|
34
|
+
if (keep.test(line))
|
|
35
|
+
out.push(line);
|
|
36
|
+
remaining--;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return headers || out.length ? out : undefined;
|
|
40
|
+
}
|
|
41
|
+
function vitestJest(lines) {
|
|
42
|
+
const summary = /^\s*(?:Test Files|Tests|Test Suites|Snapshots|Duration|Time|Start at|Ran all test suites)\b|^\s*(?:FAIL|PASS)\s{2,}\S/;
|
|
43
|
+
const out = capture(lines, /^\s*(?:FAIL\s|[×✗✕]\s|●\s|❯\s.*\s\d+\s*$|AssertionError\b)/, /expected|received|actual|Error|assert|❯|›|\bat\b.*:\d+:\d+|[+-]\s|^\s*\d+\|/i, 8, summary);
|
|
44
|
+
return out && lines.some(line => summary.test(line) || /[✓✗×✔✖]/.test(line)) ? out : undefined;
|
|
45
|
+
}
|
|
46
|
+
function nodeTest(lines) {
|
|
47
|
+
const summary = /^(?:#|ℹ)\s*(?:tests|suites|pass|fail|cancelled|skipped|todo|duration_ms)\b/;
|
|
48
|
+
if (!lines.some(line => summary.test(line) || /^TAP version/.test(line) || /^\s*[✔✖]\s/.test(line)))
|
|
49
|
+
return undefined;
|
|
50
|
+
return capture(lines, /^\s*(?:not ok\b|✖\s)/, /error|expected|actual|Error|failureType|code|at\b.*:\d+:\d+|^\s*[+-]\s|^\s*\|/i, 10, summary) ?? [];
|
|
51
|
+
}
|
|
52
|
+
function tsc(lines) {
|
|
53
|
+
const header = /^(?:\S.*?)(?:\(\d+,\d+\)|:\d+:\d+)\s*[-:]\s*error TS\d+/;
|
|
54
|
+
if (!lines.some(line => header.test(line) || /^Found \d+ errors?/.test(line)))
|
|
55
|
+
return undefined;
|
|
56
|
+
return capture(lines, header, /^\s{2,}\S/, 3, /^Found \d+ errors?|^error TS\d+/);
|
|
57
|
+
}
|
|
58
|
+
function eslint(lines) {
|
|
59
|
+
const problem = /^\s+\d+:\d+\s+(?:error|warning)\s+/;
|
|
60
|
+
if (!lines.some(line => problem.test(line)) || !lines.some(line => /^✖ \d+ problems?|^\d+ problems? \(/.test(line) || /\S+\.[cm]?[jt]sx?$|\.vue$|\.svelte$/.test(line)))
|
|
61
|
+
return undefined;
|
|
62
|
+
const out = [];
|
|
63
|
+
let file;
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (/^\S.*\.(?:[cm]?[jt]sx?|vue|svelte|json|md)$/.test(line)) {
|
|
66
|
+
file = line;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (problem.test(line)) {
|
|
70
|
+
if (file) {
|
|
71
|
+
out.push(file);
|
|
72
|
+
file = undefined;
|
|
73
|
+
}
|
|
74
|
+
out.push(line);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (/^✖ \d+ problems?|^\d+ problems? \(|potentially fixable/.test(line))
|
|
78
|
+
out.push(line);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function pytest(lines) {
|
|
83
|
+
const summary = /^=+ .*(?:passed|failed|error|skipped|no tests ran).* =+$|^=+ short test summary info =+$|^(?:FAILED|ERROR) \S+/;
|
|
84
|
+
if (!lines.some(line => summary.test(line) || /^_{3,} .+ _{3,}$/.test(line)))
|
|
85
|
+
return undefined;
|
|
86
|
+
return capture(lines, /^_{3,} .+ _{3,}$/, /^E\s|^>\s|:\d+: |Error|assert/i, 12, summary) ?? [];
|
|
87
|
+
}
|
|
88
|
+
function gitDiff(lines) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
|
|
92
|
+
if (match) {
|
|
93
|
+
files.push({ name: match[2], added: 0, removed: 0 });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const current = files.at(-1);
|
|
97
|
+
if (!current)
|
|
98
|
+
continue;
|
|
99
|
+
if (/^(?:new file mode|deleted file mode|rename (?:from|to)|Binary files)/.test(line))
|
|
100
|
+
current.note = (current.note ? `${current.note}; ` : "") + line;
|
|
101
|
+
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
102
|
+
current.added++;
|
|
103
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
104
|
+
current.removed++;
|
|
105
|
+
}
|
|
106
|
+
if (!files.length)
|
|
107
|
+
return undefined;
|
|
108
|
+
const added = files.reduce((sum, file) => sum + file.added, 0);
|
|
109
|
+
const removed = files.reduce((sum, file) => sum + file.removed, 0);
|
|
110
|
+
return [...files.map(file => `${file.name} +${file.added} -${file.removed}${file.note ? ` (${file.note})` : ""}`), `${files.length} files changed, +${added} -${removed}`];
|
|
111
|
+
}
|
|
112
|
+
function gitLog(lines) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let pendingSubject = false;
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const commit = /^commit ([0-9a-f]{7,40})\b/.exec(line);
|
|
117
|
+
if (commit) {
|
|
118
|
+
out.push(commit[1].slice(0, 12));
|
|
119
|
+
pendingSubject = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (pendingSubject && /^\s{4}\S/.test(line)) {
|
|
123
|
+
out[out.length - 1] += ` ${line.trim()}`;
|
|
124
|
+
pendingSubject = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (/^[0-9a-f]{7,40} \S/.test(line) || /^\s*(?:modified|new file|deleted|renamed|both modified):|^\?\?\s|^[ MADRCU?!]{2}\s\S/.test(line) || /^(?:On branch|Your branch|Changes (?:to be committed|not staged)|Untracked files|nothing to commit)/.test(line))
|
|
128
|
+
out.push(line);
|
|
129
|
+
}
|
|
130
|
+
return out.length ? out : undefined;
|
|
131
|
+
}
|
|
132
|
+
function npmInstall(lines) {
|
|
133
|
+
const keep = /^(?:added|removed|changed|audited|found|up to date|up-to-date)\b|\bvulnerabilit(?:y|ies)\b|^npm (?:warn|WARN|error|ERR!|notice)\b|\bdeprecated\b|^(?:warning|error)\s|^\s*Progress: resolved|^Done in|^Packages: /i;
|
|
134
|
+
const out = lines.filter(line => keep.test(line));
|
|
135
|
+
return out.some(line => /^(?:added|removed|changed|audited|up to date|up-to-date|Done in|Packages:)/i.test(line)) ? out : undefined;
|
|
136
|
+
}
|
|
137
|
+
const PARSERS = {
|
|
138
|
+
vitest_jest: vitestJest, node_test: nodeTest, tsc, eslint, pytest, git_diff: gitDiff, git_log: gitLog, npm_install: npmInstall,
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Exact lines selected by the parser for `format`, joined and capped, with the last non-empty lines as a tail so the
|
|
142
|
+
* final status is always present. Undefined when the format is `other` or its markers are absent.
|
|
143
|
+
*/
|
|
144
|
+
export function formatExcerpt(text, format) {
|
|
145
|
+
if (format === "other")
|
|
146
|
+
return undefined;
|
|
147
|
+
const lines = text.split("\n");
|
|
148
|
+
const selected = PARSERS[format](lines);
|
|
149
|
+
if (!selected)
|
|
150
|
+
return undefined;
|
|
151
|
+
const tail = lines.filter(line => line.trim()).slice(-3);
|
|
152
|
+
const body = [];
|
|
153
|
+
let size = 0;
|
|
154
|
+
let dropped = 0;
|
|
155
|
+
for (const line of selected) {
|
|
156
|
+
const clipped = line.length > 400 ? `${line.slice(0, 400)}…` : line;
|
|
157
|
+
if (size + clipped.length + 1 > LIMIT - 400) {
|
|
158
|
+
dropped++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
body.push(clipped);
|
|
162
|
+
size += clipped.length + 1;
|
|
163
|
+
}
|
|
164
|
+
const parts = [`[${format} excerpt: ${selected.length} selected lines${dropped ? `, ${dropped} omitted for size` : ""}]`, ...body];
|
|
165
|
+
// The final status line is the one the agent reads first; add the tail only when the parser did not already keep it.
|
|
166
|
+
// Diffs and logs have no status line; their last lines are hunk or body text.
|
|
167
|
+
const last = tail.at(-1);
|
|
168
|
+
if (format !== "git_diff" && format !== "git_log" && last !== undefined && !body.includes(last))
|
|
169
|
+
parts.push("[tail]", ...tail.map(line => line.length > 400 ? `${line.slice(0, 400)}…` : line));
|
|
170
|
+
return parts.join("\n");
|
|
171
|
+
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ToolCallRef } from "./action-guard.js";
|
|
2
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
3
|
-
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; 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; and redacted tool-output samples for security and
|
|
4
|
+
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; 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; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
4
5
|
/**
|
|
5
6
|
* Tool calls of the assistant message being preflighted. Pi runs `tool_call` hooks for sibling calls one after another,
|
|
6
7
|
* so judging them one request at a time costs one round trip per call; judging them together costs one round trip.
|
|
7
8
|
*/
|
|
8
|
-
export declare function siblingToolCalls(ctx: ExtensionContext):
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
input: Record<string, unknown>;
|
|
12
|
-
}>;
|
|
9
|
+
export declare function siblingToolCalls(ctx: ExtensionContext): ToolCallRef[];
|
|
13
10
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
14
11
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
15
12
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|