pi-warden 0.9.0 → 0.9.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/README.md +7 -3
- package/dist/extension.d.ts +7 -0
- package/dist/extension.js +24 -2
- package/dist/guard.js +21 -13
- package/examples/README.md +45 -0
- package/examples/config.json +26 -0
- package/examples/pi-warden.json +22 -0
- package/examples/pi-warden.md +53 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ A second pair of eyes for [Pi](https://pi.dev). It watches what the agent does,
|
|
|
6
6
|
|
|
7
7
|
The verdicts above are real output from `npm run test:live`. Independent project; not affiliated with TypeSafe AI or the Pi authors. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
|
|
8
8
|
|
|
9
|
+
Default configs and a starter rules file are in [`examples/`](examples/).
|
|
10
|
+
|
|
9
11
|
## What it does
|
|
10
12
|
|
|
11
13
|
| Guard | Watches | Does |
|
|
@@ -42,6 +44,8 @@ Requires Pi 0.85 or newer and Node.js 22.19 or newer. Pattern checks work with n
|
|
|
42
44
|
|
|
43
45
|
`TYPESAFE_API_KEY` in the environment takes precedence over the stored key. Headless runs give consent with `PI_WARDEN_ENABLED=1`.
|
|
44
46
|
|
|
47
|
+
The [examples README](examples/README.md) walks through the starter `pi-warden.md`, the project `.pi/pi-warden.json`, and the user `config.json`.
|
|
48
|
+
|
|
45
49
|
## Philosophy
|
|
46
50
|
|
|
47
51
|
pi-warden is a harness for the agent, not a gate for you. Three jobs:
|
|
@@ -72,7 +76,7 @@ The action guard receives your latest message plus up to eight earlier user and
|
|
|
72
76
|
|
|
73
77
|
### Rules
|
|
74
78
|
|
|
75
|
-
Write your project's rules as Markdown headings in `pi-warden.md` at the project root:
|
|
79
|
+
Write your project's rules as Markdown headings in `pi-warden.md` at the project root (a starter file with a dozen rules is in [`examples/pi-warden.md`](examples/pi-warden.md)):
|
|
76
80
|
|
|
77
81
|
```markdown
|
|
78
82
|
# No console statements
|
|
@@ -226,7 +230,7 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
226
230
|
|
|
227
231
|
A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides: stricter 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`, or set `notify.command`. Environment: `PI_WARDEN_ENABLED=1` (consent), `PI_WARDEN_MODE=steer|confirm|advise`.
|
|
228
232
|
|
|
229
|
-
A wince-style setup for a backend repo:
|
|
233
|
+
A wince-style setup for a backend repo (the full version is [`examples/pi-warden.json`](examples/pi-warden.json)):
|
|
230
234
|
|
|
231
235
|
```json
|
|
232
236
|
{
|
|
@@ -256,7 +260,7 @@ Obvious credentials (`Authorization` headers, `TOKEN=` and `SECRET=` assignments
|
|
|
256
260
|
|
|
257
261
|
## After updating the package
|
|
258
262
|
|
|
259
|
-
|
|
263
|
+
Restart Pi after an update; `/reload` re-imports the entry module but can leave older modules of the same package in memory. Since 0.5.2 the extension checks the shape of the config it receives; a section that an older module does not know (the symptom of two package versions in one process) switches that guard off and prints one warning naming the sections and the schema numbers. 0.9.0 crashed instead when the shape-check module itself was the stale one; since 0.9.1 the extension guards the sections it reads in its own module, so the warning appears and everything else keeps working. If you see the warning, restart Pi.
|
|
260
264
|
|
|
261
265
|
## For extension authors
|
|
262
266
|
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
|
+
import type { ShapeResult } from "./shape.js";
|
|
4
5
|
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
5
6
|
/**
|
|
6
7
|
* Tool calls of the assistant message being preflighted. Pi runs `tool_call` hooks for sibling calls one after another,
|
|
@@ -10,5 +11,11 @@ export declare function siblingToolCalls(ctx: ExtensionContext): ToolCallRef[];
|
|
|
10
11
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
11
12
|
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
12
13
|
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|
|
14
|
+
/**
|
|
15
|
+
* `completeConfig` in shape.ts fills sections an older config module lacks, but shape.ts can be the stale module too: the
|
|
16
|
+
* 0.8 version knew nothing of `rules`, so a 0.9 extension read `config.rules.enabled` on undefined and every tool call
|
|
17
|
+
* failed until Pi was restarted. The sections this build reads are therefore checked here as well, in the module that reads them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function guardCurrentSections(result: ShapeResult): ShapeResult;
|
|
13
20
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
14
21
|
export default function wardenExtension(pi: ExtensionAPI): void;
|
package/dist/extension.js
CHANGED
|
@@ -18,7 +18,7 @@ import { openTracePanel } from "./panel.js";
|
|
|
18
18
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
19
19
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
20
20
|
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
21
|
-
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
21
|
+
import { DEFAULT_TEMPLATES, proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
22
22
|
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
23
23
|
const WIDGET = PACKAGE_NAME;
|
|
24
24
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
@@ -115,6 +115,28 @@ export function slopSteer(where, symptoms, counts) {
|
|
|
115
115
|
const standing = symptoms.some(symptom => counts[symptom] >= 3) ? " Treat this as a standing rule for the rest of the session." : "";
|
|
116
116
|
return `pi-warden: the content just written to ${where} has ${named}. Fix it in your next edit: ${fixes}.${standing}`;
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* `completeConfig` in shape.ts fills sections an older config module lacks, but shape.ts can be the stale module too: the
|
|
120
|
+
* 0.8 version knew nothing of `rules`, so a 0.9 extension read `config.rules.enabled` on undefined and every tool call
|
|
121
|
+
* failed until Pi was restarted. The sections this build reads are therefore checked here as well, in the module that reads them.
|
|
122
|
+
*/
|
|
123
|
+
export function guardCurrentSections(result) {
|
|
124
|
+
const { config } = result;
|
|
125
|
+
const missing = [...result.missing];
|
|
126
|
+
if (typeof config.rules !== "object" || config.rules === null || !Array.isArray(config.rules.exclude)) {
|
|
127
|
+
if (!missing.includes("rules"))
|
|
128
|
+
missing.push("rules");
|
|
129
|
+
config.rules = { enabled: false, threshold: 1, files: [], fallback: false, maxChars: 500, exclude: [], skip: [], sensitivePaths: {} };
|
|
130
|
+
}
|
|
131
|
+
if (typeof config.widget !== "object" || config.widget === null) {
|
|
132
|
+
if (!missing.includes("widget"))
|
|
133
|
+
missing.push("widget");
|
|
134
|
+
config.widget = { ...defaultConfig().widget, enabled: false };
|
|
135
|
+
}
|
|
136
|
+
if (typeof config.widget.rules !== "string")
|
|
137
|
+
config.widget = { ...config.widget, rules: DEFAULT_TEMPLATES.rules };
|
|
138
|
+
return { config, missing };
|
|
139
|
+
}
|
|
118
140
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
119
141
|
export default function wardenExtension(pi) {
|
|
120
142
|
let client;
|
|
@@ -144,7 +166,7 @@ export default function wardenExtension(pi) {
|
|
|
144
166
|
// A partially updated module graph can hand this build a config without the sections it expects; see shape.ts.
|
|
145
167
|
let shapeReported = false;
|
|
146
168
|
const configFor = (ctx) => {
|
|
147
|
-
const { config, missing } = completeConfig(loadConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() }));
|
|
169
|
+
const { config, missing } = guardCurrentSections(completeConfig(loadConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() })));
|
|
148
170
|
if (missing.length && !shapeReported) {
|
|
149
171
|
shapeReported = true;
|
|
150
172
|
// A namespace read stays undefined (not a link error) when an older config module lacks the export.
|
package/dist/guard.js
CHANGED
|
@@ -64,7 +64,8 @@ const GIT_MESSAGE_SUBCOMMANDS = new Set(["commit", "tag", "notes", "merge", "sta
|
|
|
64
64
|
/** Interpreters whose stdin script can still run shell commands; their heredoc bodies stay in scope when they do. */
|
|
65
65
|
const INTERPRETERS = /^(?:python[\d.]*|node|ruby|perl|php|deno|bun|tsx|Rscript|lua[\d.]*)$/;
|
|
66
66
|
const EXEC_CALLS = /\b(?:os\.system|os\.popen|os\.exec\w*|subprocess|child_process|execSync|spawnSync|execFileSync|spawn\(|exec\(|system\(|popen\(|shell_exec|passthru|proc_open|Open3|IO\.popen|Deno\.run|Deno\.Command|Bun\.spawn|Bun\.\$|%x[\[{(]|`[^`\n]*\b(?:rm|git|dd|mkfs|kubectl|terraform)\b)/;
|
|
67
|
-
const HEREDOC = /<<-?\s*(?:"(\w+)"|'(\w+)'
|
|
67
|
+
const HEREDOC = /<<-?\s*(?:"(\w+)"|'(\w+)'|(\\)?(\w+))/;
|
|
68
|
+
const SUBSTITUTION = /\$\(|`/;
|
|
68
69
|
function headOf(segment) {
|
|
69
70
|
const tokens = segment.trim().split(/\s+/);
|
|
70
71
|
let index = 0;
|
|
@@ -73,7 +74,10 @@ function headOf(segment) {
|
|
|
73
74
|
const head = tokens[index];
|
|
74
75
|
return head ? head.replace(/^.*\//, "") : undefined;
|
|
75
76
|
}
|
|
76
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Quoted strings replaced by a placeholder; escapes inside double quotes are honoured, single quotes take everything.
|
|
79
|
+
* A double-quoted string that substitutes a command (`"$(...)"`, backticks) executes it, so that string stays visible.
|
|
80
|
+
*/
|
|
77
81
|
function blankQuotes(segment) {
|
|
78
82
|
let out = "";
|
|
79
83
|
for (let index = 0; index < segment.length; index++) {
|
|
@@ -89,7 +93,8 @@ function blankQuotes(segment) {
|
|
|
89
93
|
out += segment.slice(index);
|
|
90
94
|
break;
|
|
91
95
|
}
|
|
92
|
-
|
|
96
|
+
const inner = segment.slice(index + 1, end);
|
|
97
|
+
out += char === "\"" && SUBSTITUTION.test(inner) ? `${char}${inner}${char}` : `${char}[text]${char}`;
|
|
93
98
|
index = end;
|
|
94
99
|
}
|
|
95
100
|
return out;
|
|
@@ -111,8 +116,6 @@ function isDataSegment(segment) {
|
|
|
111
116
|
* (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
|
|
112
117
|
*/
|
|
113
118
|
export function stripDataText(command) {
|
|
114
|
-
if (/\$\(|`/.test(command))
|
|
115
|
-
return { text: command, stripped: false };
|
|
116
119
|
const lines = command.split("\n");
|
|
117
120
|
const out = [];
|
|
118
121
|
let stripped = false;
|
|
@@ -123,14 +126,18 @@ export function stripDataText(command) {
|
|
|
123
126
|
out.push(line);
|
|
124
127
|
continue;
|
|
125
128
|
}
|
|
126
|
-
const delimiter = heredoc[1] ?? heredoc[2] ?? heredoc[
|
|
127
|
-
|
|
129
|
+
const delimiter = heredoc[1] ?? heredoc[2] ?? heredoc[4];
|
|
130
|
+
// 'EOF', "EOF", and \EOF make the body literal; a bare EOF body is expanded, so a substitution inside it runs.
|
|
131
|
+
const literal = heredoc[4] === undefined || heredoc[3] !== undefined;
|
|
128
132
|
const body = [];
|
|
129
133
|
let close = index + 1;
|
|
130
134
|
while (close < lines.length && lines[close].replace(/^\t+/, "") !== delimiter)
|
|
131
135
|
body.push(lines[close]), close++;
|
|
132
136
|
const bodyText = body.join("\n");
|
|
133
|
-
|
|
137
|
+
// The whole pipeline on the heredoc line counts: `cat <<EOF | bash` executes the body as much as `bash <<EOF` does.
|
|
138
|
+
const heads = splitShell(line).map(segment => headOf(segment) ?? "");
|
|
139
|
+
const consumer = headOf(line.slice(0, heredoc.index)) ?? "";
|
|
140
|
+
const executed = heads.some(head => SHELL_SINKS.has(head)) || (INTERPRETERS.test(consumer) && EXEC_CALLS.test(bodyText)) || (!literal && SUBSTITUTION.test(bodyText));
|
|
134
141
|
out.push(line);
|
|
135
142
|
if (executed)
|
|
136
143
|
out.push(...body);
|
|
@@ -144,10 +151,11 @@ export function stripDataText(command) {
|
|
|
144
151
|
}
|
|
145
152
|
const joined = out.join("\n");
|
|
146
153
|
const segments = splitShell(joined);
|
|
154
|
+
// A shell sink anywhere may run text written earlier in the same command (`cat <<EOF > run.sh` then `bash run.sh`), so nothing is treated as data.
|
|
147
155
|
if (segments.some(segment => { const head = headOf(segment); return head !== undefined && SHELL_SINKS.has(head); }))
|
|
148
|
-
return { text:
|
|
156
|
+
return { text: command, stripped: false };
|
|
149
157
|
if (/\b(?:ba|z|da|k)?sh\s+-[a-zA-Z]*c\b/.test(joined))
|
|
150
|
-
return { text:
|
|
158
|
+
return { text: command, stripped: false };
|
|
151
159
|
let text = joined;
|
|
152
160
|
for (const segment of segments) {
|
|
153
161
|
if (!isDataSegment(segment) || !/["']/.test(segment))
|
|
@@ -162,13 +170,13 @@ export function stripDataText(command) {
|
|
|
162
170
|
}
|
|
163
171
|
/**
|
|
164
172
|
* rm with both recursive and force flags. Absolute, home, variable, or wildcard targets are destructive; relative ones are
|
|
165
|
-
* risky. A quote before `rm` is allowed so
|
|
173
|
+
* risky. A quote or parenthesis before `rm` is allowed so a quoted or substituted command is read; data quotes were blanked before this runs.
|
|
166
174
|
*/
|
|
167
175
|
function classifyRm(segment, cwd) {
|
|
168
|
-
const match = /(?:^|[\s"'])rm\s+(.*)$/.exec(segment);
|
|
176
|
+
const match = /(?:^|[\s"'(])rm\s+(.*)$/.exec(segment);
|
|
169
177
|
if (!match)
|
|
170
178
|
return undefined;
|
|
171
|
-
const tokens = match[1].split(/\s+/).filter(Boolean).map(token => token.replace(/["']
|
|
179
|
+
const tokens = match[1].split(/\s+/).filter(Boolean).map(token => token.replace(/["')]+$/, ""));
|
|
172
180
|
const flags = tokens.filter(token => token.startsWith("-"));
|
|
173
181
|
const targets = tokens.filter(token => !token.startsWith("-"));
|
|
174
182
|
const recursive = flags.some(flag => flag === "--recursive" || (/^-[a-zA-Z]+$/.test(flag) && /[rR]/.test(flag)));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Get started with pi-warden
|
|
2
|
+
|
|
3
|
+
Three files, each optional. Copy what you need and edit it.
|
|
4
|
+
|
|
5
|
+
| File in this folder | Copy it to | What it does |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `pi-warden.md` | `<your project>/pi-warden.md` | Your project's rules. Each `#` heading is one rule; Jev judges every write and edit against them and tells the agent which rule it broke. |
|
|
8
|
+
| `pi-warden.json` | `<your project>/.pi/pi-warden.json` | Project settings: which files the rules skip, which are never sent to Jev, sensitive paths that give the agent a note, tuned thresholds. Read only when Pi trusts the project. |
|
|
9
|
+
| `config.json` | `~/.pi/agent/pi-warden/config.json` | Your personal settings: consent, mode, whether steers show in the transcript, the audience for reply checks, notifications. `/warden config` opens it in Pi's editor. |
|
|
10
|
+
|
|
11
|
+
## Step by step
|
|
12
|
+
|
|
13
|
+
1. Install and enable:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-warden
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then in Pi run `/warden enable`, read the notice, and paste a key from [console.typesafe.ai](https://console.typesafe.ai) if you have none stored. Without a key pi-warden still runs its offline pattern checks; the rules guard needs Jev.
|
|
20
|
+
|
|
21
|
+
2. Copy `pi-warden.md` to your project root. Delete the rules you do not want, write your own. Run `/warden status`; the `Rules:` line should say `pi-warden.md (N rules)`.
|
|
22
|
+
|
|
23
|
+
3. Ask the agent to write something that breaks a rule, for example "add a console.log to src/index.ts". The write goes through, the status line shows `warden · rules · write src/index.ts · N rules · No console output in library code 0.9x · violation`, and the agent gets the rule text as a steer. Open the trace (`ctrl+shift+w`) to read exactly what it was told.
|
|
24
|
+
|
|
25
|
+
4. Optional: copy `pi-warden.json` to `.pi/pi-warden.json` for path scoping and sensitive-path notes, and `config.json` to `~/.pi/agent/pi-warden/config.json` for your own defaults.
|
|
26
|
+
|
|
27
|
+
## Writing rules that work
|
|
28
|
+
|
|
29
|
+
- One heading, one rule. Jev answers one question per rule, so a heading that bundles three things gets one blurry answer.
|
|
30
|
+
- Say what a violation looks like. "A bare `TODO` without a ticket" beats "TODOs should be tidy".
|
|
31
|
+
- Scope with `paths:` when a rule is language-specific or does not apply to tests. Globs: `**` any depth, `*` within one segment, a pattern without a slash matches at any depth.
|
|
32
|
+
- Code fences inside a rule are fine; a `#` inside a fence is not read as a heading.
|
|
33
|
+
- Rules cost tokens: one request per write or edit, roughly 600 tokens plus about 150 per rule plus the content sample. At most 31 rules are asked per request.
|
|
34
|
+
- No `pi-warden.md`? pi-warden falls back to `README.md`, `CLAUDE.md`, or `AGENTS.md` and asks one question about the whole document (cut to `rules.maxChars`, headings kept). It works, but a dedicated rules file gives sharper answers and names the rule. Set `"rules": { "fallback": false }` to turn the fallback off.
|
|
35
|
+
|
|
36
|
+
## Checking the rules from the command line
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node -e '
|
|
40
|
+
import("pi-warden").then(({ RuleStore, defaultConfig }) => {
|
|
41
|
+
const set = new RuleStore().load(process.cwd(), defaultConfig().rules);
|
|
42
|
+
for (const rule of set?.rules ?? []) console.log(rule.id, rule.paths.length ? rule.paths.join(",") : "(all files)");
|
|
43
|
+
if (set?.dropped) console.log(`${set.dropped} rules beyond the cap are ignored`);
|
|
44
|
+
});'
|
|
45
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"typesafe": true,
|
|
3
|
+
"mode": "steer",
|
|
4
|
+
"steerVisible": false,
|
|
5
|
+
"rules": {
|
|
6
|
+
"fallback": true,
|
|
7
|
+
"maxChars": 8000
|
|
8
|
+
},
|
|
9
|
+
"slop": {
|
|
10
|
+
"prose": { "audience": "a founder without a programming background" }
|
|
11
|
+
},
|
|
12
|
+
"context": {
|
|
13
|
+
"confidence": 0.8,
|
|
14
|
+
"recallTool": "auto"
|
|
15
|
+
},
|
|
16
|
+
"notify": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"cooldownMs": 10000,
|
|
19
|
+
"command": []
|
|
20
|
+
},
|
|
21
|
+
"widget": {
|
|
22
|
+
"placement": "aboveEditor",
|
|
23
|
+
"shortcut": "ctrl+shift+w",
|
|
24
|
+
"panelWidth": "40%"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": {
|
|
3
|
+
"threshold": 0.7,
|
|
4
|
+
"skip": ["tests/**", "test/**", "**/*.test.*", "**/*.spec.*", "docs/**", "**/*.md"],
|
|
5
|
+
"exclude": ["secrets/**", "**/*.pem", "**/*.key", "fixtures/**"],
|
|
6
|
+
"sensitivePaths": {
|
|
7
|
+
"**/migrations/**": "This touches a database migration: tell the user in your reply and make sure there is a rollback path",
|
|
8
|
+
"**/permissions*": "Access control changed: tell the user and ask for a security review before this is merged",
|
|
9
|
+
"**/auth/**": "Authentication code changed: tell the user; do not weaken any check without saying so",
|
|
10
|
+
".github/workflows/**": "CI configuration changed: tell the user what the pipeline will now do differently"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"action": {
|
|
14
|
+
"tools": ["bash", "powershell", "ctx_execute", "ctx_batch_execute", "ctx_execute_file", "write", "edit"],
|
|
15
|
+
"irreversible": { "warn": 0.5, "confirm": 0.7 },
|
|
16
|
+
"offTask": { "warn": 0.6, "confirm": 0.85 }
|
|
17
|
+
},
|
|
18
|
+
"slop": {
|
|
19
|
+
"prose": { "audience": "technical" }
|
|
20
|
+
},
|
|
21
|
+
"done": { "enabled": true }
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Copy this file to the root of your project as `pi-warden.md` and edit it. Every `#` heading below is one rule. The text
|
|
2
|
+
under a heading is what Jev reads when it judges a write or an edit. A `paths:` line right under a heading limits that rule
|
|
3
|
+
to matching files. Text above the first heading, like this paragraph, is ignored, so keep the intro heading-free.
|
|
4
|
+
|
|
5
|
+
Keep rules short and concrete. Say what a violation looks like. Jev is good at "must not contain X" and "every X needs a Y";
|
|
6
|
+
it is worse at taste ("code should be clean").
|
|
7
|
+
|
|
8
|
+
# No console output in library code
|
|
9
|
+
paths: src/**
|
|
10
|
+
Code under `src/` must not call `console.log`, `console.debug`, or `console.info`. Use the project logger or remove the
|
|
11
|
+
call before committing. Tests and scripts may print.
|
|
12
|
+
|
|
13
|
+
# TODO comments need a reference
|
|
14
|
+
A `TODO` or `FIXME` comment must name a ticket or issue, for example `TODO(APP-123): ...`. A bare `TODO` is a violation.
|
|
15
|
+
|
|
16
|
+
# No explicit any
|
|
17
|
+
paths: **/*.ts, **/*.tsx
|
|
18
|
+
Do not use the `any` type. Use a precise type, `unknown` with a runtime check, or a generic parameter.
|
|
19
|
+
|
|
20
|
+
# Exported functions declare their return type
|
|
21
|
+
paths: src/**/*.ts
|
|
22
|
+
Every exported function or method declares its return type instead of relying on inference.
|
|
23
|
+
|
|
24
|
+
# Errors are not swallowed
|
|
25
|
+
A `catch` block must handle the error, log it with context, or rethrow it. An empty `catch`, or one that only has a comment
|
|
26
|
+
such as `// ignore`, is a violation.
|
|
27
|
+
|
|
28
|
+
# Switch statements have a default case
|
|
29
|
+
Every `switch` has a `default` branch, even if it only throws on an unexpected value.
|
|
30
|
+
|
|
31
|
+
# No hardcoded secrets
|
|
32
|
+
Source code must not contain passwords, API keys, or tokens. They come from configuration or a secrets manager. A test fixture
|
|
33
|
+
with an obviously fake value such as `test-key` is fine.
|
|
34
|
+
|
|
35
|
+
# Boolean names read as a question
|
|
36
|
+
A boolean variable or property starts with `is`, `has`, `should`, `can`, or a similar predicate prefix, for example
|
|
37
|
+
`isEnabled`, not `enabled`.
|
|
38
|
+
|
|
39
|
+
# New exported functions get a test
|
|
40
|
+
paths: src/**
|
|
41
|
+
A newly added exported function or class comes with at least one test that exercises its main behaviour. Changing an
|
|
42
|
+
existing function does not require a new test by itself.
|
|
43
|
+
|
|
44
|
+
# Comments explain why, not what
|
|
45
|
+
A comment states a reason, a constraint, a workaround, or a non-obvious invariant. A comment that restates what the next line
|
|
46
|
+
plainly does is a violation.
|
|
47
|
+
|
|
48
|
+
# No commented-out code
|
|
49
|
+
Delete code that is no longer used. Do not leave it behind as comments.
|
|
50
|
+
|
|
51
|
+
# Migrations are reversible
|
|
52
|
+
paths: **/migrations/**
|
|
53
|
+
Every migration has a down step (or an explicit note in the file that the change cannot be reversed and why).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Guardrails for Pi built on pi-typesafe that steer the agent instead of interrupting you: Jev judges irreversible and off-task tool calls before they run, checks written code against your project rules, detects stuck loops, checks unverified done claims, and flags slop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"extensions",
|
|
40
|
+
"examples",
|
|
40
41
|
"README.md",
|
|
41
42
|
"LICENSE"
|
|
42
43
|
],
|