prae-cli 0.10.4 → 0.10.5
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/lib/bin.js +1 -1
- package/lib/{cli-CkL2G7pV.js → cli-CggsIkQW.js} +1 -1
- package/lib/{cli-Dpem2eHc.js → cli-DLwlh4y9.js} +45 -10
- package/lib/decisions-C42_kQyi.js +38 -0
- package/lib/docs/pilot-claude-code.md +11 -10
- package/lib/index.js +1 -1
- package/lib/{init-B_cGYN45.js → init-DvhJ97F9.js} +1 -1
- package/lib/{quickstart-CV2i1KDh.js → quickstart-CFk8Vj0G.js} +1 -1
- package/lib/quickstart-D_Kgl1aW.js +149 -0
- package/package.json +5 -5
- package/lib/quickstart-BsKH37Hb.js +0 -126
- /package/lib/{mcp-HD1KrudT.js → mcp-BseFg7Zu.js} +0 -0
- /package/lib/{ocsf-CRDSY64q.js → ocsf-BrMcK3Qd.js} +0 -0
- /package/lib/{receipt-check-D4opBPn-.js → receipt-check-wseVcJb_.js} +0 -0
- /package/lib/{src-C7rfOs_W.js → src-D16_Zt7u.js} +0 -0
- /package/lib/{src-BsDLT9wq.js → src-DWC4OVEg.js} +0 -0
- /package/lib/{test-run-CveqObP9.js → test-run-BG8VdLy-.js} +0 -0
package/lib/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
//#region src/bin.ts
|
|
3
3
|
const argv = process.argv.slice(2);
|
|
4
|
-
(argv[0] === "hook" && argv[1] === "claude-code" ? argv[2] === "session-start" ? import("./quickstart-
|
|
4
|
+
(argv[0] === "hook" && argv[1] === "claude-code" ? argv[2] === "session-start" ? import("./quickstart-CFk8Vj0G.js").then(({ runSessionStart }) => runSessionStart(argv.slice(3))) : import("./hook-C7ehSrzs.js").then(({ runHook }) => runHook(argv.slice(2))) : import("./cli-CggsIkQW.js").then(({ main }) => main(argv))).catch((error) => {
|
|
5
5
|
console.error(`prae: ${error instanceof Error ? error.message : error}`);
|
|
6
6
|
process.exit(1);
|
|
7
7
|
});
|
|
@@ -2030,6 +2030,12 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
|
2030
2030
|
the exit code is non-zero if any invariant no longer holds, so a
|
|
2031
2031
|
control pack can be defended in CI. --pack overrides the suite's own.
|
|
2032
2032
|
|
|
2033
|
+
prae decisions [<ledger.jsonl>] [--last <n>] [--json]
|
|
2034
|
+
The latest decisions, one line each: ALLOWED, DENIED or HELD, the
|
|
2035
|
+
tool, the path or program, and for a refusal or hold the rule and its
|
|
2036
|
+
reason. Default: the Claude Code ledger (~/.prae/claude-code/), the
|
|
2037
|
+
last 10. Read-only; prae verify checks the chain.
|
|
2038
|
+
|
|
2033
2039
|
prae verify <ledger.jsonl> [--key <public.pem>] [--checkpoint <file>]
|
|
2034
2040
|
Re-derive every hash in a decision log and report any break. With
|
|
2035
2041
|
--key, also verify each row's signature against that pinned key. With
|
|
@@ -2234,7 +2240,7 @@ async function scanFile(path, packPath, json) {
|
|
|
2234
2240
|
* not come up inspecting nothing.
|
|
2235
2241
|
*/
|
|
2236
2242
|
async function mcp(root, packPath, ledgerPath) {
|
|
2237
|
-
const { serveMcp } = await import("./mcp-
|
|
2243
|
+
const { serveMcp } = await import("./mcp-BseFg7Zu.js");
|
|
2238
2244
|
const { resolve: resolve$1 } = await import("node:path");
|
|
2239
2245
|
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
2240
2246
|
const chosen = packPath ?? bundled;
|
|
@@ -2402,7 +2408,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
2402
2408
|
* file and sets the exit code, so scripts can branch on it.
|
|
2403
2409
|
*/
|
|
2404
2410
|
async function verifyReceiptCmd(path) {
|
|
2405
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
2411
|
+
const { checkReceipt } = await import("./receipt-check-wseVcJb_.js");
|
|
2406
2412
|
let parsed$1;
|
|
2407
2413
|
try {
|
|
2408
2414
|
parsed$1 = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -2433,7 +2439,7 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
2433
2439
|
} catch (error) {
|
|
2434
2440
|
return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
2435
2441
|
}
|
|
2436
|
-
const { parseSuite, renderSuite, runSuite } = await import("./test-run-
|
|
2442
|
+
const { parseSuite, renderSuite, runSuite } = await import("./test-run-BG8VdLy-.js");
|
|
2437
2443
|
const suite = parseSuite(parsed$1);
|
|
2438
2444
|
if (!suite.ok) {
|
|
2439
2445
|
console.error(`prae: ${suitePath} is not a valid test suite`);
|
|
@@ -2459,6 +2465,34 @@ async function test(suitePath, packOverride, asJson) {
|
|
|
2459
2465
|
else for (const line of renderSuite(result)) console.log(line);
|
|
2460
2466
|
if (!result.ok) process.exit(1);
|
|
2461
2467
|
}
|
|
2468
|
+
/** `prae decisions`: the latest ledger rows in the terminal (PRA-220). */
|
|
2469
|
+
async function decisions(argv) {
|
|
2470
|
+
const { DEFAULT_LEDGER, decisionLines } = await import("./decisions-C42_kQyi.js");
|
|
2471
|
+
const given = argv[0];
|
|
2472
|
+
const path = given === void 0 || given.startsWith("--") ? DEFAULT_LEDGER : given;
|
|
2473
|
+
const last = Number(flag(argv, "last") ?? 10);
|
|
2474
|
+
if (!Number.isInteger(last) || last < 1) fail("--last needs a whole number above 0");
|
|
2475
|
+
if (!existsSync(path)) fail(`no ledger at ${path}`);
|
|
2476
|
+
let entries;
|
|
2477
|
+
try {
|
|
2478
|
+
entries = Ledger.open(path).read();
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2481
|
+
}
|
|
2482
|
+
if (argv.includes("--json")) {
|
|
2483
|
+
console.log(JSON.stringify(entries.slice(-last), null, 2));
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (entries.length === 0) {
|
|
2487
|
+
console.log(`No decisions yet in ${path}.`);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
for (const line of decisionLines(entries, {
|
|
2491
|
+
last,
|
|
2492
|
+
cwd: process.cwd()
|
|
2493
|
+
})) console.log(line);
|
|
2494
|
+
console.log(`\n${Math.min(last, entries.length)} of ${entries.length} decisions · ${path}`);
|
|
2495
|
+
}
|
|
2462
2496
|
async function verify$1(path, keyPath, checkpointPath) {
|
|
2463
2497
|
let ledger;
|
|
2464
2498
|
try {
|
|
@@ -2557,7 +2591,7 @@ async function report(path, out, packPath) {
|
|
|
2557
2591
|
* that step is a proposal a human approves, and it starts from this list.
|
|
2558
2592
|
*/
|
|
2559
2593
|
async function instructions(from, asJson) {
|
|
2560
|
-
const { ingest } = await import("./src-
|
|
2594
|
+
const { ingest } = await import("./src-D16_Zt7u.js");
|
|
2561
2595
|
const result = ingest(from);
|
|
2562
2596
|
if (asJson) {
|
|
2563
2597
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2593,7 +2627,7 @@ async function instructions(from, asJson) {
|
|
|
2593
2627
|
* on purpose rather than something that happened to their policy.
|
|
2594
2628
|
*/
|
|
2595
2629
|
async function propose(from, options) {
|
|
2596
|
-
const [{ ingest }, { extract }] = await Promise.all([import("./src-
|
|
2630
|
+
const [{ ingest }, { extract }] = await Promise.all([import("./src-D16_Zt7u.js"), import("./src-DWC4OVEg.js")]);
|
|
2597
2631
|
const read = ingest(from);
|
|
2598
2632
|
if (read.files.length === 0) {
|
|
2599
2633
|
console.log(`No instruction files found at or above ${from}.`);
|
|
@@ -2653,8 +2687,8 @@ async function consoleOut(path, argv) {
|
|
|
2653
2687
|
const instructionsDir = flag(argv, "instructions");
|
|
2654
2688
|
if (instructionsDir !== void 0) {
|
|
2655
2689
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
2656
|
-
import("./src-
|
|
2657
|
-
import("./src-
|
|
2690
|
+
import("./src-D16_Zt7u.js"),
|
|
2691
|
+
import("./src-DWC4OVEg.js"),
|
|
2658
2692
|
import("./instructions-BqT2wN_4.js")
|
|
2659
2693
|
]);
|
|
2660
2694
|
const read = ingest(instructionsDir);
|
|
@@ -2840,7 +2874,7 @@ async function checkpoint(path, keyPath, out) {
|
|
|
2840
2874
|
}
|
|
2841
2875
|
}
|
|
2842
2876
|
async function exportLedger(path, options) {
|
|
2843
|
-
const { toOcsf, ndjson, sendToHec } = await import("./ocsf-
|
|
2877
|
+
const { toOcsf, ndjson, sendToHec } = await import("./ocsf-BrMcK3Qd.js");
|
|
2844
2878
|
const { createRequire: createRequire$1 } = await import("node:module");
|
|
2845
2879
|
const { version } = createRequire$1(import.meta.url)("../package.json");
|
|
2846
2880
|
let ledger;
|
|
@@ -2923,6 +2957,7 @@ async function main(argv) {
|
|
|
2923
2957
|
if (path === void 0) fail("verify needs a ledger file");
|
|
2924
2958
|
return verify$1(path, flag(rest, "key"), flag(rest, "checkpoint"));
|
|
2925
2959
|
}
|
|
2960
|
+
case "decisions": return decisions(rest);
|
|
2926
2961
|
case "chain": {
|
|
2927
2962
|
const path = rest[0];
|
|
2928
2963
|
if (path === void 0) fail("chain needs a ledger file");
|
|
@@ -2971,7 +3006,7 @@ async function main(argv) {
|
|
|
2971
3006
|
}
|
|
2972
3007
|
case "init":
|
|
2973
3008
|
if (rest[0] === "claude-code") {
|
|
2974
|
-
const { initClaudeCode, stableNode } = await import("./init-
|
|
3009
|
+
const { initClaudeCode, stableNode } = await import("./init-DvhJ97F9.js");
|
|
2975
3010
|
const prompt = process.stdin.isTTY && process.stdout.isTTY ? (await import("node:readline/promises")).createInterface({
|
|
2976
3011
|
input: process.stdin,
|
|
2977
3012
|
output: process.stdout
|
|
@@ -2994,7 +3029,7 @@ async function main(argv) {
|
|
|
2994
3029
|
case "hook": {
|
|
2995
3030
|
if (rest[0] !== "claude-code") fail("hook takes claude-code");
|
|
2996
3031
|
if (rest[1] === "session-start") {
|
|
2997
|
-
const { runSessionStart } = await import("./quickstart-
|
|
3032
|
+
const { runSessionStart } = await import("./quickstart-CFk8Vj0G.js");
|
|
2998
3033
|
return runSessionStart(rest.slice(2));
|
|
2999
3034
|
}
|
|
3000
3035
|
const { runHook } = await import("./hook-C7ehSrzs.js");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
|
|
4
|
+
//#region src/decisions.ts
|
|
5
|
+
/** Where `prae init claude-code` keeps the ledger unless told otherwise. */
|
|
6
|
+
const DEFAULT_LEDGER = join(homedir(), ".prae", "claude-code", "ledger.jsonl");
|
|
7
|
+
const LABEL = {
|
|
8
|
+
allow: "ALLOWED",
|
|
9
|
+
ask: "HELD",
|
|
10
|
+
deny: "DENIED",
|
|
11
|
+
correct: "CORRECTED"
|
|
12
|
+
};
|
|
13
|
+
/** A path inside `cwd` as the user typed it; anything else as it is. */
|
|
14
|
+
function shown(subject, cwd) {
|
|
15
|
+
if (!isAbsolute(subject)) return subject.replace(/^\.\//, "");
|
|
16
|
+
const rel = relative(cwd, subject);
|
|
17
|
+
return rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep) ? rel : subject;
|
|
18
|
+
}
|
|
19
|
+
/** One line per row, oldest first, the last `last` rows of `entries`. */
|
|
20
|
+
function decisionLines(entries, options) {
|
|
21
|
+
const rows = entries.slice(-options.last).map((e) => {
|
|
22
|
+
const label = `${LABEL[e.verdict]}${e.shadow && e.verdict !== "allow" ? " (shadow, ran anyway)" : ""}`;
|
|
23
|
+
const what = e.subject !== void 0 ? shown(e.subject, options.cwd) : e.command?.program ?? "";
|
|
24
|
+
const why = e.verdict === "allow" || e.rule === void 0 ? "" : `${e.rule} · ${e.reason ?? ""}`;
|
|
25
|
+
return [
|
|
26
|
+
`row ${e.seq}`,
|
|
27
|
+
label,
|
|
28
|
+
e.tool,
|
|
29
|
+
what,
|
|
30
|
+
why
|
|
31
|
+
];
|
|
32
|
+
});
|
|
33
|
+
const width = (i) => Math.max(0, ...rows.map((r) => r[i]?.length ?? 0));
|
|
34
|
+
return rows.map((r) => r.map((cell, i) => i < r.length - 1 ? cell.padEnd(width(i)) : cell).join(" ").trimEnd());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { DEFAULT_LEDGER, decisionLines };
|
|
@@ -70,22 +70,22 @@ so new tool calls go through PRAE without a restart.
|
|
|
70
70
|
|
|
71
71
|
### The quickstart inside Claude Code
|
|
72
72
|
|
|
73
|
-
From prae-cli 0.10.4, `init` also installs a five-minute guided tour.
|
|
73
|
+
From prae-cli 0.10.4, `init` also installs a five-minute guided tour, `/prae-quickstart`. From 0.10.5 it is a build–verify loop (PRA-220).
|
|
74
74
|
|
|
75
|
-
- **The
|
|
75
|
+
- **The skill** is written beside the settings file:
|
|
76
76
|
- `~/.claude/skills/` for `--scope user`;
|
|
77
77
|
- `./.claude/skills/` for `--scope project`;
|
|
78
78
|
- `.claude/skills/` inside the managed settings directory.
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- `prae explain`, which you run yourself, because PRAE's pack is not the agent's to touch.
|
|
80
|
+
It is written for your install, with your paths and mode.
|
|
81
|
+
- **The tour.** Claude builds a tiny project in `./.prae-quickstart/`, one increment at a time. After each increment it **verifies** (runs the tests), **inspects** what PRAE decided (`prae decisions`) and **continues**.
|
|
82
|
+
1. A function and its test: every call is allowed.
|
|
83
|
+
2. Database configuration: reading `.env` is denied (BASE-005). Claude continues the way the refusal points: configuration from the environment, documented in `.env.example`.
|
|
84
|
+
3. A build, then cleaning it: `rm -r` is held for you (BASE-002).
|
|
86
85
|
|
|
87
|
-
It ends with
|
|
88
|
-
- **
|
|
86
|
+
It ends with `prae verify` on the ledger, `prae explain` (which you run yourself, because PRAE's pack is not the agent's to touch) and links to prae-ai.com.
|
|
87
|
+
- **Pre-approved calls.** The skill's `allowed-tools` pre-approves only the tour's own safe calls in Claude Code: edits inside `./.prae-quickstart/`, its tests and build script, and `prae decisions`/`prae verify`. PRAE still judges every one of them, so the only prompt is PRAE's own hold.
|
|
88
|
+
- **The offer.** A `SessionStart` entry (`matcher: "startup"`) shows, in the first session after the install, `PRAE is active … Type /prae-quickstart for a five-minute tour.` Claude offers the tour once. It then leaves `quickstart-offered` in the state directory and stays silent in every later session. It never fails a session.
|
|
89
89
|
|
|
90
90
|
Options:
|
|
91
91
|
|
|
@@ -123,6 +123,7 @@ Read the record:
|
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
125
|
P=~/.prae/claude-code
|
|
126
|
+
prae decisions # the last 10: ALLOWED, DENIED or HELD, with the rule and reason
|
|
126
127
|
prae verify $P/ledger.jsonl --key $P/prae-signing.pub.pem
|
|
127
128
|
prae report $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out report.html
|
|
128
129
|
prae console $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out console/
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./src-DO3IFxZ-.js";
|
|
2
2
|
import "./src-Tshjqoz6.js";
|
|
3
|
-
import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-
|
|
3
|
+
import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-DLwlh4y9.js";
|
|
4
4
|
import "./facts-BKzQ9GLI.js";
|
|
5
5
|
import "./views-Hh3G5zpv.js";
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as parsePack } from "./src-DO3IFxZ-.js";
|
|
2
2
|
import { d as generateKeypair, u as fingerprint } from "./src-Tshjqoz6.js";
|
|
3
|
-
import { a as skill, o as skillFile } from "./quickstart-
|
|
3
|
+
import { a as skill, o as skillFile } from "./quickstart-D_Kgl1aW.js";
|
|
4
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as skill, i as sessionStart, n as SKILL, o as skillFile, r as runSessionStart, t as OFFERED } from "./quickstart-
|
|
1
|
+
import { a as skill, i as sessionStart, n as SKILL, o as skillFile, r as runSessionStart, t as OFFERED } from "./quickstart-D_Kgl1aW.js";
|
|
2
2
|
|
|
3
3
|
export { OFFERED, SKILL, runSessionStart, sessionStart, skill, skillFile };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/claude-code/quickstart.ts
|
|
5
|
+
const SKILL = "prae-quickstart";
|
|
6
|
+
const OFFERED = "quickstart-offered";
|
|
7
|
+
/**
|
|
8
|
+
* Where Claude Code loads skills for a settings file: `skills/` beside
|
|
9
|
+
* `~/.claude/settings.json` or `./.claude/settings.json`, and
|
|
10
|
+
* `.claude/skills/` inside the managed settings directory
|
|
11
|
+
* (https://code.claude.com/docs/en/skills).
|
|
12
|
+
*/
|
|
13
|
+
function skillFile(settings) {
|
|
14
|
+
const dir = dirname(settings);
|
|
15
|
+
return join(basename(settings) === "managed-settings.json" ? join(dir, ".claude", "skills") : join(dir, "skills"), SKILL, "SKILL.md");
|
|
16
|
+
}
|
|
17
|
+
const SITE = "https://prae-ai.com";
|
|
18
|
+
/**
|
|
19
|
+
* The `/prae-quickstart` skill, written for this install's paths and mode.
|
|
20
|
+
*
|
|
21
|
+
* The tour is a build–verify loop (PRA-220): Claude builds a tiny project in
|
|
22
|
+
* increments, and after each one verifies it (the tests), inspects what PRAE
|
|
23
|
+
* decided (`prae decisions`), and continues, adjusting when PRAE refused.
|
|
24
|
+
* `allowed-tools` pre-approves the tour's own safe calls in Claude Code, so
|
|
25
|
+
* the only prompt is PRAE's hold; PRAE still judges every call.
|
|
26
|
+
*/
|
|
27
|
+
function skill(i) {
|
|
28
|
+
const enforce = i.mode === "enforce";
|
|
29
|
+
const tour = ".prae-quickstart";
|
|
30
|
+
const decisions = `${i.prae} decisions ${i.ledger}`;
|
|
31
|
+
return `---
|
|
32
|
+
name: ${SKILL}
|
|
33
|
+
description: A five-minute guided tour of PRAE in this Claude Code session, as a build–verify loop. Claude builds a tiny project in steps, and after each step runs the tests, shows what PRAE decided (allowed, denied or held) and why, then continues. Use it when the user asks for the PRAE quickstart or tour, or accepts the offer of one.
|
|
34
|
+
allowed-tools:
|
|
35
|
+
${[
|
|
36
|
+
`Write(./${tour}/**)`,
|
|
37
|
+
`Edit(./${tour}/**)`,
|
|
38
|
+
`Bash(node --test ./${tour}/*)`,
|
|
39
|
+
`Bash(node ./${tour}/scripts/build.js)`,
|
|
40
|
+
`Bash(ls ./${tour}/*)`,
|
|
41
|
+
`Bash(${i.prae} decisions *)`,
|
|
42
|
+
`Bash(${i.prae} verify *)`
|
|
43
|
+
].map((a) => ` - ${JSON.stringify(a)}`).join("\n")}
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# PRAE quickstart: build, verify, inspect, continue
|
|
47
|
+
|
|
48
|
+
PRAE checks each of your tool calls against a policy pack before the call runs (a Claude Code \`PreToolUse\` hook), and records every decision in a signed, hash-chained ledger. Show the user what that looks like in real work. Build a tiny project in \`./${tour}/\`, one increment at a time, and run this loop for each increment:
|
|
49
|
+
|
|
50
|
+
1. **Build:** make the change.
|
|
51
|
+
2. **Verify:** run the tests.
|
|
52
|
+
3. **Inspect:** run \`${decisions} --last 8\`. Talk only about the rows since the previous inspect, and don't narrate counting them. The inspect call is checked too, so it is always the last row. Say in one sentence what PRAE decided and why.
|
|
53
|
+
4. **Continue:** go on to the next increment. Where PRAE refused something, adjust the approach the way the step says.
|
|
54
|
+
|
|
55
|
+
Before each increment, say in one sentence what you are about to build. Keep the commentary short.
|
|
56
|
+
|
|
57
|
+
## This install
|
|
58
|
+
|
|
59
|
+
- **Mode:** ${enforce ? "**enforce**. PRAE returns its refusals and holds to Claude Code." : "**shadow**. PRAE records every decision and blocks nothing. A call it would refuse still runs, and its row in `prae decisions` says \"shadow, ran anyway\"."}
|
|
60
|
+
- **Policy pack:** \`${i.pack}\`
|
|
61
|
+
- **Ledger:** \`${i.ledger}\`
|
|
62
|
+
- **PRAE CLI:** \`${i.prae}\`
|
|
63
|
+
|
|
64
|
+
## Rules for the tour
|
|
65
|
+
|
|
66
|
+
- **Run the whole tour in one go.** Don't stop to ask whether to continue. The only pause is PRAE's hold in increment 3.
|
|
67
|
+
- **Make every call yourself, with your own tools.** Never suggest \`!\` shell mode: it runs outside every hook, so PRAE never sees it.
|
|
68
|
+
- **Work only in \`./${tour}/\`.** Its \`.env\` is a synthetic canary, not a real secret.
|
|
69
|
+
- **Report what actually happened.** If the pack in force decides differently from what a step expects, say so. The pack decides, not this guide.
|
|
70
|
+
- **Never work around a refusal.** A refusal changes the approach; it is not an obstacle to route around.
|
|
71
|
+
|
|
72
|
+
## 0. Set up
|
|
73
|
+
|
|
74
|
+
Use the Write tool to create:
|
|
75
|
+
- \`./${tour}/package.json\`: \`{ "name": "prae-quickstart", "private": true, "type": "module" }\`
|
|
76
|
+
- \`./${tour}/.env\`: \`DATABASE_URL=postgres://demo:canary-not-a-secret@localhost:5432/demo\`
|
|
77
|
+
|
|
78
|
+
## Increment 1: a function and its test
|
|
79
|
+
|
|
80
|
+
- **Build:**
|
|
81
|
+
- \`./${tour}/src/slug.js\` exporting \`slugify(text)\`: lowercase, each run of characters other than a–z and 0–9 becomes \`-\`, and no leading or trailing \`-\`.
|
|
82
|
+
- \`./${tour}/test/slug.test.js\`: two \`node:test\` assertions.
|
|
83
|
+
- **Verify:** \`node --test ./${tour}/test/*.test.js\`
|
|
84
|
+
- **Inspect:** every call so far is ALLOWED. PRAE checked each one before it ran, and none needed a person. PRAE never answers "allow" on its own: it can only narrow what Claude Code would let through.
|
|
85
|
+
|
|
86
|
+
## Increment 2: database configuration
|
|
87
|
+
|
|
88
|
+
- **Build:** the app needs the database address. Read \`./${tour}/.env\` with the Read tool to find it.
|
|
89
|
+
- **Inspect:** ${enforce ? "the read is DENIED under BASE-005, and nothing was read. Tell the user that **Ctrl+O** shows PRAE's own message: the rule, the reason and the ledger row. Claude Code labels any hook refusal \"hook error\", but PRAE's text says \"denied\"." : "the row says DENIED (shadow, ran anyway): enforce mode would have refused this read under BASE-005. Point that out."}
|
|
90
|
+
- **Continue, the way the refusal points:** configuration must not depend on the agent reading secrets.
|
|
91
|
+
- \`./${tour}/src/config.js\` reads \`process.env.DATABASE_URL\`, and throws an error naming the variable when it is unset.
|
|
92
|
+
- \`./${tour}/.env.example\` documents the variable with a placeholder value.
|
|
93
|
+
- \`./${tour}/test/config.test.js\` covers both cases.
|
|
94
|
+
- **Verify:** \`node --test ./${tour}/test/*.test.js\`
|
|
95
|
+
- **Inspect:** the new calls are ALLOWED.
|
|
96
|
+
|
|
97
|
+
## Increment 3: a build, and cleaning it
|
|
98
|
+
|
|
99
|
+
- **Build:** \`./${tour}/scripts/build.js\` copies \`src/\` to \`build/\`, resolving both from the script's own directory.
|
|
100
|
+
- **Verify:** \`node ./${tour}/scripts/build.js\`, then the tests again.
|
|
101
|
+
- **Clean:** run \`rm -r ./${tour}/build\`. ${enforce ? "PRAE holds it for a person (BASE-002), and Claude Code asks the user with PRAE's reason. Tell them either answer is safe here, because it is build output in a scratch folder. Then **wait for their answer**." : "In shadow mode PRAE does not hold it. Claude Code's own permission prompt may still ask."}
|
|
102
|
+
- **Inspect:** the row says HELD${enforce ? "" : " (shadow, ran anyway)"} under BASE-002. Then check whether \`./${tour}/build\` still exists, with \`ls ./${tour}/build\`. Claude Code does not tell you what the person answered, and the ledger records the hold, not the answer. So say what you observe: still there means they declined and nothing was deleted; gone means they approved.
|
|
103
|
+
|
|
104
|
+
## 4. The whole record
|
|
105
|
+
|
|
106
|
+
- **Verify the ledger:** \`${i.prae} verify ${i.ledger} --key ${i.publicKey}\`. Each row is hash-chained to the one before it and signed, so an edited, removed or reordered row shows up as a break.
|
|
107
|
+
- **Why a rule fired:** ask the user to run this **in their own terminal**, not through you:
|
|
108
|
+
|
|
109
|
+
\`${i.prae} explain ${i.pack} Read file_path=./${tour}/.env\`
|
|
110
|
+
|
|
111
|
+
It prints every rule considered and the one that decided. Explain why you don't run it yourself: PRAE's pack, its hook settings and its signing key are protected from the agent (rule PRAE-SELF), so a session cannot change the policy that judges it.
|
|
112
|
+
- **Changing the policy:** the pack is \`${i.pack}\`. The user edits it, never you. They check it with \`${i.prae} check ${i.pack}\`, and the next call is judged by the new pack, with no restart.
|
|
113
|
+
${enforce ? "" : `- **Turning enforcement on:** the user runs \`${i.prae} init claude-code --settings ${i.settings} --enforce --apply\` in their own terminal, then starts a new session.
|
|
114
|
+
`}
|
|
115
|
+
## Finish
|
|
116
|
+
|
|
117
|
+
- **The loop, in one line:** build a step, verify it, inspect what PRAE decided, continue. \`prae decisions\` is the inspect step in any session.
|
|
118
|
+
- **More:** ${SITE}/products/developer-agents/ covers PRAE for Claude Code and other coding agents. ${SITE}/developers/ covers the CLI, the policy packs and the ledger.
|
|
119
|
+
- **Cleanup:** \`./${tour}/\` stays. The user can delete it. A recursive delete is held again, which is expected.
|
|
120
|
+
- **Again:** the tour can be rerun any time with \`/${SKILL}\`.
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
/** The SessionStart hook's stdout: the one-time offer, then nothing. */
|
|
124
|
+
function sessionStart(dir) {
|
|
125
|
+
try {
|
|
126
|
+
const marker = join(dir, OFFERED);
|
|
127
|
+
if (existsSync(marker)) return "";
|
|
128
|
+
writeFileSync(marker, `${(/* @__PURE__ */ new Date()).toISOString()}\n`);
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
systemMessage: `PRAE is active: each tool call is checked against your policy before it runs. New to PRAE? Type /${SKILL} for a five-minute tour.`,
|
|
131
|
+
hookSpecificOutput: {
|
|
132
|
+
hookEventName: "SessionStart",
|
|
133
|
+
additionalContext: `PRAE was just installed, and this is the user's first session with it. PRAE checks each of your tool calls against a policy before the call runs. In your first reply, offer once, in one sentence, a five-minute guided tour (the /${SKILL} skill), then carry on with what the user asked. Run the tour only if they accept. Don't offer it again.`
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** `prae hook claude-code session-start --dir <dir>`. */
|
|
141
|
+
function runSessionStart(argv) {
|
|
142
|
+
const at = argv.indexOf("--dir");
|
|
143
|
+
const dir = at === -1 ? void 0 : argv[at + 1];
|
|
144
|
+
const out = dir === void 0 ? "" : sessionStart(dir);
|
|
145
|
+
if (out !== "") process.stdout.write(`${out}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
export { skill as a, sessionStart as i, SKILL as n, skillFile as o, runSessionStart as r, OFFERED as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prae-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"description": "Operator policy for agent harnesses: validate packs, explain a decision, test a pack against security invariants, verify a decision log, and provision a governed DSH profile.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"typescript": "^5.9.2",
|
|
43
43
|
"vitest": "^3.2.4",
|
|
44
44
|
"@prae/controls": "0.1.0",
|
|
45
|
-
"@prae/extract": "0.1.0",
|
|
46
45
|
"@prae/instructions": "0.1.0",
|
|
47
|
-
"@prae/
|
|
46
|
+
"@prae/extract": "0.1.0",
|
|
48
47
|
"@prae/kernel": "0.1.0",
|
|
49
|
-
"@prae/
|
|
50
|
-
"@prae/
|
|
48
|
+
"@prae/inspect": "0.1.0",
|
|
49
|
+
"@prae/ledger": "0.1.0",
|
|
50
|
+
"@prae/tokens": "0.1.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"test": "vitest run",
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
//#region src/claude-code/quickstart.ts
|
|
5
|
-
const SKILL = "prae-quickstart";
|
|
6
|
-
const OFFERED = "quickstart-offered";
|
|
7
|
-
/**
|
|
8
|
-
* Where Claude Code loads skills for a settings file: `skills/` beside
|
|
9
|
-
* `~/.claude/settings.json` or `./.claude/settings.json`, and
|
|
10
|
-
* `.claude/skills/` inside the managed settings directory
|
|
11
|
-
* (https://code.claude.com/docs/en/skills).
|
|
12
|
-
*/
|
|
13
|
-
function skillFile(settings) {
|
|
14
|
-
const dir = dirname(settings);
|
|
15
|
-
return join(basename(settings) === "managed-settings.json" ? join(dir, ".claude", "skills") : join(dir, "skills"), SKILL, "SKILL.md");
|
|
16
|
-
}
|
|
17
|
-
const SITE = "https://prae-ai.com";
|
|
18
|
-
/** The `/prae-quickstart` skill, written for this install's paths and mode. */
|
|
19
|
-
function skill(i) {
|
|
20
|
-
const enforce = i.mode === "enforce";
|
|
21
|
-
const tour = ".prae-quickstart";
|
|
22
|
-
return `---
|
|
23
|
-
name: ${SKILL}
|
|
24
|
-
description: A five-minute guided tour of PRAE in this Claude Code session. It shows what PRAE allows, refuses and holds, where each decision is recorded, and how to change the policy. Use it when the user asks for the PRAE quickstart or tour, or accepts the offer of one.
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
# PRAE quickstart
|
|
28
|
-
|
|
29
|
-
PRAE checks each of your tool calls against a policy pack before the call runs (a Claude Code \`PreToolUse\` hook), and records every decision in a signed, hash-chained ledger. Walk the user through it, one step at a time. For each step:
|
|
30
|
-
- say in one sentence what you are about to do;
|
|
31
|
-
- do it;
|
|
32
|
-
- say what happened and why.
|
|
33
|
-
|
|
34
|
-
Keep it short. Stop and wait only where a step says to.
|
|
35
|
-
|
|
36
|
-
## This install
|
|
37
|
-
|
|
38
|
-
- **Mode:** ${enforce ? "**enforce**. PRAE returns its refusals and holds to Claude Code." : "**shadow**. PRAE records every decision and blocks nothing: a call it would refuse still runs, and its ledger row is marked shadow."}
|
|
39
|
-
- **Policy pack:** \`${i.pack}\`
|
|
40
|
-
- **Ledger:** \`${i.ledger}\`
|
|
41
|
-
- **PRAE CLI:** \`${i.prae}\`
|
|
42
|
-
|
|
43
|
-
## Rules for the tour
|
|
44
|
-
|
|
45
|
-
- **Make every call yourself, with your own tools.** Never suggest \`!\` shell mode: it runs outside every hook, so PRAE never sees it.
|
|
46
|
-
- **Work only in \`./${tour}/\`,** which you create in step 1. The credentials file there is a synthetic canary, not a real secret.
|
|
47
|
-
- **Report what actually happened.** If the pack in force decides differently from what a step expects, say so: the pack decides, not this guide.
|
|
48
|
-
- **Never work around a refusal.** Seeing it is the point.
|
|
49
|
-
|
|
50
|
-
## 1. Set up the scratch directory
|
|
51
|
-
|
|
52
|
-
Use the Write tool to create:
|
|
53
|
-
- \`./${tour}/notes.md\`: one line, "PRAE quickstart scratch file."
|
|
54
|
-
- \`./${tour}/credentials.json\`: \`{"api_key": "prae-quickstart-canary-not-a-secret"}\`
|
|
55
|
-
- \`./${tour}/build/app.js\`: \`console.log('build output')\`
|
|
56
|
-
|
|
57
|
-
These writes are inside the working tree, so the baseline allows them.
|
|
58
|
-
|
|
59
|
-
## 2. An ordinary call: allowed
|
|
60
|
-
|
|
61
|
-
Read \`./${tour}/notes.md\`. PRAE says nothing, and Claude Code's own permission flow is unchanged. PRAE never answers "allow" on its own: it can only narrow what Claude Code would let through.
|
|
62
|
-
|
|
63
|
-
## 3. A secret: refused
|
|
64
|
-
|
|
65
|
-
Read \`./${tour}/credentials.json\` with the Read tool. ${enforce ? "PRAE refuses the read before it runs (baseline rule BASE-005). Tell the user to press **Ctrl+O** to see PRAE's own message: the rule, the reason and the ledger row. Claude Code labels any hook refusal \"hook error\", but PRAE's text says \"denied\"." : "In shadow mode the read goes through. The ledger records the refusal PRAE would have returned (BASE-005), marked shadow. Point that out."}
|
|
66
|
-
|
|
67
|
-
## 4. A destructive command: held
|
|
68
|
-
|
|
69
|
-
Run \`rm -r ./${tour}/build\`. ${enforce ? "PRAE holds it for a person (BASE-002), and Claude Code asks the user with PRAE's reason. Tell them either answer is safe here, because it is a scratch folder. Then **wait for their answer**. If they decline, nothing is deleted. If the command runs, they approved it. Claude Code does not tell you a prompt was shown, so do not report that it ran without a hold." : "In shadow mode PRAE does not hold it. Claude Code's own permission prompt may still ask. The ledger records the hold PRAE would have placed (BASE-002)."}
|
|
70
|
-
|
|
71
|
-
## 5. The record
|
|
72
|
-
|
|
73
|
-
Run \`${i.prae} verify ${i.ledger} --key ${i.publicKey}\` and show the result. Each row is hash-chained to the one before it and signed, so an edited or reordered row shows up as a break. The rows from this tour are in it.
|
|
74
|
-
|
|
75
|
-
Offer to write a printable report: \`${i.prae} report ${i.ledger} --out ./${tour}/report.html\`.
|
|
76
|
-
|
|
77
|
-
## 6. Why a rule fired
|
|
78
|
-
|
|
79
|
-
Ask the user to run this **in their own terminal**, not through you:
|
|
80
|
-
|
|
81
|
-
\`${i.prae} explain ${i.pack} Read file_path=./${tour}/credentials.json\`
|
|
82
|
-
|
|
83
|
-
It prints the trace: every rule considered, and the one that decided. Explain why you don't run it yourself. PRAE's pack, its hook settings and its signing key are protected from the agent (rule PRAE-SELF), so a session cannot change the policy that judges it. The ledger is readable, so step 5 works.
|
|
84
|
-
|
|
85
|
-
## 7. Changing the policy
|
|
86
|
-
|
|
87
|
-
The pack is the JSON file \`${i.pack}\`. The user edits it, never you. They check it with \`${i.prae} check ${i.pack}\`. The next tool call is judged by the new pack, with no restart.
|
|
88
|
-
|
|
89
|
-
${enforce ? "" : `## 8. Turning enforcement on
|
|
90
|
-
|
|
91
|
-
Shadow mode is for seeing what PRAE would do. To have it refuse and hold for real, the user runs \`${i.prae} init claude-code --settings ${i.settings} --enforce --apply\` in their own terminal, then starts a new session.
|
|
92
|
-
|
|
93
|
-
`}## Finish
|
|
94
|
-
|
|
95
|
-
- **More:** ${SITE}/products/developer-agents/ covers PRAE for Claude Code and other coding agents. ${SITE}/developers/ covers the CLI, the policy packs and the ledger.
|
|
96
|
-
- **Cleanup:** \`./${tour}/\` stays. The user can delete it. A recursive delete is held again, which is expected.
|
|
97
|
-
- **Again:** the tour can be rerun any time with \`/${SKILL}\`.
|
|
98
|
-
`;
|
|
99
|
-
}
|
|
100
|
-
/** The SessionStart hook's stdout: the one-time offer, then nothing. */
|
|
101
|
-
function sessionStart(dir) {
|
|
102
|
-
try {
|
|
103
|
-
const marker = join(dir, OFFERED);
|
|
104
|
-
if (existsSync(marker)) return "";
|
|
105
|
-
writeFileSync(marker, `${(/* @__PURE__ */ new Date()).toISOString()}\n`);
|
|
106
|
-
return JSON.stringify({
|
|
107
|
-
systemMessage: `PRAE is active: each tool call is checked against your policy before it runs. New to PRAE? Type /${SKILL} for a five-minute tour.`,
|
|
108
|
-
hookSpecificOutput: {
|
|
109
|
-
hookEventName: "SessionStart",
|
|
110
|
-
additionalContext: `PRAE was just installed, and this is the user's first session with it. PRAE checks each of your tool calls against a policy before the call runs. In your first reply, offer once, in one sentence, a five-minute guided tour (the /${SKILL} skill), then carry on with what the user asked. Run the tour only if they accept. Don't offer it again.`
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
} catch {
|
|
114
|
-
return "";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/** `prae hook claude-code session-start --dir <dir>`. */
|
|
118
|
-
function runSessionStart(argv) {
|
|
119
|
-
const at = argv.indexOf("--dir");
|
|
120
|
-
const dir = at === -1 ? void 0 : argv[at + 1];
|
|
121
|
-
const out = dir === void 0 ? "" : sessionStart(dir);
|
|
122
|
-
if (out !== "") process.stdout.write(`${out}\n`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
//#endregion
|
|
126
|
-
export { skill as a, sessionStart as i, SKILL as n, skillFile as o, runSessionStart as r, OFFERED as t };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|