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 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 that receives pi-warden's Jev requests. */
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";
6
- /** The host the consent disclosure names as the destination, without scheme: `api.typesafe.ai`, `openrouter.ai`. */
7
- export declare function backendHost(backend: JudgmentBackend): string;
8
- /** The environment variable that carries the backend's key, for messages that tell the user what to set. */
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;
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 { DECISIONS_BACKENDS, DEFAULT_BACKEND } from "pi-typesafe";
2
- /** The host the consent disclosure names as the destination, without scheme: `api.typesafe.ai`, `openrouter.ai`. */
3
- export function backendHost(backend) {
4
- return new URL(DECISIONS_BACKENDS[backend].host).host;
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 environment variable that carries the backend's key, for messages that tell the user what to set. */
7
- export function keyEnvFor(backend) {
8
- return DECISIONS_BACKENDS[backend].keyEnv ?? DECISIONS_BACKENDS[DEFAULT_BACKEND].keyEnv ?? "TYPESAFE_API_KEY";
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; pi-typesafe's `usesTypesafeKey`, which its package does not export. */
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 keyEnvFor(backend) === keyEnvFor(DEFAULT_BACKEND);
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 { resolveBackend } from "./backend.js";
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: resolveBackend(raw.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"),
@@ -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. */
@@ -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 { JudgmentBackend, JudgmentsOffReason } from "./backend.js";
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">, backend: JudgmentBackend, headless: boolean, rejectedEnv?: string): string;
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 { backendHost, disclosureFor, judgeOptions, keyEnvFor, loginStoresKey, resolveBackend } from "./backend.js";
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, backend, headless, rejectedEnv) {
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 "no_key": return `warden: Jev judgments are off (no key for ${backend}). Set ${keyEnvFor(backend)}${loginStoresKey(backend) ? " or run /typesafe login" : ""}.`;
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: config.typesafeBackend });
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.typesafeBackend, judgmentsHeadless, auth?.kind === "environment" ? auth.keyName : undefined));
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
- return client ??= watched(createTypeSafe(judgeOptions(config)));
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 ${keyEnvFor(judgeConfig?.typesafeBackend ?? "typesafe")} or run /warden enable.`
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
- const auth = describeAuth(authState({ backend: config.typesafeBackend }));
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.typesafeBackend !== "typesafe" ? ` (${config.typesafeBackend})` : ""}; ${auth.text}`,
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.typesafeBackend, !ctx.hasUI)}` : ""}`);
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.typesafeBackend, !ctx.hasUI)}` : ""}`);
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.typesafeBackend, !ctx.hasUI)}` : ""} /warden rules shows the rule set locally.`);
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.typesafeBackend, !ctx.hasUI)}` : ""} /warden rules shows the rule set locally.`);
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 ${keyEnvFor(config.typesafeBackend)} explicitly.`, "warning");
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
- if (!await ctx.ui.confirm("Enable TypeSafe judgments for pi-warden?", disclosure))
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, { backend: config.typesafeBackend });
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 ${keyEnvFor(config.typesafeBackend)}`}`}. This stays on in new sessions until /warden disable.`);
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
- 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 ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
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 { resolveBackend } from "./backend.js";
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: resolveBackend(source.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.73.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:replay": "node scripts/rules-replay.mjs",
67
- "eval:rules-turn": "node scripts/rules-turn-bench.mjs" },
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.7.0"
88
+ "pi-typesafe": "^0.8.0"
89
89
  }
90
90
  }