pi-warden 0.40.1 → 0.47.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/CHANGELOG.md +72 -0
- package/README.md +1 -1
- package/dist/action-guard.d.ts +11 -4
- package/dist/action-guard.js +1 -1
- package/dist/backend.d.ts +4 -0
- package/dist/backend.js +4 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +15 -1
- package/dist/conscience.d.ts +9 -3
- package/dist/conscience.js +13 -4
- package/dist/extension.d.ts +8 -1
- package/dist/extension.js +246 -72
- package/dist/guard.d.ts +104 -5
- package/dist/guard.js +348 -12
- package/dist/holds.d.ts +2 -0
- package/dist/holds.js +5 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/judge-cooldown.d.ts +44 -0
- package/dist/judge-cooldown.js +92 -0
- package/dist/panel.d.ts +5 -2
- package/dist/panel.js +5 -2
- package/dist/redact.d.ts +5 -3
- package/dist/redact.js +17 -4
- package/dist/rules-file.d.ts +23 -8
- package/dist/rules-file.js +61 -31
- package/dist/rules.d.ts +15 -1
- package/dist/rules.js +35 -8
- package/dist/shape.d.ts +46 -0
- package/dist/shape.js +76 -1
- package/dist/trace-file.d.ts +50 -0
- package/dist/trace-file.js +141 -0
- package/dist/trace.d.ts +12 -1
- package/dist/trace.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,78 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
8
8
|
|
|
9
9
|
<!-- Empty. Next release starts here. -->
|
|
10
10
|
|
|
11
|
+
## 0.47.0
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- A recursive `rm` whose every target is scratch the agent created in this session under the OS temp directory (`mkdir`, `mktemp`, a `write`, or a temp directory a command printed and created) warns as `rm-session-scratch` instead of holding as destructive. Targets are resolved through `..` and symlinks first; a temp root, a wildcard, a variable, a substitution, `sudo` or another privilege prefix, content older than the directory it sits in, or any target that was not created this session (or was replaced since) keeps the hold. The created paths live in memory and reset on `session_start`.
|
|
16
|
+
|
|
17
|
+
## 0.46.0
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- When a guard would ask Jev but cannot, Warden says so once per session and reason, with the fix: `warden: Jev judgments are off (no consent). Run /warden enable.` (`Set PI_WARDEN_ENABLED=1.` headless), `(no key for <backend>). Set <key variable>.` (with ` or run /typesafe login` on the TypeSafe backend, the only one login stores a key for), or for a rejected key `(the key saved by /typesafe login was rejected). Run /typesafe login.` or `(the key in <key variable> was rejected). Check the key, then run /warden status.` A headless session gets a status message. A spent request budget keeps its own warning. Before this, judgments stopped silently.
|
|
22
|
+
- Trace file: the `session` line has `judgments` (`on`, or `off:<reason>` with reason `no_consent`, `no_key`, `key_rejected`, or `budget`), and a `judgments` line records a later change.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The conscience traces why no judge was available (`no_consent`, `no_key`, `key_rejected`, or `budget`). Before this, it traced `no_consent` whenever the judge was missing, also when consent was given and the saved key was rejected.
|
|
27
|
+
|
|
28
|
+
## 0.45.0
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- A judge cooldown. After three consecutive timeout, network, or other failures, or one auth or configuration failure, pi-warden stops asking Jev for a minute instead of paying a full request timeout on every guarded action. One warning names the failure kind, the duration, and for an auth failure the fix; one info notice says when judgments resume. Guards run as they do with no judge configured, nothing is sent during the window, and `/warden status` counts the checks that ran without Jev. Configure with `judge.failuresBeforeCooldown` and `judge.cooldownMs` (capped at 10 minutes); state is per session. A malformed request that pi-typesafe rejects locally does not count. A request that hits pi-warden's own `timeoutMs` reaches the SDK as an abort, so it is told apart from a user's cancel by the signal's reason and counted as a timeout.
|
|
33
|
+
|
|
34
|
+
## 0.44.1
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- The credential and untrusted-output notice rides the tool result: the banner in the result content and the trace record stay, and the separate steer message is gone, so a notice on the last tool result of a turn no longer costs an extra model turn.
|
|
39
|
+
- Credential detection no longer fires on placeholder values (`<redacted>`, `<...>`, `***`, `REDACTED`, `xxxx`), plain numbers with `_` or `,` separators, or a key name whose value is only an identifier or expression in code (`findSecrets(text)`, `output.secretIds`).
|
|
40
|
+
|
|
41
|
+
## 0.44.0
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- `PI_WARDEN_TRACE_DIR`: with an absolute path, Warden appends its trace to `<path>/<session id>.jsonl`, one owner-only file per Pi session, so a host that runs Pi in RPC mode can read what the status line and the sidebar would show. Each line is a JSON object with `"v": 1` and a `kind`: `session` (session id, `~`-shortened working directory, Warden version, mode, time), `entry` (a numeric `id` unique in the file, time, and the trace entry as stored), or `amend` (the entry `id`, the added line, time). The 100-entry limit of the sidebar does not apply to the file. A write error is reported once and stops the file for the session; guards and tool calls do not change. Unset, empty, or relative: no file. `docs/configuration.md` lists the record format and `docs/data-handling.md` lists the file.
|
|
46
|
+
- `/warden trace` in RPC mode sends the last 20 trace entries as one text notification, newest last, with the trace file path when the file is on. RPC mode settles the overlay request without building the sidebar, so before this the command did nothing there. The terminal sidebar does not change.
|
|
47
|
+
|
|
48
|
+
## 0.43.0
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- The off-task judgment and the conscience recommendation both judge with a task spine in the request state: the thread's first user turn (the goal), the latest user turn, and up to four earlier user turns (newest first). The whole spine is capped at 1200 characters — `task_history` is clipped first, then `goal`; the latest turn is never clipped. A follow-up like "now the tests" is no longer judged without the goal it belongs to. Approval still comes from the latest user turn only; the spine is context, never authorization, and no threshold moved (the conscience beta policy's pinned questionHash still matches). The action approval question now names `spine` beside `context` as text that cannot grant approval. A thread with one user turn sends no spine, so the budget is not spent on a copy of the task. A queued prompt admitted at `message_start` is also assessed with the spine. Both request paths cap a spine they receive at four history entries and the same per-field lengths (goal 1200, each entry 750 characters), whoever built it. After a compaction the goal is still the thread's first user turn: the branch keeps the summarized entries, and a compaction summary never becomes the goal. `docs/data-handling.md` lists the spine for the action guard and the conscience.
|
|
53
|
+
|
|
54
|
+
## 0.42.1
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- A write the action guard holds, denies, or the user declines no longer gets a rules or slop steer. The steer said "the content just written" about content that was never written. A confirm-dialog write gets the steer after the user allows it. An approved retry of a held write is judged again and gets its own steer. The trace still records the rule findings of the held write, marked as not told.
|
|
59
|
+
|
|
60
|
+
## 0.42.0
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- A judged `bash` call now also asks whether the command will print far more output than the agent needs. At or above `context.largeOutput.threshold` (default `0.85`) the agent is told once per command family per session to redirect or filter it, for example to a file with `tail -40`. The call is never held. `context.largeOutput.enabled: false` removes the question. `scripts/context-cases.mjs` has eight labelled commands to calibrate it.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- The rules guard no longer judges writes to files outside the project root or paths ignored by the project's `.gitignore`. Those files are not project code and the rules in `pi-warden.md` do not apply to them.
|
|
69
|
+
|
|
70
|
+
## 0.41.0
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- The A/B eval (`npm run eval:ab`) now scores two more axes per run, shown side by side for the two cells in an "Outcome and waste" section of the report. Outcome: whether every check the task declares passes when the runner re-runs it, the diff violation count, and whether the final reply claimed tests/build success without the agent ever running that check. Waste, read from the saved session log (`eval/waste.mjs`): tool-call count, retries (same tool, same or near-same input, after a failure), reverts (a `git checkout`/`git restore` naming a path, or a `write` restoring a file to earlier content), total tokens, and wall seconds.
|
|
75
|
+
|
|
76
|
+
## 0.40.2
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- The first-run notice no longer names a fallback document when `rules.files` is what resolved. It was decided by whether `pi-warden.md` existed, so a project whose rules come from `rules.files` was told `AGENTS.md` was judging it while its own configured files were the ones in force — and the notice's remedy, creating a `pi-warden.md`, is the one thing that shadows those files. The notice now asks the rule store which tier answered and fires only when a fallback document really is in force, listing `rules.files` among the tiers it looked at.
|
|
81
|
+
- The escalation request carries the rules the config resolves, not a document of the resolver's own choosing. It walked `pi-warden.md` and then `AGENTS.md` / `CLAUDE.md` / `README.md` while the rules guard was judging with `rules.files`, so a project with configured rules sent `AGENTS.md` prose as its rules, or no rules at all when it had no such file. It now takes every file in `rules.files`, in order, before the fallback names, and honors `rules.fallback: false`.
|
|
82
|
+
|
|
11
83
|
## 0.40.1
|
|
12
84
|
|
|
13
85
|
### Fixed
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ paths: src/**/*.ts
|
|
|
68
68
|
Every exported function declares its return type.
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Jev judges every write and quotes violations back. Rules can be things no linter checks: "a TODO must name a ticket", "comments must not restate the code". Without `pi-warden.md`, the first of `
|
|
71
|
+
Jev judges every write and quotes violations back. Rules can be things no linter checks: "a TODO must name a ticket", "comments must not restate the code". Without `pi-warden.md`, the first of `AGENTS.md`/`CLAUDE.md`/`README.md` is judged instead. [Examples and config details.](docs/configuration.md)
|
|
72
72
|
|
|
73
73
|
## Does it actually help?
|
|
74
74
|
|
package/dist/action-guard.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ActionGuardConfig,
|
|
2
|
-
import type { PreviousAction, TaskMessage, Verdict } from "./guard.js";
|
|
1
|
+
import type { ActionGuardConfig, LargeOutputConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
2
|
+
import type { EvaluateOptions, PreviousAction, ScratchRecords, TaskMessage, Verdict } from "./guard.js";
|
|
3
|
+
import type { TaskSpine } from "./shape.js";
|
|
3
4
|
import type { Judge } from "pi-typesafe";
|
|
4
5
|
/** One tool call as the agent proposed it. `id` is Pi's tool call id, stable across hooks and retries. */
|
|
5
6
|
export interface ToolCallRef {
|
|
@@ -12,6 +13,8 @@ export interface Conversation {
|
|
|
12
13
|
task: string | undefined;
|
|
13
14
|
/** Scope context only: approval still comes from `task`, never from this history. */
|
|
14
15
|
context?: readonly TaskMessage[] | undefined;
|
|
16
|
+
/** The task spine: the thread's goal and earlier user turns, so follow-ups are judged with the goal they belong to. Scope context only: it never authorizes; approval still comes from `task`. */
|
|
17
|
+
spine?: TaskSpine | undefined;
|
|
15
18
|
/** Tool calls in the same assistant message, this one included; they are judged together. */
|
|
16
19
|
siblings?: readonly ToolCallRef[] | undefined;
|
|
17
20
|
/** The agent's own words in that message (or its latest text under this prompt); shared by the siblings. Explains, never authorizes. */
|
|
@@ -25,10 +28,14 @@ export interface InspectOptions {
|
|
|
25
28
|
signal?: AbortSignal | undefined;
|
|
26
29
|
slop?: SlopGuardConfig | undefined;
|
|
27
30
|
security?: SecurityConfig | undefined;
|
|
28
|
-
/**
|
|
29
|
-
|
|
31
|
+
/** Adds the large-output question to bash requests. */
|
|
32
|
+
largeOutput?: LargeOutputConfig | undefined;
|
|
33
|
+
/** The rules guard's switch, forwarded to `evaluateAction`: off keeps the rules content out of the request. */
|
|
34
|
+
rules?: EvaluateOptions["rules"];
|
|
30
35
|
/** Calls allowed in the previous turn; the regret question about them rides this call's request, never a sibling's. */
|
|
31
36
|
previousActions?: readonly PreviousAction[] | undefined;
|
|
37
|
+
/** Real paths the agent created under the temp directory in this session, forwarded to `evaluateAction`. */
|
|
38
|
+
scratch?: ScratchRecords | undefined;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* The Action guard for one session. `evaluateAction` judges a single call; this module owns what spans calls:
|
package/dist/action-guard.js
CHANGED
|
@@ -20,7 +20,7 @@ export class ActionGuard {
|
|
|
20
20
|
const { task } = conversation;
|
|
21
21
|
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
22
22
|
const retryAfterHold = this.holdPending && this.lastHoldPrompt !== task;
|
|
23
|
-
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, rules: options.rules, retryAfterHold, previousActions });
|
|
23
|
+
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan, spine: conversation.spine }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, largeOutput: options.largeOutput, rules: options.rules, retryAfterHold, previousActions, scratch: options.scratch });
|
|
24
24
|
// A retry after a hold stays sequential because an approval consumed by one sibling changes the question for the next.
|
|
25
25
|
if (options.judge && !retryAfterHold) {
|
|
26
26
|
for (const sibling of conversation.siblings ?? []) {
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { TypeSafeOptions } from "pi-typesafe";
|
|
2
2
|
/** The judgment backend that receives pi-warden's Jev requests. */
|
|
3
3
|
export type JudgmentBackend = "typesafe" | "openrouter";
|
|
4
|
+
/** Why no judge is available: consent not given, no key for the backend, a saved 401 or 403, or the request budget spent. */
|
|
5
|
+
export type JudgmentsOffReason = "no_consent" | "no_key" | "key_rejected" | "budget";
|
|
4
6
|
/** The host the consent disclosure names as the destination, without scheme: `api.typesafe.ai`, `openrouter.ai`. */
|
|
5
7
|
export declare function backendHost(backend: JudgmentBackend): string;
|
|
6
8
|
/** The environment variable that carries the backend's key, for messages that tell the user what to set. */
|
|
7
9
|
export declare function keyEnvFor(backend: JudgmentBackend): string;
|
|
10
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; pi-typesafe's `usesTypesafeKey`, which its package does not export. */
|
|
11
|
+
export declare function loginStoresKey(backend: JudgmentBackend): boolean;
|
|
8
12
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
9
13
|
export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
|
|
10
14
|
/**
|
package/dist/backend.js
CHANGED
|
@@ -7,6 +7,10 @@ export function backendHost(backend) {
|
|
|
7
7
|
export function keyEnvFor(backend) {
|
|
8
8
|
return DECISIONS_BACKENDS[backend].keyEnv ?? DECISIONS_BACKENDS[DEFAULT_BACKEND].keyEnv ?? "TYPESAFE_API_KEY";
|
|
9
9
|
}
|
|
10
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; pi-typesafe's `usesTypesafeKey`, which its package does not export. */
|
|
11
|
+
export function loginStoresKey(backend) {
|
|
12
|
+
return keyEnvFor(backend) === keyEnvFor(DEFAULT_BACKEND);
|
|
13
|
+
}
|
|
10
14
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
11
15
|
export function disclosureFor(backend, disclosure) {
|
|
12
16
|
return backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
package/dist/config.d.ts
CHANGED
|
@@ -193,6 +193,13 @@ export interface ContextConfig {
|
|
|
193
193
|
formatConfidence: number;
|
|
194
194
|
/** Append a compact evidence appendix to the summary during compaction. */
|
|
195
195
|
compactAppendix: boolean;
|
|
196
|
+
/** Prevention before the call: a bash action request asks whether the command will print far more than the agent needs. Never holds. */
|
|
197
|
+
largeOutput: LargeOutputConfig;
|
|
198
|
+
}
|
|
199
|
+
export interface LargeOutputConfig {
|
|
200
|
+
enabled: boolean;
|
|
201
|
+
/** P(the command prints far more than the agent needs) at or above which the agent is steered once per command family per session. */
|
|
202
|
+
threshold: number;
|
|
196
203
|
}
|
|
197
204
|
export interface RunawayConfig {
|
|
198
205
|
enabled: boolean;
|
|
@@ -205,6 +212,12 @@ export interface RunawayConfig {
|
|
|
205
212
|
/** After stopping, start one follow-up turn that names the repeat and asks for the one next step; once per user prompt. */
|
|
206
213
|
recover: boolean;
|
|
207
214
|
}
|
|
215
|
+
export interface JudgeConfig {
|
|
216
|
+
/** Consecutive timeout, network, or other failures before judgments pause; one auth or configuration failure is enough. */
|
|
217
|
+
failuresBeforeCooldown: number;
|
|
218
|
+
/** How long a failing judge is left alone; guards run pattern-only until the next request after it. */
|
|
219
|
+
cooldownMs: number;
|
|
220
|
+
}
|
|
208
221
|
export interface NotifyConfig {
|
|
209
222
|
/** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. Off by default; opt in per user or project. */
|
|
210
223
|
enabled: boolean;
|
|
@@ -304,6 +317,7 @@ export interface WardenConfig {
|
|
|
304
317
|
context: ContextConfig;
|
|
305
318
|
runaway: RunawayConfig;
|
|
306
319
|
notify: NotifyConfig;
|
|
320
|
+
judge: JudgeConfig;
|
|
307
321
|
/** Triage of async subagent reports: Jev separates what needs the agent awake from what is only context. */
|
|
308
322
|
subagent: SubagentConfig;
|
|
309
323
|
/** The status line above the editor and the trace panel. */
|
package/dist/config.js
CHANGED
|
@@ -44,9 +44,10 @@ export function defaultConfig() {
|
|
|
44
44
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
45
45
|
security: { enabled: true, threshold: 0.7 },
|
|
46
46
|
rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
47
|
-
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true },
|
|
47
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, largeOutput: { enabled: true, threshold: 0.85 } },
|
|
48
48
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
49
49
|
notify: { enabled: false, cooldownMs: 10000, command: [] },
|
|
50
|
+
judge: { cooldownMs: 60000, failuresBeforeCooldown: 3 },
|
|
50
51
|
subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
|
|
51
52
|
widget: defaultWidgetConfig(),
|
|
52
53
|
steerVisible: false,
|
|
@@ -322,6 +323,14 @@ function applyNotify(base, raw, allowCommand) {
|
|
|
322
323
|
? [...raw.command] : base.command;
|
|
323
324
|
return { enabled: boolean(raw.enabled, base.enabled), cooldownMs: cooldown, command };
|
|
324
325
|
}
|
|
326
|
+
/** A project config cannot switch the judge off for longer than this by setting a huge cooldown. */
|
|
327
|
+
const MAX_JUDGE_COOLDOWN_MS = 10 * 60 * 1000;
|
|
328
|
+
function applyJudge(base, raw) {
|
|
329
|
+
if (!isObject(raw))
|
|
330
|
+
return base;
|
|
331
|
+
const cooldown = typeof raw.cooldownMs === "number" && Number.isSafeInteger(raw.cooldownMs) && raw.cooldownMs >= 0 ? Math.min(raw.cooldownMs, MAX_JUDGE_COOLDOWN_MS) : base.cooldownMs;
|
|
332
|
+
return { cooldownMs: cooldown, failuresBeforeCooldown: positiveInteger(raw.failuresBeforeCooldown, base.failuresBeforeCooldown) };
|
|
333
|
+
}
|
|
325
334
|
function applySubagent(base, raw) {
|
|
326
335
|
if (!isObject(raw))
|
|
327
336
|
return base;
|
|
@@ -407,6 +416,7 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
407
416
|
subagent: applySubagent(base.subagent, raw.subagent),
|
|
408
417
|
// A project file may switch notifications off or on, but never names a command to run.
|
|
409
418
|
notify: applyNotify(base.notify, raw.notify, source === "user"),
|
|
419
|
+
judge: applyJudge(base.judge, raw.judge),
|
|
410
420
|
action: applyAction(base.action, raw.action, timeoutMs, source),
|
|
411
421
|
stuck: applyStuck(base.stuck, raw.stuck),
|
|
412
422
|
done: applyDone(base.done, raw.done),
|
|
@@ -423,6 +433,10 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
423
433
|
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
424
434
|
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
425
435
|
compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
|
|
436
|
+
largeOutput: isObject(raw.context.largeOutput) ? {
|
|
437
|
+
enabled: boolean(raw.context.largeOutput.enabled, base.context.largeOutput.enabled),
|
|
438
|
+
threshold: probability(raw.context.largeOutput.threshold, base.context.largeOutput.threshold),
|
|
439
|
+
} : base.context.largeOutput,
|
|
426
440
|
} : base.context,
|
|
427
441
|
};
|
|
428
442
|
}
|
package/dist/conscience.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { Questions } from "pi-typesafe";
|
|
9
|
+
import type { JudgmentsOffReason } from "./backend.js";
|
|
9
10
|
import type { ConscienceConfig } from "./config.js";
|
|
11
|
+
import type { TaskSpine } from "./shape.js";
|
|
10
12
|
export type Disposition = "advance" | "awaiting_user" | "no_gap" | "unclear";
|
|
11
13
|
/** Capability roles the session model assigns to index entries. */
|
|
12
14
|
export type CapabilityRole = "research" | "evidence" | "execution" | "delegation" | "review" | "conversation";
|
|
@@ -64,7 +66,7 @@ export interface AssessmentResult {
|
|
|
64
66
|
/** Number of judge requests issued for this assessment. */
|
|
65
67
|
requestCount: number;
|
|
66
68
|
}
|
|
67
|
-
export type SkipReason = "disabled" | "no_consent" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
69
|
+
export type SkipReason = "disabled" | "no_consent" | "no_key" | "key_rejected" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
68
70
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
69
71
|
export declare const SCORE_LEVELS: readonly ["No useful contribution to the current request, or conflicts with the supplied constraints.", "Related to the subject, but already covered, premature, or unable to resolve the current need.", "Addresses a concrete unmet need at the current step without displacing the active workflow.", "Supplies a missing prerequisite or directly applicable documented method needed for the current step."];
|
|
70
72
|
/** Strip absolute paths, URLs, and credentials from a description. */
|
|
@@ -91,11 +93,13 @@ export declare function buildBatchQuestions(batch: Array<{
|
|
|
91
93
|
};
|
|
92
94
|
/**
|
|
93
95
|
* Build the full state object for the judge request (spec §3 state contract).
|
|
96
|
+
* `spine`, when given, carries the task spine — the thread's goal and earlier user turns — beside `task`;
|
|
97
|
+
* scope context only, it never authorizes anything and it is not part of any question.
|
|
94
98
|
*/
|
|
95
99
|
export declare function buildState(task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[], batch: Array<{
|
|
96
100
|
opaqueId: string;
|
|
97
101
|
candidate: Candidate;
|
|
98
|
-
}
|
|
102
|
+
}>, spine?: TaskSpine | undefined): Record<string, unknown>;
|
|
99
103
|
/**
|
|
100
104
|
* Compute a deterministic hash of the assessment questions for deduplication.
|
|
101
105
|
* Recursively sorts all object keys for canonical form.
|
|
@@ -138,6 +142,8 @@ export interface Judge {
|
|
|
138
142
|
}
|
|
139
143
|
export interface ConscienceDeps {
|
|
140
144
|
judge: Judge | undefined;
|
|
145
|
+
/** Why `judge` is undefined, as the extension computed it; the skip reason. `no_consent` when not given. */
|
|
146
|
+
judgmentsOff?: JudgmentsOffReason | undefined;
|
|
141
147
|
config: ConscienceConfig;
|
|
142
148
|
/** Shared timeout from WardenConfig. The effective deadline is min(conscience.timeoutMs, sharedTimeoutMs). */
|
|
143
149
|
sharedTimeoutMs: number;
|
|
@@ -160,4 +166,4 @@ export interface ConscienceDeps {
|
|
|
160
166
|
export declare function assess(prompt: string, recentContext: string, skills: Skill[], tools: {
|
|
161
167
|
name: string;
|
|
162
168
|
description: string;
|
|
163
|
-
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps): Promise<AssessmentResult>;
|
|
169
|
+
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps, spine?: TaskSpine | undefined): Promise<AssessmentResult>;
|
package/dist/conscience.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { choice, noul, score } from "pi-typesafe";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { redact } from "./redact.js";
|
|
4
|
+
import { SPINE_GOAL_LIMIT, SPINE_HISTORY_LIMIT, SPINE_HISTORY_TURNS } from "./shape.js";
|
|
4
5
|
import { fileContentHash, toolSourceHash } from "./hashing.js";
|
|
5
6
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
6
7
|
export const SCORE_LEVELS = [
|
|
@@ -70,8 +71,10 @@ export function buildBatchQuestions(batch, task, recentContext, activeSkills, su
|
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
72
73
|
* Build the full state object for the judge request (spec §3 state contract).
|
|
74
|
+
* `spine`, when given, carries the task spine — the thread's goal and earlier user turns — beside `task`;
|
|
75
|
+
* scope context only, it never authorizes anything and it is not part of any question.
|
|
73
76
|
*/
|
|
74
|
-
export function buildState(task, recentContext, activeSkills, suppliedSkills, batch) {
|
|
77
|
+
export function buildState(task, recentContext, activeSkills, suppliedSkills, batch, spine) {
|
|
75
78
|
const candidates = {};
|
|
76
79
|
for (const { opaqueId, candidate } of batch) {
|
|
77
80
|
const entry = candidate.indexEntry;
|
|
@@ -82,6 +85,12 @@ export function buildState(task, recentContext, activeSkills, suppliedSkills, ba
|
|
|
82
85
|
return {
|
|
83
86
|
task: sanitizeDescription(redact(task)),
|
|
84
87
|
context: sanitizeDescription(redact(recentContext)),
|
|
88
|
+
// The spine's `task` is deliberately not repeated here: the `task` field above already carries it.
|
|
89
|
+
// Same per-field limits as the action request, for a spine built outside taskSpine.
|
|
90
|
+
...(spine ? { spine: {
|
|
91
|
+
goal: sanitizeDescription(redact(spine.goal)).slice(0, SPINE_GOAL_LIMIT),
|
|
92
|
+
task_history: spine.history.slice(0, SPINE_HISTORY_TURNS).map(turn => sanitizeDescription(redact(turn)).slice(0, SPINE_HISTORY_LIMIT)),
|
|
93
|
+
} } : {}),
|
|
85
94
|
active_skills: activeSkills,
|
|
86
95
|
supplied_skills: suppliedSkills,
|
|
87
96
|
candidates,
|
|
@@ -276,14 +285,14 @@ function parseScoreAnswer(answer) {
|
|
|
276
285
|
* Pre-measurement: returns trace-only results. No steers, no auto-loading.
|
|
277
286
|
* Issues concurrent requests (up to four in flight), skills first, shared deadline.
|
|
278
287
|
*/
|
|
279
|
-
export async function assess(prompt, recentContext, skills, tools, activeSkills, suppliedSkills, deps) {
|
|
288
|
+
export async function assess(prompt, recentContext, skills, tools, activeSkills, suppliedSkills, deps, spine) {
|
|
280
289
|
const start = deps.now?.() ?? Date.now();
|
|
281
290
|
const { config, judge, sharedTimeoutMs } = deps;
|
|
282
291
|
if (!config.enabled) {
|
|
283
292
|
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "disabled" };
|
|
284
293
|
}
|
|
285
294
|
if (!judge) {
|
|
286
|
-
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: "no_consent" };
|
|
295
|
+
return { disposition: "no_gap", selected: null, usefulness: 0, pAdvance: 0, questionHash: "", elapsedMs: 0, requestCount: 0, skipReason: deps.judgmentsOff ?? "no_consent" };
|
|
287
296
|
}
|
|
288
297
|
const { candidates, skillOverflow, toolOverflow } = eligibleCandidates(skills, tools, config, activeSkills, suppliedSkills, deps.globalIndex, deps.projectIndex);
|
|
289
298
|
if (candidates.length === 0) {
|
|
@@ -345,7 +354,7 @@ export async function assess(prompt, recentContext, skills, tools, activeSkills,
|
|
|
345
354
|
const opaqueBatch = assignOpaqueIds(work.items.map(e => e.capped.candidate));
|
|
346
355
|
const { questions, dispositionKey } = buildBatchQuestions(opaqueBatch, prompt, recentContext, activeSkills, suppliedSkills);
|
|
347
356
|
state_.hash = questionHash(questions);
|
|
348
|
-
const state = buildState(prompt, recentContext, activeSkills, suppliedSkills, opaqueBatch);
|
|
357
|
+
const state = buildState(prompt, recentContext, activeSkills, suppliedSkills, opaqueBatch, spine);
|
|
349
358
|
const remaining = deadline - (deps.now?.() ?? Date.now());
|
|
350
359
|
if (remaining <= 0) {
|
|
351
360
|
categoryFailed[work.category] = true;
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { JudgmentBackend, JudgmentsOffReason } from "./backend.js";
|
|
2
3
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
4
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
5
|
import type { ShapeResult } from "./shape.js";
|
|
5
|
-
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name, role, lead, useWhen, examples when an index entry matches; bare description otherwise; full skill instructions never go to Jev). The index is built locally by the session model; only sanitized entries reach Jev; advertised locations never do. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
|
+
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request, the task spine it is judged against (the first request of the thread and up to four redacted earlier requests), and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request unless the rules guard is off (`rules.enabled: false`), which keeps that content on this machine; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. For the conscience coach (recommend mode): your current request (2000 redacted characters), the same task spine (the first request of the thread and up to four redacted earlier requests), up to four recent user/assistant text messages (500 redacted characters each with roles), and sanitized candidate metadata (skill/tool name, role, lead, useWhen, examples when an index entry matches; bare description otherwise; full skill instructions never go to Jev). The index is built locally by the session model; only sanitized entries reach Jev; advertised locations never do. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
7
|
/** Which guard spent the user's attention. The status line reports one count per guard. */
|
|
7
8
|
export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "prose" | "runaway" | "subagent" | "conscience";
|
|
8
9
|
/**
|
|
@@ -24,6 +25,12 @@ export declare function siblingToolCalls(ctx: ExtensionContext): ToolCallRef[];
|
|
|
24
25
|
* tool calls only, the latest assistant text since the user's prompt. Sent as `plan`; it explains the step and cannot approve it.
|
|
25
26
|
*/
|
|
26
27
|
export declare function assistantPlan(ctx: ExtensionContext): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The once-per-session notice: why Jev judgments are off and what turns them on. `rejectedEnv` is the environment
|
|
30
|
+
* variable that holds a rejected key, or undefined when the rejected key is the one `/typesafe login` saved. Never
|
|
31
|
+
* names a key value or a path.
|
|
32
|
+
*/
|
|
33
|
+
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">, backend: JudgmentBackend, headless: boolean, rejectedEnv?: string): string;
|
|
27
34
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
28
35
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
29
36
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|