pi-warden 0.36.0 → 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 +78 -0
- package/README.md +7 -3
- package/dist/backend.d.ts +4 -7
- package/dist/backend.js +11 -19
- package/dist/compact.d.ts +63 -0
- package/dist/compact.js +84 -0
- package/dist/config.d.ts +42 -1
- package/dist/config.js +44 -2
- package/dist/conscience.d.ts +163 -0
- package/dist/conscience.js +437 -0
- package/dist/extension.d.ts +4 -3
- package/dist/extension.js +735 -20
- package/dist/extension.js.bak +785 -25
- package/dist/guard.js +9 -5
- package/dist/hashing.d.ts +4 -0
- package/dist/hashing.js +22 -0
- package/dist/index-cmd.d.ts +87 -0
- package/dist/index-cmd.js +330 -0
- package/dist/load.d.ts +56 -0
- package/dist/load.js +257 -0
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +14 -2
- package/dist/trace.d.ts +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,84 @@ 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
|
+
|
|
56
|
+
## 0.38.3
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- `/warden config` keeps its panel handle and toggles shut, instead of stacking a second overlay against its own header hint.
|
|
60
|
+
- A bare `/warden` runs `status` again, and `config set <key> <value>` / `get <key>` reach the setting — the argument split dropped the default action and kept only the word after the subcommand.
|
|
61
|
+
|
|
62
|
+
## 0.38.2
|
|
63
|
+
|
|
64
|
+
### Docs
|
|
65
|
+
- Conscience recommendation calibration on recorded sessions: first measurement scripts, report, and guard calibration tables.
|
|
66
|
+
|
|
67
|
+
## 0.38.1
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- Verified against Pi 0.87.0; dev dependency updated.
|
|
71
|
+
|
|
72
|
+
## 0.38.0
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- Conscience coach: disabled by default pending calibration. Recommends or loads skills and tools before the agent acts via `before_agent_start`. Trace-only until a measured policy ships. Load mode reads skill files from disk with path-rule checks, size bounds, frontmatter validation, and credential canary detection. 48 adversarial tests plus the fixture set.
|
|
76
|
+
|
|
77
|
+
### Docs
|
|
78
|
+
- Conscience recommendation calibration (2026-09-22): first measurement on 609 turns across 4 projects. Tool recommendation 84% precision, skill recommendation unmeasured pending human labels. No threshold meets the 95% gate; closest is 0.95 at 91%. Candidate policy recorded (not active); `docs/guards.md` updated. `eval/reports/2026-09-22-conscience-recommend/`.
|
|
79
|
+
## 0.37.1
|
|
80
|
+
|
|
81
|
+
### Docs
|
|
82
|
+
- CONTRIBUTING.md: reflecting the evidence floor for destructive patterns; version bump is optional for outside PRs. docs/guards.md: same stale wording fixed.
|
|
83
|
+
|
|
84
|
+
## 0.37.0
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
- Compaction evidence appendix (`context.compactAppendix`, default true): after compaction succeeds, the extension sends a deterministic summary of session evidence — saved outputs, last checks, held actions, stuck state, and the active task — as one custom message so the agent can prefer saved paths over re-running commands. All strings are redacted. Does not spend a steer unit.
|
|
88
|
+
|
|
11
89
|
## 0.36.0
|
|
12
90
|
|
|
13
91
|
### Changed
|
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
|
|
18
|
-
2. `/warden
|
|
19
|
-
3.
|
|
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 `
|
|
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 {
|
|
2
|
-
|
|
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
|
|
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 ===
|
|
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 `
|
|
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 ===
|
|
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 :
|
|
27
|
+
return raw === "typesafe" || raw === "openrouter" ? raw : DEFAULT_BACKEND;
|
|
36
28
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface SavedOutput {
|
|
2
|
+
tool: string;
|
|
3
|
+
path: string;
|
|
4
|
+
/** Uncompressed size in bytes. */
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CheckEntry {
|
|
8
|
+
command: string;
|
|
9
|
+
passed: boolean;
|
|
10
|
+
/** Short label: "current run", "run 2", or "current run, #3". */
|
|
11
|
+
when: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HeldEntry {
|
|
14
|
+
tool: string;
|
|
15
|
+
/** Redacted preview of the call (command, path, or input); ≤120 chars. */
|
|
16
|
+
preview: string;
|
|
17
|
+
outcome: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StuckState {
|
|
20
|
+
failures: number;
|
|
21
|
+
sameStrategyScore: number | undefined;
|
|
22
|
+
currentCallFamily: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface CompactSnapshot {
|
|
25
|
+
savedOutputs: readonly SavedOutput[];
|
|
26
|
+
/** Capped at 5 in the hook before this is called. */
|
|
27
|
+
checks: readonly CheckEntry[];
|
|
28
|
+
/** Capped at 10 in the hook before this is called. */
|
|
29
|
+
holds: readonly HeldEntry[];
|
|
30
|
+
stuck: StuckState | undefined;
|
|
31
|
+
activeTask: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Returns empty string for an empty snapshot. Capped at 2 000 chars. */
|
|
34
|
+
export declare function compactAppendix(snapshot: CompactSnapshot): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build a `CompactSnapshot` from the extension's session-scoped state. All string values are redacted; saved-output
|
|
37
|
+
* paths are local and allowed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildCompactSnapshot(options: {
|
|
40
|
+
savedOutputs: Array<{
|
|
41
|
+
tool: string;
|
|
42
|
+
path: string;
|
|
43
|
+
bytes: number;
|
|
44
|
+
}>;
|
|
45
|
+
checks: Array<{
|
|
46
|
+
command: string;
|
|
47
|
+
passed: boolean;
|
|
48
|
+
runIndex: number;
|
|
49
|
+
indexInRun: number;
|
|
50
|
+
}>;
|
|
51
|
+
holds: Array<{
|
|
52
|
+
tool: string;
|
|
53
|
+
preview: string;
|
|
54
|
+
outcome: string;
|
|
55
|
+
}>;
|
|
56
|
+
stuck: {
|
|
57
|
+
failures: number;
|
|
58
|
+
sameStrategyScore: number | undefined;
|
|
59
|
+
currentCallFamily: string | undefined;
|
|
60
|
+
} | undefined;
|
|
61
|
+
activeTask: string | undefined;
|
|
62
|
+
runs: number;
|
|
63
|
+
}): CompactSnapshot;
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction evidence appendix. Deterministic, pure, no Jev request.
|
|
3
|
+
*
|
|
4
|
+
* After Pi fires `session_compact`, the extension builds a snapshot from session memory and passes it to
|
|
5
|
+
* `compactAppendix`. The returned text is sent as one custom message so the agent can prefer saved paths
|
|
6
|
+
* over re-running commands.
|
|
7
|
+
*/
|
|
8
|
+
import { redact } from "./redact.js";
|
|
9
|
+
const MAX_CHARS = 2000;
|
|
10
|
+
/** Returns empty string for an empty snapshot. Capped at 2 000 chars. */
|
|
11
|
+
export function compactAppendix(snapshot) {
|
|
12
|
+
const sections = [];
|
|
13
|
+
if (snapshot.savedOutputs.length) {
|
|
14
|
+
const items = snapshot.savedOutputs
|
|
15
|
+
.map(item => `- ${redact(item.tool)} → ${redact(item.path)} (${item.bytes} bytes)`)
|
|
16
|
+
.join("\n");
|
|
17
|
+
sections.push(`### Saved full outputs\n${items}`);
|
|
18
|
+
}
|
|
19
|
+
if (snapshot.checks.length) {
|
|
20
|
+
const items = snapshot.checks
|
|
21
|
+
.map(c => `- ${c.passed ? "✓" : "✗"} ${c.when}: ${redact(c.command)}`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
sections.push(`### Last checks\n${items}`);
|
|
24
|
+
}
|
|
25
|
+
if (snapshot.holds.length) {
|
|
26
|
+
const items = snapshot.holds
|
|
27
|
+
.map(h => `- ${redact(h.tool)}: ${redact(h.outcome)}${h.preview ? ` (${redact(h.preview)})` : ""}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
sections.push(`### Held actions\n${items}`);
|
|
30
|
+
}
|
|
31
|
+
if (snapshot.stuck) {
|
|
32
|
+
const s = snapshot.stuck;
|
|
33
|
+
const family = s.currentCallFamily ? `; family: ${redact(s.currentCallFamily)}` : "";
|
|
34
|
+
const score = s.sameStrategyScore !== undefined ? `; same-strategy: ${s.sameStrategyScore.toFixed(2)}` : "";
|
|
35
|
+
sections.push(`### Stuck state\n- failures: ${s.failures}${score}${family}`);
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.activeTask) {
|
|
38
|
+
sections.push(`### Active task\n${redact(snapshot.activeTask)}`);
|
|
39
|
+
}
|
|
40
|
+
if (sections.length === 0)
|
|
41
|
+
return "";
|
|
42
|
+
const body = sections.join("\n\n");
|
|
43
|
+
const appendix = [
|
|
44
|
+
"=== PI-WARDEN COMPACT EVIDENCE ===",
|
|
45
|
+
"Evidence warden kept across compaction. Prefer the saved-output paths below over re-running commands that produced them.",
|
|
46
|
+
"",
|
|
47
|
+
body,
|
|
48
|
+
"=== END PI-WARDEN COMPACT EVIDENCE ===",
|
|
49
|
+
].join("\n");
|
|
50
|
+
const marker = "\n… [truncated]";
|
|
51
|
+
if (appendix.length <= MAX_CHARS)
|
|
52
|
+
return appendix;
|
|
53
|
+
return appendix.slice(0, MAX_CHARS - marker.length) + marker;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a `CompactSnapshot` from the extension's session-scoped state. All string values are redacted; saved-output
|
|
57
|
+
* paths are local and allowed.
|
|
58
|
+
*/
|
|
59
|
+
export function buildCompactSnapshot(options) {
|
|
60
|
+
const savedOutputs = options.savedOutputs.map(o => ({
|
|
61
|
+
tool: o.tool,
|
|
62
|
+
path: o.path,
|
|
63
|
+
bytes: o.bytes,
|
|
64
|
+
}));
|
|
65
|
+
const maxChecks = 5;
|
|
66
|
+
const checks = options.checks.slice(-maxChecks).map(c => ({
|
|
67
|
+
command: c.command,
|
|
68
|
+
passed: c.passed,
|
|
69
|
+
when: options.runs <= 1 ? "current run" : `run ${c.runIndex}${c.indexInRun > 0 ? `, #${c.indexInRun + 1}` : ""}`,
|
|
70
|
+
}));
|
|
71
|
+
const maxHolds = 10;
|
|
72
|
+
const holds = options.holds.slice(-maxHolds).map(h => ({
|
|
73
|
+
tool: h.tool,
|
|
74
|
+
preview: h.preview,
|
|
75
|
+
outcome: h.outcome,
|
|
76
|
+
}));
|
|
77
|
+
const stuck = options.stuck ? {
|
|
78
|
+
failures: options.stuck.failures,
|
|
79
|
+
sameStrategyScore: options.stuck.sameStrategyScore,
|
|
80
|
+
currentCallFamily: options.stuck.currentCallFamily,
|
|
81
|
+
} : undefined;
|
|
82
|
+
const activeTask = options.activeTask ? redact(options.activeTask) : undefined;
|
|
83
|
+
return { savedOutputs, checks, holds, stuck, activeTask };
|
|
84
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -191,6 +191,8 @@ export interface ContextConfig {
|
|
|
191
191
|
recallTool: RecallTool;
|
|
192
192
|
/** Minimum P(format) before a format-specific parser builds the excerpt instead of the generic head/tail one. */
|
|
193
193
|
formatConfidence: number;
|
|
194
|
+
/** Append a compact evidence appendix to the summary during compaction. */
|
|
195
|
+
compactAppendix: boolean;
|
|
194
196
|
}
|
|
195
197
|
export interface RunawayConfig {
|
|
196
198
|
enabled: boolean;
|
|
@@ -239,6 +241,43 @@ export interface LearningConfig {
|
|
|
239
241
|
/** Days to keep hold records in SQLite before pruning. 0 disables pruning. */
|
|
240
242
|
retentionDays: number;
|
|
241
243
|
}
|
|
244
|
+
export type ConscienceSkillMode = "off" | "recommend" | "load";
|
|
245
|
+
export interface ConscienceSkillConfig {
|
|
246
|
+
/** off: no automatic skill selection; recommend: name a skill and ask the agent to load it; load: supply instructions directly. */
|
|
247
|
+
mode: ConscienceSkillMode;
|
|
248
|
+
/** Case-sensitive skill names to exclude; * is the only wildcard. */
|
|
249
|
+
exclude: string[];
|
|
250
|
+
}
|
|
251
|
+
export interface ConscienceToolConfig {
|
|
252
|
+
/** Suggest tools including evidence/research tools; never execute or enable them directly. */
|
|
253
|
+
enabled: boolean;
|
|
254
|
+
/** Case-sensitive tool names to exclude; * is the only wildcard. */
|
|
255
|
+
exclude: string[];
|
|
256
|
+
}
|
|
257
|
+
export interface ConscienceConfig {
|
|
258
|
+
/** Master switch for the conscience module. */
|
|
259
|
+
enabled: boolean;
|
|
260
|
+
/** Skill selection settings. */
|
|
261
|
+
skills: ConscienceSkillConfig;
|
|
262
|
+
/** Tool suggestion settings. */
|
|
263
|
+
tools: ConscienceToolConfig;
|
|
264
|
+
/** Total wall-clock deadline for one assessment (ms). Effective deadline is min(this, shared timeoutMs). */
|
|
265
|
+
timeoutMs: number;
|
|
266
|
+
/** Max assessments per admitted operator prompt, including the initial. */
|
|
267
|
+
maxAssessments: number;
|
|
268
|
+
/** Max new guidance deliveries per admitted operator prompt. */
|
|
269
|
+
maxNudges: number;
|
|
270
|
+
/** Max UTF-8 bytes per skill file for automatic loading. */
|
|
271
|
+
maxSkillBytes: number;
|
|
272
|
+
/** Max cumulative automatic loading bytes per admitted prompt. */
|
|
273
|
+
maxLoadedBytes: number;
|
|
274
|
+
/** P(useful now) must reach this to select a candidate for recommendation. Default 1.0 (trace-only until calibrated per spec §7). */
|
|
275
|
+
recommendThreshold: number;
|
|
276
|
+
/** P(advance) from the disposition question must reach this before a candidate is considered. Default 0.70. */
|
|
277
|
+
advanceThreshold: number;
|
|
278
|
+
/** P(useful now) must reach this to auto-load. Must be >= recommendThreshold. Default 1.0 (trace-only until calibrated). */
|
|
279
|
+
loadThreshold: number;
|
|
280
|
+
}
|
|
242
281
|
export interface WardenConfig {
|
|
243
282
|
/** Master switch. false disables every guard, including offline pattern checks. */
|
|
244
283
|
enabled: boolean;
|
|
@@ -279,10 +318,12 @@ export interface WardenConfig {
|
|
|
279
318
|
steerBudget: number;
|
|
280
319
|
/** Learning and adaptation settings. */
|
|
281
320
|
learning: LearningConfig;
|
|
321
|
+
/** Broad-consideration coach: recommends or loads skills and tools before the agent acts. */
|
|
322
|
+
conscience: ConscienceConfig;
|
|
282
323
|
}
|
|
283
324
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
284
325
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
285
|
-
export declare const CONFIG_SCHEMA =
|
|
326
|
+
export declare const CONFIG_SCHEMA = 7;
|
|
286
327
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
|
287
328
|
export declare function defaultConfig(): WardenConfig;
|
|
288
329
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
package/dist/config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function isRecallTool(value) {
|
|
|
10
10
|
}
|
|
11
11
|
export const PACKAGE_NAME = "pi-warden";
|
|
12
12
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
13
|
-
export const CONFIG_SCHEMA =
|
|
13
|
+
export const CONFIG_SCHEMA = 7;
|
|
14
14
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
15
15
|
export function defaultConfig() {
|
|
16
16
|
return {
|
|
@@ -44,7 +44,7 @@ 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 },
|
|
47
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true },
|
|
48
48
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
49
49
|
notify: { enabled: false, cooldownMs: 10000, command: [] },
|
|
50
50
|
subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
|
|
@@ -53,6 +53,20 @@ export function defaultConfig() {
|
|
|
53
53
|
notices: false,
|
|
54
54
|
steerBudget: 3,
|
|
55
55
|
learning: { adaptiveThresholds: true, patternAnalysis: true, minHoldsForAdaptive: 20, adaptationRate: 0.1, retentionDays: 365 },
|
|
56
|
+
conscience: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
skills: { mode: "recommend", exclude: [] },
|
|
59
|
+
tools: { enabled: true, exclude: [] },
|
|
60
|
+
timeoutMs: 3000,
|
|
61
|
+
maxAssessments: 3,
|
|
62
|
+
maxNudges: 2,
|
|
63
|
+
maxSkillBytes: 32768,
|
|
64
|
+
maxLoadedBytes: 65536,
|
|
65
|
+
// Beta policy thresholds (measured 2026-09-22); disabled by default, conscience.enabled is the switch.
|
|
66
|
+
recommendThreshold: 0.80,
|
|
67
|
+
advanceThreshold: 0.70,
|
|
68
|
+
loadThreshold: 1.0,
|
|
69
|
+
},
|
|
56
70
|
};
|
|
57
71
|
}
|
|
58
72
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
|
@@ -408,6 +422,7 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
408
422
|
duplicateMinChars: positiveInteger(raw.context.duplicateMinChars, base.context.duplicateMinChars),
|
|
409
423
|
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
410
424
|
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
425
|
+
compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
|
|
411
426
|
} : base.context,
|
|
412
427
|
};
|
|
413
428
|
}
|
|
@@ -428,6 +443,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
428
443
|
notices: boolean(raw.notices, base.notices),
|
|
429
444
|
steerBudget: typeof raw.steerBudget === "number" && Number.isInteger(raw.steerBudget) && raw.steerBudget >= 0 ? raw.steerBudget : base.steerBudget,
|
|
430
445
|
learning: applyLearning(base.learning, raw.learning),
|
|
446
|
+
conscience: applyConscience(base.conscience, raw.conscience),
|
|
431
447
|
};
|
|
432
448
|
}
|
|
433
449
|
function applyLearning(base, raw) {
|
|
@@ -441,6 +457,32 @@ function applyLearning(base, raw) {
|
|
|
441
457
|
retentionDays: Math.max(0, positiveInteger(raw.retentionDays, base.retentionDays)),
|
|
442
458
|
};
|
|
443
459
|
}
|
|
460
|
+
function applyConscience(base, raw) {
|
|
461
|
+
if (!isObject(raw))
|
|
462
|
+
return base;
|
|
463
|
+
const skillsRaw = isObject(raw.skills) ? raw.skills : undefined;
|
|
464
|
+
const toolsRaw = isObject(raw.tools) ? raw.tools : undefined;
|
|
465
|
+
const mode = typeof skillsRaw?.mode === "string" && ["off", "recommend", "load"].includes(skillsRaw.mode) ? skillsRaw.mode : base.skills.mode;
|
|
466
|
+
return {
|
|
467
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
468
|
+
skills: {
|
|
469
|
+
mode,
|
|
470
|
+
exclude: Array.isArray(skillsRaw?.exclude) ? skillsRaw.exclude.filter((x) => typeof x === "string") : base.skills.exclude,
|
|
471
|
+
},
|
|
472
|
+
tools: {
|
|
473
|
+
enabled: boolean(toolsRaw?.enabled, base.tools.enabled),
|
|
474
|
+
exclude: Array.isArray(toolsRaw?.exclude) ? toolsRaw.exclude.filter((x) => typeof x === "string") : base.tools.exclude,
|
|
475
|
+
},
|
|
476
|
+
timeoutMs: Math.max(100, Math.min(10000, typeof raw.timeoutMs === "number" ? raw.timeoutMs : base.timeoutMs)),
|
|
477
|
+
maxAssessments: Math.max(1, Math.min(10, typeof raw.maxAssessments === "number" ? raw.maxAssessments : base.maxAssessments)),
|
|
478
|
+
maxNudges: Math.max(1, Math.min(5, typeof raw.maxNudges === "number" ? raw.maxNudges : base.maxNudges)),
|
|
479
|
+
maxSkillBytes: Math.max(1024, Math.min(131072, typeof raw.maxSkillBytes === "number" ? raw.maxSkillBytes : base.maxSkillBytes)),
|
|
480
|
+
maxLoadedBytes: Math.max(1024, Math.min(262144, typeof raw.maxLoadedBytes === "number" ? raw.maxLoadedBytes : base.maxLoadedBytes)),
|
|
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)),
|
|
483
|
+
loadThreshold: Math.max(0, Math.min(1, typeof raw.loadThreshold === "number" ? raw.loadThreshold : base.loadThreshold)),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
444
486
|
/** Project files may tune the guards but cannot grant TypeSafe consent, change the mode, or raise budgets. */
|
|
445
487
|
export function applyProjectOverrides(base, raw) {
|
|
446
488
|
if (!isObject(raw))
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conscience module: broad-consideration coach that recommends or loads
|
|
3
|
+
* skills and tools before the agent acts.
|
|
4
|
+
*
|
|
5
|
+
* Pre-measurement: all selections are trace-only. No steers, no auto-loading.
|
|
6
|
+
*/
|
|
7
|
+
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { Questions } from "pi-typesafe";
|
|
9
|
+
import type { ConscienceConfig } from "./config.js";
|
|
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
|
+
}
|
|
29
|
+
/** One candidate for assessment: a skill or a tool. */
|
|
30
|
+
export interface Candidate {
|
|
31
|
+
/** "skill" or "tool". */
|
|
32
|
+
kind: "skill" | "tool";
|
|
33
|
+
/** Stable id: skill name or tool name. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Human-readable description (sanitized: no absolute paths or URLs). */
|
|
36
|
+
description: string;
|
|
37
|
+
/** True when this candidate belongs to a category that was truncated (catalog incomplete). */
|
|
38
|
+
incomplete?: boolean;
|
|
39
|
+
/** Full skill metadata, present for kind=skill. */
|
|
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;
|
|
45
|
+
}
|
|
46
|
+
/** The result of one assessment. */
|
|
47
|
+
export interface AssessmentResult {
|
|
48
|
+
/** The disposition chosen by the shared question. */
|
|
49
|
+
disposition: Disposition;
|
|
50
|
+
/** The selected candidate, if any. */
|
|
51
|
+
selected: Candidate | null;
|
|
52
|
+
/** P(useful now) = P(direct_useful) + P(prerequisite). */
|
|
53
|
+
usefulness: number;
|
|
54
|
+
/** P(advance) from the disposition answer. */
|
|
55
|
+
pAdvance: number;
|
|
56
|
+
/** Question hash for deduplication. */
|
|
57
|
+
questionHash: string;
|
|
58
|
+
/** Time spent on this assessment (ms). */
|
|
59
|
+
elapsedMs: number;
|
|
60
|
+
/** Why no selection was made, if applicable. */
|
|
61
|
+
skipReason?: SkipReason | undefined;
|
|
62
|
+
/** Error category when skipReason is "error": timeout, network, configuration, auth, or other. */
|
|
63
|
+
errorCategory?: string | undefined;
|
|
64
|
+
/** Number of judge requests issued for this assessment. */
|
|
65
|
+
requestCount: number;
|
|
66
|
+
}
|
|
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";
|
|
68
|
+
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
69
|
+
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
|
+
/** Strip absolute paths, URLs, and credentials from a description. */
|
|
71
|
+
export declare function sanitizeDescription(text: string): string;
|
|
72
|
+
/** Opaque IDs: c1, c2, ... in stable order. */
|
|
73
|
+
export declare function opaqueId(index: number): string;
|
|
74
|
+
/** Map candidates to opaque IDs, returning a stable ordered list. */
|
|
75
|
+
export declare function assignOpaqueIds(candidates: Candidate[]): Array<{
|
|
76
|
+
opaqueId: string;
|
|
77
|
+
candidate: Candidate;
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Build assessment questions for a batch of candidates.
|
|
81
|
+
* State carries structured data; each question names its candidate field.
|
|
82
|
+
* Returns questions, the disposition key, and the opaqueId→candidate map.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildBatchQuestions(batch: Array<{
|
|
85
|
+
opaqueId: string;
|
|
86
|
+
candidate: Candidate;
|
|
87
|
+
}>, task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[]): {
|
|
88
|
+
questions: Questions;
|
|
89
|
+
dispositionKey: string;
|
|
90
|
+
idMap: Map<string, Candidate>;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Build the full state object for the judge request (spec §3 state contract).
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildState(task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[], batch: Array<{
|
|
96
|
+
opaqueId: string;
|
|
97
|
+
candidate: Candidate;
|
|
98
|
+
}>): Record<string, unknown>;
|
|
99
|
+
/**
|
|
100
|
+
* Compute a deterministic hash of the assessment questions for deduplication.
|
|
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.
|
|
114
|
+
*/
|
|
115
|
+
export declare function questionHash(questions: Questions): string;
|
|
116
|
+
/** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. Attaches index entries when available. */
|
|
117
|
+
export declare function eligibleCandidates(skills: Skill[], tools: {
|
|
118
|
+
name: string;
|
|
119
|
+
description: string;
|
|
120
|
+
}[], config: ConscienceConfig, activeSkills: string[], suppliedSkills: string[], globalIndex?: {
|
|
121
|
+
entries: IndexEntry[];
|
|
122
|
+
} | undefined, projectIndex?: {
|
|
123
|
+
entries: IndexEntry[];
|
|
124
|
+
} | undefined): {
|
|
125
|
+
candidates: Candidate[];
|
|
126
|
+
skillOverflow: boolean;
|
|
127
|
+
toolOverflow: boolean;
|
|
128
|
+
};
|
|
129
|
+
export interface Judge {
|
|
130
|
+
evaluate(request: {
|
|
131
|
+
state: unknown;
|
|
132
|
+
questions: Questions;
|
|
133
|
+
}, options?: {
|
|
134
|
+
timeoutMs?: number;
|
|
135
|
+
}): Promise<{
|
|
136
|
+
answers: Record<string, unknown>;
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
139
|
+
export interface ConscienceDeps {
|
|
140
|
+
judge: Judge | undefined;
|
|
141
|
+
config: ConscienceConfig;
|
|
142
|
+
/** Shared timeout from WardenConfig. The effective deadline is min(conscience.timeoutMs, sharedTimeoutMs). */
|
|
143
|
+
sharedTimeoutMs: number;
|
|
144
|
+
/** Current wall-clock time (ms). Injected for testability. */
|
|
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;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Run assessment for the given prompt revision.
|
|
157
|
+
* Pre-measurement: returns trace-only results. No steers, no auto-loading.
|
|
158
|
+
* Issues concurrent requests (up to four in flight), skills first, shared deadline.
|
|
159
|
+
*/
|
|
160
|
+
export declare function assess(prompt: string, recentContext: string, skills: Skill[], tools: {
|
|
161
|
+
name: string;
|
|
162
|
+
description: string;
|
|
163
|
+
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps): Promise<AssessmentResult>;
|