pi-warden 0.6.0 → 0.8.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 +30 -14
- package/dist/config.d.ts +33 -1
- package/dist/config.js +38 -5
- package/dist/excerpt.d.ts +25 -0
- package/dist/excerpt.js +171 -0
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +130 -15
- package/dist/index.d.ts +14 -6
- package/dist/index.js +8 -4
- package/dist/notify.d.ts +42 -0
- package/dist/notify.js +99 -0
- package/dist/output.d.ts +26 -4
- package/dist/output.js +31 -5
- package/dist/panel.js +1 -1
- package/dist/recall.d.ts +13 -0
- package/dist/recall.js +68 -0
- package/dist/runaway.d.ts +66 -0
- package/dist/runaway.js +115 -0
- package/dist/saver.d.ts +23 -6
- package/dist/saver.js +44 -6
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +14 -3
- package/dist/trace.d.ts +3 -1
- package/dist/trace.js +10 -0
- package/dist/widget.d.ts +5 -0
- package/dist/widget.js +14 -0
- 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 **runaway** guard that stops a reply repeating the same block over and over, **desktop notifications** when the agent needs you back, 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
|
|
|
@@ -64,6 +64,10 @@ A `TYPESAFE_API_KEY` environment variable takes precedence over the stored key.
|
|
|
64
64
|
|
|
65
65
|
Keeps the last 12 tool results for the current prompt. When the latest result failed and at least 3 failures have accumulated, it first checks for exact repeats offline (same call, same output with timings and addresses normalised). Otherwise one Jev request judges the sequence: `same_strategy` (Noul), `approach_change` (Score: identical / cosmetic / meaningfully different), `progress` (Noul). Same-strategy ≥ 0.7 counts as stuck: you get a notification, and with `nudge: true` (default) the agent gets a steer message asking for a new hypothesis or a blocker report. At most one Jev check per 3 results.
|
|
66
66
|
|
|
67
|
+
### Runaway guard (`message_update`)
|
|
68
|
+
|
|
69
|
+
A model that degenerates mid-reply repeats the same lines over and over — *"PR green. Merge. Executing:"* followed by the same command block, thirty times, and no tool call — until the token limit or you press Esc. Nothing else stops it, because no tool runs and no turn ends. pi-warden reads the stream as it arrives: per token it only appends to a buffer; every 256 characters it counts identical paragraphs (normalised, at least 24 characters) and checks whether the text ends with one unit repeated back to back. When a block repeats `repeats` times (4) in the reply, or `thinkingRepeats` times (10) in thinking, the run is aborted and you get an error-level notification. Code only: nothing is sent anywhere, so this works without TypeSafe consent. With `recover: true` (default) the agent then gets one follow-up turn, shown in the transcript, that names the repeat and asks for the one next step — call the tool, or answer in three sentences, or ask you; a second runaway for the same prompt is stopped and left for you. Calibrated on 43,000 local assistant messages: ordinary replies repeat a paragraph twice at most, thinking up to seven times while drafting code, and the one runaway repeated its block 28 times; the guard stopped only that one, at half its length.
|
|
70
|
+
|
|
67
71
|
### Done-check (`agent_end`)
|
|
68
72
|
|
|
69
73
|
Tracks each run's evidence: code changes (`write`, `edit`) and check commands (`npm test`, `pytest`, `cargo test`, `tsc`, `eslint`, `go test`, `make test`, and similar, whether run through `bash` or `ctx_execute`) with pass/fail; context-mode's inline `Command exited with code N` counts as a failure. When a run ends with a normal assistant message after code changes and no passing check, one Jev request judges the message: `claims_done`, `claims_verified`, `verification_applies` (Noul), `outcome` (Choice: complete / partial / blocked / other). A completion claim ≥ 0.7 that is not a blocker or question, for a task where checks would mean something (≥ 0.5; prose and file housekeeping score ~0.05), is reported as unverified; a verification claim with no check run is reported as a false claim. With `nudge: true` (default) the agent receives one follow-up turn asking it to run the checks or say plainly that nothing was verified — once per user prompt.
|
|
@@ -88,17 +92,25 @@ Nudges from the stuck, done, slop, and prose guards are custom messages in the a
|
|
|
88
92
|
|
|
89
93
|
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
94
|
|
|
91
|
-
###
|
|
95
|
+
### Desktop notifications
|
|
96
|
+
|
|
97
|
+
You are rarely watching the terminal while the agent works, so the moments that need you reach the desktop: a held call the agent is about to ask you about, a confirm dialog waiting for an answer, and a runaway stop. macOS uses `osascript` (Notification Center, with a sound); Linux tries `notify-send`, `dunstify`, `gdbus`, `kdialog`, `zenity`, then `powershell.exe` for WSL; Windows shows a toast through PowerShell. The notifier is probed once per session; the text is the reason, never the command. Interactive sessions only — a headless run or a subagent has nobody to call — and one notification per `cooldownMs` (10 s), so sibling holds in one turn do not stack. `"notify": { "enabled": false }` turns it off; `"command": ["curl", "-d", "{body}", "https://ntfy.sh/your-topic"]` in the user file replaces the desktop tool with your own relay (no shell; `{title}`/`{body}` are replaced and also set as `PI_WARDEN_TITLE`/`PI_WARDEN_BODY`). A project file may switch notifications off but never names a command.
|
|
98
|
+
|
|
99
|
+
### Tool-output security and context saving
|
|
92
100
|
|
|
93
101
|
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
102
|
|
|
95
103
|
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
104
|
|
|
97
|
-
|
|
105
|
+
**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.
|
|
106
|
+
|
|
107
|
+
**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.
|
|
108
|
+
|
|
109
|
+
**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.
|
|
98
110
|
|
|
99
|
-
**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, 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
|
|
111
|
+
**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.
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
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
114
|
|
|
103
115
|
### Task continuity
|
|
104
116
|
|
|
@@ -137,11 +149,12 @@ Templates in `config.widget` control the text. Segments are separated by ` · `;
|
|
|
137
149
|
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
|
|
138
150
|
"prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
|
|
139
151
|
"security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
|
|
140
|
-
"context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes"
|
|
152
|
+
"context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes",
|
|
153
|
+
"runaway": "warden · runaway · {kind} · {count}× repeated · {chars} chars · {signal} · {status}"
|
|
141
154
|
}
|
|
142
155
|
```
|
|
143
156
|
|
|
144
|
-
Tokens — action: `tool level source irreversible offTask scope approved slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time`; prose: `wordy cliches jargon status reasons model ms flags time`; stuck: `failures sameStrategy approachChange progress status source reasons model ms flags time`; done: `changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time`. Security tokens: `tool injection exfiltration status`; context tokens: `tool retention bytesSaved`. A minimal line: `"action": "⚔ {tool} {level} · {irreversible}/{offTask}"`. Set `"enabled": false` to hide the line (the trace and panel keep working); `"shortcut": ""` disables the keybinding.
|
|
157
|
+
Tokens — action: `tool level source irreversible offTask scope approved slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time`; prose: `wordy cliches jargon status reasons model ms flags time`; stuck: `failures sameStrategy approachChange progress status source reasons model ms flags time`; done: `changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time`. Security tokens: `tool injection exfiltration status`; context tokens: `tool retention bytesSaved`; runaway tokens: `kind count chars signal block status time`. A minimal line: `"action": "⚔ {tool} {level} · {irreversible}/{offTask}"`. Set `"enabled": false` to hide the line (the trace and panel keep working); `"shortcut": ""` disables the keybinding.
|
|
145
158
|
|
|
146
159
|
## Configuration
|
|
147
160
|
|
|
@@ -164,7 +177,9 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
164
177
|
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
|
|
165
178
|
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
|
|
166
179
|
"security": { "enabled": true, "threshold": 0.7 },
|
|
167
|
-
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8 },
|
|
180
|
+
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
|
|
181
|
+
"runaway": { "enabled": true, "repeats": 4, "thinkingRepeats": 10, "minChars": 400, "recover": true },
|
|
182
|
+
"notify": { "enabled": true, "cooldownMs": 10000, "command": [] },
|
|
168
183
|
"slop": {
|
|
169
184
|
"enabled": true,
|
|
170
185
|
"threshold": 0.7,
|
|
@@ -175,11 +190,11 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
175
190
|
}
|
|
176
191
|
```
|
|
177
192
|
|
|
178
|
-
A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (for example stricter thresholds, extra guarded tools, or `"done": { "enabled": false }`). Project files are read only when Pi trusts the project, and they can never grant `typesafe` consent, change `mode`,
|
|
193
|
+
A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (for example stricter thresholds, extra guarded tools, or `"done": { "enabled": false }`). Project files are read only when Pi trusts the project, and they can never grant `typesafe` consent, change `mode`, raise `timeoutMs`/`maxRequests`, or set `notify.command`. Environment: `PI_WARDEN_ENABLED=1` (consent), `PI_WARDEN_MODE=steer|confirm|advise`. Files from 0.1.x that set `action.timeoutMs`/`action.maxRequests` keep working.
|
|
179
194
|
|
|
180
195
|
## Data handling
|
|
181
196
|
|
|
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.
|
|
197
|
+
- 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 and the runaway guard send nothing. No unrelated files or telemetry.
|
|
183
198
|
- 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
199
|
- 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
200
|
- Judgments are model output. Thresholds are yours to tune; a hold is information for the agent and for you, not a verdict on either.
|
|
@@ -208,15 +223,16 @@ steerReason(verdict, { canApprove: true }); // the text the agent receives for
|
|
|
208
223
|
|
|
209
224
|
`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.
|
|
210
225
|
|
|
211
|
-
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.
|
|
226
|
+
Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`, `formatVerdict`, the question sets, the stuck detector (`AttemptWindow`, `makeAttempt`, `evaluateStuck`, `stuckNudge`), the runaway guard (`RunawayMonitor`, `findRepeats`, `runawayNudge`), the notifier (`detectNotifier`, `notifierCommand`, `sendNotification`), the done-check (`classifyToolResult`, `recordOutcome`, `needsDoneCheck`, `evaluateDone`, `doneNudge`), and the config helpers.
|
|
212
227
|
|
|
213
228
|
## Development
|
|
214
229
|
|
|
215
230
|
```bash
|
|
216
231
|
npm install
|
|
217
232
|
npm run check # typecheck, offline tests (mocked transport), build
|
|
218
|
-
npm run test:live #
|
|
219
|
-
node scripts/security-cases.mjs
|
|
233
|
+
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
|
|
234
|
+
node scripts/security-cases.mjs # the 9-case output-security and task-continuity set on its own
|
|
235
|
+
node scripts/context-cases.mjs # 15 labelled outputs that tune retention and format for the context saver
|
|
220
236
|
npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
|
|
221
237
|
node scripts/slop-cases.mjs # 22 billable cases that tune the slop and prose questions
|
|
222
238
|
npm run preview # re-render docs/preview.png from the recorded live verdicts (needs a Chrome binary)
|
package/dist/config.d.ts
CHANGED
|
@@ -64,7 +64,37 @@ 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 interface RunawayConfig {
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
/** Identical text paragraphs in one streaming reply before the run is stopped. Ordinary replies repeat a paragraph twice at most. */
|
|
77
|
+
repeats: number;
|
|
78
|
+
/** The same limit for thinking, where code drafting repeats paragraphs legitimately. */
|
|
79
|
+
thinkingRepeats: number;
|
|
80
|
+
/** Characters streamed before the first check. */
|
|
81
|
+
minChars: number;
|
|
82
|
+
/** After stopping, start one follow-up turn that names the repeat and asks for the one next step; once per user prompt. */
|
|
83
|
+
recover: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface NotifyConfig {
|
|
86
|
+
/** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. */
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
/** Sibling holds in one turn produce one notification; a second within this many milliseconds is skipped. */
|
|
89
|
+
cooldownMs: number;
|
|
90
|
+
/**
|
|
91
|
+
* Your own notifier as an argv (no shell), for ssh sessions or a phone relay: `{title}` and `{body}` in an argument are
|
|
92
|
+
* replaced, and both are in PI_WARDEN_TITLE / PI_WARDEN_BODY. Empty: detect the desktop's own tool. User file only.
|
|
93
|
+
*/
|
|
94
|
+
command: string[];
|
|
95
|
+
}
|
|
96
|
+
export type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
|
|
97
|
+
export declare function isRecallTool(value: unknown): value is RecallTool;
|
|
68
98
|
export type WardenMode = "steer" | "confirm" | "advise";
|
|
69
99
|
export interface WardenConfig {
|
|
70
100
|
/** Master switch. false disables every guard, including offline pattern checks. */
|
|
@@ -87,6 +117,8 @@ export interface WardenConfig {
|
|
|
87
117
|
slop: SlopGuardConfig;
|
|
88
118
|
security: SecurityConfig;
|
|
89
119
|
context: ContextConfig;
|
|
120
|
+
runaway: RunawayConfig;
|
|
121
|
+
notify: NotifyConfig;
|
|
90
122
|
/** The status line above the editor and the trace panel. */
|
|
91
123
|
widget: WidgetConfig;
|
|
92
124
|
/** Show steer messages in the transcript. They are always visible in the trace panel. */
|
|
@@ -94,7 +126,7 @@ export interface WardenConfig {
|
|
|
94
126
|
}
|
|
95
127
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
96
128
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
97
|
-
export declare const CONFIG_SCHEMA =
|
|
129
|
+
export declare const CONFIG_SCHEMA = 4;
|
|
98
130
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
|
99
131
|
export declare function defaultConfig(): WardenConfig;
|
|
100
132
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
package/dist/config.js
CHANGED
|
@@ -3,9 +3,13 @@ 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
|
-
export const CONFIG_SCHEMA =
|
|
12
|
+
export const CONFIG_SCHEMA = 4;
|
|
9
13
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
10
14
|
export function defaultConfig() {
|
|
11
15
|
return {
|
|
@@ -26,7 +30,9 @@ 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 },
|
|
34
|
+
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
35
|
+
notify: { enabled: true, cooldownMs: 10000, command: [] },
|
|
30
36
|
widget: defaultWidgetConfig(),
|
|
31
37
|
steerVisible: false,
|
|
32
38
|
};
|
|
@@ -100,6 +106,26 @@ function applyStuck(base, raw) {
|
|
|
100
106
|
nudge: boolean(raw.nudge, base.nudge),
|
|
101
107
|
};
|
|
102
108
|
}
|
|
109
|
+
function applyRunaway(base, raw) {
|
|
110
|
+
if (!isObject(raw))
|
|
111
|
+
return base;
|
|
112
|
+
return {
|
|
113
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
114
|
+
// One occurrence is not a repeat, so the floor is 2.
|
|
115
|
+
repeats: Math.max(2, positiveInteger(raw.repeats, base.repeats)),
|
|
116
|
+
thinkingRepeats: Math.max(2, positiveInteger(raw.thinkingRepeats, base.thinkingRepeats)),
|
|
117
|
+
minChars: positiveInteger(raw.minChars, base.minChars),
|
|
118
|
+
recover: boolean(raw.recover, base.recover),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function applyNotify(base, raw, allowCommand) {
|
|
122
|
+
if (!isObject(raw))
|
|
123
|
+
return base;
|
|
124
|
+
const cooldown = typeof raw.cooldownMs === "number" && Number.isSafeInteger(raw.cooldownMs) && raw.cooldownMs >= 0 ? raw.cooldownMs : base.cooldownMs;
|
|
125
|
+
const command = allowCommand && Array.isArray(raw.command) && raw.command.every((arg) => typeof arg === "string") && (raw.command.length === 0 || raw.command[0].trim())
|
|
126
|
+
? [...raw.command] : base.command;
|
|
127
|
+
return { enabled: boolean(raw.enabled, base.enabled), cooldownMs: cooldown, command };
|
|
128
|
+
}
|
|
103
129
|
function applyDone(base, raw) {
|
|
104
130
|
if (!isObject(raw))
|
|
105
131
|
return base;
|
|
@@ -138,6 +164,7 @@ function applyWidget(base, raw) {
|
|
|
138
164
|
prose: template(raw.prose, base.prose),
|
|
139
165
|
security: template(raw.security, base.security),
|
|
140
166
|
context: template(raw.context, base.context),
|
|
167
|
+
runaway: template(raw.runaway, base.runaway),
|
|
141
168
|
};
|
|
142
169
|
}
|
|
143
170
|
/** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
|
|
@@ -148,8 +175,11 @@ function applyShared(base, raw) {
|
|
|
148
175
|
maxRequests: positiveInteger(raw.maxRequests ?? legacy.maxRequests, base.maxRequests),
|
|
149
176
|
};
|
|
150
177
|
}
|
|
151
|
-
function applyGuards(base, raw, timeoutMs) {
|
|
178
|
+
function applyGuards(base, raw, timeoutMs, source) {
|
|
152
179
|
return {
|
|
180
|
+
runaway: applyRunaway(base.runaway, raw.runaway),
|
|
181
|
+
// A project file may switch notifications off or on, but never names a command to run.
|
|
182
|
+
notify: applyNotify(base.notify, raw.notify, source === "user"),
|
|
153
183
|
action: applyAction(base.action, raw.action, timeoutMs),
|
|
154
184
|
stuck: applyStuck(base.stuck, raw.stuck),
|
|
155
185
|
done: applyDone(base.done, raw.done),
|
|
@@ -162,6 +192,9 @@ function applyGuards(base, raw, timeoutMs) {
|
|
|
162
192
|
enabled: boolean(raw.context.enabled, base.context.enabled),
|
|
163
193
|
tailMinChars: positiveInteger(raw.context.tailMinChars, base.context.tailMinChars),
|
|
164
194
|
confidence: probability(raw.context.confidence, base.context.confidence),
|
|
195
|
+
duplicateMinChars: positiveInteger(raw.context.duplicateMinChars, base.context.duplicateMinChars),
|
|
196
|
+
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
197
|
+
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
165
198
|
} : base.context,
|
|
166
199
|
};
|
|
167
200
|
}
|
|
@@ -175,7 +208,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
175
208
|
typesafe: boolean(raw.typesafe, base.typesafe),
|
|
176
209
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
177
210
|
...shared,
|
|
178
|
-
...applyGuards(base, raw, shared.timeoutMs),
|
|
211
|
+
...applyGuards(base, raw, shared.timeoutMs, "user"),
|
|
179
212
|
widget: applyWidget(base.widget, raw.widget),
|
|
180
213
|
steerVisible: boolean(raw.steerVisible, base.steerVisible),
|
|
181
214
|
};
|
|
@@ -184,7 +217,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
184
217
|
export function applyProjectOverrides(base, raw) {
|
|
185
218
|
if (!isObject(raw))
|
|
186
219
|
return base;
|
|
187
|
-
return { ...base, enabled: boolean(raw.enabled, base.enabled), ...applyGuards(base, raw, base.timeoutMs) };
|
|
220
|
+
return { ...base, enabled: boolean(raw.enabled, base.enabled), ...applyGuards(base, raw, base.timeoutMs, "project") };
|
|
188
221
|
}
|
|
189
222
|
export function loadConfig(options = {}) {
|
|
190
223
|
let config = applyUserOverrides(defaultConfig(), readJson(userConfigPath()));
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
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
|
|
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.";
|
|
5
5
|
/**
|
|
6
6
|
* Tool calls of the assistant message being preflighted. Pi runs `tool_call` hooks for sibling calls one after another,
|
|
7
7
|
* so judging them one request at a time costs one round trip per call; judging them together costs one round trip.
|