Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Notable changes to pi-warden, newest first. Versions follow semver. The publishe

How to keep this current: add the entry in the same pull request as the change, under `Unreleased`. The release commit renames `Unreleased` to the version it ships and adds its own notes. Entries before 0.10.0 are one-line summaries taken from the release commit headers; the detail for those is in `git log`.

## Unreleased

## 0.90.0

### Changed

- A config file that still sets `conscience.loadThreshold` or `learning.adaptiveThresholds` (documented in 0.74.1, removed since) now gets one config warning per key, from the user file and from a project file. The warning says the key was removed in 1.0 and is ignored, and what happens now. The key still has no effect. `docs/configuration.md` has a "Removed in 1.0" note.

## 0.89.1

### Added
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ After building, `/warden index` reports which skill and tool descriptions would
| `waste.search` | Default `true`. A search is `grep` or `rg` on one named file, not a glob and not `-r`. The note fires on the 3rd search of one file in the last 10 calls when two of them use the same or an overlapping pattern and nothing wrote that file between them. |
| `waste.recheck` | Default `true`. A check is a test, lint, typecheck, or build command (`npm test`, `npm run check`, `npx tsc`, `eslint`, `vitest`, `jest`, `pytest`, `ruff`, `mypy`, `cargo test`, `go test`, `make test`, `flutter test`, `dart analyze`, and their `bun`/`pnpm`/`uv` forms). The note fires when the same runner and target runs again in the last 10 calls with a different output filter, the earlier output was cut by a pipe, the earlier result showed no failure, and nothing wrote a file in between. |

### Removed in 1.0

`conscience.loadThreshold` and `learning.adaptiveThresholds` (documented in 0.74.1) are removed. A user file or a project file that still sets one loads, ignores the key, and adds one config warning for it. The conscience names a skill and asks the agent to load it; no score loads a skill by itself, and `conscience.skills.mode` is the only switch. Thresholds never change on their own; `/warden recommend` suggests changes from your hold history and you apply them.

## Project config

A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides: thresholds, extra guarded tools, `rules.files`, `rules.skip`, `rules.sensitivePaths`, or `"done": { "enabled": false }`. Project files are read only when Pi trusts the project. They can never grant `typesafe` consent, change `mode`, raise `timeoutMs` or `maxRequests`, set `notify.command`, or turn relevance compaction on or off (`compaction.enabled`). A key a project file cannot set at all is ignored without a warning.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pi-warden",
"version": "0.89.1",
"version": "0.90.0",
"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.",
"type": "module",
"license": "MIT",
Expand Down
19 changes: 17 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,10 +1069,25 @@ function applyGuards(base: WardenConfig, raw: Json, timeoutMs: number, source: "
};
}

/** Settings 0.74.1 documented and 1.0 removed: no code reads them. One warning per key found in a file, saying what happens now. */
const REMOVED_KEYS: ReadonlyArray<{ section: "conscience" | "learning"; key: string; now: string }> = [
{ section: "conscience", key: "loadThreshold", now: "no score loads a skill by itself; the conscience names a skill and asks the agent to load it, and conscience.skills.mode is the only switch for loading" },
{ section: "learning", key: "adaptiveThresholds", now: "thresholds never change on their own; /warden recommend suggests changes from your hold history and you apply them" },
];

function removedKeyWarnings(raw: Json, source: "user" | "project"): string[] {
const warnings: string[] = [];
for (const { section, key, now } of REMOVED_KEYS) {
const group = raw[section];
if (isObject(group) && key in group) warnings.push(`${source === "project" ? "project file: " : ""}${section}.${key} was removed in 1.0 and is ignored; ${now}`);
}
return warnings;
}

