@xynogen/pix-commands 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/package.json +2 -1
- package/src/extension.test.ts +5 -4
- package/src/extension.ts +2 -2
- package/src/unattended.test.ts +79 -0
- package/src/unattended.ts +215 -0
- package/src/afk.ts +0 -35
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
|
|
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
|
+
"version": "0.4.0",
|
|
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,6 +45,7 @@
|
|
|
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
50
|
"@xynogen/pix-runtime": "^0.6.0"
|
|
50
51
|
}
|
package/src/extension.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|