import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { appendSystemPromptOnce, type AppendableSystemPromptOptions } from "./append-system-prompt.ts";
import { sameReviewSessionIdentity, resolveCanonicalGitRepositoryIdentity, type ReviewSessionIdentity } from "./review-session-standing-permission.ts";
import { realpath } from "node:fs/promises";
import { gentlePiConfigHome } from "./agent-home.ts";
import { isPermissionMode, resolvePermissionModePolicy, writePermissionModePolicy, type PermissionMode } from "./permission-mode-policy.ts";
export type { PermissionMode } from "./permission-mode-policy.ts";
export const MODE_STATUS_KEY = "gentle:mode";
export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate usage accounting; hard safety and independent consent remain.";
export const YOLO_STATUS_KEY = "gentle:yolo";
export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
export const SUPER_YOLO_STATUS_TEXT = "💥 SUPER YOLO ON — no permission prompts; private paths stay blocked";
export const SUPER_YOLO_DIRECTIVE = `
SUPER YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace. No permission questions are asked in this mode: proceed through ordinary and destructive shell work (including rm, resets, force operations and configured-guard confirmations) without confirmation dialogs, and make implementation and delivery choices directly.
The ONLY hard line is private material: never read, print, copy, transmit or commit secrets or private credentials — .env files, .ssh, keychains, *.pem/*.key/*.p12/*.pfx, secrets/ directories, cloud credential files. If a task seems to need them, stop and ask the human.
This does NOT override explicit human restrictions in the live conversation, and never auto-answers ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Children remain bounded by their delegated task; they receive no independent delivery grant or Super Yolo inheritance.
`;
export const YOLO_DIRECTIVE = `
YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
This does NOT override explicit human restrictions, repository policy, project trust, configured confirmations or blocks, or the authorized task scope. Ask before scope expansion, privacy-sensitive disclosure, genuinely unresolved consequential product choices, or ambiguous destinations or credentials. Never invent destinations or credentials.
Destructive/data-loss confirmations remain mandatory and independent. Never auto-answer ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Review standing permission and RDD are unchanged. Children remain bounded by their delegated task; they receive no independent delivery grant or YOLO inheritance.
`;
/** Legacy standalone API; separate from review grants and their registry. */
export class YoloSessionPolicy {
private grant: ReviewSessionIdentity | undefined;
private generation = 0;
get epoch(): number { return this.generation; }
get enabled(): boolean { return this.grant !== undefined; }
reset(): void { this.generation += 1; this.grant = undefined; }
set(enabled: boolean, identity: ReviewSessionIdentity | undefined, expectedEpoch = this.epoch): boolean {
if (expectedEpoch !== this.epoch) return false;
this.reset();
if (!enabled) return true;
if (identity === undefined) return false;
this.grant = identity;
return true;
}
active(identity: ReviewSessionIdentity | undefined): boolean {
if (!this.grant) return false;
if (identity !== undefined && sameReviewSessionIdentity(this.grant, identity)) return true;
this.reset();
return false;
}
}
/** Remove our exact blocks as well as adding them: bridge options can be reused. */
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean, superActive = false): void {
if (!options) return;
options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
.replace(`\n\n${SUPER_YOLO_DIRECTIVE}`, "").replace(SUPER_YOLO_DIRECTIVE, "");
if (superActive) appendSystemPromptOnce(options, SUPER_YOLO_DIRECTIVE);
else if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
}
const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
export const YOLO_DISPLAY = { on: "ON", off: "OFF", unavailable: "UNAVAILABLE" } as const;
export type YoloDisplay = (typeof YOLO_DISPLAY)[keyof typeof YOLO_DISPLAY];
const HOST_UI_CHANNEL = "gentle:yolo:host-ui";
const DISCOVERY_TIMEOUT_MS = 100;
export interface YoloUiAdapter {
read(): Promise;
readMode?(): Promise;
readSnapshot?(): Promise<{ yolo: YoloDisplay; mode: PermissionMode; defaultMode: PermissionMode }>;
setMode?(mode: PermissionMode): Promise;
readDefault?(): PermissionMode;
saveDefault?(mode: PermissionMode): Promise;
toggle(): Promise;
observe(refresh: () => void): () => void;
dispose(): void;
}
interface HostUiRequest { context: ExtensionContext; respond(adapter: YoloUiAdapter): void }
function isUiAdapter(value: unknown): value is YoloUiAdapter {
if (!value || typeof value !== "object") return false;
const adapter = value as Partial;
return [adapter.read, adapter.toggle, adapter.observe, adapter.dispose].every(fn => typeof fn === "function");
}
/** emit is void (and listeners may be wrapped async); explicitly bound the callback wait. */
export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContext): Promise {
return new Promise(resolve => {
let settled = false;
const timer = setTimeout(() => { settled = true; resolve(undefined); }, DISCOVERY_TIMEOUT_MS);
const respond = (value: unknown) => {
if (!isUiAdapter(value)) return;
if (settled) { value.dispose(); return; }
settled = true; clearTimeout(timer); resolve(value);
};
try { pi.events.emit(HOST_UI_CHANNEL, { context, respond } satisfies HostUiRequest); }
catch { settled = true; clearTimeout(timer); resolve(undefined); }
});
}
export interface YoloSessionController {
active(context: ExtensionContext): Promise;
superActive(context: ExtensionContext): Promise;
mode(context: ExtensionContext, admission?: { toolCallId: string }): Promise;
setMode(mode: PermissionMode, context: ExtensionContext): Promise;
defaultMode(): PermissionMode;
saveDefault(mode: PermissionMode, context: ExtensionContext): Promise;
epoch(): number;
humanAction(action: HumanAction, context: ExtensionContext, current?: () => boolean): Promise;
reset(context: ExtensionContext): void;
}
interface ModeIdentity {
manager: ExtensionContext["sessionManager"];
id: string;
workspace: string;
}
/** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
options: { onChange?: () => void; resolveIdentity?: typeof resolveCanonicalGitRepositoryIdentity } = {}): YoloSessionController {
const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
let generation = 0;
let ownerGeneration = 0;
let liveMode: PermissionMode = "normal";
let binding: ModeIdentity | undefined;
let interactiveGrant = false;
// A same-session revocation must not replay the global elevated preference.
const seen = new WeakMap