pi-warden 0.38.3 → 0.40.1

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,51 @@ 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.40.1
12
+
13
+ ### Fixed
14
+
15
+ - An `rm` in a compound command (`rm -rf build && npm test`) no longer makes every word after it a separate target. Target extraction now runs per shell segment, so a compound command stops producing one Jev question per trailing token and no longer trips TypeSafe's 32-question limit on long command lines. A single `rm` naming more targets than the request budget still can; chunking is separate.
16
+ - An `rm` followed by a newline and another command produced no targets at all, so it was never scoped; it now yields the targets of its own line.
17
+
18
+ ## 0.40.0
19
+
20
+ ### Fixed
21
+
22
+ - `/warden status` and `/warden enable` now describe the key of the configured `typesafeBackend`. With `"openrouter"`, status said "TypeSafe key: missing" on the same line that counted judgments, and enable opened the TypeSafe login prompt, so consent could not be saved. Both call pi-typesafe's backend-aware `authState` and `ensureApiKey`; with no OpenRouter key, enable reports the variable to set instead of prompting (#57).
23
+ - The headless hint and the enable confirmation name the backend's own environment variable.
24
+
25
+ ### Changed
26
+
27
+ - `src/backend.ts` reads hosts and key variables from pi-typesafe's `DECISIONS_BACKENDS` instead of keeping its own copy; `keyAvailable` is replaced by `authState({ backend }).usable`, which also honours a rejected key. Requires pi-typesafe 0.7.0.
28
+
29
+ ## 0.39.1
30
+
31
+ ### Changed
32
+ - Bump `pi-typesafe` from `^0.6.1` to `^0.6.2`. Programmatic API is unchanged; the 0.6.2 tool schema now documents its own payload fields, and no pi-warden text duplicated it.
33
+
34
+ ## 0.39.0
35
+
36
+ ### Docs
37
+ - README quick start now carries the four setup commands (`/warden enable`, `/warden init`, `/warden index`, `/warden test`) and the beta note; `docs/commands.md` gains rows for `/warden init --force` and `/warden index`.
38
+ - Conscience policy measurement on the per-project corpus with the split disposition gate: beta candidate policy record (89% pooled precision, 74/83; 95% gate not met), comparison against the first measurement and the remeasure, labelled-subset reproduction of the live-run gate numbers.
39
+ - Conscience recommendation remeasurement after index + question changes: before-and-after table, labelled-subset comparison, research-role tool rate, candidate policy update.
40
+
41
+ ### Added
42
+ - `/warden index` command: builds a local capability index with the session model. Entries carry `lead`, `useWhen`, `examples`, and `role` instead of bare names and descriptions. The conscience uses index entries when the source hash matches; bare descriptions are the fallback. Index files live at `~/.pi/agent/pi-warden/index/global.json` and `projects/<hash>.json`. Once-per-session nudge when the index is missing or stale.
43
+ - `conscience.advanceThreshold` config key (default `0.70`): separate gate for the disposition question's P(advance), independent of the usefulness threshold.
44
+ - `CONSCIENCE_BETA_POLICY` in `src/load.ts` (questionHash `fb2d35042f667b3c`, model `jev-1.13.0`, usefulness 0.80, advance 0.70), held in the extension's closure and passed to the activation gate; a test pins the question wording to the policy hash, and `questionHash` is computed over the disposition question plus one canonical candidate question so it no longer moves with batch shape.
45
+ - Capability roles (`research`, `evidence`, `execution`, `delegation`, `review`, `conversation`) in index entries and candidate state; role-based guidance in Score questions.
46
+
47
+ ### Changed
48
+ - Conscience defaults: `recommendThreshold` 1.0 → 0.80 (beta policy thresholds); still off by default — `conscience.enabled: true` is the one switch.
49
+ - The replay script omits prompt excerpts from the selected-candidates table unless `PI_WARDEN_OWNER_REPORTS=1`, so committed reports carry no Linear ids or branch names; scrub greps include `CON-[0-9]+`.
50
+ - Replay script `scripts/conscience-replay.mjs` gates delivery on `advanceThreshold` (default 0.70) and reports pi-warden status rows below the gate.
51
+ - Conscience question wording reverted to baseline after four calibration iterations; the disposition gate, not wording, was the lever.
52
+
53
+ ### Fixed
54
+ - The conscience activation gate now fails closed: with no active policy, or a policy whose hash or model does not match the request, no recommendation is delivered (traced `no_policy`). Previously an absent policy let delivery through, contradicting `docs/configuration.md`. The gate also compares the policy's model against the model that actually answered instead of the literal `"jev-latest"`.
55
+
11
56
  ## 0.38.3
