pi-warden 0.73.0 → 0.74.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 +17 -0
- package/dist/backend.d.ts +33 -14
- package/dist/backend.js +27 -14
- package/dist/config.d.ts +4 -2
- package/dist/config.js +5 -2
- package/dist/conscience.d.ts +1 -1
- package/dist/extension.d.ts +2 -2
- package/dist/extension.js +58 -20
- package/dist/shape.js +8 -2
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,23 @@ 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.74.0
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `typesafeBackend` accepts `"commandcode"`: judgments go to api.commandcode.ai under `/provider/v1/systemone`, with the key from `COMMANDCODE_API_KEY` and the model `typesafe/jev`.
|
|
16
|
+
- `typesafeBackend` accepts a caller-supplied endpoint object (`{ "label", "host", "path", "keyEnv", "defaultModel" }`, plus optional model-list fields) for a gateway that serves the same decisions protocol (pi-typesafe 0.8.0). The object is passed to the judge as written and validated on every call. It reads only its own `keyEnv` variable: the TypeSafe key and the `/typesafe login` store are never sent to it. `/warden status`, the `/warden enable` dialog, and the `/warden test` confirmation name the label, host, and model sent for any non-TypeSafe backend, and the consent disclosure names the real destination host.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- An unknown `typesafeBackend` name, or an endpoint object the judge refuses, no longer silently falls back to `"typesafe"`: judgments turn off, the refusal message is shown once and in `/warden status`, and nothing is sent to api.typesafe.ai because a value was mistyped.
|
|
21
|
+
|
|
22
|
+
## 0.73.1
|
|
23
|
+
|
|
24
|
+
### Tests
|
|
25
|
+
|
|
26
|
+
- `tests/host-compat.test.ts`: the fake host's per-handler timeout stays referenced while a dispatch runs and is cleared before `emit()` returns, so an awaited `emit()` always has a loop wakeup. The timer was unref'd, so on Node 22 the event loop could drain mid-dispatch and node:test cancelled the rest of the file with `Promise resolution is still pending but the event loop has already resolved`.
|
|
27
|
+
|
|
11
28
|
## 0.73.0
|
|
12
29
|
|
|
13
30
|
### Fixed
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import type { TypeSafeOptions } from "pi-typesafe";
|
|
2
|
-
/** The judgment backend
|
|
3
|
-
export type JudgmentBackend =
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
/** The
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { BackendSpec, TypeSafeOptions } from "pi-typesafe";
|
|
2
|
+
/** The judgment backend: a registry name ("typesafe", "openrouter", "commandcode") or a caller-supplied endpoint object. */
|
|
3
|
+
export type JudgmentBackend = BackendSpec;
|
|
4
|
+
/** The judgment destination as the config carries it, plus the refusal that turns judgments off when the configured value was refused. */
|
|
5
|
+
export interface BackendSetting {
|
|
6
|
+
/** The spec judgments go to; undefined only when the configured value was refused. */
|
|
7
|
+
readonly typesafeBackend: JudgmentBackend | undefined;
|
|
8
|
+
/** Why the configured value was refused: the once-per-session notice and `/warden status` quote it. */
|
|
9
|
+
readonly backendRefusal: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** What a raw `typesafeBackend` value resolves to: the spec judgments go to, or the refusal that turns them off. Exactly one is set. */
|
|
12
|
+
export type BackendResolution = {
|
|
13
|
+
readonly typesafeBackend: JudgmentBackend;
|
|
14
|
+
readonly backendRefusal?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
readonly typesafeBackend?: undefined;
|
|
17
|
+
readonly backendRefusal: string;
|
|
18
|
+
};
|
|
19
|
+
/** Why no judge is available: consent not given, the backend refused, no key for the backend, a saved 401 or 403, or the request budget spent. */
|
|
20
|
+
export type JudgmentsOffReason = "no_consent" | "bad_backend" | "no_key" | "key_rejected" | "budget";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a raw config value to the spec judgments go to, kept as written, or to the refusal that turns judgments off.
|
|
23
|
+
* pi-typesafe validates the spec again on every call. An unknown name or an object it refuses is never silently mapped
|
|
24
|
+
* to the default backend: a mistyped value must not send judgments to api.typesafe.ai.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveJudgmentBackend(raw: unknown): BackendResolution;
|
|
27
|
+
/** The name messages show for a backend: the registry name, or an endpoint's validated label. */
|
|
28
|
+
export declare function backendName(backend: JudgmentBackend | undefined): string;
|
|
29
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; true only for "typesafe". */
|
|
30
|
+
export declare function loginStoresKey(backend: JudgmentBackend | undefined): boolean;
|
|
31
|
+
/** One phrase naming where judgments go — the label, the host, and the model sent — for consent text and status lines. */
|
|
32
|
+
export declare function describeBackend(backend: JudgmentBackend | undefined): string;
|
|
12
33
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
13
|
-
export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
|
|
34
|
+
export declare function disclosureFor(backend: JudgmentBackend | undefined, disclosure: string): string;
|
|
14
35
|
/**
|
|
15
36
|
* Build the options forwarded to `createTypeSafe`.
|
|
16
37
|
* The caller must gate on `authState({ backend }).usable` before calling `createTypeSafe`.
|
|
@@ -20,5 +41,3 @@ export declare function judgeOptions(config: {
|
|
|
20
41
|
timeoutMs: number;
|
|
21
42
|
typesafeBackend: JudgmentBackend;
|
|
22
43
|
}): TypeSafeOptions;
|
|
23
|
-
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
24
|
-
export declare function resolveBackend(raw: unknown): JudgmentBackend;
|
package/dist/backend.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { DEFAULT_BACKEND, backendHost, resolveBackend } from "pi-typesafe";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a raw config value to the spec judgments go to, kept as written, or to the refusal that turns judgments off.
|
|
4
|
+
* pi-typesafe validates the spec again on every call. An unknown name or an object it refuses is never silently mapped
|
|
5
|
+
* to the default backend: a mistyped value must not send judgments to api.typesafe.ai.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveJudgmentBackend(raw) {
|
|
8
|
+
if (raw === undefined || raw === null)
|
|
9
|
+
return { typesafeBackend: DEFAULT_BACKEND };
|
|
10
|
+
try {
|
|
11
|
+
resolveBackend(raw);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return { backendRefusal: error instanceof Error ? error.message : String(error) };
|
|
15
|
+
}
|
|
16
|
+
return { typesafeBackend: raw };
|
|
5
17
|
}
|
|
6
|
-
/** The
|
|
7
|
-
export function
|
|
8
|
-
return
|
|
18
|
+
/** The name messages show for a backend: the registry name, or an endpoint's validated label. */
|
|
19
|
+
export function backendName(backend) {
|
|
20
|
+
return backend === undefined ? DEFAULT_BACKEND : typeof backend === "string" ? backend : resolveBackend(backend).label;
|
|
9
21
|
}
|
|
10
|
-
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores;
|
|
22
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; true only for "typesafe". */
|
|
11
23
|
export function loginStoresKey(backend) {
|
|
12
|
-
return
|
|
24
|
+
return backend === DEFAULT_BACKEND;
|
|
25
|
+
}
|
|
26
|
+
/** One phrase naming where judgments go — the label, the host, and the model sent — for consent text and status lines. */
|
|
27
|
+
export function describeBackend(backend) {
|
|
28
|
+
const resolved = resolveBackend(backend);
|
|
29
|
+
return `${resolved.label} at ${backendHost(backend)}, model ${resolved.defaultModel ?? "none configured"}`;
|
|
13
30
|
}
|
|
14
31
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
15
32
|
export function disclosureFor(backend, disclosure) {
|
|
16
|
-
return backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
|
33
|
+
return backend === undefined || backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
|
17
34
|
}
|
|
18
35
|
/**
|
|
19
36
|
* Build the options forwarded to `createTypeSafe`.
|
|
@@ -26,7 +43,3 @@ export function judgeOptions(config) {
|
|
|
26
43
|
...(config.typesafeBackend === DEFAULT_BACKEND ? {} : { backend: config.typesafeBackend }),
|
|
27
44
|
};
|
|
28
45
|
}
|
|
29
|
-
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
30
|
-
export function resolveBackend(raw) {
|
|
31
|
-
return raw === "typesafe" || raw === "openrouter" ? raw : DEFAULT_BACKEND;
|
|
32
|
-
}
|
package/dist/config.d.ts
CHANGED
|
@@ -368,8 +368,10 @@ export interface WardenConfig {
|
|
|
368
368
|
enabled: boolean;
|
|
369
369
|
/** Consent to send task and action summaries to api.typesafe.ai. Set by /warden enable; never by a project file. */
|
|
370
370
|
typesafe: boolean;
|
|
371
|
-
/** The decisions service the judgments go to. User file only: a project must not redirect judgments to another vendor. */
|
|
372
|
-
typesafeBackend: JudgmentBackend;
|
|
371
|
+
/** The decisions service the judgments go to: a name ("typesafe", "openrouter", "commandcode") or a caller-supplied endpoint object. User file only: a project must not redirect judgments to another vendor. Undefined when the configured value was refused. */
|
|
372
|
+
typesafeBackend: JudgmentBackend | undefined;
|
|
373
|
+
/** Why the configured typesafeBackend was refused: judgments stay off, and the once-per-session notice and /warden status quote this. */
|
|
374
|
+
backendRefusal: string | undefined;
|
|
373
375
|
/**
|
|
374
376
|
* steer (default): a confirm-level call is held and the agent receives the judgment as its tool result, so it re-plans or asks
|
|
375
377
|
* the user in chat. confirm: open a dialog and let the user decide (falls back to steer without a UI). advise: never hold; report only.
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveJudgmentBackend } from "./backend.js";
|
|
5
5
|
import { defaultHostDirs } from "./host-dirs.js";
|
|
6
6
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
7
7
|
import { defaultWidgetConfig } from "./widget.js";
|
|
@@ -31,6 +31,7 @@ export function defaultConfig() {
|
|
|
31
31
|
enabled: true,
|
|
32
32
|
typesafe: false,
|
|
33
33
|
typesafeBackend: "typesafe",
|
|
34
|
+
backendRefusal: undefined,
|
|
34
35
|
mode: "steer",
|
|
35
36
|
timeoutMs: 5000,
|
|
36
37
|
maxRequests: 500,
|
|
@@ -482,10 +483,12 @@ export function applyUserOverrides(base, raw) {
|
|
|
482
483
|
if (!isObject(raw))
|
|
483
484
|
return base;
|
|
484
485
|
const shared = applyShared(base, raw);
|
|
486
|
+
const backend = resolveJudgmentBackend(raw.typesafeBackend);
|
|
485
487
|
return {
|
|
486
488
|
enabled: boolean(raw.enabled, base.enabled),
|
|
487
489
|
typesafe: boolean(raw.typesafe, base.typesafe),
|
|
488
|
-
typesafeBackend:
|
|
490
|
+
typesafeBackend: backend.typesafeBackend,
|
|
491
|
+
backendRefusal: backend.backendRefusal,
|
|
489
492
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
490
493
|
...shared,
|
|
491
494
|
...applyGuards(base, raw, shared.timeoutMs, "user"),
|
package/dist/conscience.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface AssessmentResult {
|
|
|
66
66
|
/** Number of judge requests issued for this assessment. */
|
|
67
67
|
requestCount: number;
|
|
68
68
|
}
|
|
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";
|
|
69
|
+
export type SkipReason = "disabled" | "no_consent" | "bad_backend" | "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";
|
|
70
70
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
71
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."];
|
|
72
72
|
/** Strip absolute paths, URLs, and credentials from a description. */
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { HostDirs } from "./host-dirs.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { BackendSetting, JudgmentsOffReason } from "./backend.js";
|
|
4
4
|
import type { ToolCallRef } from "./action-guard.js";
|
|
5
5
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
6
6
|
import type { ShapeResult } from "./shape.js";
|
|
@@ -37,7 +37,7 @@ export declare function assistantPlan(ctx: ExtensionContext, visibleAction?: boo
|
|
|
37
37
|
* variable that holds a rejected key, or undefined when the rejected key is the one `/typesafe login` saved. Never
|
|
38
38
|
* names a key value or a path.
|
|
39
39
|
*/
|
|
40
|
-
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">,
|
|
40
|
+
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">, setting: BackendSetting, headless: boolean, rejectedEnv?: string): string;
|
|
41
41
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
42
42
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
43
43
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|
package/dist/extension.js
CHANGED
|
@@ -6,9 +6,9 @@ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { defaultHostDirs } from "./host-dirs.js";
|
|
7
7
|
import * as tuiModule from "@earendil-works/pi-tui";
|
|
8
8
|
const MouseRegion = tuiModule.MouseRegion;
|
|
9
|
-
import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
9
|
+
import { authState, backendHost, createTypeSafe, describeAuth, resolveBackend } from "pi-typesafe";
|
|
10
10
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
11
|
-
import {
|
|
11
|
+
import { backendName, describeBackend, disclosureFor, judgeOptions, loginStoresKey } from "./backend.js";
|
|
12
12
|
import { ActionGuard } from "./action-guard.js";
|
|
13
13
|
import { adaptHost } from "./host-compat.js";
|
|
14
14
|
import { assistantView, formatMuted, NEVER_MUTED, STEER_KINDS, SteerStats, SteerWatch, steerStatsPath } from "./adaptive.js";
|
|
@@ -201,10 +201,11 @@ function planForCall(entries) {
|
|
|
201
201
|
* variable that holds a rejected key, or undefined when the rejected key is the one `/typesafe login` saved. Never
|
|
202
202
|
* names a key value or a path.
|
|
203
203
|
*/
|
|
204
|
-
export function judgmentsOffText(reason,
|
|
204
|
+
export function judgmentsOffText(reason, setting, headless, rejectedEnv) {
|
|
205
205
|
switch (reason) {
|
|
206
206
|
case "no_consent": return `warden: Jev judgments are off (no consent). ${headless ? "Set PI_WARDEN_ENABLED=1." : "Run /warden enable."}`;
|
|
207
|
-
case "
|
|
207
|
+
case "bad_backend": return `warden: Jev judgments are off (typesafeBackend refused: ${setting.backendRefusal ?? "the configured value is not usable"})`;
|
|
208
|
+
case "no_key": return `warden: Jev judgments are off (no key for ${backendName(setting.typesafeBackend)}). Set ${resolveBackend(setting.typesafeBackend).keyEnv}${loginStoresKey(setting.typesafeBackend) ? " or run /typesafe login" : ""}.`;
|
|
208
209
|
case "key_rejected": return rejectedEnv
|
|
209
210
|
? `warden: Jev judgments are off (the key in ${rejectedEnv} was rejected). Check the key, then run /warden status.`
|
|
210
211
|
: "warden: Jev judgments are off (the key saved by /typesafe login was rejected). Run /typesafe login.";
|
|
@@ -557,11 +558,15 @@ export default function wardenExtension(host) {
|
|
|
557
558
|
const consentGiven = (config) => config.typesafe || process.env.PI_WARDEN_ENABLED === "1";
|
|
558
559
|
/** Why no judge is available, or undefined when one is. The notice, the trace file, and the conscience all use it. */
|
|
559
560
|
const judgmentsOffReason = (config) => {
|
|
561
|
+
// A refused backend is a config error the user must see even before consent or keys matter.
|
|
562
|
+
const backend = config.typesafeBackend;
|
|
563
|
+
if (config.backendRefusal !== undefined || backend === undefined)
|
|
564
|
+
return "bad_backend";
|
|
560
565
|
if (!consentGiven(config))
|
|
561
566
|
return "no_consent";
|
|
562
567
|
if (budgetExhausted)
|
|
563
568
|
return "budget";
|
|
564
|
-
const auth = authState({ backend
|
|
569
|
+
const auth = authState({ backend });
|
|
565
570
|
if (auth.usable)
|
|
566
571
|
return undefined;
|
|
567
572
|
// No key in effect, or one whose last request came back 401 or 403.
|
|
@@ -578,8 +583,8 @@ export default function wardenExtension(host) {
|
|
|
578
583
|
if (reason === undefined || reason === "budget" || judgmentsReported.has(reason))
|
|
579
584
|
return;
|
|
580
585
|
judgmentsReported.add(reason);
|
|
581
|
-
const auth = reason === "key_rejected" ? authState({ backend: config.typesafeBackend }) : undefined;
|
|
582
|
-
judgmentsNotify?.(judgmentsOffText(reason, config
|
|
586
|
+
const auth = reason === "key_rejected" && config.typesafeBackend !== undefined ? authState({ backend: config.typesafeBackend }) : undefined;
|
|
587
|
+
judgmentsNotify?.(judgmentsOffText(reason, config, judgmentsHeadless, auth?.kind === "environment" ? auth.keyName : undefined));
|
|
583
588
|
};
|
|
584
589
|
const consentSource = (config) => config.typesafe ? "/warden enable" : process.env.PI_WARDEN_ENABLED === "1" ? "PI_WARDEN_ENABLED" : undefined;
|
|
585
590
|
/** A consent flag is not proof that judgments happen; check the key state for the chosen backend. */
|
|
@@ -594,7 +599,26 @@ export default function wardenExtension(host) {
|
|
|
594
599
|
stats.cooldownSkips++;
|
|
595
600
|
return undefined;
|
|
596
601
|
}
|
|
597
|
-
|
|
602
|
+
if (client)
|
|
603
|
+
return client;
|
|
604
|
+
const backend = config.typesafeBackend;
|
|
605
|
+
// Reached only if a config ever sets no backend and no refusal at once; treating it as off keeps the invariant.
|
|
606
|
+
if (backend === undefined)
|
|
607
|
+
return undefined;
|
|
608
|
+
try {
|
|
609
|
+
client = watched(createTypeSafe(judgeOptions({ maxRequests: config.maxRequests, timeoutMs: config.timeoutMs, typesafeBackend: backend })));
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
// An endpoint the config accepts can still be unusable (one that names no defaultModel); a hook must never crash.
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
+
if (!judgmentsReported.has("bad_backend")) {
|
|
615
|
+
judgmentsReported.add("bad_backend");
|
|
616
|
+
traceFile?.judgments(judgmentsState("bad_backend"));
|
|
617
|
+
judgmentsNotify?.(`warden: Jev judgments are off (typesafeBackend refused: ${message})`);
|
|
618
|
+
}
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
return client;
|
|
598
622
|
};
|
|
599
623
|
/** Every guard reaches the backend through `evaluate`, so this one seam sees each failure and each success. */
|
|
600
624
|
const watched = (typesafe) => new Proxy(typesafe, {
|
|
@@ -625,7 +649,7 @@ export default function wardenExtension(host) {
|
|
|
625
649
|
}
|
|
626
650
|
const seconds = Math.round(event.ms / 1000);
|
|
627
651
|
const remedy = event.kind === "auth"
|
|
628
|
-
? ` Set ${
|
|
652
|
+
? ` Set ${resolveBackend(judgeConfig?.typesafeBackend).keyEnv} or run /warden enable.`
|
|
629
653
|
: event.kind === "configuration" ? " /warden status shows the backend setup." : "";
|
|
630
654
|
noticeUi.notify(`warden: judgments paused for ${seconds}s after a ${event.kind} failure; pattern checks run alone until then.${remedy}`, "warning");
|
|
631
655
|
};
|
|
@@ -2686,7 +2710,9 @@ export default function wardenExtension(host) {
|
|
|
2686
2710
|
try {
|
|
2687
2711
|
const config = configFor(ctx);
|
|
2688
2712
|
if (action === "status") {
|
|
2689
|
-
|
|
2713
|
+
// A refused backend has no key to describe; the refusal itself is the status line.
|
|
2714
|
+
const backend = config.typesafeBackend;
|
|
2715
|
+
const auth = config.backendRefusal !== undefined || backend === undefined ? undefined : describeAuth(authState({ backend }));
|
|
2690
2716
|
const source = consentSource(config);
|
|
2691
2717
|
const usage = client?.getUsage();
|
|
2692
2718
|
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.rules.enabled && "rules", config.context.enabled && "context", config.runaway.enabled && "runaway", config.subagent.enabled && "subagent triage", config.notify.enabled && "desktop notifications"].filter(Boolean).join(", ");
|
|
@@ -2695,7 +2721,7 @@ export default function wardenExtension(host) {
|
|
|
2695
2721
|
? `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, not yet measurable (${ls.allowed} allowed).`
|
|
2696
2722
|
: `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, ${ls.labeled} labeled, ${ls.declined + ls.replanned} stood (${ls.declined + ls.replanned}/${ls.labeled}), ${ls.allowed} allowed (${ls.accepted} accepted, ${ls.regretted} regretted).`;
|
|
2697
2723
|
report([
|
|
2698
|
-
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.
|
|
2724
|
+
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.backendRefusal !== undefined || backend === undefined || backend === "typesafe" ? "" : ` (${describeBackend(backend)})`}; ${config.backendRefusal !== undefined ? `judgments are off: typesafeBackend refused: ${config.backendRefusal}` : auth?.text ?? ""}`,
|
|
2699
2725
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.offPlan} off plan (${stats.offPlanTraceOnly} trace-only), ${stats.offTask} off task, ${stats.slop} slop notes, ${stats.ruleViolations}/${stats.ruleChecks} rule violations, ${stats.pathNotes} sensitive-path notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.runaway} runaway stops, ${stats.subagentWoken}/${stats.subagentReports} subagent reports woken, ${stats.restatements} restatements, ${stats.errors} TypeSafe errors, ${stats.cooldownSkips} checks without Jev during a judge cooldown; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}. Steer budget: ${config.steerBudget === 0 ? "off" : `${config.steerBudget} per run`}.`,
|
|
2700
2726
|
formatSteers(stats),
|
|
2701
2727
|
`${formatMuted(steerStats.muted(), config.steers)}${stats.steersMuted ? ` This session: ${stats.steersMuted} steer${stats.steersMuted === 1 ? "" : "s"} kept in the trace only.` : ""}`,
|
|
@@ -2730,7 +2756,7 @@ export default function wardenExtension(host) {
|
|
|
2730
2756
|
switch (result.status) {
|
|
2731
2757
|
// No key, no consent, or a spent budget: say why and send nothing.
|
|
2732
2758
|
case "no-judge":
|
|
2733
|
-
report(`Rules audit sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config
|
|
2759
|
+
report(`Rules audit sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config, !ctx.hasUI)}` : ""}`);
|
|
2734
2760
|
return;
|
|
2735
2761
|
case "no-files":
|
|
2736
2762
|
report(`Rules audit sent nothing: ${result.reason}.`);
|
|
@@ -2759,7 +2785,7 @@ export default function wardenExtension(host) {
|
|
|
2759
2785
|
switch (result.status) {
|
|
2760
2786
|
// No key, no consent, or a spent budget: say why and send nothing.
|
|
2761
2787
|
case "no-judge":
|
|
2762
|
-
report(`Bench sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config
|
|
2788
|
+
report(`Bench sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config, !ctx.hasUI)}` : ""}`);
|
|
2763
2789
|
return;
|
|
2764
2790
|
case "skipped":
|
|
2765
2791
|
report(`Bench sent nothing: ${result.reason}.`);
|
|
@@ -2776,7 +2802,7 @@ export default function wardenExtension(host) {
|
|
|
2776
2802
|
const result = await checkRules({ set: rulesGuard.store.load(ctx.cwd, config.rules), judge: judgeFor(config), timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
2777
2803
|
if (result.source === "skipped") {
|
|
2778
2804
|
// No key, no consent, or a spent budget: say why and send nothing.
|
|
2779
|
-
report(`Rules check sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config
|
|
2805
|
+
report(`Rules check sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config, !ctx.hasUI)}` : ""} /warden rules shows the rule set locally.`);
|
|
2780
2806
|
return;
|
|
2781
2807
|
}
|
|
2782
2808
|
if (result.source === "typesafe")
|
|
@@ -2811,7 +2837,7 @@ export default function wardenExtension(host) {
|
|
|
2811
2837
|
const off = judgmentsOffReason(config);
|
|
2812
2838
|
const judge = judgeFor(config);
|
|
2813
2839
|
if (!judge) {
|
|
2814
|
-
report(`Rules calibrate sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config
|
|
2840
|
+
report(`Rules calibrate sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config, !ctx.hasUI)}` : ""} /warden rules shows the rule set locally.`);
|
|
2815
2841
|
return;
|
|
2816
2842
|
}
|
|
2817
2843
|
let history;
|
|
@@ -2991,15 +3017,24 @@ export default function wardenExtension(host) {
|
|
|
2991
3017
|
return;
|
|
2992
3018
|
}
|
|
2993
3019
|
if (action === "enable") {
|
|
3020
|
+
if (config.backendRefusal !== undefined) {
|
|
3021
|
+
report(`Cannot enable judgments: typesafeBackend refused: ${config.backendRefusal} Fix the value in ${userConfigPath(dirs)}, then run /warden enable again.`, "warning");
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const backend = config.typesafeBackend;
|
|
2994
3025
|
if (!ctx.hasUI) {
|
|
2995
|
-
report(`Consent needs an interactive session. For headless runs set PI_WARDEN_ENABLED=1 and ${
|
|
3026
|
+
report(`Consent needs an interactive session. For headless runs set PI_WARDEN_ENABLED=1 and ${resolveBackend(backend).keyEnv} explicitly.`, "warning");
|
|
2996
3027
|
return;
|
|
2997
3028
|
}
|
|
2998
|
-
|
|
3029
|
+
// For a non-TypeSafe backend the dialog names who answers: the label, host, and model sent.
|
|
3030
|
+
const consentText = backend === undefined || backend === "typesafe"
|
|
3031
|
+
? disclosure
|
|
3032
|
+
: `Judgments go to ${describeBackend(backend)}. ${disclosureFor(backend, disclosure)}`;
|
|
3033
|
+
if (!await ctx.ui.confirm("Enable TypeSafe judgments for pi-warden?", consentText))
|
|
2999
3034
|
return;
|
|
3000
3035
|
// One flow: consent, then a key if none is configured yet. TypeSafe prompts, verifies, and stores the key for every
|
|
3001
3036
|
// pi-typesafe consumer; any other backend has no login, so a missing key is reported with the variable to set.
|
|
3002
|
-
const key = await ensureApiKey(ctx, {
|
|
3037
|
+
const key = await ensureApiKey(ctx, backend === undefined ? {} : { backend });
|
|
3003
3038
|
if (!key) {
|
|
3004
3039
|
report("No key entered; pi-warden stays on pattern checks only. Run /warden enable again when you have a key from console.typesafe.ai.", "warning");
|
|
3005
3040
|
return;
|
|
@@ -3007,7 +3042,7 @@ export default function wardenExtension(host) {
|
|
|
3007
3042
|
const path = setUserSetting("typesafe", true, dirs);
|
|
3008
3043
|
client = undefined;
|
|
3009
3044
|
budgetExhausted = false;
|
|
3010
|
-
report(`TypeSafe judgments enabled and saved to ${path}${key.login ? `; key verified (${key.login.models} model${key.login.models === 1 ? "" : "s"}) and stored at ${key.login.path}` : ` using the ${key.source === "stored" ? "stored key" : `key from ${
|
|
3045
|
+
report(`TypeSafe judgments enabled and saved to ${path}${key.login ? `; key verified (${key.login.models} model${key.login.models === 1 ? "" : "s"}) and stored at ${key.login.path}` : ` using the ${key.source === "stored" ? "stored key" : `key from ${resolveBackend(config.typesafeBackend).keyEnv}`}`}. This stays on in new sessions until /warden disable.`);
|
|
3011
3046
|
return;
|
|
3012
3047
|
}
|
|
3013
3048
|
if (action === "disable") {
|
|
@@ -3149,7 +3184,10 @@ export default function wardenExtension(host) {
|
|
|
3149
3184
|
}
|
|
3150
3185
|
if (action === "test") {
|
|
3151
3186
|
const judge = judgeFor(config);
|
|
3152
|
-
|
|
3187
|
+
// For a non-TypeSafe backend the confirmation names who answers: the label, host, and model sent.
|
|
3188
|
+
const backend = config.typesafeBackend;
|
|
3189
|
+
const destination = backend === undefined || backend === "typesafe" ? backendHost(backend) : describeBackend(backend);
|
|
3190
|
+
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Prepare the demo environment") goes to ${destination} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
|
|
3153
3191
|
return;
|
|
3154
3192
|
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Prepare the demo environment" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge, rules: config.rules });
|
|
3155
3193
|
const deliveryVerdict = ctx.hasUI ? verdict : agentDeliveryVerdict(verdict);
|
package/dist/shape.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isMode } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveJudgmentBackend } from "./backend.js";
|
|
3
3
|
import { redact } from "./redact.js";
|
|
4
4
|
import { DEFAULT_TEMPLATES } from "./widget.js";
|
|
5
5
|
/** The config layout this extension build expects; compared with the loaded config module's CONFIG_SCHEMA. */
|
|
@@ -8,6 +8,10 @@ const off = { enabled: false };
|
|
|
8
8
|
// Duplicated from defaultConfig(): this file must work when the config module is stale and lacks the key.
|
|
9
9
|
const coreTools = () => ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
10
10
|
const proseOff = () => ({ ...off, audience: "technical", threshold: 1, trend: 3, minChars: 1 });
|
|
11
|
+
// A stale config module hands back either its own resolution or a raw value; both end up resolved here.
|
|
12
|
+
const resolveSetting = (source) => source.backendRefusal !== undefined
|
|
13
|
+
? { typesafeBackend: source.typesafeBackend, backendRefusal: source.backendRefusal }
|
|
14
|
+
: resolveJudgmentBackend(source.typesafeBackend);
|
|
11
15
|
/**
|
|
12
16
|
* `loadConfig()` always returns a complete object, yet live sessions crashed at `config.slop.prose.enabled` after a package
|
|
13
17
|
* update. A partially updated module graph (extension from one version, config from another) is the only known way to get
|
|
@@ -23,10 +27,12 @@ export function completeConfig(loaded) {
|
|
|
23
27
|
missing.push(key);
|
|
24
28
|
return fallback;
|
|
25
29
|
};
|
|
30
|
+
const backend = resolveSetting(source);
|
|
26
31
|
const config = {
|
|
27
32
|
enabled: source.enabled ?? true,
|
|
28
33
|
typesafe: source.typesafe ?? false,
|
|
29
|
-
typesafeBackend:
|
|
34
|
+
typesafeBackend: backend.typesafeBackend,
|
|
35
|
+
backendRefusal: backend.backendRefusal,
|
|
30
36
|
mode: isMode(source.mode) ? source.mode : "steer",
|
|
31
37
|
timeoutMs: source.timeoutMs ?? 5000,
|
|
32
38
|
maxRequests: source.maxRequests ?? 500,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "Makes the Pi agent follow your project's rules. Jev judges every write against your pi-warden.md and quotes the broken rule back to the agent, names slop, breaks stuck loops, calls out unverified done claims, compresses large tool output, and holds the rare destructive command. Built on pi-typesafe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"eval:judge": "node scripts/judge-bench.mjs",
|
|
64
64
|
"eval:replay": "node scripts/rules-replay.mjs",
|
|
65
65
|
"eval:rules": "node scripts/rules-bench.mjs",
|
|
66
|
-
"eval:
|
|
67
|
-
|
|
66
|
+
"eval:rules-turn": "node scripts/rules-turn-bench.mjs"
|
|
67
|
+
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@earendil-works/pi-coding-agent": ">=0.85.1 <1",
|
|
70
70
|
"@earendil-works/pi-tui": ">=0.85.1 <1"
|
|
@@ -85,6 +85,6 @@
|
|
|
85
85
|
"typescript": "^6.0.0"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"pi-typesafe": "^0.
|
|
88
|
+
"pi-typesafe": "^0.8.0"
|
|
89
89
|
}
|
|
90
90
|
}
|