@xynogen/pix-commands 0.3.8 → 0.4.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 CHANGED
@@ -4,7 +4,8 @@ Pi extension providing focused slash commands:
4
4
 
5
5
  - `/clear` — flush Pi's cached model data.
6
6
  - `/btw <question>` — ask an isolated side question without interrupting the main agent.
7
- - `/afk` — toggle AFK mode for unattended runs.
7
+ - `/afk` — toggle AFK mode for unattended runs (yellow auto-allow, red/root auto-deny).
8
+ - `/yolo` — toggle YOLO mode for unattended runs (auto-approve nearly everything incl. red/root; capable models only, session consent required).
8
9
 
9
10
  ## `/clear`
10
11
 
@@ -50,9 +51,66 @@ AFK mode off — approval prompts restored.
50
51
  The point is to let the agent keep working on safe, medium-risk actions while you
51
52
  are away, without silently permitting destructive ones. It pairs with the herdr
52
53
  notification bridge in `pix-runtime`: you still get pinged when something truly
53
- needs you (a red gate or sudo), and auto-deny turns that into a stop rather than
54
+ needs you (a red gate or root), and auto-deny turns that into a stop rather than
54
55
  an approval.
55
56
 
57
+ ## `/yolo`
58
+
59
+ `/yolo` toggles YOLO mode: **almost** every permission gate auto-approves,
60
+ **including red (critical) commands and root (`sudo_run`)**. For the vast
61
+ majority of actions there is no human confirming, so a destructive or
62
+ irreversible action can run before anyone stops it. Use it only when you accept
63
+ that. A few things still stop you — the four guardrails below are the deliberate
64
+ exceptions to "auto-approve everything."
65
+
66
+ ```text
67
+ YOLO mode on (model score 82) — every gate including red and root auto-approves.
68
+ ```
69
+
70
+ Four guardrails temper the blast radius:
71
+
72
+ - **One-time session consent.** The first `/yolo` of each session opens a
73
+ blocking modal spelling out the damage risk and the AS-IS/no-liability
74
+ disclaimer; you must explicitly accept before YOLO arms. Cancel leaves the
75
+ normal prompts in place. Consent is session-scoped — a fresh session asks
76
+ again, so arming YOLO is always a conscious act, never a forgotten flag.
77
+
78
+ - **Capable models only.** YOLO refuses to turn on unless the active model has a
79
+ benchmark score of at least `75` (from `pix-data`). A weaker or off-catalog
80
+ model is rejected with a message, because auto-approving red and root demands a
81
+ model that reasons well about consequences.
82
+ - **Root still needs a ticket.** `sudo_run` auto-approves only when a valid PAM
83
+ ticket is already cached — the password cannot be auto-typed, so a first root
84
+ command with no cached ticket still shows the password prompt. Bare `sudo` in
85
+ `bash` is always redirected to `sudo_run`, never run directly.
86
+ - **Circuit breaker.** A short list of catastrophic, unrecoverable commands —
87
+ `rm -rf /` or `~`, `dd`/redirect onto a raw disk, `mkfs` on a device, a fork
88
+ bomb — is exempt from *every* mode, including YOLO (see
89
+ [`pix-gate`](https://www.npmjs.com/package/@xynogen/pix-gate)). This does **not**
90
+ forbid the command: a genuine `dd`-to-USB or `mkfs` still runs, it just falls
91
+ back to a one-time Allow/Deny prompt instead of a silent auto-approve. One
92
+ extra click, only under YOLO, only on the handful of commands you can't undo.
93
+ Mirrors Claude Code's `bypassPermissions` floor.
94
+
95
+ `/afk` and `/yolo` are mutually exclusive; turning one on turns the other off.
96
+
97
+ ### Mode awareness (both modes)
98
+
99
+ A relaxed gate is dangerous mainly because the *model* keeps acting as if a human
100
+ will still catch a bad call. To close that gap, while either mode is active a
101
+ short banner is injected into every turn via `before_agent_start`, telling the
102
+ model the safety net is off and what it now owns:
103
+
104
+ - **AFK** — plan around auto-deny of red and root; do not depend on a denied step
105
+ succeeding; stop and summarize when a denial blocks progress.
106
+ - **YOLO** — before any red or root action, state in the reply why it is
107
+ necessary, its blast radius and worst-case fallout, and whether it is
108
+ reversible; if it cannot be justified, do not run it; always prefer the least
109
+ destructive path.
110
+
111
+ The banner is only present while a mode is on, so it costs no baseline tokens
112
+ otherwise.
113
+
56
114
  ## Install
57
115
 
58
116
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-commands",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "description": "Pi extension — slash commands for cache clearing and isolated side questions",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -45,7 +45,8 @@
45
45
  "@earendil-works/pi-tui": "*"
46
46
  },
47
47
  "dependencies": {
48
+ "@xynogen/pix-data": "^0.4.0",
48
49
  "@xynogen/pix-pretty": "^1.13.0",
49
- "@xynogen/pix-runtime": "^0.6.0"
50
+ "@xynogen/pix-runtime": "^0.7.0"
50
51
  }
51
52
  }