12
57
 
13
58
  ### Fixed
package/README.md CHANGED
@@ -14,9 +14,12 @@ Instead of interrupting you for every problem, Warden usually feeds the issue ba
14
14
  pi install npm:pi-warden
15
15
  ```
16
16
 
17
- 1. `/warden enable` to activate. Paste a key from [console.typesafe.ai](https://console.typesafe.ai) if you have one (hidden input, saved owner-only), or skip for offline-only mode.
18
- 2. `/warden test` to see one synthetic verdict.
19
- 3. Work as usual. Status line shows verdicts; `ctrl+shift+w` opens the trace sidebar.
17
+ 1. `/warden enable` to turn judgments on. Paste a key from [console.typesafe.ai](https://console.typesafe.ai) (hidden input, saved owner-only), or skip for offline-only mode.
18
+ 2. `/warden init` to write a starter `pi-warden.md`. The rules guard enforces what you put there.
19
+ 3. `/warden index` (recommended) so the conscience knows your skills and tools from their real descriptions.
20
+ 4. `/warden test` to see one synthetic verdict, then work as usual. The status line shows verdicts; `ctrl+shift+w` opens the trace sidebar.
21
+
22
+ The conscience is beta and off by default. Turn it on with `conscience.enabled: true` in the user config.
20
23
 
21
24
  Works without any key (offline guards: pattern list, runaway stop, sensitive-path notes, credential warnings). Requires Pi 0.85+, Node 22.19+.
22
25
 
@@ -32,6 +35,7 @@ Works without any key (offline guards: pattern list, runaway stop, sensitive-pat
32
35
  | **Security** | code and output | Hardcoded secrets, injection risks |
33
36
  | **Runaway** | reply stream | Stops replies that begin repeating themselves |
34
37
  | **Subagent triage** | async child reports | Keeps noisy background work from waking the parent |
38
+ | **Conscience** (beta, off) | each operator prompt | Recommends a skill or tool from the capability index before the agent acts; enable with `conscience.enabled: true` |
35
39
 
36
40
  ## How intervention works
37
41
 
package/dist/backend.d.ts CHANGED
@@ -1,18 +1,15 @@
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
- /** The host the consent disclosure names as the destination. */
4
+ /** The host the consent disclosure names as the destination, without scheme: `api.typesafe.ai`, `openrouter.ai`. */
5
5
  export declare function backendHost(backend: JudgmentBackend): string;
6
+ /** The environment variable that carries the backend's key, for messages that tell the user what to set. */
7
+ export declare function keyEnvFor(backend: JudgmentBackend): string;
6
8
  /** Adapt the consent text to the active backend by substituting the destination host. */
7
9
  export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
8
- /**
9
- * Whether the right credential exists for the chosen backend.
10
- * TypeSafe uses the pi-typesafe keystore; other backends use an environment variable.
11
- */
12
- export declare function keyAvailable(backend: JudgmentBackend, env: Record<string, string | undefined>, keystoreUsable: () => boolean): boolean;
13
10
  /**
14
11
  * Build the options forwarded to `createTypeSafe`.
15
- * The caller must gate on `keyAvailable` before calling `createTypeSafe`.
12
+ * The caller must gate on `authState({ backend }).usable` before calling `createTypeSafe`.
16
13
  */
17
14
  export declare function judgeOptions(config: {
18
15
  maxRequests: number;
package/dist/backend.js CHANGED
@@ -1,36 +1,28 @@
1
- import { authState } from "pi-typesafe";
2
- const BACKENDS = {
3
- typesafe: { host: "api.typesafe.ai" },
4
- openrouter: { host: "openrouter.ai", keyEnv: "OPENROUTER_API_KEY" },
5
- };
6
- /** The host the consent disclosure names as the destination. */
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`. */
7
3
  export function backendHost(backend) {
8
- return BACKENDS[backend].host;
4
+ return new URL(DECISIONS_BACKENDS[backend].host).host;
5
+ }
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";
9
9
  }
10
10
  /** Adapt the consent text to the active backend by substituting the destination host. */
