@oisin-ee/oisinpi 0.0.0-stage → 0.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +17 -2
  3. package/node_modules/@oisin-ee/pi-guardrails/README.md +29 -0
  4. package/node_modules/@oisin-ee/pi-guardrails/dist/agent-hook.js +8 -0
  5. package/node_modules/@oisin-ee/pi-guardrails/dist/common.js +66 -0
  6. package/node_modules/@oisin-ee/pi-guardrails/dist/event-registry.js +42 -0
  7. package/node_modules/@oisin-ee/pi-guardrails/dist/lifecycle-runner.js +59 -0
  8. package/node_modules/@oisin-ee/pi-guardrails/dist/policies/edit.js +98 -0
  9. package/node_modules/@oisin-ee/pi-guardrails/dist/policies/shell.js +1091 -0
  10. package/node_modules/@oisin-ee/pi-guardrails/dist/shell-parser.js +120 -0
  11. package/node_modules/@oisin-ee/pi-guardrails/package.json +46 -0
  12. package/node_modules/@oisin-ee/pi-guardrails/src/agent-hook.ts +9 -0
  13. package/node_modules/@oisin-ee/pi-guardrails/src/common.ts +116 -0
  14. package/node_modules/@oisin-ee/pi-guardrails/src/event-registry.ts +51 -0
  15. package/node_modules/@oisin-ee/pi-guardrails/src/lifecycle-runner.ts +81 -0
  16. package/node_modules/@oisin-ee/pi-guardrails/src/pi/index.ts +100 -0
  17. package/node_modules/@oisin-ee/pi-guardrails/src/pi/tool-inputs.ts +254 -0
  18. package/node_modules/@oisin-ee/pi-guardrails/src/pi/tool-timeouts.ts +91 -0
  19. package/node_modules/@oisin-ee/pi-guardrails/src/policies/edit.ts +121 -0
  20. package/node_modules/@oisin-ee/pi-guardrails/src/policies/shell.ts +1255 -0
  21. package/node_modules/@oisin-ee/pi-guardrails/src/shell-parser.ts +173 -0
  22. package/node_modules/@oisin-ee/pi-prewalk/README.md +46 -0
  23. package/node_modules/@oisin-ee/pi-prewalk/package.json +40 -0
  24. package/node_modules/@oisin-ee/pi-prewalk/src/adapter.ts +294 -0
  25. package/node_modules/@oisin-ee/pi-prewalk/src/branch.ts +69 -0
  26. package/node_modules/@oisin-ee/pi-prewalk/src/config.ts +68 -0
  27. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core.d.ts +253 -0
  28. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core.js +616 -0
  29. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core_bg.wasm +0 -0
  30. package/node_modules/@oisin-ee/pi-prewalk/src/generated/prewalk_core_bg.wasm.d.ts +11 -0
  31. package/node_modules/@oisin-ee/pi-prewalk/src/index.ts +82 -0
  32. package/node_modules/@oisin-ee/pi-prewalk/src/ports.ts +31 -0
  33. package/node_modules/@oisin-ee/pi-prewalk/src/projection.ts +35 -0
  34. package/node_modules/@oisin-ee/pi-prewalk/src/wasm.ts +19 -0
  35. package/node_modules/@oisin-ee/pi-todo/README.md +49 -0
  36. package/node_modules/@oisin-ee/pi-todo/package.json +37 -0
  37. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core.d.ts +150 -0
  38. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core.js +723 -0
  39. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core_bg.wasm +0 -0
  40. package/node_modules/@oisin-ee/pi-todo/src/generated/todo_core_bg.wasm.d.ts +14 -0
  41. package/node_modules/@oisin-ee/pi-todo/src/index.ts +63 -0
  42. package/node_modules/@oisin-ee/pi-todo/src/lifecycle.ts +90 -0
  43. package/node_modules/@oisin-ee/pi-todo/src/reminder.ts +30 -0
  44. package/node_modules/@oisin-ee/pi-todo/src/source.ts +18 -0
  45. package/node_modules/@oisin-ee/pi-todo/src/state.ts +28 -0
  46. package/node_modules/@oisin-ee/pi-todo/src/tool.ts +55 -0
  47. package/node_modules/@oisin-ee/pi-todo/src/wasm.ts +31 -0
  48. package/node_modules/@oisin-ee/pi-wrangle/LICENSE +21 -0
  49. package/node_modules/@oisin-ee/pi-wrangle/README.md +504 -0
  50. package/node_modules/@oisin-ee/pi-wrangle/package.json +56 -0
  51. package/node_modules/@oisin-ee/pi-wrangle/pi/board.ts +133 -0
  52. package/node_modules/@oisin-ee/pi-wrangle/pi/inbox.ts +149 -0
  53. package/node_modules/@oisin-ee/pi-wrangle/pi/index.ts +145 -0
  54. package/node_modules/@oisin-ee/pi-wrangle/pi/panel.ts +227 -0
  55. package/node_modules/@oisin-ee/pi-wrangle/plugins/wrangle/skills/wrangle/SKILL.md +110 -0
  56. package/package.json +41 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @oisin-ee/oisinpi
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#10](https://github.com/oisin-ee/oisinpi/pull/10) [`1c8612b`](https://github.com/oisin-ee/oisinpi/commit/1c8612b7dd1ce00e948d6fd6d3a421f956629e1a) Thanks [@momo-momokaya](https://github.com/apps/momo-momokaya)! - `@oisin-ee/oisinpi` replaces `@oisin-ee/pi-prewalk-kit`. It bundles pi-prewalk, pi-todo, pi-guardrails, and
8
+ pi-wrangle 0.11.0. `@oisin-ee/pi-guardrails` is new: the agent hook policies for pi, Claude Code (`agent-hook`), and
9
+ Codex, ported from the dotfiles hooks runtime.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [[`1c8612b`](https://github.com/oisin-ee/oisinpi/commit/1c8612b7dd1ce00e948d6fd6d3a421f956629e1a)]:
14
+ - @oisin-ee/pi-guardrails@0.2.0
15
+ - @oisin-ee/pi-prewalk@0.2.0
16
+ - @oisin-ee/pi-todo@0.2.0
package/README.md CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # @oisin-ee/oisinpi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Oisin's [pi](https://pi.dev) packages in one install, at versions tested together:
4
+
5
+ | Package | Contents |
6
+ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
7
+ | [`@oisin-ee/pi-prewalk`](https://www.npmjs.com/package/@oisin-ee/pi-prewalk) | A frontier model plans and makes the first edit; a cheaper model finishes. |
8
+ | [`@oisin-ee/pi-todo`](https://www.npmjs.com/package/@oisin-ee/pi-todo) | The `todo` tool, which also opens the prewalk gate. |
9
+ | [`@oisin-ee/pi-wrangle`](https://www.npmjs.com/package/@oisin-ee/pi-wrangle) | The board, `/wrangle`, and the inbox for children and jobs across hosts. |
10
+ | [`@oisin-ee/pi-guardrails`](https://www.npmjs.com/package/@oisin-ee/pi-guardrails) | Git safety, the wait gate, and the agent-typing gate on every tool call. |
11
+
12
+ ```bash
13
+ pi install npm:@oisin-ee/oisinpi
14
+ ```
15
+
16
+ The collection bundles the four packages in its tarball and adds no code. No member imports another, so each one also
17
+ installs by itself. pi-wrangle needs the `wrangle` binary at the same version
18
+ ([oisin-ee/wrangle](https://github.com/oisin-ee/wrangle)).
@@ -0,0 +1,29 @@
1
+ # @oisin-ee/pi-guardrails
2
+
3
+ Agent guardrails for [pi](https://pi.dev), Claude Code, and Codex. One set of policies checks every shell and edit call:
4
+
5
+ - **Git safety:** no `--no-verify`, no discarding uncommitted work, no stash push and pop in one command, worktrees only
6
+ under `.worktrees/`, no on-the-fly identity.
7
+ - **Wait gate:** no `sleep`, `watch`, follow flags, `--wait`/`--watch`, or wait subcommands. Start long work in the
8
+ background instead.
9
+ - **Agent typing:** no `herdr agent prompt` or `pane send-*` into another agent's terminal. Use `wrangle send`.
10
+ - **GitOps:** no imperative `kubectl`, `helm`, `argocd`, or `nix` mutations, and no `fsck`/`mkfs`.
11
+ - **Generated paths:** no hand-edits of lockfiles or generated files.
12
+
13
+ In pi, it also fills a missing shell timeout (120 s) and blocks one above 10 minutes, naming each tool's unit.
14
+ Shell text is parsed with `shfmt`, which must be on `PATH`. A command that cannot be parsed is blocked.
15
+
16
+ ## pi
17
+
18
+ ```bash
19
+ pi install npm:@oisin-ee/pi-guardrails
20
+ ```
21
+
22
+ ## Claude Code and Codex
23
+
24
+ Install the package globally, then run `agent-hook` as a `PreToolUse` hook for shell and edit tools. It reads the hook
25
+ payload on stdin and exits 2 with the reason on stderr to block.
26
+
27
+ ```json
28
+ { "type": "command", "command": "agent-hook", "timeout": 30 }
29
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * The hook command for Claude Code and Codex: `agent-hook` in their PreToolUse
4
+ * hooks. Built to `dist/agent-hook.js`, because Node does not strip types from
5
+ * files under `node_modules`.
6
+ */
7
+ import { main } from "./lifecycle-runner.js";
8
+ process.exitCode = await main();
@@ -0,0 +1,66 @@
1
+ /* Shared shapes and helpers for the hook payloads and the policies. */
2
+ import { shellCallGuards, shellCalls } from "./shell-parser.js";
3
+ const BLOCK_CODE = 2, SHELL_TOOLS = new Set(["Bash", "bash", "shell"]), EDIT_TOOLS = new Set([
4
+ "Edit",
5
+ "Write",
6
+ "MultiEdit",
7
+ "apply_patch",
8
+ "write",
9
+ "edit",
10
+ "patch",
11
+ ]), isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text = (value) => (typeof value === "string" ? value : undefined),
12
+ /**
13
+ * The shell text of a hook payload's tool input.
14
+ * @param {HookPayload} payload A hook payload.
15
+ * @returns {string} The command, or "".
16
+ */
17
+ commandFromPayload = (payload) => {
18
+ const input = payload.tool_input ?? {};
19
+ return text(input["command"]) ?? text(input["cmd"]) ?? text(input["commandText"]) ?? "";
20
+ },
21
+ /**
22
+ * The calls in `command`. An argument list from an argv-style spawn is
23
+ * already one call; only shell text needs parsing.
24
+ * @param {Command} command Shell text or an argument list.
25
+ * @returns {string[][]} One word list per call.
26
+ */
27
+ splitShell = (command) => typeof command === "string" ? shellCalls(command) : [[...command]],
28
+ /**
29
+ * The `&&` guard pipeline of each splitShell piece. An argv-style call has no
30
+ * shell operators, so no guard.
31
+ * @param {Command} command Shell text or an argument list.
32
+ * @returns {(string[][] | null)[]} One guard per call.
33
+ */
34
+ splitShellGuards = (command) => typeof command === "string" ? shellCallGuards(command) : [null],
35
+ /**
36
+ * The words of the first call in `command`.
37
+ * @param {Command} command Shell text or an argument list.
38
+ * @returns {string[]} The words, or none.
39
+ */
40
+ tokens = (command) => typeof command === "string" ? (shellCalls(command)[0] ?? []) : [...command],
41
+ /**
42
+ * A command word without quotes, directory, or `@version`.
43
+ * @param {string} token A command word.
44
+ * @returns {string} The bare command name.
45
+ */
46
+ commandName = (token) => (token
47
+ .replaceAll(/^["']|["']$/gu, "")
48
+ .split("/")
49
+ .pop() ?? "").split("@")[0] ?? "",
50
+ /**
51
+ * Blocks a tool call: writes `message` and returns exit code 2.
52
+ * @param {string} message The reason shown to the agent.
53
+ * @param {HookIO} io Where the message goes.
54
+ * @returns {HookResult} The blocking result.
55
+ */
56
+ block = (message, io) => {
57
+ io.stderr(`${message}\n`);
58
+ return { code: BLOCK_CODE };
59
+ },
60
+ /**
61
+ * Allows a tool call, with optional output for the harness.
62
+ * @param {Readonly<Record<string, unknown>>} extra Keys printed as JSON.
63
+ * @returns {HookResult} The allowing result.
64
+ */
65
+ ok = (extra = {}) => ({ code: 0, ...extra });
66
+ export { BLOCK_CODE, EDIT_TOOLS, SHELL_TOOLS, block, commandFromPayload, commandName, isRecord, ok, splitShell, splitShellGuards, tokens, };
@@ -0,0 +1,42 @@
1
+ /* Routes a Claude Code or Codex hook event to the policies. */
2
+ import { EDIT_TOOLS, SHELL_TOOLS, block, commandFromPayload, ok } from "./common.js";
3
+ import { SHELL_CLASSIFIERS } from "./policies/shell.js";
4
+ import { classifyEdits, editInputsFromPayload } from "./policies/edit.js";
5
+ const preShell = (runtime, payload) => {
6
+ const command = commandFromPayload(payload), cwd = payload.cwd ?? runtime.cwd;
7
+ for (const classify of SHELL_CLASSIFIERS) {
8
+ const message = classify(command, cwd);
9
+ if (message !== null) {
10
+ return message;
11
+ }
12
+ }
13
+ return null;
14
+ },
15
+ /**
16
+ * The verdict for one hook event. Only `PreToolUse` on a shell or edit tool
17
+ * can block; a policy that throws, such as an unparsable command, blocks.
18
+ * @param {Runtime} runtime The working directory and stderr.
19
+ * @param {HookPayload} payload The hook payload.
20
+ * @returns {HookResult} Exit code 2 to block, otherwise 0.
21
+ */
22
+ runEvent = (runtime, payload) => {
23
+ if (payload.hook_event_name !== "PreToolUse") {
24
+ return ok();
25
+ }
26
+ const tool = payload.tool_name ?? "";
27
+ if (SHELL_TOOLS.has(tool)) {
28
+ try {
29
+ const message = preShell(runtime, payload);
30
+ return message === null ? ok() : block(message, runtime);
31
+ }
32
+ catch (error) {
33
+ return block(error instanceof Error ? error.message : String(error), runtime);
34
+ }
35
+ }
36
+ if (EDIT_TOOLS.has(tool)) {
37
+ const message = classifyEdits(editInputsFromPayload(payload), payload.cwd ?? runtime.cwd);
38
+ return message === null ? ok() : block(message, runtime);
39
+ }
40
+ return ok();
41
+ };
42
+ export { runEvent };
@@ -0,0 +1,59 @@
1
+ /*
2
+ * The Claude Code and Codex hook command. It reads one hook payload on stdin,
3
+ * prints the verdict's extra keys as JSON, and exits with the verdict's code:
4
+ * 2 blocks the tool call. An internal failure is recorded and allows the call,
5
+ * so a broken install never stops every tool.
6
+ */
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { isRecord } from "./common.js";
11
+ import { runEvent } from "./event-registry.js";
12
+ /**
13
+ * The verdict for one raw hook payload.
14
+ * @param {HookRun} run The payload and the process context.
15
+ * @returns {HookResult} The verdict; a payload that is not an object allows.
16
+ */
17
+ const runLifecycleHook = (run) => {
18
+ const parsed = typeof run.payload === "string" ? JSON.parse(run.payload) : run.payload;
19
+ if (!isRecord(parsed)) {
20
+ return { code: 0 };
21
+ }
22
+ return runEvent({
23
+ cwd: run.cwd ?? process.cwd(),
24
+ stderr: run.stderr ??
25
+ ((message) => {
26
+ process.stderr.write(message);
27
+ }),
28
+ }, parsed);
29
+ }, errorRoot = () => join(process.env["XDG_STATE_HOME"] ?? join(process.env["HOME"] ?? homedir(), ".local", "state"), "agent-hooks", "runtime"), readStdin = async () => {
30
+ const chunks = [];
31
+ for await (const chunk of process.stdin) {
32
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
33
+ }
34
+ return Buffer.concat(chunks).toString("utf8");
35
+ },
36
+ /**
37
+ * Runs the hook on this process's stdin and returns the exit code.
38
+ * @returns {Promise<number>} The process exit code.
39
+ */
40
+ main = async () => {
41
+ const raw = await readStdin();
42
+ if (raw.trim().length === 0) {
43
+ return 0;
44
+ }
45
+ try {
46
+ const { code, ...output } = runLifecycleHook({ payload: raw });
47
+ if (Object.keys(output).length > 0) {
48
+ process.stdout.write(`${JSON.stringify(output)}\n`);
49
+ }
50
+ return code;
51
+ }
52
+ catch (error) {
53
+ const message = error instanceof Error ? error.message : String(error), root = errorRoot();
54
+ await mkdir(root, { recursive: true });
55
+ await writeFile(join(root, "last-error.txt"), `${message}\n`);
56
+ return 0;
57
+ }
58
+ };
59
+ export { main, runLifecycleHook };
@@ -0,0 +1,98 @@
1
+ /* The generated-path gate: lockfiles and generated files change only through their tools. */
2
+ import { basename, dirname, resolve } from "node:path";
3
+ const LOCKFILES = new Set([
4
+ "package-lock.json",
5
+ "pnpm-lock.yaml",
6
+ "yarn.lock",
7
+ "bun.lockb",
8
+ "Cargo.lock",
9
+ "composer.lock",
10
+ "Gemfile.lock",
11
+ "poetry.lock",
12
+ "uv.lock",
13
+ "flake.lock",
14
+ "go.sum",
15
+ ]), PACKAGE_LOCKFILES = new Set([
16
+ "package-lock.json",
17
+ "pnpm-lock.yaml",
18
+ "yarn.lock",
19
+ "bun.lockb",
20
+ ]), GENERATED_PATH = new RegExp([
21
+ /\.gen\./u.source,
22
+ /_pb2(?:_grpc)?\.py$/u.source,
23
+ /\.pb\.go$/u.source,
24
+ /_generated\./u.source,
25
+ /\.g\.dart$/u.source,
26
+ /\.freezed\.dart$/u.source,
27
+ /(?:^|\/)(?:generated|__generated__|\.gen)\//u.source,
28
+ ].join("|"), "u"), PATCH_FILE = /^\*\*\* (?<operation>Add|Update|Delete) File: (?<path>.+)$/u, PATCH_MOVE = /^\*\*\* Move to: (?<path>.+)$/u, text = (value) => (typeof value === "string" ? value : undefined),
29
+ /**
30
+ * The files an `apply_patch` body touches.
31
+ * @param {string} command The patch text.
32
+ * @returns {EditInput[]} One entry per file header or move.
33
+ */
34
+ patchInputs = (command = "") => {
35
+ const inputs = [], lines = command.split("\n"), start = lines.indexOf("*** Begin Patch"), end = lines.indexOf("*** End Patch", start + 1);
36
+ if (start === -1 || end === -1) {
37
+ return inputs;
38
+ }
39
+ for (const line of lines.slice(start + 1, end)) {
40
+ const file = PATCH_FILE.exec(line)?.groups;
41
+ if (file?.["operation"] !== undefined && file["path"] !== undefined) {
42
+ inputs.push({ filePath: file["path"], operation: file["operation"].toLowerCase() });
43
+ }
44
+ const move = PATCH_MOVE.exec(line)?.groups?.["path"];
45
+ if (move !== undefined) {
46
+ inputs.push({ filePath: move, operation: "move" });
47
+ }
48
+ }
49
+ return inputs;
50
+ },
51
+ /**
52
+ * The files a hook payload's edit touches.
53
+ * @param {HookPayload} payload A hook payload.
54
+ * @returns {EditInput[]} The files and operations.
55
+ */
56
+ editInputsFromPayload = (payload) => {
57
+ const input = payload.tool_input ?? {};
58
+ if (payload.tool_name === "apply_patch" || payload.tool_name === "patch") {
59
+ return patchInputs(text(input["command"]) ?? "");
60
+ }
61
+ const filePath = text(input["file_path"]) ?? text(input["filePath"]) ?? text(input["path"]) ?? "";
62
+ return filePath === "" ? [] : [{ filePath, operation: "update" }];
63
+ },
64
+ /**
65
+ * Why `filePath` is generated, or null.
66
+ * @param {string} filePath A file path.
67
+ * @returns {string | null} The reason and the tool to use.
68
+ */
69
+ generatedReason = (filePath) => {
70
+ if (LOCKFILES.has(basename(filePath))) {
71
+ return "lockfile. Use its package manager";
72
+ }
73
+ return GENERATED_PATH.test(filePath) ? "generated file. Use its code generator" : null;
74
+ },
75
+ // Deleting a JS lockfile together with its package.json removes the package, which no tool does.
76
+ removesPackage = (input, inputs, cwd) => {
77
+ if (input.operation !== "delete" || !PACKAGE_LOCKFILES.has(basename(input.filePath))) {
78
+ return false;
79
+ }
80
+ const manifest = resolve(dirname(resolve(cwd, input.filePath)), "package.json"), changes = inputs.filter((item) => resolve(cwd, item.filePath) === manifest);
81
+ return changes.length === 1 && changes[0]?.operation === "delete";
82
+ },
83
+ /**
84
+ * The block message for the first hand-edit of a generated path, or null.
85
+ * @param {readonly EditInput[]} inputs The files an edit touches.
86
+ * @param {string} cwd The directory relative paths resolve from.
87
+ * @returns {string | null} The block message.
88
+ */
89
+ classifyEdits = (inputs, cwd) => {
90
+ for (const input of inputs) {
91
+ const reason = generatedReason(input.filePath);
92
+ if (reason !== null && !removesPackage(input, inputs, cwd)) {
93
+ return `generated-path: blocked hand-edit of ${input.filePath}: ${reason}.`;
94
+ }
95
+ }
96
+ return null;
97
+ };
98
+ export { classifyEdits, editInputsFromPayload, generatedReason, patchInputs };