@@ -6,6 +6,7 @@ import extension from "./extension.ts";
6
6
  afterEach(() => {
7
7
  delete (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
8
8
  delete (globalThis as { __pixAfk?: boolean }).__pixAfk;
9
+ delete (globalThis as { __pixYolo?: boolean }).__pixYolo;
9
10
  });
10
11
 
11
12
  describe("pix-commands registration", () => {
@@ -29,11 +30,11 @@ describe("pix-commands registration", () => {
29
30
  return { pi, commands, handlers, renderers };
30
31
  }
31
32
 
32
- test("registers /clear, /btw, /afk, and the BTW renderer once per Pi instance", () => {
33
+ test("registers /clear, /btw, /afk, /yolo, and the BTW renderer once per Pi instance", () => {
33
34
  const { pi, commands, renderers } = host();
34
35
  extension(pi);
35
36
  extension(pi);
36
- expect(commands).toEqual(["clear", "btw", "afk"]);
37
+ expect(commands).toEqual(["clear", "btw", "afk", "yolo"]);
37
38
  expect(renderers).toEqual(["pix-btw-answer"]);
38
39
  });
39
40
 
@@ -42,8 +43,8 @@ describe("pix-commands registration", () => {
42
43
  const second = host();
43
44
  extension(first.pi);
44
45
  extension(second.pi);
45
- expect(first.commands).toEqual(["clear", "btw", "afk"]);
46
- expect(second.commands).toEqual(["clear", "btw", "afk"]);
46
+ expect(first.commands).toEqual(["clear", "btw", "afk", "yolo"]);
47
+ expect(second.commands).toEqual(["clear", "btw", "afk", "yolo"]);
47
48
  });
48
49
 
49
50
  test("/afk toggles shared state and status", async () => {
package/src/extension.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { once } from "@xynogen/pix-runtime/once";
3
- import registerAfk from "./afk.ts";
4
3
  import { registerBtw } from "./btw/index.ts";
5
4
  import registerClear from "./clear.ts";
5
+ import registerUnattended from "./unattended.ts";
6
6
 
7
7
  export default function (pi: ExtensionAPI): void {
8
8
  once(pi, "pix-commands", () => {
9
9
  registerClear(pi);
10
10
  registerBtw(pi);
11
- registerAfk(pi);
11
+ registerUnattended(pi);
12
12
  });
13
13
  }
@@ -0,0 +1,79 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ confirmYoloConsent,
4
+ getMode,
5
+ modelScore,
6
+ setMode,
7
+ unattendedBanner,
8
+ YOLO_MIN_SCORE,
9
+ } from "./unattended.ts";
10
+
11
+ type G = { __pixAfk?: boolean; __pixYolo?: boolean; __pixYoloConsent?: boolean };
12
+
13
+ afterEach(() => {
14
+ delete (globalThis as G).__pixAfk;
15
+ delete (globalThis as G).__pixYolo;
16
+ delete (globalThis as G).__pixYoloConsent;
17
+ });
18
+
19
+ describe("unattended mode state", () => {
20
+ test("setMode keeps the two globals mutually exclusive", () => {
21
+ setMode("afk");
22
+ expect(getMode()).toBe("afk");
23
+ expect((globalThis as G).__pixYolo).toBe(false);
24
+
25
+ setMode("yolo");
26
+ expect(getMode()).toBe("yolo");
27
+ expect((globalThis as G).__pixAfk).toBe(false);
28
+
29
+ setMode("off");
30
+ expect(getMode()).toBe("off");
31
+ });
32
+ });
33
+
34
+ describe("modelScore", () => {
35
+ test("returns null for an empty / off-catalog model", () => {
36
+ expect(modelScore({ model: undefined })).toBeNull();
37
+ expect(modelScore({ model: { id: "definitely-not-a-real-model-xyz" } })).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe("unattendedBanner", () => {
42
+ test("off => no banner", () => {
43
+ setMode("off");
44
+ expect(unattendedBanner()).toBeUndefined();
45
+ });
46
+
47
+ test("afk banner names auto-deny of red and root", () => {
48
+ setMode("afk");
49
+ const b = unattendedBanner() ?? "";
50
+ expect(b).toContain('mode="afk"');
51
+ expect(b).toContain("auto-DENY");
52
+ });
53
+
54
+ test("yolo banner demands red/root self-justification", () => {
55
+ setMode("yolo");
56
+ const b = unattendedBanner() ?? "";
57
+ expect(b).toContain('mode="yolo"');
58
+ expect(b).toContain("blast radius");
59
+ expect(b).toContain("reversible");
60
+ });
61
+ });
62
+
63
+ describe("YOLO score threshold", () => {
64
+ test("is a sane capability floor", () => {
65
+ expect(YOLO_MIN_SCORE).toBe(75);
66
+ });
67
+ });
68
+
69
+ describe("confirmYoloConsent (session gate)", () => {
70
+ test("short-circuits true once consent is recorded for the session", async () => {
71
+ (globalThis as G).__pixYoloConsent = true;
72
+ // No ui passed: if it did not short-circuit it would return false.
73
+ expect(await confirmYoloConsent({})).toBe(true);
74
+ });
75
+
76
+ test("refuses when there is no ui to render the warning", async () => {
77
+ expect(await confirmYoloConsent({})).toBe(false);
78
+ });
79
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Unattended modes — /afk and /yolo.
3
+ *
4
+ * Both relax the permission gate so the agent keeps moving without a human
5
+ * clicking Allow/Deny. The danger is not the relaxed gate itself: it is that
6
+ * the *model* keeps acting as if a human will still catch a bad call. When the
7
+ * net is gone the model must KNOW, so every turn we inject an awareness banner
8
+ * (see {@link unattendedBanner}) and, for YOLO, a red/root self-justification
9
+ * directive.
10
+ *
11
+ * Behavior read by pix-gate (`unattendedGateDecision`) and pix-sudo via the
12
+ * `__pixAfk` / `__pixYolo` globals:
13
+ * off — every gate prompts (default).
14
+ * afk — yellow auto-allow; red + root auto-DENY.
15
+ * yolo — everything auto-allows, including red + root (root still needs a
16
+ * cached PAM ticket; the password cannot be auto-typed).
17
+ */
18
+
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ import { lookupBenchmark } from "@xynogen/pix-data";
21
+ import { showOverlay } from "@xynogen/pix-pretty/gate-overlay";
22
+ import { icon } from "@xynogen/pix-pretty/icon-catalog";
23
+
24
+ /** Minimum model score allowed to hold YOLO. Below this, red+root auto-approve is off-limits. */
25
+ export const YOLO_MIN_SCORE = 75;
26
+
27
+ export type UnattendedMode = "off" | "afk" | "yolo";
28
+ type UnattendedGlobal = typeof globalThis & {
29
+ __pixAfk?: boolean;
30
+ __pixYolo?: boolean;
31
+ /** Session-scoped: user acknowledged the YOLO damage warning this session. */
32
+ __pixYoloConsent?: boolean;
33
+ };
34
+ type StatusUI = {
35
+ theme: { fg(color: "error", text: string): string };
36
+ setStatus(key: string, text: string | undefined): void;
37
+ notify(message: string, kind?: string): void;
38
+ };
39
+ type ModelCtx = { model?: { id?: string } };
40
+
41
+ const STATUS_KEY = "unattended";
42
+
43
+ export function getMode(): UnattendedMode {
44
+ const g = globalThis as UnattendedGlobal;
45
+ if (g.__pixYolo === true) return "yolo";
46
+ if (g.__pixAfk === true) return "afk";
47
+ return "off";
48
+ }
49
+
50
+ /** Set the mode; the two globals are mutually exclusive so gate/sudo never see both. */
51
+ export function setMode(mode: UnattendedMode): void {
52
+ const g = globalThis as UnattendedGlobal;
53
+ g.__pixAfk = mode === "afk";
54
+ g.__pixYolo = mode === "yolo";
55
+ }
56
+
57
+ function syncStatus(ui: StatusUI): void {
58
+ // ponytail: reuses the existing "afk"/"warn" glyphs instead of adding a YOLO
59
+ // icon to pix-pretty (that is a public-API minor bump + approval).
60
+ const labels: Record<UnattendedMode, string | undefined> = {
61
+ yolo: `${icon("warn")} YOLO`,
62
+ afk: `${icon("afk")} AFK`,
63
+ off: undefined,
64
+ };
65
+ const label = labels[getMode()];
66
+ ui.setStatus(STATUS_KEY, label ? ui.theme.fg("error", label) : undefined);
67
+ }
68
+
69
+ /**
70
+ * Blocking, session-scoped consent gate. The first `/yolo` enable of a session
71
+ * shows a modal spelling out the damage risk and the no-liability disclaimer;
72
+ * the user must explicitly accept before YOLO can arm. Consent does NOT persist
73
+ * across sessions — a fresh session asks again (moral speed-bump, not a
74
+ * once-and-forget click-through).
75
+ */
76
+ export async function confirmYoloConsent(ctx: ModelCtx): Promise<boolean> {
77
+ const g = globalThis as UnattendedGlobal;
78
+ if (g.__pixYoloConsent === true) return true;
79
+ const ui = (ctx as { ui?: unknown }).ui as Parameters<typeof showOverlay>[0] | undefined;
80
+ if (!ui) return false;
81
+ const result = await showOverlay(ui, {
82
+ mode: "confirm",
83
+ accent: "error",
84
+ title: "⚠ YOLO MODE — no human confirms anything",
85
+ body: [
86
+ "Every gate auto-approves, including RED and root. Destructive,",
87
+ "irreversible actions (data loss, wiped disks, force-push) can run",
88
+ "and CANNOT be undone. Only the circuit breaker stays active.",
89
+ "Watch the model closely — it can rationalize or downplay a risky",
90
+ "action to get past its own self-justification. Do not trust the",
91
+ "reasoning blindly.",
92
+ "You accept all risk; AS IS, no liability (MIT).",
93
+ ],
94
+ choices: [
95
+ { value: "no", label: "Cancel", description: "Keep approval prompts" },
96
+ {
97
+ value: "yes",
98
+ label: "Accept all risk",
99
+ description: "Enable YOLO this session",
100
+ },
101
+ ],
102
+ });
103
+ const ok = result.action === "approved";
104
+ if (ok) g.__pixYoloConsent = true;
105
+ return ok;
106
+ }
107
+
108
+ /** Current model's benchmark score, or null when unknown/off-catalog. */
109
+ export function modelScore(ctx: ModelCtx): number | null {
110
+ const id = (ctx.model?.id ?? "").replace(/^[a-z]+\//i, "");
111
+ if (!id) return null;
112
+ return lookupBenchmark(id)?.overallScore ?? null;
113
+ }
114
+
115
+ /**
116
+ * Per-turn awareness banner. Injected via `before_agent_start` so the model is
117
+ * told, every turn, that the human safety net is off and what it now owns.
118
+ */
119
+ export function unattendedBanner(): string | undefined {
120
+ const mode = getMode();
121
+ if (mode === "yolo") {
122
+ return [
123
+ '<pix-unattended mode="yolo">',
124
+ "YOLO MODE ACTIVE. Every permission gate — including RED/critical commands and",
125
+ "root (sudo_run) — auto-approves with NO human confirming. The safety net is OFF:",
126
+ "nothing will stop a destructive or irreversible action before it runs. You alone",
127
+ "are accountable for the fallout.",
128
+ "Before any red-tier or root action, first state in your reply: (1) why it is",
129
+ "necessary, (2) its blast radius and worst-case fallout, (3) whether it is",
130
+ "reversible. If you cannot justify it, do not run it. Always prefer the least",
131
+ "destructive path that still does the job.",
132
+ "</pix-unattended>",
133
+ ].join("\n");
134
+ }
135
+ if (mode === "afk") {
136
+ return [
137
+ '<pix-unattended mode="afk">',
138
+ "AFK MODE ACTIVE. The user is away. Medium-risk (yellow) gates auto-approve; RED/",
139
+ "critical commands and root (sudo_run) auto-DENY and will fail. No human will",
140
+ "confirm anything this turn.",
141
+ "Plan around the auto-deny: do not depend on a red or root step succeeding. State",
142
+ "the intent and consequence of any gated action. When a denied step blocks",
143
+ "progress, stop and summarize what needs the user.",
144
+ "</pix-unattended>",
145
+ ].join("\n");
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ export default function registerUnattended(pi: ExtensionAPI): void {
151
+ pi.on("session_start", (_event, ctx) => syncStatus(ctx.ui));
152
+
153
+ // Every turn: prepend the awareness banner while a mode is active.
154
+ pi.on("before_agent_start", async (event) => {
155
+ const banner = unattendedBanner();
156
+ if (!banner) return undefined;
157
+ const existing = (event as { systemPrompt?: string }).systemPrompt ?? "";
158
+ return { systemPrompt: `${banner}\n\n${existing}` };
159
+ });
160
+
161
+ pi.registerCommand("afk", {
162
+ description: "Toggle AFK mode — yellow gates auto-allow; red and root auto-deny",
163
+ handler: async (_args, ctx) => {
164
+ const turningOn = getMode() !== "afk";
165
+ setMode(turningOn ? "afk" : "off");
166
+ syncStatus(ctx.ui);
167
+ ctx.ui.notify(
168
+ turningOn
169
+ ? "AFK mode on — yellow gates auto-allow; red and root auto-deny."
170
+ : "AFK mode off — approval prompts restored.",
171
+ turningOn ? "warning" : "info",
172
+ );
173
+ },
174
+ });
175
+
176
+ pi.registerCommand("yolo", {
177
+ description: "Toggle YOLO mode — auto-approve everything including red and root",
178
+ handler: async (_args, ctx) => {
179
+ if (getMode() === "yolo") {
180
+ setMode("off");
181
+ syncStatus(ctx.ui);
182
+ ctx.ui.notify("YOLO mode off — approval prompts restored.", "info");
183
+ return;
184
+ }
185
+ const score = modelScore(ctx as ModelCtx);
186
+ if (score === null) {
187
+ ctx.ui.notify(
188
+ `YOLO refused — cannot verify this model scores ≥ ${YOLO_MIN_SCORE}. ` +
189
+ "Auto-approving red and root needs a benchmarked, capable model.",
190
+ "error",
191
+ );
192
+ return;
193
+ }
194
+ if (score < YOLO_MIN_SCORE) {
195
+ ctx.ui.notify(
196
+ `YOLO refused — model score ${score} is below ${YOLO_MIN_SCORE}. ` +
197
+ "Use a more capable model to auto-approve red and root actions.",
198
+ "error",
199
+ );
200
+ return;
201
+ }
202
+ if (!(await confirmYoloConsent(ctx as ModelCtx))) {
203
+ ctx.ui.notify("YOLO cancelled — approval prompts remain in place.", "info");
204
+ return;
205
+ }
206
+ setMode("yolo");
207
+ syncStatus(ctx.ui);
208
+ ctx.ui.notify(
209
+ `YOLO mode on (model score ${score}) — every gate including red and root ` +
210
+ "auto-approves. No human will stop a destructive action. You are accountable.",
211
+ "warning",
212
+ );
213
+ },
214
+ });
215
+ }
package/src/afk.ts DELETED
@@ -1,35 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { icon } from "@xynogen/pix-pretty/icon-catalog";
3
-
4
- const STATUS_KEY = "afk";
5
-
6
- type AfkGlobal = typeof globalThis & { __pixAfk?: boolean };
7
- type StatusUI = {
8
- theme: { fg(color: "error", text: string): string };
9
- setStatus(key: string, text: string | undefined): void;
10
- };
11
-
12
- function setAfkStatus(ui: StatusUI, active: boolean): void {
13
- ui.setStatus(STATUS_KEY, active ? ui.theme.fg("error", `${icon("afk")} AFK`) : undefined);
14
- }
15
-
16
- export default function registerAfk(pi: ExtensionAPI): void {
17
- pi.on("session_start", (_event, ctx) => {
18
- setAfkStatus(ctx.ui, (globalThis as AfkGlobal).__pixAfk === true);
19
- });
20
-
21
- pi.registerCommand("afk", {
22
- description: "Toggle unattended gate behavior",
23
- handler: async (_args, ctx) => {
24
- const state = !(globalThis as AfkGlobal).__pixAfk;
25
- (globalThis as AfkGlobal).__pixAfk = state;
26
- setAfkStatus(ctx.ui, state);
27
- ctx.ui.notify(
28
- state
29
- ? "AFK mode on — yellow gates auto-allow; red and sudo auto-deny."
30
- : "AFK mode off — approval prompts restored.",
31
- state ? "warning" : "info",
32
- );
33
- },
34
- });
35
- }