11
11
  export function disclosureFor(backend, disclosure) {
12
- return backend === "typesafe" ? disclosure : disclosure.replace(BACKENDS.typesafe.host, BACKENDS[backend].host);
13
- }
14
- /**
15
- * Whether the right credential exists for the chosen backend.
16
- * TypeSafe uses the pi-typesafe keystore; other backends use an environment variable.
17
- */
18
- export function keyAvailable(backend, env, keystoreUsable) {
19
- const keyEnv = BACKENDS[backend].keyEnv;
20
- return keyEnv ? (env[keyEnv]?.trim() ?? "") !== "" : keystoreUsable();
12
+ return backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
21
13
  }
22
14
  /**
23
15
  * Build the options forwarded to `createTypeSafe`.
24
- * The caller must gate on `keyAvailable` before calling `createTypeSafe`.
16
+ * The caller must gate on `authState({ backend }).usable` before calling `createTypeSafe`.
25
17
  */
26
18
  export function judgeOptions(config) {
27
19
  return {
28
20
  maxRequests: config.maxRequests,
29
21
  timeoutMs: config.timeoutMs,
30
- ...(config.typesafeBackend === "typesafe" ? {} : { backend: config.typesafeBackend }),
22
+ ...(config.typesafeBackend === DEFAULT_BACKEND ? {} : { backend: config.typesafeBackend }),
31
23
  };
32
24
  }
33
25
  /** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
34
26
  export function resolveBackend(raw) {
35
- return raw === "typesafe" || raw === "openrouter" ? raw : "typesafe";
27
+ return raw === "typesafe" || raw === "openrouter" ? raw : DEFAULT_BACKEND;
36
28
  }
package/dist/config.d.ts CHANGED
@@ -273,6 +273,8 @@ export interface ConscienceConfig {
273
273
  maxLoadedBytes: number;
274
274
  /** P(useful now) must reach this to select a candidate for recommendation. Default 1.0 (trace-only until calibrated per spec §7). */
275
275
  recommendThreshold: number;
276
+ /** P(advance) from the disposition question must reach this before a candidate is considered. Default 0.70. */
277
+ advanceThreshold: number;
276
278
  /** P(useful now) must reach this to auto-load. Must be >= recommendThreshold. Default 1.0 (trace-only until calibrated). */
277
279
  loadThreshold: number;
278
280
  }
package/dist/config.js CHANGED
@@ -57,13 +57,14 @@ export function defaultConfig() {
57
57
  enabled: false,
58
58
  skills: { mode: "recommend", exclude: [] },
59
59
  tools: { enabled: true, exclude: [] },
60
- timeoutMs: 1500,
60
+ timeoutMs: 3000,
61
61
  maxAssessments: 3,
62
62
  maxNudges: 2,
63
63
  maxSkillBytes: 32768,
64
64
  maxLoadedBytes: 65536,
65
- // Calibration targets from spec §7. Until measured, 1.0 keeps production trace-only.
66
- recommendThreshold: 1.0,
65
+ // Beta policy thresholds (measured 2026-09-22); disabled by default, conscience.enabled is the switch.
66
+ recommendThreshold: 0.80,
67
+ advanceThreshold: 0.70,
67
68
  loadThreshold: 1.0,
68
69
  },
69
70
  };
@@ -478,6 +479,7 @@ function applyConscience(base, raw) {
478
479
  maxSkillBytes: Math.max(1024, Math.min(131072, typeof raw.maxSkillBytes === "number" ? raw.maxSkillBytes : base.maxSkillBytes)),
479
480
  maxLoadedBytes: Math.max(1024, Math.min(262144, typeof raw.maxLoadedBytes === "number" ? raw.maxLoadedBytes : base.maxLoadedBytes)),
480
481
  recommendThreshold: Math.max(0, Math.min(1, typeof raw.recommendThreshold === "number" ? raw.recommendThreshold : base.recommendThreshold)),
482
+ advanceThreshold: Math.max(0, Math.min(1, typeof raw.advanceThreshold === "number" ? raw.advanceThreshold : base.advanceThreshold)),
481
483
  loadThreshold: Math.max(0, Math.min(1, typeof raw.loadThreshold === "number" ? raw.loadThreshold : base.loadThreshold)),
482
484
  };
483
485
  }
@@ -8,6 +8,24 @@ import type { Skill } from "@earendil-works/pi-coding-agent";
8
8
  import type { Questions } from "pi-typesafe";
9
9
  import type { ConscienceConfig } from "./config.js";
