From d3264cccde792e827fad326cde375c4051f6e21b Mon Sep 17 00:00:00 2001 From: rekt Date: Wed, 5 Aug 2026 17:38:29 +0000 Subject: [PATCH 1/2] fix: satisfy Ponytail lint and type checks --- plugins/ponytail.ts | 238 +++++++++++++++++++++------------- tests/ponytail-plugin.test.ts | 145 +++++++++++++-------- 2 files changed, 234 insertions(+), 149 deletions(-) diff --git a/plugins/ponytail.ts b/plugins/ponytail.ts index d3e4a42..40a467b 100644 --- a/plugins/ponytail.ts +++ b/plugins/ponytail.ts @@ -1,3 +1,5 @@ +// Oxlint: This distributable plugin intentionally embeds Ponytail's instructions and uses Amp's async APIs. +// oxlint-disable eslint/max-lines, eslint/max-lines-per-function, eslint/max-statements, oxc/no-async-await import type { PluginAPI, PluginCommandContext, ThreadID } from "@ampcode/plugin"; export const description = @@ -15,9 +17,12 @@ export type PonytailCommand = const DEFAULT_MODE: PonytailMode = "full"; const DEFAULT_MODE_KEY = "ponytail.defaultMode"; const PONYTAIL_URL = "https://github.com/dietrichgebert/ponytail"; +const PONYTAIL_COMMAND_PATTERN = /^(?:[/@$])?ponytail(?::ponytail)?(?:\s+(?.*))?$/u; +const TABLE_MODE_PATTERN = /^\|\s*\*\*(?.+?)\*\*\s*\|/u; +const EXAMPLE_MODE_PATTERN = /^-\s*(?[^:]+):\s*"/u; -// Adapted from Ponytail's canonical skills/ponytail/SKILL.md. Keeping the -// complete body here makes the installed, single-file Amp plugin work offline. +// Adapted from Ponytail's canonical skills/ponytail/SKILL.md. +// Keeping the complete body here makes the installed, single-file Amp plugin work offline. const PONYTAIL_SKILL_BODY = `# Ponytail You are a lazy senior developer. Lazy means efficient, not careless. You have @@ -120,102 +125,132 @@ changed or session end. The shortest path to done is the right path.`; -export function normalizeMode(value: unknown): PonytailMode | null { - if (typeof value !== "string") return null; +export const normalizeMode = (value: unknown): PonytailMode | undefined => { + if (typeof value !== "string") { + return; + } const normalized = value.trim().toLowerCase(); - return PONYTAIL_MODES.includes(normalized as PonytailMode) ? (normalized as PonytailMode) : null; -} + if (!PONYTAIL_MODES.includes(normalized as PonytailMode)) { + return; + } + + return normalized as PonytailMode; +}; -export function isDeactivationCommand(value: unknown): boolean { +export const isDeactivationCommand = (value: unknown): boolean => { const normalized = String(value ?? "") .trim() .toLowerCase() - .replace(/[.!?\s]+$/, ""); + .replace(/[.!?\s]+$/u, ""); return normalized === "stop ponytail" || normalized === "normal mode"; -} +}; -export function parsePonytailCommand(value: unknown): PonytailCommand | null { +export const parsePonytailCommand = (value: unknown): PonytailCommand | undefined => { if (isDeactivationCommand(value)) { - return { type: "set-mode", mode: "off" }; + return { mode: "off", type: "set-mode" }; } const command = String(value ?? "") .trim() .toLowerCase() - .match(/^(?:[/@$])?ponytail(?::ponytail)?(?:\s+(.*))?$/); + .match(PONYTAIL_COMMAND_PATTERN); + if (!command || !command.groups) { + return; + } - if (!command) return null; + const { arguments: argumentText } = command.groups; + let argumentsValue = ""; + if (argumentText) { + argumentsValue = argumentText.trim(); + } + if (!argumentsValue || argumentsValue === "status") { + return { type: "status" }; + } - const args = command[1]?.trim(); - if (!args || args === "status") return { type: "status" }; + const [primary, secondary, extra] = argumentsValue.split(/\s+/u); + if (primary === "default") { + if (!extra) { + const mode = normalizeMode(secondary); + if (mode) { + return { mode, type: "set-default" }; + } + } - const parts = args.split(/\s+/); - if (parts[0] === "default") { - const mode = parts.length === 2 ? normalizeMode(parts[1]) : null; - return mode ? { type: "set-default", mode } : { type: "invalid" }; + return { type: "invalid" }; + } + + if (!secondary) { + const mode = normalizeMode(primary); + if (mode) { + return { mode, type: "set-mode" }; + } } - const mode = parts.length === 1 ? normalizeMode(parts[0]) : null; - return mode ? { type: "set-mode", mode } : { type: "invalid" }; -} + return { type: "invalid" }; +}; -export function filterSkillBodyForMode(body: string, mode: PonytailMode): string { - return body - .split(/\r?\n/) +const modeFromMatch = (match: RegExpMatchArray | null): PonytailMode | undefined => { + if (!match || !match.groups) { + return; + } + return normalizeMode(match.groups.mode); +}; + +export const filterSkillBodyForMode = (body: string, mode: PonytailMode): string => + body + .split(/\r?\n/u) .filter((line) => { - const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/); - if (tableLabel) { - const labelMode = normalizeMode(tableLabel[1]); - if (labelMode && labelMode !== "off") return labelMode === mode; + const tableMode = modeFromMatch(line.match(TABLE_MODE_PATTERN)); + if (tableMode && tableMode !== "off") { + return tableMode === mode; } - const exampleLabel = line.match(/^-\s*([^:]+):\s*"/); - if (exampleLabel) { - const labelMode = normalizeMode(exampleLabel[1]); - if (labelMode && labelMode !== "off") return labelMode === mode; + const exampleMode = modeFromMatch(line.match(EXAMPLE_MODE_PATTERN)); + if (exampleMode && exampleMode !== "off") { + return exampleMode === mode; } return true; }) .join("\n"); -} -export function getPonytailInstructions(mode: PonytailMode): string { - if (mode === "off") return ""; +export const getPonytailInstructions = (mode: PonytailMode): string => { + if (mode === "off") { + return ""; + } return `PONYTAIL MODE ACTIVE — level: ${mode}\n\n${filterSkillBodyForMode(PONYTAIL_SKILL_BODY, mode)}`; -} +}; -function effectiveDefaultMode(config: Record): PonytailMode { - return ( - normalizeMode(process.env.PONYTAIL_DEFAULT_MODE) ?? - normalizeMode(config[DEFAULT_MODE_KEY]) ?? - DEFAULT_MODE - ); -} +const effectiveDefaultMode = (config: Record): PonytailMode => + normalizeMode(process.env.PONYTAIL_DEFAULT_MODE) ?? + normalizeMode(config[DEFAULT_MODE_KEY]) ?? + DEFAULT_MODE; -function requireThread(ctx: PluginCommandContext): ctx is PluginCommandContext & { +const requireThread = ( + ctx: PluginCommandContext, +): ctx is PluginCommandContext & { thread: NonNullable; -} { - return Boolean(ctx.thread); -} +} => Boolean(ctx.thread); -export default function ponytailPlugin(amp: PluginAPI) { +const ponytailPlugin = (amp: PluginAPI): void => { const threadModes = new Map(); let configuredDefaultMode: PonytailMode = normalizeMode(process.env.PONYTAIL_DEFAULT_MODE) ?? DEFAULT_MODE; let receivedConfigurationUpdate = false; - const applyConfiguration = (config: Record) => { + const applyConfiguration = (config: Record): void => { configuredDefaultMode = effectiveDefaultMode(config); }; const loadConfiguration = amp.configuration .get() .then((config) => { - if (!receivedConfigurationUpdate) applyConfiguration(config); + if (!receivedConfigurationUpdate) { + applyConfiguration(config); + } }) .catch((error: unknown) => { amp.logger.log("Unable to read Ponytail configuration; using the fallback default.", error); @@ -229,24 +264,27 @@ export default function ponytailPlugin(amp: PluginAPI) { configurationSubscription.unsubscribe(); }); - const getThreadMode = async (threadID: ThreadID) => { + const getThreadMode = async (threadID: ThreadID): Promise => { await loadConfiguration; - if (!threadModes.has(threadID)) { - threadModes.set(threadID, configuredDefaultMode); + const currentMode = threadModes.get(threadID); + if (currentMode) { + return currentMode; } - return threadModes.get(threadID) as PonytailMode; + + threadModes.set(threadID, configuredDefaultMode); + return configuredDefaultMode; }; - const setThreadMode = (threadID: ThreadID, mode: PonytailMode) => { + const setThreadMode = (threadID: ThreadID, mode: PonytailMode): void => { threadModes.set(threadID, mode); }; - const setDefaultMode = async (mode: PonytailMode) => { + const setDefaultMode = async (mode: PonytailMode): Promise => { await amp.configuration.update({ [DEFAULT_MODE_KEY]: mode }, "global"); configuredDefaultMode = normalizeMode(process.env.PONYTAIL_DEFAULT_MODE) ?? mode; }; - const notify = async (message: string, ui: PluginCommandContext["ui"]) => { + const notify = async (message: string, ui: PluginCommandContext["ui"]): Promise => { try { await ui.notify(message); } catch (error) { @@ -265,37 +303,44 @@ export default function ponytailPlugin(amp: PluginAPI) { const command = parsePonytailCommand(event.message); let commandResult = ""; - if (command?.type === "set-mode") { - currentMode = command.mode; - setThreadMode(event.thread.id, currentMode); - commandResult = `Ponytail mode changed to ${currentMode} for this thread.`; - } else if (command?.type === "set-default") { - await setDefaultMode(command.mode); - commandResult = - configuredDefaultMode === command.mode - ? `Default Ponytail mode set to ${command.mode}. The current thread remains ${currentMode}.` - : `Saved default ${command.mode}, but PONYTAIL_DEFAULT_MODE keeps the effective default at ${configuredDefaultMode}. The current thread remains ${currentMode}.`; - } else if (command?.type === "status") { - commandResult = `Ponytail status: current ${currentMode}; default ${configuredDefaultMode}.`; - } else if (command?.type === "invalid") { - commandResult = - "Unknown Ponytail mode. Use off, lite, full, ultra, status, or default ."; + if (command) { + if (command.type === "set-mode") { + currentMode = command.mode; + setThreadMode(event.thread.id, currentMode); + commandResult = `Ponytail mode changed to ${currentMode} for this thread.`; + } else if (command.type === "set-default") { + await setDefaultMode(command.mode); + commandResult = `Saved default ${command.mode}, but PONYTAIL_DEFAULT_MODE keeps the effective default at ${configuredDefaultMode}. The current thread remains ${currentMode}.`; + if (configuredDefaultMode === command.mode) { + commandResult = `Default Ponytail mode set to ${command.mode}. The current thread remains ${currentMode}.`; + } + } else if (command.type === "status") { + commandResult = `Ponytail status: current ${currentMode}; default ${configuredDefaultMode}.`; + } else { + commandResult = + "Unknown Ponytail mode. Use off, lite, full, ultra, status, or default ."; + } } - if (commandResult) await notify(commandResult, ctx.ui); + if (commandResult) { + await notify(commandResult, ctx.ui); + } const instructions = getPonytailInstructions(currentMode); const content = [commandResult, instructions].filter(Boolean).join("\n\n"); - return content ? { message: { content } } : undefined; + if (content) { + return { message: { content } }; + } + return {}; }); amp.registerCommand( "ponytail-mode", { - title: "Change mode", category: "ponytail", description: "Set Ponytail intensity for the active thread.", + title: "Change mode", }, async (ctx) => { if (!requireThread(ctx)) { @@ -305,14 +350,16 @@ export default function ponytailPlugin(amp: PluginAPI) { const currentMode = await getThreadMode(ctx.thread.id); const selected = await ctx.ui.select({ - title: "Ponytail mode", + initialValue: currentMode, message: "lite suggests the lazier option; full enforces the ladder; ultra challenges unnecessary work.", options: [...PONYTAIL_MODES], - initialValue: currentMode, + title: "Ponytail mode", }); const mode = normalizeMode(selected); - if (!mode) return; + if (!mode) { + return; + } setThreadMode(ctx.thread.id, mode); await ctx.ui.notify(`Ponytail mode set to ${mode} for this thread.`); @@ -322,13 +369,16 @@ export default function ponytailPlugin(amp: PluginAPI) { amp.registerCommand( "ponytail-status", { - title: "Show status", category: "ponytail", description: "Show the active thread mode and configured default.", + title: "Show status", }, async (ctx) => { await loadConfiguration; - const current = ctx.thread ? await getThreadMode(ctx.thread.id) : "(no active thread)"; + let current: PonytailMode | "(no active thread)" = "(no active thread)"; + if (ctx.thread) { + current = await getThreadMode(ctx.thread.id); + } await ctx.ui.notify(`Ponytail: current ${current}; default ${configuredDefaultMode}.`); }, ); @@ -336,43 +386,47 @@ export default function ponytailPlugin(amp: PluginAPI) { amp.registerCommand( "ponytail-default-mode", { - title: "Set default mode", category: "ponytail", description: "Set the Ponytail mode used by new Amp threads.", + title: "Set default mode", }, async (ctx) => { await loadConfiguration; const selected = await ctx.ui.select({ - title: "Default Ponytail mode", + initialValue: configuredDefaultMode, message: "This applies to new threads and is saved in Amp settings.", options: [...PONYTAIL_MODES], - initialValue: configuredDefaultMode, + title: "Default Ponytail mode", }); const mode = normalizeMode(selected); - if (!mode) return; + if (!mode) { + return; + } await setDefaultMode(mode); const overridden = configuredDefaultMode !== mode; - await ctx.ui.notify( - overridden - ? `Saved ${mode}, but PONYTAIL_DEFAULT_MODE keeps the effective default at ${configuredDefaultMode}.` - : `Default Ponytail mode set to ${mode}.`, - ); + let message = `Default Ponytail mode set to ${mode}.`; + if (overridden) { + message = `Saved ${mode}, but PONYTAIL_DEFAULT_MODE keeps the effective default at ${configuredDefaultMode}.`; + } + await ctx.ui.notify(message); }, ); amp.registerCommand( "ponytail-help", { - title: "Open documentation", category: "ponytail", description: "Open the Ponytail documentation on GitHub.", + title: "Open documentation", }, async (ctx) => { await ctx.system.open(PONYTAIL_URL); }, ); -} +}; + +export default ponytailPlugin; /* MIT License diff --git a/tests/ponytail-plugin.test.ts b/tests/ponytail-plugin.test.ts index aa08750..a4c85b7 100644 --- a/tests/ponytail-plugin.test.ts +++ b/tests/ponytail-plugin.test.ts @@ -1,34 +1,49 @@ +// Oxlint: Async callbacks and intentionally minimal no-op methods model Amp's Plugin API. +// oxlint-disable eslint/max-lines-per-function, eslint/no-empty-function, eslint/require-await, oxc/no-async-await, typescript/explicit-function-return-type import { describe, expect, test } from "bun:test"; - import ponytailPlugin, { filterSkillBodyForMode, getPonytailInstructions, isDeactivationCommand, parsePonytailCommand, } from "../plugins/ponytail"; +import type { PluginAPI } from "@ampcode/plugin"; + +type Handler = (...arguments_: unknown[]) => unknown; -type Handler = (event: any, context?: any) => any; +interface Harness { + commands: Map; + handlers: Map; + notifications: string[]; + updateConfiguration: (config: Record) => void; + updates: [Record, string | undefined][]; +} -function createAmp(defaultMode?: string) { +const createAmp = (defaultMode?: string): Harness => { const handlers = new Map(); const commands = new Map(); const notifications: string[] = []; - const updates: Array<[Record, string | undefined]> = []; - let configurationObserver: Handler | undefined; + const updates: [Record, string | undefined][] = []; + let configurationObserver: Handler | false = false; const amp = { configuration: { - get: async () => (defaultMode ? { "ponytail.defaultMode": defaultMode } : {}), - update: async (value: Record, target?: string) => { - updates.push([value, target]); + get: async () => { + if (defaultMode) { + return { "ponytail.defaultMode": defaultMode }; + } + return {}; }, subscribe(observer: Handler) { configurationObserver = observer; return { unsubscribe() {} }; }, + update: async (value: Record, target?: string) => { + updates.push([value, target]); + }, }, - logger: { log() {} }, helpers: { isPluginUINotAvailableError: () => false }, + logger: { log() {} }, on(event: string, handler: Handler) { handlers.set(event, handler); return { unsubscribe() {} }; @@ -38,60 +53,76 @@ function createAmp(defaultMode?: string) { }, registerCommand(id: string, _options: unknown, handler: Handler) { commands.set(id, handler); - return { unsubscribe() {}, setAvailability() {} }; + return { setAvailability() {}, unsubscribe() {} }; }, }; - ponytailPlugin(amp as any); + ponytailPlugin(amp as unknown as PluginAPI); return { - handlers, commands, + handlers, notifications, - updates, updateConfiguration(config: Record) { - configurationObserver?.(config); + if (configurationObserver) { + configurationObserver(config); + } }, + updates, }; -} +}; -function commandContext(notifications: string[], selected?: string, threadID = "T-test") { - return { - thread: { id: threadID }, - ui: { - notify: async (message: string) => { - notifications.push(message); - }, - select: async () => selected, +const commandContext = (notifications: string[], selected?: string, threadID = "T-test") => ({ + system: { open: async () => {} }, + thread: { id: threadID }, + ui: { + notify: async (message: string) => { + notifications.push(message); }, - system: { open: async () => {} }, - }; -} + select: async () => selected, + }, +}); -function eventContext(notifications: string[]) { - return { - ui: { - notify: async (message: string) => { - notifications.push(message); - }, +const eventContext = (notifications: string[]) => ({ + ui: { + notify: async (message: string) => { + notifications.push(message); }, - }; -} + }, +}); + +const getHandler = (handlers: Map, name: string): Handler => { + const handler = handlers.get(name); + if (!handler) { + throw new Error(`Missing ${name} handler`); + } + return handler; +}; + +const messageContent = (result: unknown): string => { + const candidate = result as { message?: { content?: unknown } }; + const content = candidate.message && candidate.message.content; + if (typeof content !== "string") { + throw new TypeError("Expected an agent-start message result"); + } + return content; +}; describe("Ponytail command parsing", () => { test("accepts Amp and upstream command forms", () => { expect(parsePonytailCommand("/ponytail lite")).toEqual({ - type: "set-mode", mode: "lite", + type: "set-mode", }); expect(parsePonytailCommand("@ponytail default ultra")).toEqual({ - type: "set-default", mode: "ultra", + type: "set-default", }); expect(parsePonytailCommand("/ponytail:ponytail status")).toEqual({ type: "status", }); - expect(parsePonytailCommand("please use ponytail")).toBeNull(); + expect(parsePonytailCommand("please use ponytail")).toBeUndefined(); + expect(parsePonytailCommand("ponytail seems too strict")).toEqual({ type: "invalid" }); }); test("only deactivates for standalone commands", () => { @@ -129,98 +160,98 @@ describe("Ponytail instructions", () => { describe("Amp integration", () => { test("injects the configured default on every turn", async () => { const { handlers, notifications } = createAmp("lite"); - const agentStart = handlers.get("agent.start")!; + const agentStart = getHandler(handlers, "agent.start"); const result = await agentStart( { - thread: { id: "T-one" }, message: "Fix the bug", + thread: { id: "T-one" }, }, eventContext(notifications), ); - expect(result.message.content).toContain("PONYTAIL MODE ACTIVE — level: lite"); - expect(result.message.content).not.toContain("| **full** |"); + expect(messageContent(result)).toContain("PONYTAIL MODE ACTIVE — level: lite"); + expect(messageContent(result)).not.toContain("| **full** |"); }); test("keeps mode changes isolated per thread", async () => { const { handlers, notifications } = createAmp(); - const agentStart = handlers.get("agent.start")!; + const agentStart = getHandler(handlers, "agent.start"); const context = eventContext(notifications); await agentStart( { - thread: { id: "T-one" }, message: "/ponytail ultra", + thread: { id: "T-one" }, }, context, ); const first = await agentStart( { - thread: { id: "T-one" }, message: "Implement it", + thread: { id: "T-one" }, }, context, ); const second = await agentStart( { - thread: { id: "T-two" }, message: "Implement it", + thread: { id: "T-two" }, }, context, ); - expect(first.message.content).toContain("level: ultra"); - expect(second.message.content).toContain("level: full"); + expect(messageContent(first)).toContain("level: ultra"); + expect(messageContent(second)).toContain("level: full"); expect(notifications).toContain("Ponytail mode changed to ultra for this thread."); }); test("uses live default changes for new threads", async () => { const { handlers, notifications, updateConfiguration } = createAmp(); - const agentStart = handlers.get("agent.start")!; + const agentStart = getHandler(handlers, "agent.start"); updateConfiguration({ "ponytail.defaultMode": "lite" }); const result = await agentStart( { - thread: { id: "T-new" }, message: "Implement it", + thread: { id: "T-new" }, }, eventContext(notifications), ); - expect(result.message.content).toContain("level: lite"); + expect(messageContent(result)).toContain("level: lite"); }); test("stops injecting after exact deactivation", async () => { const { handlers, notifications } = createAmp(); - const agentStart = handlers.get("agent.start")!; + const agentStart = getHandler(handlers, "agent.start"); const context = eventContext(notifications); const stopped = await agentStart( { - thread: { id: "T-one" }, message: "stop ponytail", + thread: { id: "T-one" }, }, context, ); const next = await agentStart( { - thread: { id: "T-one" }, message: "Keep working", + thread: { id: "T-one" }, }, context, ); - expect(stopped.message.content).toBe("Ponytail mode changed to off for this thread."); + expect(messageContent(stopped)).toBe("Ponytail mode changed to off for this thread."); expect(notifications).toContain("Ponytail mode changed to off for this thread."); - expect(next).toBeUndefined(); + expect(next).toEqual({}); }); test("registers native mode and default controls", async () => { const { commands, notifications, updates } = createAmp(); - await commands.get("ponytail-mode")!(commandContext(notifications, "ultra")); - await commands.get("ponytail-default-mode")!(commandContext(notifications, "lite")); + await getHandler(commands, "ponytail-mode")(commandContext(notifications, "ultra")); + await getHandler(commands, "ponytail-default-mode")(commandContext(notifications, "lite")); expect(notifications).toContain("Ponytail mode set to ultra for this thread."); expect(updates).toEqual([[{ "ponytail.defaultMode": "lite" }, "global"]]); From 88f636b2faa97bfa32f896c2de6d50a009f10579 Mon Sep 17 00:00:00 2001 From: o-az <23618431+o-az@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:15:16 -0700 Subject: [PATCH 2/2] save Signed-off-by: o-az <23618431+o-az@users.noreply.github.com> --- mise.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mise.toml b/mise.toml index 0e3ee20..b21e962 100644 --- a/mise.toml +++ b/mise.toml @@ -37,10 +37,14 @@ betterleaks = "latest" [hooks] postinstall = "hk install --mise" +[tasks] +description = "run tests" +run = "bun test" + [tasks.check] description = "format & lint files" run = "hk fix --all && hk check --all" [tasks.ci] description = "run CI tasks" -run = "hk fix --all && hk check --all" +run = "hk fix --all && hk check --all && bun test"