Adding support for Copilot Marketplace plugin (#47)

* 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>
This commit is contained in:
Max Felker II
2026-06-15 11:02:59 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Copilot
parent 1c420ad2f3
commit c1c80f3cc8
9 changed files with 193 additions and 19 deletions
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Smoke test for the Copilot plugin adapter: keep command wiring minimal and
// ensure the debt command is part of the shared command surface.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const REQUIRED_COMMAND_FILES = [
'ponytail.toml',
'ponytail-review.toml',
'ponytail-audit.toml',
'ponytail-debt.toml',
];
function readJSON(relPath) {
return JSON.parse(fs.readFileSync(path.join(root, relPath), 'utf8'));
}
test('copilot plugin command directory includes ponytail-debt', () => {
const manifest = readJSON('.github/plugin/plugin.json');
assert.equal(manifest.name, 'ponytail');
assert.equal(manifest.commands, 'commands/');
for (const file of REQUIRED_COMMAND_FILES) {
assert.ok(
fs.existsSync(path.join(root, manifest.commands, file)),
`missing command file: ${manifest.commands}${file}`,
);
}
});
+15 -3
View File
@@ -16,6 +16,12 @@ const MANIFEST = 'gemini-extension.json';
const EXTENSION_NAME = 'ponytail';
// Floating refs are a supply-chain footgun; the manifest version must be pinned.
const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
const VERSIONED_MANIFESTS = [
'gemini-extension.json',
'.claude-plugin/plugin.json',
'.codex-plugin/plugin.json',
'.github/plugin/plugin.json',
];
// Gemini auto-discovers these by directory; the manifest is only useful if they exist.
const REUSED_COMMANDS = ['commands/ponytail.toml', 'commands/ponytail-review.toml'];
const REUSED_SKILLS = ['skills/ponytail/SKILL.md'];
@@ -46,9 +52,15 @@ test('manifest names the ponytail extension with a pinned version', () => {
});
test('version stays aligned with the other plugin manifests', () => {
const manifest = loadManifest();
const claude = JSON.parse(read('.claude-plugin/plugin.json'));
assert.equal(manifest.version, claude.version);
const versions = VERSIONED_MANIFESTS.map((rel) => {
const manifest = JSON.parse(read(rel));
assert.match(manifest.version, PINNED_SEMVER, `${rel} version must be pinned semver`);
return manifest.version;
});
const [sharedVersion, ...rest] = versions;
for (const version of rest) {
assert.equal(version, sharedVersion);
}
});
test('contextFileName resolves to a file carrying the ponytail rules', () => {
+39
View File
@@ -99,5 +99,44 @@ assert.equal(
'flag must not land in ~/.claude when CLAUDE_CONFIG_DIR is set',
);
const copilotData = path.join(temp, 'copilot-data');
const codexData = path.join(temp, 'codex-data-shadow');
result = run('ponytail-activate.js', {
HOME: home,
USERPROFILE: home,
COPILOT_PLUGIN_DATA: copilotData,
PLUGIN_DATA: codexData,
PONYTAIL_DEFAULT_MODE: 'full',
});
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(copilotData, '.ponytail-active'), 'utf8'), 'full');
assert.equal(
fs.existsSync(path.join(codexData, '.ponytail-active')),
false,
'copilot hooks must not write mode state to codex PLUGIN_DATA',
);
output = JSON.parse(result.stdout);
assert.match(output.additionalContext, /PONYTAIL MODE ACTIVE — level: full/);
result = run(
'ponytail-mode-tracker.js',
{
HOME: home,
USERPROFILE: home,
COPILOT_PLUGIN_DATA: copilotData,
PLUGIN_DATA: codexData,
},
JSON.stringify({ prompt: '/ponytail ultra' }),
);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(copilotData, '.ponytail-active'), 'utf8'), 'ultra');
assert.equal(
fs.existsSync(path.join(codexData, '.ponytail-active')),
false,
'copilot mode tracker must keep codex PLUGIN_DATA untouched',
);
output = JSON.parse(result.stdout);
assert.deepEqual(output, {});
fs.rmSync(temp, { recursive: true, force: true });
console.log('hook compatibility checks passed');