pi-warden 0.71.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 +54 -0
- package/dist/adaptive.d.ts +3 -2
- package/dist/adaptive.js +4 -3
- package/dist/backend.d.ts +33 -14
- package/dist/backend.js +27 -14
- package/dist/config.d.ts +13 -8
- package/dist/config.js +20 -20
- package/dist/conscience.d.ts +1 -1
- package/dist/extension.d.ts +14 -3
- package/dist/extension.js +410 -64
- package/dist/guard.d.ts +2 -1
- package/dist/guard.js +4 -4
- package/dist/holds.d.ts +1 -1
- package/dist/holds.js +3 -2
- package/dist/host-compat.d.ts +2 -0
- package/dist/host-compat.js +132 -0
- package/dist/host-dirs.d.ts +10 -0
- package/dist/host-dirs.js +21 -0
- package/dist/index-cmd.d.ts +6 -5
- package/dist/index-cmd.js +16 -24
- package/dist/index.d.ts +12 -4
- package/dist/index.js +6 -2
- package/dist/init.js +31 -3
- package/dist/learning.d.ts +12 -11
- package/dist/learning.js +60 -38
- package/dist/loops.d.ts +2 -1
- package/dist/loops.js +3 -2
- package/dist/panel.d.ts +4 -1
- package/dist/panel.js +11 -6
- package/dist/prefs.d.ts +1 -1
- package/dist/prefs.js +3 -2
- package/dist/rules-audit.d.ts +208 -0
- package/dist/rules-audit.js +417 -0
- package/dist/rules-calibrate.d.ts +196 -0
- package/dist/rules-calibrate.js +474 -0
- package/dist/rules-lint.d.ts +2 -0
- package/dist/rules-lint.js +7 -2
- package/dist/rules-log.d.ts +6 -1
- package/dist/rules-log.js +9 -3
- package/dist/rules-report.d.ts +3 -0
- package/dist/rules-report.js +5 -0
- package/dist/rules.d.ts +72 -5
- package/dist/rules.js +116 -54
- package/dist/shape.js +8 -2
- package/dist/turn-rules.d.ts +86 -0
- package/dist/turn-rules.js +232 -0
- package/dist/widget-render.d.ts +23 -0
- package/dist/widget-render.js +82 -0
- package/dist/widget.d.ts +5 -22
- package/dist/widget.js +3 -80
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,60 @@ 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
|
+
|
|
28
|
+
## 0.73.0
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- pi-warden runs on oh-my-pi and other hosts that load Pi extensions through a compatibility layer. `src/host-compat.ts` maps the host API onto the Pi contract: `before_agent_start` gets `systemPromptOptions` with the active skills, and an `appendSystemPrompt` becomes the host's `systemPrompt` array. An input the extension handles is also marked `handled`. `agent_settled` runs after pi-warden's `agent_end` handlers have been dispatched and the host becomes idle. If background work is cancelled without another `agent_end`, the adapter waits for that work to drain. Command contexts get `getSystemPromptOptions()`. On upstream Pi the API is not changed.
|
|
33
|
+
- Data paths follow the host: the user config, the `/warden index` directory, and `holds.db` use the host's agent directory (`~/.pi/agent` on Pi, `~/.omp/agent` on oh-my-pi), and the project file uses the host's config directory (`.pi/pi-warden.json` or `.omp/pi-warden.json`).
|
|
34
|
+
- On a host whose overlay handle cannot release focus, Escape closes the trace and config panels.
|
|
35
|
+
- `import { defaultConfig } from "pi-warden"` works again without the optional `@earendil-works/pi-coding-agent` peer and now also without the optional `@earendil-works/pi-tui` peer: the library entry has no runtime import of either. Host directories come from the new injectable `HostDirs` parameter on every path function (`defaultHostDirs()` reads `PI_CODING_AGENT_DIR` and falls back to `~/.pi/agent`; existing callers keep working unchanged), and `src/extension.ts` builds them once from the host and passes them to every call. The entry's only pi-tui code — the rendered status stack — moved to `src/widget-render.ts`, which the entry does not re-export.
|
|
36
|
+
- On a host whose agent directory is not the Pi default (oh-my-pi), an interactive session shows a one-time notice when `~/.pi/agent/pi-warden` exists and the notice has not been shown on this host: it names the target and legacy folders and a command that moves the fresh data aside and copies the legacy data in its place, and records a marker beside the data folder so it shows once. Headless sessions show nothing.
|
|
37
|
+
|
|
38
|
+
### Docs
|
|
39
|
+
|
|
40
|
+
- `docs/configuration.md`, `docs/data-handling.md`, `docs/commands.md`, and `docs/faq.md` say that paths follow the host. `docs/configuration.md` and `docs/data-handling.md` add a one-time migration note for existing oh-my-pi users: close all Pi and oh-my-pi sessions, then run `{ [ ! -e "$HOME/.omp/agent/pi-warden" ] || mv "$HOME/.omp/agent/pi-warden" "$HOME/.omp/agent/pi-warden.before-migration"; } && cp -R "$HOME/.pi/agent/pi-warden" "$HOME/.omp/agent/pi-warden"` and copy each project's `.pi/pi-warden.json` to `.omp/pi-warden.json` (keep the original). oh-my-pi shows this notice once per machine.
|
|
41
|
+
|
|
42
|
+
## 0.72.0
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- A project rule may cite where its wording came from with a `source: <file>:<line>` header beside `paths:`, for example `source: AGENTS.md:65`. `/warden rules` shows it, and when the rule fires the steer names it: `"<rule>" (from AGENTS.md line 65) ...`. A bad value is ignored and `/warden rules` reports the line.
|
|
47
|
+
- `/warden init` compiles an existing `AGENTS.md` or `CLAUDE.md` into small rules. The prompt hands the agent the instruction file's lines with line numbers and asks for one rule per instruction that can be judged from one changed file, each with a `source:` header and a `paths:` header when the instruction concerns certain files, wording close to the source, and a concrete violation pattern. Instructions that need other files, repository history, or the task, and anything a linter or type checker enforces, are left out of the file and listed in the reply with the reason. The reply suggests `/warden rules check` when the user has a key; the check is never run automatically. When `pi-warden.md` already exists the prompt is unchanged.
|
|
48
|
+
- `/warden rules audit [paths...] [--max N] [--yes]` judges existing files against the project rules as if each had just been written: one Jev request per file, at most four at a time, over the source files under the given paths (default: the project) that git does not ignore and at least one rule applies to, capped at `--max` (default 50) with the number left out reported. A confirm dialog names the file count and what leaves the machine before anything is sent; a headless run needs `--yes`. The output is a table by rule (files judged, files flagged, mean score) and the flagged files with their rule scores, worst first, plus a Markdown copy at `.pi-warden/rules-audit.md`. Nothing is recorded in the rules log. With no key or no consent it says so and sends nothing.
|
|
49
|
+
- `/warden bench [--runs N]` (default 10) measures what a rules check costs on this machine: the fixed built-in sample file judged N times against the active rules, reporting p50 and p95 latency, requests, mean input tokens per check, and estimated cost per check and per 100 edits. No confirmation is needed — the sample is built in and no project content is sent — and with no key or no consent it says so and sends nothing.
|
|
50
|
+
- `/warden rules calibrate [--commits N] [--max N] [--yes]` replays the changed files of the last N non-merge commits (default 20) through the project rules and reports, per rule, how often it applied and fired, the mean score, and the flags `fires on everything` and `undecided`, worst first, then the rules no file in the sample reached; a rule with no fire in the sample reports `no violation in sample` and is not flagged, because replayed commits are mostly compliant code. Each changed file is one `edit` request (the removed lines as `oldText`, the added lines as `newText`, the file after the commit as context), capped at `--max` (default 40); binary, generated, gitignored, `rules.exclude`, and `rules.skip` files are skipped, and a rule fires at its own `threshold:` cutoff. The scores go to the local rules log with `source: "calibrate"`, which `/warden report` counts apart from live verdicts. Nothing is sent before a confirm dialog showing the request count and the redacted diffs; a headless run needs the explicit `--yes`.
|
|
51
|
+
- `/warden rules tune` sends the session's agent one prompt with the rules the latest calibrate flagged `fires on everything` or `undecided`, or that `/warden rules check` (this session) flagged: each rule's current text and why it was flagged, and the ask for a rewrite that is concrete and judgeable from one changed file. The agent edits `pi-warden.md` with its own tools. With nothing flagged it says so and sends nothing.
|
|
52
|
+
- A project rule may carry `when: turn` beside `paths:`, `threshold:`, and `severity:` (default `edit`). A `when: turn` rule is judged once at the end of each agent run against the whole diff the run made and the user's task for that run, in one request, instead of on every edit — the home for rules one edit cannot answer, such as a change that adds more than the task asked for, a new abstraction with a single use, or logic duplicated across files. Turn rules are never asked about a single edit. The header lines parse in any order and at most once; a bad value is ignored and `/warden rules` reports the line.
|
|
53
|
+
- The end-of-run pass also judges files that shell commands changed in ways the per-edit guard cannot see (`sed -i`, `patch`, `git apply`, a generator's output redirected to a file, a subagent's writes): each changed file that no `write`, `edit`, or literal shell write already judged during the run is judged once, with its diff as the edit and the same per-edit question. The baseline is a read-only snapshot of the working tree taken in the background at run start (a temporary index file; the working tree, the real index, and the stash list are never touched), diffed at run end and including new untracked files git does not ignore. Every git call is asynchronous, so no hook blocks while git hashes the tree; the run end waits for a snapshot still running. The diff is capped at `rules.maxChars` per file and in total with the cut named. The unseen-change requests are capped at five files per run — the same cap as the files one shell command writes — with the first files in diff order judged and the rest named in the trace. Findings are one steer at the end of the run through the done-check's delivery, at most once per run and within the per-run steer budget; nothing holds. With no `when: turn` rules and no change the per-edit guard missed, no request is made and nothing is said.
|
|
54
|
+
|
|
55
|
+
### Tests
|
|
56
|
+
|
|
57
|
+
- Offline coverage for the history replay: hunk extraction from `git log -p`, the skip rules (binary, generated, ignored, `rules.exclude`, `rules.skip`), the request cap, the confirm and `--yes` paths, the report flags and their order at the `/warden report` thresholds, the `source: "calibrate"` log records, and the tune prompt (with no prompt when nothing is flagged), on temporary git repositories with a fake judge.
|
|
58
|
+
|
|
59
|
+
- Rules replay (`scripts/rules-replay.mjs`, `npm run eval:replay`): replays the `write` and `edit` calls of past Pi sessions in a project against that project's current rules, one request per judged call, and writes a review sheet (every flagged call plus a fixed-seed sample of unflagged calls) outside the repository beside aggregate counts. `--score` turns labelled sheets into precision on the flagged items and the estimated miss rate from the unflagged sample. The first run on this repository's own sessions is in `eval/reports/2026-09-27-rules-replay/`, unlabelled pending an independent reviewer. Offline tests cover the session replay and the output boundaries.
|
|
60
|
+
|
|
61
|
+
### Docs
|
|
62
|
+
|
|
63
|
+
- `docs/commands.md`, `docs/guards.md`, and `docs/data-handling.md` cover the two commands and what the replay sends.
|
|
64
|
+
|
|
11
65
|
## 0.71.0
|
|
12
66
|
|
|
13
67
|
### Added
|
package/dist/adaptive.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HostDirs } from "./host-dirs.js";
|
|
1
2
|
import type { SteersConfig } from "./config.js";
|
|
2
3
|
/**
|
|
3
4
|
* Per-model steer adaptation. A steer kind that a model follows rarely or disputes often becomes trace-only for that
|
|
@@ -32,8 +33,8 @@ export interface MutedPair {
|
|
|
32
33
|
followed: number;
|
|
33
34
|
disputed: number;
|
|
34
35
|
}
|
|
35
|
-
/** Where the counts live: next to the user config, in pi-warden's data folder. */
|
|
36
|
-
export declare function steerStatsPath(): string;
|
|
36
|
+
/** Where the counts live: next to the user config, in pi-warden's data folder. `PI_WARDEN_STEER_STATS` overrides it. */
|
|
37
|
+
export declare function steerStatsPath(dirs?: HostDirs): string;
|
|
37
38
|
export declare const DEFAULT_STEERS: SteersConfig;
|
|
38
39
|
/** True when the counts put the pair under the follow floor or over the dispute ceiling. */
|
|
39
40
|
export declare function failing(kind: SteerKind, counts: Counts, config: SteersConfig): boolean;
|
package/dist/adaptive.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
4
4
|
import { userConfigPath } from "./config.js";
|
|
5
|
+
import { defaultHostDirs } from "./host-dirs.js";
|
|
5
6
|
export const STEER_KINDS = [
|
|
6
7
|
"intent-mismatch", "off-task", "should-proceed", "warn-headless", "large-output", "slop", "rules", "sensitive-path",
|
|
7
8
|
"security-write", "stuck", "repeat", "done", "prose", "conscience", "loops", "runaway", "subagent",
|
|
@@ -15,9 +16,9 @@ export const STEER_KINDS = [
|
|
|
15
16
|
export const NEVER_MUTED = new Set([
|
|
16
17
|
"hold", "confirm", "deny", "masking", "credential-notice", "done", "runaway", "subagent",
|
|
17
18
|
]);
|
|
18
|
-
/** Where the counts live: next to the user config, in pi-warden's data folder. */
|
|
19
|
-
export function steerStatsPath() {
|
|
20
|
-
return process.env.PI_WARDEN_STEER_STATS ?? join(dirname(userConfigPath()), "steer-stats.json");
|
|
19
|
+
/** Where the counts live: next to the user config, in pi-warden's data folder. `PI_WARDEN_STEER_STATS` overrides it. */
|
|
20
|
+
export function steerStatsPath(dirs = defaultHostDirs()) {
|
|
21
|
+
return process.env.PI_WARDEN_STEER_STATS ?? join(dirname(userConfigPath(dirs)), "steer-stats.json");
|
|
21
22
|
}
|
|
22
23
|
export const DEFAULT_STEERS = { adaptive: true, minSteers: 30, minFollowed: 0.2, maxDisputed: 0.4, recheckEvery: 30, probeEvery: 5 };
|
|
23
24
|
const zero = () => ({ sent: 0, followed: 0, disputed: 0 });
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import type { TypeSafeOptions } from "pi-typesafe";
|
|
2
|
-
/** The judgment backend
|
|
3
|
-
export type JudgmentBackend =
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
/** The
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { BackendSpec, TypeSafeOptions } from "pi-typesafe";
|
|
2
|
+
/** The judgment backend: a registry name ("typesafe", "openrouter", "commandcode") or a caller-supplied endpoint object. */
|
|
3
|
+
export type JudgmentBackend = BackendSpec;
|
|
4
|
+
/** The judgment destination as the config carries it, plus the refusal that turns judgments off when the configured value was refused. */
|
|
5
|
+
export interface BackendSetting {
|
|
6
|
+
/** The spec judgments go to; undefined only when the configured value was refused. */
|
|
7
|
+
readonly typesafeBackend: JudgmentBackend | undefined;
|
|
8
|
+
/** Why the configured value was refused: the once-per-session notice and `/warden status` quote it. */
|
|
9
|
+
readonly backendRefusal: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** What a raw `typesafeBackend` value resolves to: the spec judgments go to, or the refusal that turns them off. Exactly one is set. */
|
|
12
|
+
export type BackendResolution = {
|
|
13
|
+
readonly typesafeBackend: JudgmentBackend;
|
|
14
|
+
readonly backendRefusal?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
readonly typesafeBackend?: undefined;
|
|
17
|
+
readonly backendRefusal: string;
|
|
18
|
+
};
|
|
19
|
+
/** Why no judge is available: consent not given, the backend refused, no key for the backend, a saved 401 or 403, or the request budget spent. */
|
|
20
|
+
export type JudgmentsOffReason = "no_consent" | "bad_backend" | "no_key" | "key_rejected" | "budget";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a raw config value to the spec judgments go to, kept as written, or to the refusal that turns judgments off.
|
|
23
|
+
* pi-typesafe validates the spec again on every call. An unknown name or an object it refuses is never silently mapped
|
|
24
|
+
* to the default backend: a mistyped value must not send judgments to api.typesafe.ai.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveJudgmentBackend(raw: unknown): BackendResolution;
|
|
27
|
+
/** The name messages show for a backend: the registry name, or an endpoint's validated label. */
|
|
28
|
+
export declare function backendName(backend: JudgmentBackend | undefined): string;
|
|
29
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; true only for "typesafe". */
|
|
30
|
+
export declare function loginStoresKey(backend: JudgmentBackend | undefined): boolean;
|
|
31
|
+
/** One phrase naming where judgments go — the label, the host, and the model sent — for consent text and status lines. */
|
|
32
|
+
export declare function describeBackend(backend: JudgmentBackend | undefined): string;
|
|
12
33
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
13
|
-
export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
|
|
34
|
+
export declare function disclosureFor(backend: JudgmentBackend | undefined, disclosure: string): string;
|
|
14
35
|
/**
|
|
15
36
|
* Build the options forwarded to `createTypeSafe`.
|
|
16
37
|
* The caller must gate on `authState({ backend }).usable` before calling `createTypeSafe`.
|
|
@@ -20,5 +41,3 @@ export declare function judgeOptions(config: {
|
|
|
20
41
|
timeoutMs: number;
|
|
21
42
|
typesafeBackend: JudgmentBackend;
|
|
22
43
|
}): TypeSafeOptions;
|
|
23
|
-
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
24
|
-
export declare function resolveBackend(raw: unknown): JudgmentBackend;
|
package/dist/backend.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { DEFAULT_BACKEND, backendHost, resolveBackend } from "pi-typesafe";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a raw config value to the spec judgments go to, kept as written, or to the refusal that turns judgments off.
|
|
4
|
+
* pi-typesafe validates the spec again on every call. An unknown name or an object it refuses is never silently mapped
|
|
5
|
+
* to the default backend: a mistyped value must not send judgments to api.typesafe.ai.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveJudgmentBackend(raw) {
|
|
8
|
+
if (raw === undefined || raw === null)
|
|
9
|
+
return { typesafeBackend: DEFAULT_BACKEND };
|
|
10
|
+
try {
|
|
11
|
+
resolveBackend(raw);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return { backendRefusal: error instanceof Error ? error.message : String(error) };
|
|
15
|
+
}
|
|
16
|
+
return { typesafeBackend: raw };
|
|
5
17
|
}
|
|
6
|
-
/** The
|
|
7
|
-
export function
|
|
8
|
-
return
|
|
18
|
+
/** The name messages show for a backend: the registry name, or an endpoint's validated label. */
|
|
19
|
+
export function backendName(backend) {
|
|
20
|
+
return backend === undefined ? DEFAULT_BACKEND : typeof backend === "string" ? backend : resolveBackend(backend).label;
|
|
9
21
|
}
|
|
10
|
-
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores;
|
|
22
|
+
/** Whether the backend takes the TypeSafe key, the only one `/typesafe login` stores; true only for "typesafe". */
|
|
11
23
|
export function loginStoresKey(backend) {
|
|
12
|
-
return
|
|
24
|
+
return backend === DEFAULT_BACKEND;
|
|
25
|
+
}
|
|
26
|
+
/** One phrase naming where judgments go — the label, the host, and the model sent — for consent text and status lines. */
|
|
27
|
+
export function describeBackend(backend) {
|
|
28
|
+
const resolved = resolveBackend(backend);
|
|
29
|
+
return `${resolved.label} at ${backendHost(backend)}, model ${resolved.defaultModel ?? "none configured"}`;
|
|
13
30
|
}
|
|
14
31
|
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
15
32
|
export function disclosureFor(backend, disclosure) {
|
|
16
|
-
return backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
|
33
|
+
return backend === undefined || backend === DEFAULT_BACKEND ? disclosure : disclosure.replace(backendHost(DEFAULT_BACKEND), backendHost(backend));
|
|
17
34
|
}
|
|
18
35
|
/**
|
|
19
36
|
* Build the options forwarded to `createTypeSafe`.
|
|
@@ -26,7 +43,3 @@ export function judgeOptions(config) {
|
|
|
26
43
|
...(config.typesafeBackend === DEFAULT_BACKEND ? {} : { backend: config.typesafeBackend }),
|
|
27
44
|
};
|
|
28
45
|
}
|
|
29
|
-
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
30
|
-
export function resolveBackend(raw) {
|
|
31
|
-
return raw === "typesafe" || raw === "openrouter" ? raw : DEFAULT_BACKEND;
|
|
32
|
-
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JudgmentBackend } from "./backend.js";
|
|
2
|
+
import type { HostDirs } from "./host-dirs.js";
|
|
2
3
|
import type { WidgetConfig } from "./widget.js";
|
|
3
4
|
export interface Threshold {
|
|
4
5
|
/** P(yes) at or above this shows a warning and continues. */
|
|
@@ -367,8 +368,10 @@ export interface WardenConfig {
|
|
|
367
368
|
enabled: boolean;
|
|
368
369
|
/** Consent to send task and action summaries to api.typesafe.ai. Set by /warden enable; never by a project file. */
|
|
369
370
|
typesafe: boolean;
|
|
370
|
-
/** The decisions service the judgments go to. User file only: a project must not redirect judgments to another vendor. */
|
|
371
|
-
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;
|
|
372
375
|
/**
|
|
373
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
|
|
374
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.
|
|
@@ -417,9 +420,9 @@ export declare const PACKAGE_NAME = "pi-warden";
|
|
|
417
420
|
export declare const CONFIG_SCHEMA = 10;
|
|
418
421
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
|
419
422
|
export declare function defaultConfig(): WardenConfig;
|
|
420
|
-
/**
|
|
421
|
-
export declare function userConfigPath(): string;
|
|
422
|
-
export declare function projectConfigPath(cwd: string): string;
|
|
423
|
+
/** The user file is in the host's agent directory: `~/.pi/agent` on Pi, `~/.omp/agent` on oh-my-pi. `PI_CODING_AGENT_DIR` overrides it. */
|
|
424
|
+
export declare function userConfigPath(dirs?: HostDirs): string;
|
|
425
|
+
export declare function projectConfigPath(cwd: string, dirs?: HostDirs): string;
|
|
423
426
|
type Json = Record<string, unknown>;
|
|
424
427
|
export declare function isMode(value: unknown): value is WardenMode;
|
|
425
428
|
/** Unknown keys and invalid values fall back to the base; nothing throws on a malformed file. */
|
|
@@ -430,13 +433,15 @@ export interface LoadOptions {
|
|
|
430
433
|
cwd?: string;
|
|
431
434
|
/** Project overrides are applied only when the caller vouches for the project (Pi's trust decision). */
|
|
432
435
|
projectTrusted?: boolean;
|
|
436
|
+
/** Host directories; defaults to `defaultHostDirs()`. */
|
|
437
|
+
dirs?: HostDirs;
|
|
433
438
|
}
|
|
434
439
|
export declare function loadConfig(options?: LoadOptions): WardenConfig;
|
|
435
440
|
/** Reads only the user file, for editing and persisting consent. */
|
|
436
|
-
export declare function readUserConfig(): Json;
|
|
437
|
-
export declare function writeUserConfig(raw: Json): string;
|
|
441
|
+
export declare function readUserConfig(dirs?: HostDirs): Json;
|
|
442
|
+
export declare function writeUserConfig(raw: Json, dirs?: HostDirs): string;
|
|
438
443
|
/** Persists one top-level user setting without disturbing the rest of the file. */
|
|
439
|
-
export declare function setUserSetting(key: "typesafe" | "enabled" | "mode", value: boolean | WardenMode): string;
|
|
444
|
+
export declare function setUserSetting(key: "typesafe" | "enabled" | "mode", value: boolean | WardenMode, dirs?: HostDirs): string;
|
|
440
445
|
export declare function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
|
|
441
446
|
export declare function getNestedValue(obj: Record<string, unknown>, path: string): unknown;
|
|
442
447
|
/** Coerce a CLI string into a JSON primitive so `/warden config set` stays ergonomic. */
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
5
|
-
import {
|
|
4
|
+
import { resolveJudgmentBackend } from "./backend.js";
|
|
5
|
+
import { defaultHostDirs } from "./host-dirs.js";
|
|
6
6
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
7
7
|
import { defaultWidgetConfig } from "./widget.js";
|
|
8
8
|
// Tests of UI code are proven by running them, not by looking at them.
|
|
@@ -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,
|
|
@@ -89,16 +90,12 @@ export function defaultConfig() {
|
|
|
89
90
|
waste: { enabled: true, tip: false, every: 20, sleep: true, paging: true, search: true, recheck: true },
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
|
-
/**
|
|
93
|
-
export function userConfigPath() {
|
|
94
|
-
|
|
95
|
-
const agentDir = configured
|
|
96
|
-
? (configured === "~" || configured.startsWith("~/") ? join(homedir(), configured.slice(1)) : configured)
|
|
97
|
-
: join(homedir(), ".pi", "agent");
|
|
98
|
-
return join(agentDir, PACKAGE_NAME, "config.json");
|
|
93
|
+
/** The user file is in the host's agent directory: `~/.pi/agent` on Pi, `~/.omp/agent` on oh-my-pi. `PI_CODING_AGENT_DIR` overrides it. */
|
|
94
|
+
export function userConfigPath(dirs = defaultHostDirs()) {
|
|
95
|
+
return join(dirs.agentDir, PACKAGE_NAME, "config.json");
|
|
99
96
|
}
|
|
100
|
-
export function projectConfigPath(cwd) {
|
|
101
|
-
return join(cwd,
|
|
97
|
+
export function projectConfigPath(cwd, dirs = defaultHostDirs()) {
|
|
98
|
+
return join(cwd, dirs.configDirName, PROJECT_CONFIG_FILE);
|
|
102
99
|
}
|
|
103
100
|
function isObject(value) {
|
|
104
101
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -486,10 +483,12 @@ export function applyUserOverrides(base, raw) {
|
|
|
486
483
|
if (!isObject(raw))
|
|
487
484
|
return base;
|
|
488
485
|
const shared = applyShared(base, raw);
|
|
486
|
+
const backend = resolveJudgmentBackend(raw.typesafeBackend);
|
|
489
487
|
return {
|
|
490
488
|
enabled: boolean(raw.enabled, base.enabled),
|
|
491
489
|
typesafe: boolean(raw.typesafe, base.typesafe),
|
|
492
|
-
typesafeBackend:
|
|
490
|
+
typesafeBackend: backend.typesafeBackend,
|
|
491
|
+
backendRefusal: backend.backendRefusal,
|
|
493
492
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
494
493
|
...shared,
|
|
495
494
|
...applyGuards(base, raw, shared.timeoutMs, "user"),
|
|
@@ -578,23 +577,24 @@ export function applyProjectOverrides(base, raw) {
|
|
|
578
577
|
return { ...base, enabled: boolean(raw.enabled, base.enabled), ...applyGuards(base, raw, base.timeoutMs, "project"), prefs: applyPrefs(base.prefs, raw.prefs) };
|
|
579
578
|
}
|
|
580
579
|
export function loadConfig(options = {}) {
|
|
581
|
-
|
|
580
|
+
const dirs = options.dirs ?? defaultHostDirs();
|
|
581
|
+
let config = applyUserOverrides(defaultConfig(), readJson(userConfigPath(dirs)));
|
|
582
582
|
if (options.cwd && options.projectTrusted)
|
|
583
|
-
config = applyProjectOverrides(config, readJson(projectConfigPath(options.cwd)));
|
|
583
|
+
config = applyProjectOverrides(config, readJson(projectConfigPath(options.cwd, dirs)));
|
|
584
584
|
return config;
|
|
585
585
|
}
|
|
586
586
|
/** Reads only the user file, for editing and persisting consent. */
|
|
587
|
-
export function readUserConfig() {
|
|
588
|
-
return readJson(userConfigPath()) ?? {};
|
|
587
|
+
export function readUserConfig(dirs = defaultHostDirs()) {
|
|
588
|
+
return readJson(userConfigPath(dirs)) ?? {};
|
|
589
589
|
}
|
|
590
|
-
export function writeUserConfig(raw) {
|
|
591
|
-
const path = userConfigPath();
|
|
590
|
+
export function writeUserConfig(raw, dirs = defaultHostDirs()) {
|
|
591
|
+
const path = userConfigPath(dirs);
|
|
592
592
|
writeFileAtomicSync(path, `${JSON.stringify(raw, null, 2)}\n`);
|
|
593
593
|
return path;
|
|
594
594
|
}
|
|
595
595
|
/** Persists one top-level user setting without disturbing the rest of the file. */
|
|
596
|
-
export function setUserSetting(key, value) {
|
|
597
|
-
return writeUserConfig({ ...readUserConfig(), [key]: value });
|
|
596
|
+
export function setUserSetting(key, value, dirs = defaultHostDirs()) {
|
|
597
|
+
return writeUserConfig({ ...readUserConfig(dirs), [key]: value }, dirs);
|
|
598
598
|
}
|
|
599
599
|
export function setNestedValue(obj, path, value) {
|
|
600
600
|
const keys = path.split(".");
|
package/dist/conscience.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface AssessmentResult {
|
|
|
66
66
|
/** Number of judge requests issued for this assessment. */
|
|
67
67
|
requestCount: number;
|
|
68
68
|
}
|
|
69
|
-
export type SkipReason = "disabled" | "no_consent" | "no_key" | "key_rejected" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
69
|
+
export type SkipReason = "disabled" | "no_consent" | "bad_backend" | "no_key" | "key_rejected" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
70
70
|
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
71
71
|
export declare const SCORE_LEVELS: readonly ["No useful contribution to the current request, or conflicts with the supplied constraints.", "Related to the subject, but already covered, premature, or unable to resolve the current need.", "Addresses a concrete unmet need at the current step without displacing the active workflow.", "Supplies a missing prerequisite or directly applicable documented method needed for the current step."];
|
|
72
72
|
/** Strip absolute paths, URLs, and credentials from a description. */
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { HostDirs } from "./host-dirs.js";
|
|
3
|
+
import type { BackendSetting, JudgmentsOffReason } from "./backend.js";
|
|
3
4
|
import type { ToolCallRef } from "./action-guard.js";
|
|
4
5
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
5
6
|
import type { ShapeResult } from "./shape.js";
|
|
@@ -36,7 +37,7 @@ export declare function assistantPlan(ctx: ExtensionContext, visibleAction?: boo
|
|
|
36
37
|
* variable that holds a rejected key, or undefined when the rejected key is the one `/typesafe login` saved. Never
|
|
37
38
|
* names a key value or a path.
|
|
38
39
|
*/
|
|
39
|
-
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">,
|
|
40
|
+
export declare function judgmentsOffText(reason: Exclude<JudgmentsOffReason, "budget">, setting: BackendSetting, headless: boolean, rejectedEnv?: string): string;
|
|
40
41
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
41
42
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
42
43
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|
|
@@ -48,4 +49,14 @@ export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[
|
|
|
48
49
|
export declare function guardCurrentSections(result: ShapeResult): ShapeResult;
|
|
49
50
|
/** Test-only: set the index-running state and write paths for the action guard bypass. */
|
|
50
51
|
export declare function _testSetIndexRunning(running: boolean, paths?: string[]): void;
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The one-time migration message for a host whose agent directory is not Pi's default, when data
|
|
54
|
+
* still lives only under the default and this host has not shown the notice yet: undefined
|
|
55
|
+
* otherwise. The caller checks the filesystem for data and the marker. The directories are compared
|
|
56
|
+
* in absolute, symlink-free form: the same directory under another spelling (trailing slash, `.`
|
|
57
|
+
* segment, symlink) gets no notice, because the command would move the only copy aside. The command
|
|
58
|
+
* moves the fresh data this host may have created aside (non-destructive, and works when the target
|
|
59
|
+
* folder does not exist) and copies the legacy data whole in its place.
|
|
60
|
+
*/
|
|
61
|
+
export declare function legacyDataNotice(dirs: HostDirs, piDefault: HostDirs, exists: (path: string) => boolean): string | undefined;
|
|
62
|
+
export default function wardenExtension(host: ExtensionAPI): void;
|