10
10
  export type Disposition = "advance" | "awaiting_user" | "no_gap" | "unclear";
11
+ /** Capability roles the session model assigns to index entries. */
12
+ export type CapabilityRole = "research" | "evidence" | "execution" | "delegation" | "review" | "conversation";
13
+ /** Minimal index entry shape used by the conscience; full validation lives in index-cmd. */
14
+ export interface IndexEntry {
15
+ kind: "skill" | "tool";
16
+ name: string;
17
+ scope: "global" | "project";
18
+ sourceHash: string;
19
+ role: CapabilityRole;
20
+ lead: string;
21
+ useWhen: string[];
22
+ notWhen: string[];
23
+ inputs: string;
24
+ examples: string[];
25
+ thin: boolean;
26
+ truncated?: boolean;
27
+ _location?: string;
28
+ }
11
29
  /** One candidate for assessment: a skill or a tool. */
12
30
  export interface Candidate {
13
31
  /** "skill" or "tool". */
@@ -20,6 +38,10 @@ export interface Candidate {
20
38
  incomplete?: boolean;
21
39
  /** Full skill metadata, present for kind=skill. */
22
40
  skill?: Skill;
41
+ /** Capability role, present when the candidate has an index entry. */
42
+ role?: CapabilityRole;
43
+ /** Index entry when the candidate has a matching entry; bare description is the fallback. */
44
+ indexEntry?: IndexEntry;
23
45
  }
24
46
  /** The result of one assessment. */
25
47
  export interface AssessmentResult {
@@ -77,13 +99,29 @@ export declare function buildState(task: string, recentContext: string, activeSk
77
99
  /**
78
100
  * Compute a deterministic hash of the assessment questions for deduplication.
79
101
  * Recursively sorts all object keys for canonical form.
102
+ *
103
+ * Opaque candidate ids (c1, c2, …) are positional, not semantic: which batch a prompt happens to
104
+ * produce must not change the hash the activation gate compares. A conscience question set
105
+ * (recognised by the `conscience_disposition` key) is therefore hashed as the disposition question
106
+ * plus one canonical candidate question, keyed `c1` — every candidate question carries the same
107
+ * wording by design (per-candidate state lives in the request state, not the question text), so any
108
+ * one of them is the canonical representative.
109
+ *
110
+ * Why the hash was constant before this change: each assessment issues several requests (one per
111
+ * batch), each with its own question keys, and `assess` kept only the hash of whichever batch wrote
112
+ * `state_.hash` last. Every recorded replay row showed `fb2d35042f667b3c` only because those final
113
+ * batches happened to serialize identically — an accident of batch order, not a guarantee.
80
114
  */
81
115
  export declare function questionHash(questions: Questions): string;
82
- /** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. */
116
+ /** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. Attaches index entries when available. */
83
117
  export declare function eligibleCandidates(skills: Skill[], tools: {
84
118
  name: string;
85
119
  description: string;
86
- }[], config: ConscienceConfig, activeSkills: string[], suppliedSkills: string[]): {
120
+ }[], config: ConscienceConfig, activeSkills: string[], suppliedSkills: string[], globalIndex?: {
121
+ entries: IndexEntry[];
122
+ } | undefined, projectIndex?: {
123
+ entries: IndexEntry[];
124
+ } | undefined): {
87
125
  candidates: Candidate[];
88
126
  skillOverflow: boolean;
89
127
  toolOverflow: boolean;
@@ -105,11 +143,19 @@ export interface ConscienceDeps {
105
143
  sharedTimeoutMs: number;
106
144
  /** Current wall-clock time (ms). Injected for testability. */
107
145
  now?: () => number;
146
+ /** Pre-loaded global index, if any. */
147
+ globalIndex?: {
148
+ entries: IndexEntry[];
149
+ } | undefined;
150
+ /** Pre-loaded project index, if any. */
151
+ projectIndex?: {
152
+ entries: IndexEntry[];
153
+ } | undefined;
108
154
  }
109
155
  /**
110
156
  * Run assessment for the given prompt revision.
111
157
  * Pre-measurement: returns trace-only results. No steers, no auto-loading.
112
- * Issues sequential requests: skills first, then tools in the remaining envelope.
158
+ * Issues concurrent requests (up to four in flight), skills first, shared deadline.
113
159
  */
114
160
  export declare function assess(prompt: string, recentContext: string, skills: Skill[], tools: {
115
161
  name: string;