/** Unknown keys and invalid values fall back to the base; nothing throws on a malformed file. */
export function applyUserOverrides(base: WardenConfig, raw: unknown): WardenConfig {
if (!isObject(raw)) return base;
const warnings: string[] = [];
const warnings: string[] = removedKeyWarnings(raw, "user");
const shared = applyShared(base, raw);
const backend = resolveJudgmentBackend(raw.typesafeBackend);
const guards = applyGuards(base, raw, shared.timeoutMs, "user", warnings);
Expand Down Expand Up @@ -1197,7 +1212,7 @@ function applyConscience(base: ConscienceConfig, raw: unknown): ConscienceConfig
*/
export function applyProjectOverrides(base: WardenConfig, raw: unknown): WardenConfig {
if (!isObject(raw)) return base;
const warnings: string[] = [];
const warnings: string[] = removedKeyWarnings(raw, "project");
const enabled = projectSwitch("enabled", raw.enabled, base.enabled, warnings);
const guards = applyGuards(base, raw, base.timeoutMs, "project", warnings);
return { ...base, enabled, ...guards, prefs: applyPrefs(base.prefs, raw.prefs), warnings: [...(base.warnings ?? []), ...warnings] };
Expand Down
26 changes: 24 additions & 2 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ test("action.shouldProceed.hold, the deprecated name of threshold, still sets it
assert.deepEqual(completeConfig(stale as never).config.action.shouldProceed, { threshold: 0.45, steer: true });
});

test("a config file that still sets the removed keys loads cleanly", async () => {
test("a config file that still sets the removed keys loads, ignores them, and warns once for each of the two documented keys", async () => {
const path = userConfigPath();
await mkdir(join(path, ".."), { recursive: true });
await writeFile(path, JSON.stringify({
Expand All @@ -533,7 +533,9 @@ test("a config file that still sets the removed keys loads cleanly", async () =>
assert.deepEqual(config.learning, { patternAnalysis: false, retentionDays: 30, allowedRetentionDays: 90 }, "the removed keys are gone; the allowed-call retention keeps its default");
assert.equal("loadThreshold" in config.conscience, false);
assert.equal(config.conscience.recommendThreshold, 0.9, "the rest of the section still applies");
assert.deepEqual(config.warnings, []);
assert.equal(config.warnings.length, 2, "one warning for conscience.loadThreshold and one for learning.adaptiveThresholds; the two undocumented keys give none");
assert.ok(config.warnings.some(warning => warning.startsWith("conscience.loadThreshold was removed in 1.0")));
assert.ok(config.warnings.some(warning => warning.startsWith("learning.adaptiveThresholds was removed in 1.0")));
} finally {
await rm(path, { force: true });
}
Expand All @@ -547,3 +549,23 @@ test("an arming rule with no action is still ignored, now with one config warnin
assert.deepEqual(config.action.armingRules.map(rule => rule.id), ["kept"]);
assert.deepEqual(config.warnings, ['arming rule "no-action": has no action and is ignored; set action to confirm, hold, or block']);
});

test("removed keys: conscience.loadThreshold and learning.adaptiveThresholds each give one warning, from the user file and from a project file", () => {
const removed = [
{ section: "conscience", key: "loadThreshold", value: 0.9, now: /no score loads a skill by itself/ },
{ section: "learning", key: "adaptiveThresholds", value: true, now: /\/warden recommend suggests changes/ },
] as const;
for (const { section, key, value, now } of removed) {
const user = applyUserOverrides(defaultConfig(), { [section]: { [key]: value } });
assert.equal(user.warnings.length, 1, `${key}: one warning from the user file`);
assert.match(user.warnings[0]!, new RegExp(`^${section}\\.${key} was removed in 1\\.0 and is ignored; `));
assert.match(user.warnings[0]!, now);
const project = applyProjectOverrides(defaultConfig(), { [section]: { [key]: value } });
assert.equal(project.warnings.length, 1, `${key}: one warning from a project file`);
assert.match(project.warnings[0]!, new RegExp(`^project file: ${section}\\.${key} was removed in 1\\.0 and is ignored; `));
}
const both = applyUserOverrides(defaultConfig(), { conscience: { loadThreshold: 1 }, learning: { adaptiveThresholds: false } });
assert.equal(both.warnings.length, 2, "two keys, two warnings");
assert.deepEqual(applyUserOverrides(defaultConfig(), { conscience: { recommendThreshold: 0.9 }, learning: { patternAnalysis: false } }).warnings, [], "a config without them gives none");
assert.deepEqual(applyProjectOverrides(defaultConfig(), { conscience: { enabled: true }, learning: {} }).warnings, []);
});
Loading