* feat: add Grok Build adapter (revive #561 on current main)
Thin Grok packaging under .grok-plugin/ with root plugin.json path
overrides (hooks + MCP). SessionStart/UserPromptSubmit/SubagentStart
reuse shared hooks/ponytail-*.js; mode state under GROK_PLUGIN_DATA.
Rebases the approach from #561 onto current main: keep Qoder detection
and output paths, add isGrok, export getGrokPluginDataDir, drop bash-only
exec from Grok hooks, and document install/enable/uninstall on the
front-page README (en/es/ko) plus agent-portability.
Direct install works today:
grok plugin install DietrichGebert/ponytail --trust
Marketplace root source ("./") matches Claude; Grok's scanner still
rejects it (see xai-org/plugin-marketplace#123 class of bugs).
Co-authored-by: Vinícius Souza <souza.vinicius@bb.com.br>
* fix(grok): drop MCP, harden host detection and tests
Review feedback on #661:
- Remove MCP wiring (git install never installs ponytail-mcp deps; no
other host ships MCP; hooks+skills cover always-on)
- Drop static plugin-index.json (optional catalog fluff)
- Clear GROK_PLUGIN_* in hooks.test.js so host suites cannot leak
- Exclusive isGrok after Copilot/Codex; state falls back to ROOT not ~/.claude
- Tighten Qoder regression assert; structural checks for plugin.json/hooks
- List Grok Build among skill-capable hosts in README
* refactor(grok): DRY — reuse Claude/Codex hooks map
Second review pass for #661:
- Delete .grok-plugin/hooks.json (near-copy of claude-codex-hooks.json).
Root plugin.json points at the shared map; Grok sets CLAUDE_PLUGIN_ROOT.
- Drop getGrokPluginDataDir; inline GROK_PLUGIN_DATA || ROOT like other hosts.
- Grok uses Claude-compatible writeHookOutput (raw SessionStart, JSON
SubagentStart) instead of a separate raw-only branch.
- Slim .grok-plugin/marketplace.json to match .claude-plugin.
- Tests: shared-map assert, SubagentStart JSON under Grok, Qoder isolation.
* fix(grok): use native skill activation
* chore: drop unrelated Qoder formatting
---------
Co-authored-by: Vinícius Souza <souza.vinicius@bb.com.br>
VS Code Copilot never sets COPILOT_PLUGIN_DATA, only CLAUDE_PLUGIN_ROOT
(pointed at an .vscode/agent-plugins/... install path), so isCopilot was
false and ponytail assumed native Claude Code — emitting the statusline
setup nudge, which VS Code Copilot doesn't read. Also guard stateDir:
it was built from the same unset COPILOT_PLUGIN_DATA whenever isCopilot
resolved via this new fallback, so fall back to getClaudeDir() there too.
commandWindows is not part of the supported hooks schema for the Claude.ai
plugin marketplace validator, which rejects it as an unknown field. The
shared `command` field already runs cross-platform: Claude Code expands
${CLAUDE_PLUGIN_ROOT} before the shell sees it, VS Code Copilot ignores
commandWindows and runs `command` through PowerShell on Windows, and `node`
is natively available in both bash and PowerShell.
Update the regression test to assert commandWindows is absent and remove
the now-unused CMD_VAR_SYNTAX guard. Fix comment placeholder syntax
({{CLAUDE_PLUGIN_ROOT}} → ${CLAUDE_PLUGIN_ROOT}) and narrow test name.
Writes a flag file the first time the STATUSLINE SETUP NEEDED nudge fires, and skips it on later sessions so it stops nagging every start. Includes a regression test that a second session stays silent.
review is a session-only mode set by /ponytail-review, never a persistent
default. The Claude/Codex mode-tracker already enforces this (#377), but two
paths did not:
- pi-extension /ponytail default review was accepted, persisting review as the
default, so the next session started in review with a blank status-bar icon.
- getDefaultMode accepted review from PONYTAIL_DEFAULT_MODE or a stale config.
Route default validation through RUNTIME_MODES (off/lite/full/ultra) in
getDefaultMode, writeDefaultMode, and the pi-extension default command. review
stays valid as a session mode. Adds regression tests for all three paths.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The three shared `command` fields in claude-codex-hooks.json used
`exec node "..."`. `exec` is a bash/zsh builtin with no PowerShell
equivalent. Some hosts run the `command` field through PowerShell on
Windows regardless of the PowerShell-native `commandWindows` field:
VS Code Copilot always does (it never reads `commandWindows`), and
native Claude Code launched from Git Bash was reported doing the same
(#569). PowerShell then fails on the first token with
CommandNotFoundException, so every SessionStart / SubagentStart /
UserPromptSubmit hook errors out on Windows.
Plain `node "..."` runs natively in both bash and PowerShell. Dropping
`exec` only gives up the POSIX optimization of replacing the wrapper
shell with node; the wrapper-process pileup that #461 added `exec` to
avoid is already handled by each hook's stdin self-exit guard
(#443/#477), so there is no regression on Codex/zsh.
Updated the hooks-windows regression test to assert the shell-agnostic
form instead of requiring `exec`.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: Codex CLI SessionStart additionalContext at top level (#505)
Codex CLI expects additionalContext at the top level of the hook output,
not nested under hookSpecificOutput. Flattened the Codex branch of
writeHookOutput so the injected instructions actually reach the agent.
Fixes#505
* Merge main; move #99 Codex @ponytail report assertion to top-level additionalContext (#505)
---------
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
Qwen models reject multiple system messages — push() added a second
entry. Append to the last existing entry instead; fall back to push
when the array is empty.
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
* feat(hooks): opt-in agent-type scoping for SubagentStart injection (#506)
The SubagentStart hook injected the ~1,300-token persona into every
subagent, including read-only search agents where it is dead weight.
Consumers could not narrow a plugin-provided hook from their own
settings, so the scoping has to live here.
Add an opt-in allowlist: PONYTAIL_SUBAGENT_AGENTS (comma-separated env
var) or a subagentAgents array in config.json, matching getDefaultMode
precedence. The hook now reads agent_type from stdin and skips
injection only on a positive mismatch; unconfigured or missing
agent_type keeps the inject-everywhere behavior from issue #252. Stdin
reading follows the never-hang contract from issue #443.
Closes#506
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: regex matcher, stdin-free default path, exit on skip
Rework the SubagentStart scoping per review on #522:
- Replace the comma-separated PONYTAIL_SUBAGENT_AGENTS allowlist with a
regex PONYTAIL_SUBAGENT_MATCHER, matching the reviewed API on #507 and
the PONYTAIL_DEFAULT_MODE naming convention.
- Default path (no matcher) injects synchronously and never reads stdin,
fixing the #443-class Windows regression where the persona was delayed
until a 1s timeout.
- On a definite agent_type mismatch the hook now process.exit(0)s
immediately instead of lingering until the fallback timer.
- Drop the config-file (subagentAgents) source, so no config read on the
subagent hot path; env-only keeps it lazy (YAGNI).
- Revert the unrelated tests/hooks-windows.test.js change; the scoped
path mirrors the mode-tracker never-hang contract already tested there.
Fail open on unparseable input, missing agent_type, invalid regex, or
timeout so scoping never silently drops the persona (#252).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
OpenCode's legacy plugin loader iterates every exported function in a
plugin module and tries to invoke each one with the plugin context object.
parseCommandFile was exported alongside the default plugin function, so
the loader called it with an object instead of a file path and crashed
with 'path must be a string or a file descriptor' before any hook ran,
which aborted the whole plugin load.
Move the frontmatter parser to its own .cjs sibling; the plugin module
now exposes exactly one top-level export (the default plugin function).
The plugin requires the parser via createRequire, and the existing
parseCommandFile tests now require the sibling directly.
Co-authored-by: ayushere <ayushere@users.noreply.github.com>
* fix: report active level on bare /ponytail instead of resetting to default
* remove . ponytail-active
---------
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
Claude Code had no way to change the persistent default from within a session:
plain /ponytail switches are session-scoped by design ('sticks until session
end'), and only env/config could set the default. So /ponytail off never
survived a restart, which is what #329 hit.
Add /ponytail default <mode> to the mode-tracker hook, matching pi's existing
command: it writes defaultMode to config (survives restart) and does NOT touch
the current session flag, keeping plain switches transient. review is rejected
as a default (#377); only off/lite/full/ultra are accepted. Test covers persist,
transient-switch-does-not-persist, and review-rejected.
Closes#329
The activation hook detects a missing statusline in getClaudeDir()/settings.json
(CLAUDE_CONFIG_DIR-aware) but the nudge text told the user and the agent to add
the statusLine config to ~/.claude/settings.json. With CLAUDE_CONFIG_DIR set,
that is the wrong file: Claude Code never reads it and the badge never turns on.
Point the nudge at the settingsPath the hook already computed, in both the
shell-safe and manual branches.
Fixes#250
writeDefaultMode was writing a fresh { defaultMode } object, silently
dropping any other fields in config.json. Now it reads the existing config,
updates only defaultMode, and writes it back preserving all other fields.
Fixes#490
tests/hermes-plugin.test.js spawned python3 with no fallback. On Windows
python3 is the Store app-execution-alias stub that exits non-zero even when
Python is installed. Reuse the probe/fallback pattern from
benchmarks/correctness.js: try python3, fall back to python, cache the result.
Fixes#498
The README tells users to run 'node scripts/uninstall.js' to clean up state
left outside the plugin folder, but the package files list didn't include
scripts/, so npm-installed users didn't have the file. Add scripts/uninstall.js
to files (its only dependency, hooks/, already ships). Guard it with a test
that asserts the files entry and that the file exists.
Co-authored-by: Anmolnoor <52135717+Anmolnoor@users.noreply.github.com>
If settings.json contained invalid JSON, JSON.parse threw a SyntaxError, which
has no .code, so the catch rethrew it and crashed the script — after the mode
flag and config file were already removed, leaving cleanup half-done. Handle
SyntaxError explicitly: warn that the statusLine entry couldn't be removed and
leave the file untouched, since invalid JSON can't be safely edited. Adds a
regression test that a malformed settings.json exits 0, warns, and is left
byte-for-byte intact.
Closes#434
Co-authored-by: isaukywhite <50426537+isaukywhite@users.noreply.github.com>
uninstall deleted the whole statusLine key whenever the command contained
'ponytail-statusline'. For a user-combined statusline (e.g. caveman && ponytail)
that destroyed the other plugin's statusline too. Only remove the statusLine
when ponytail owns the whole command; if it's combined with other segments,
leave it and tell the user to strip ponytail's part by hand. Never leaves a
malformed husk. Adds a regression test for the combined case.
Closes#374
Co-authored-by: nanaubusiness <195150264+nanaubusiness@users.noreply.github.com>
On Windows, Claude Code runs the UserPromptSubmit hook through a PowerShell
if {} wrapper that can swallow the piped prompt JSON, so stdin 'end' never
fires and ponytail-mode-tracker.js blocks forever, freezing the session.
Make the hook non-blocking: extract the handler into finish() guarded by a
done flag, and add a 1s unref()'d fallback timeout plus the existing stdin
error handler that both call finish() then exit 0. unref() means zero added
latency on the normal path where 'end' fires first. Adds a regression test
that spawns the hook with a never-closing stdin pipe and asserts self-exit.
Reconciles #453 onto main after #227 (stdin error handler) and #474 (exec
node hooks) landed.
Closes#443
Co-authored-by: Hasnan42 <154899774+Hasnan42@users.noreply.github.com>
Previously only checked 4 of 6. ponytail-gain.toml and
ponytail-help.toml are now validated.
Closes#381
Co-authored-by: Admin <admin@Admins-MacBook-Pro.local>
* feat: inject ponytail ruleset into subagents via SubagentStart hook
SessionStart additionalContext is parent-thread only, so every Task-spawned
agent ran ponytail-unaware. Add a SubagentStart hook that injects the active
ruleset into each subagent, reusing getPonytailInstructions. Native Claude
needs the hookSpecificOutput JSON form (not raw stdout), so writeHookOutput
grows a SubagentStart branch; readMode exposes the live flag.
Workflow- and team-spawned coverage is undocumented upstream; verify in a
fresh session once installed.
Closes#252
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1dMag33yz1Kf1jS24Aira
* fix: address QA round 1 — make hook tests hermetic + cover Codex SubagentStart
QA round 1 (panel + deepseek-v4-pro) found two real test issues:
- The new subagent block (and the pre-existing claudeEnv block) used a no-op
`delete env.PLUGIN_DATA`; run() spreads process.env, so a PLUGIN_DATA /
COPILOT_PLUGIN_DATA leaked from the shell would steer writeHookOutput into the
codex/copilot branch and silently mis-fire the native-Claude assertions.
Fixed at the source: neutralize both vars once at the top, like CLAUDE_CONFIG_DIR.
- The Codex SubagentStart branch (claude-codex-hooks.json is shared by both plugin
manifests) had zero coverage. Added a codex-path assertion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1dMag33yz1Kf1jS24Aira
* fix: use PowerShell-safe command form for SubagentStart hook
Match the post-#265 'node ...; exit 0' form used by the sibling hooks. The old 'command -v node ... || exit 0' form fails tests/hooks-windows.test.js (POSIX-guard and non-blocking asserts) once this branch merges onto current main.
---------
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
* feat: register slash commands from .opencode/command/*.md
Add parseCommandFile() to read frontmatter-described markdown files
and wire them into opencode's config.command during init.
Extend the config hook to scan .opencode/command/ and register each
.md file as a named slash command.
Update the plugin doc comment to reflect the npm install path
(opencode-ponytail) vs the old relative path.
* chore: rename package to opencode-ponytail
Align package name with npm convention for opencode plugins.
Update keywords to include opencode-plugin and opencode tags.
Fix description back to original correct wording (grammatical
regression introduced during editing).
* chore: add npm metadata and publish workflow
Add author, homepage, repository, bugs, main, exports, files, and
publishConfig fields to package.json for npm publishing.
Add .github/workflows/publish.yml to auto-publish to npm on version
tags (v*) with provenance.
* docs: add npm plugin install for opencode-ponytail
---------
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
The SessionStart nudge built a statusLine command by interpolating the
plugin's __dirname path into a double-quoted shell string. A clone path
containing shell metacharacters (quotes, &, $, backtick, ;) could break
out when the suggested command later runs via the statusline shell.
Low severity in practice: the path is the install location, so triggering
it requires installing into a maliciously-named directory, i.e. the
attacker already controls the filesystem. Hardening it anyway.
Gate the snippet behind isShellSafe() (allowlist of ordinary path chars,
allowing : \ / for normal Windows and POSIX paths). Unsafe paths fall
back to a manual-setup instruction instead of an embeddable command. An
allowlist beats a per-shell escaper, which is its own edge-case bug farm.
Refs #200
#213 guarded cleanup with a flag + named function + process.once. But
fs.rmSync with force:true already no-ops on a missing path, so the guard
and the explicit end-of-file call are unnecessary. Collapse to a single
process.on('exit') handler.
Refs #204
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The deactivation check matched the phrase anywhere in the prompt, so an
ordinary request like "add a normal mode toggle" silently turned ponytail
off for the rest of the session. Match the whole message instead (trimmed,
case-insensitive, trailing punctuation ignored) through a shared helper used
by both the Claude/Codex hook and the pi extension.
Fixes#161
Adds .openclaw/skills/ (ponytail + review/audit/debt/help) generated from the canonical skills/ (verbatim body, no drift), a generator script, and a drift test. Verified live: loads as Ready in OpenClaw 2026.6.6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ships the previously-missing /ponytail-help adapter files (commands/ponytail-help.toml, .opencode/command/ponytail-help.md) and adds tests/commands.test.js, a parity guard asserting every pi-registered command has both adapter files. Thanks @hooni0918.
* Add GitHub Copilot plugin and marketplace manifests for Ponytail
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Copilot hook adapters and plugin data runtime precedence
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document Copilot plugin install flow and instruction fallback mode
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix Copilot hooks for native output context and state-only mode tracking
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Copilot CLI namespaced command examples
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Collapse Copilot hooks into shared activate/mode-tracker
The Copilot hook files duplicated ponytail-activate.js and
ponytail-mode-tracker.js, differing only in output shape. Move that
difference into writeHookOutput (isCopilot branch) and point
copilot-hooks.json at the shared hooks. Deletes both forks (-73 lines).
Refs #1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Align Copilot manifest version to 4.4.0 with cross-manifest parity test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make Copilot and Codex host detection exclusive in runtime output routing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Copilot debt command validation with a pull request acceptance checklist
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Removed PR template
* Drop tautological copilot command-form test
The namespaced-form assertion built '/ponytail:ponytail-debt' from two
constants and compared it to itself — it tests string concatenation, not
wiring. The file-exists check above already catches a renamed manifest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: refine ruleset from a full-project field review
A reviewer ran ponytail across a 9-phase rewrite (protocol, PC app, simulator,
RPi daemon, ESP32 firmware) and flagged three gaps. All three land in SKILL.md
and propagate to AGENTS.md + the rule copies:
- Promote the one-runnable-check rule to a headline ("Lazy code without its
check is unfinished"), enforced as a check-rule-copies invariant.
- Hardware carve-out in "When NOT to be lazy": a real device is never the spec
ideal (clock drift, sensor offset), leave the calibration knob.
- Clarify the Output rule: explanation the user explicitly asked for is not
debt, only unrequested prose is.
Fallback instructions kept in sync. Rule-copy check + tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add a behavior gate proving the refinements actually fire
The refinements were verified as injected text, but injected != behavioral.
This adds a behavior eval that probes each refined rule on a task that should
trigger it:
- hardware -> does the output leave a calibration knob?
- explanation -> when a write-up is explicitly requested, is it given in full?
- onecheck -> is a runnable check left behind?
benchmarks/behavior.yaml runs the probes (baseline vs ponytail arm); the
grader benchmarks/behavior.js is proven by tests/behavior.test.js (8 cases,
RED/GREEN, no API key, runs in CI). Live-confirmed: the model under the
current ruleset passes all three gates, graded by the same grader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ponytail-activate.js and ponytail-runtime.js hardcoded ~/.claude for the
flag file and settings lookup, ignoring CLAUDE_CONFIG_DIR. Add a shared
getClaudeDir() to ponytail-config.js and use it in both. Regression test
added to hooks.test.js.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(benchmarks): add correctness assertion - proves less code is not broken code
The existing benchmark measures lines-of-code (loc.js) but never checks
whether the generated code actually works. This adds a functional
correctness gate (correctness.js) that extracts code from fenced blocks
and runs per-task checks:
- email validator: spawns Python, asserts accept/reject on 5 inputs
- debounce: spawns Node, asserts delayed execution + reset on re-call
- csv sum: spawns Python with a test CSV, asserts correct total (351)
- countdown (React): structural check (useState + useEffect + decrement)
- rate limiter (FastAPI): structural check (limit logic + framework usage)
12 unit tests (node:test) cover good/bad outputs for every task plus the
unknown-task edge case. Existing tests and rule-copy checks unaffected.
* fix: address review feedback
- csv check: use regex lookaround instead of substring match to prevent
false positives (e.g. 13510 containing '351')
- ratelimit: fix operator precedence in block finder by adding parens
around the || inside the !b.lang guard
- README: note that React/FastAPI checks are structural only, add
prerequisites section (Python 3, pandas, Node.js 18+)
- test: add regression test for csv substring false positive
* feat: add Gemini CLI support
Add a thin Gemini CLI extension adapter (closes#22). The manifest points
contextFileName at the existing AGENTS.md for always-on rules and reuses the
repo's commands/*.toml (/ponytail, /ponytail-review) and skills/, which Gemini
CLI auto-discovers — no rule text is duplicated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: read manifest inside cases for a clean RED
The manifest was parsed at module scope, so reverting the adapter crashed the
whole test file at load (ENOENT) and collapsed all four cases into one
unreadable failure. Read it inside each case via a helper that asserts existence
first, so a missing or malformed manifest surfaces as clear per-case assertion
failures instead of a stack trace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Windows the lifecycle hooks run via PowerShell, which does not expand
cmd.exe-style %CLAUDE_PLUGIN_ROOT%. The path was passed literally, so the hook
launcher could not find the script and both SessionStart and UserPromptSubmit
failed with exit code 1 (issue #19). Switch the two commandWindows entries to
$env:CLAUDE_PLUGIN_ROOT, keeping the working node + .js invocation.
Add a regression test that rejects cmd.exe %VAR% syntax in commandWindows and
asserts every hook command points at a script that actually ships in hooks/.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>