Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"description": "GitHub Copilot port of pstack for Copilot CLI and the VS Code agents window.",
"plugins": [
{
"name": "pstack",
"name": "pstack-copilot",
"source": "./plugins/pstack",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. GitHub Copilot port of pstack-claude 0.9.57. Original pstack by Lauren Tan (poteto); Claude Code port by Michael Denyer.",
"version": "1.1.1"
"version": "1.2.0"
}
]
}
2 changes: 1 addition & 1 deletion NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This repository is a GitHub Copilot port of [pstack-claude](https://github.com/m
`plugins/pstack/` is generated by [`tools/build.mjs`](tools/build.mjs) from that commit. The port's own work is:

- `overlay/skills/poteto-mode/references/copilot-tools.md`
- `overlay/hooks/hooks.json` and `overlay/hooks/session-start.sh`
- `overlay/com.github.copilot/hooks/hooks.json` and `overlay/com.github.copilot/hooks/session-start.sh`
- the `edits` table, mandate rewrite, and manifests in `tools/build.mjs`
- `tools/check.mjs` and `tools/eval.sh`

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pstack for GitHub Copilot

[![check](https://github.com/hackernotfound/pstack-vscode/actions/workflows/check.yml/badge.svg)](https://github.com/hackernotfound/pstack-vscode/actions/workflows/check.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![version](https://img.shields.io/badge/version-1.1.1-7C3AED.svg)](https://github.com/hackernotfound/pstack-vscode/releases/tag/v1.1.1)
[![check](https://github.com/hackernotfound/pstack-vscode/actions/workflows/check.yml/badge.svg)](https://github.com/hackernotfound/pstack-vscode/actions/workflows/check.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![version](https://img.shields.io/badge/version-1.2.0-7C3AED.svg)](https://github.com/hackernotfound/pstack-vscode/releases/tag/v1.2.0)

**Run [pstack](https://github.com/cursor/plugins/tree/main/pstack) in the VS Code agents window with GitHub Copilot.**

Expand Down Expand Up @@ -32,22 +32,24 @@ This port fixes all four and leaves pstack's own content untouched. Each fix is
"chat.plugins.marketplaces": ["hackernotfound/pstack-vscode"]
```

2. In the Extensions view, search `@agentPlugins`, install **pstack**, and accept the trust prompt.
2. In the Extensions view, search `@agentPlugins`, install **pstack-copilot**, and accept the trust prompt.
3. Open the agents window and pick **Copilot** in the session target picker.

That's it. Under **Customizations**, pstack now shows up in **Plugins**, its hook in **Hooks**, and `poteto-mode` in **Skills**.
That's it. Under **Customizations**, pstack-copilot now shows up in **Plugins**, its hook in **Hooks**, and `poteto-mode` in **Skills**.

**Want to see your subagents while they run?** Follow the one-time [agents window setup](docs/agents-window.md). It adds a live list of running subagents, side-by-side subagent panes, and chat tabs, close to [T3 Code](https://t3.codes).

### Copilot CLI

```shell
copilot plugin marketplace add hackernotfound/pstack-vscode
copilot plugin install pstack@pstack-vscode
copilot plugin install pstack-copilot@pstack-vscode
```

VS Code also lists plugins you install this way.

> **Installed 1.1.1 or earlier?** The plugin was called `pstack` then. Run `copilot plugin uninstall pstack`, then install `pstack-copilot` as above. In VS Code, uninstall **pstack** and install **pstack-copilot**. A `chat.pluginLocations` path to `plugins/pstack` keeps working as it is.

> **Using the Claude target too?** The Claude target loads your Claude Code plugins from `~/.claude`. Keep pstack-claude for that target, and use this port for **Copilot**.

## Use it
Expand Down Expand Up @@ -106,7 +108,7 @@ flowchart LR
- **A Copilot tool map.** [`copilot-tools.md`](overlay/skills/poteto-mode/references/copilot-tools.md) translates every Claude tool and model name the skills use. `poteto-mode` points Copilot at it.
- **Bare skill names** in the startup instructions, such as `poteto-mode` rather than `pstack:poteto-mode`.
- **No plugin agents.** Copilot dispatches its built-in `general-purpose` agent and has it load `poteto-mode` first, which is all `pstack:poteto-agent` does. Shipping no agents also keeps VS Code's agent picker free of duplicates.
- **A Copilot-only manifest.** VS Code treats any plugin with a `.claude-plugin/` folder as a Claude plugin, so the port ships only a Copilot `plugin.json`.
- **An [Agent Plugins 1.0](https://agent-plugins.org) manifest.** The startup hook lives under `com.github.copilot/`, where Copilot looks for it in that format. The port ships no `.claude-plugin/` folder, because VS Code treats any plugin with one as a Claude plugin.

Tested on VS Code 1.140.0 and its bundled Copilot runtime, and on Copilot CLI 1.0.91. Updating to a newer pstack, the generated files, and the live tests are covered in [docs/maintaining.md](docs/maintaining.md).

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.1
1.2.0
2 changes: 1 addition & 1 deletion docs/maintaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
`node tools/check.mjs` runs in CI on every push and pull request. It rebuilds the plugin and fails if:

- the rebuilt tree differs from the committed one
- a manifest, agent, hook, or skill is missing or malformed
- the manifest is not Agent Plugins 1.0, or an agent, hook, or skill is missing or malformed
- the startup instructions name a skill with the `pstack:` prefix, or the hook output is not top-level `additionalContext`
- the plugin contains a symlink, or an executable outside the reviewed list
- the plugin ships any agent (VS Code's agents window lists every plugin agent twice)
Expand Down
2 changes: 1 addition & 1 deletion overlay/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pstack (GitHub Copilot port)
# pstack-copilot (GitHub Copilot port of pstack)

Lauren Tan's [pstack](https://github.com/cursor/plugins/tree/main/pstack) is an opinionated skill stack that improves agent outcomes. This build runs it on GitHub Copilot: the VS Code agents window with the Copilot target, and Copilot CLI. It is generated from Michael Denyer's [pstack-claude](https://github.com/michael-denyer/pstack-claude).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\""
"command": "\"${PLUGIN_ROOT}/com.github.copilot/hooks/session-start.sh\""
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/pstack/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pstack (GitHub Copilot port)
# pstack-copilot (GitHub Copilot port of pstack)

Lauren Tan's [pstack](https://github.com/cursor/plugins/tree/main/pstack) is an opinionated skill stack that improves agent outcomes. This build runs it on GitHub Copilot: the VS Code agents window with the Copilot target, and Copilot CLI. It is generated from Michael Denyer's [pstack-claude](https://github.com/michael-denyer/pstack-claude).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\""
"command": "\"${PLUGIN_ROOT}/com.github.copilot/hooks/session-start.sh\""
}
]
}
Expand Down
13 changes: 8 additions & 5 deletions plugins/pstack/plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"name": "pstack",
"displayName": "pstack (GitHub Copilot port)",
"version": "1.1.1",
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "pstack-copilot",
"version": "1.2.0",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. GitHub Copilot port of pstack-claude 0.9.57. Original pstack by Lauren Tan (poteto); Claude Code port by Michael Denyer.",
"author": {
"name": "hackernotfound",
"url": "https://github.com/hackernotfound"
},
"homepage": "https://github.com/hackernotfound/pstack-vscode",
"repository": "https://github.com/hackernotfound/pstack-vscode",
"license": "MIT",
"keywords": [
"pstack",
Expand All @@ -16,6 +18,7 @@
"skills",
"subagents"
],
"skills": "./skills/",
"hooks": "./hooks/hooks.json"
"extensions": {
"com.github.copilot": {}
}
}
28 changes: 17 additions & 11 deletions tools/build.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

Expand All @@ -9,6 +9,9 @@ const upstream = JSON.parse(readFileSync(join(root, 'upstream.json'), 'utf8'));
const version = readFileSync(join(root, 'VERSION'), 'utf8').trim();
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`VERSION must be MAJOR.MINOR.PATCH, got ${JSON.stringify(version)}`);
const out = join(root, 'plugins/pstack');
const name = 'pstack-copilot';
// Agent Plugins 1.0 keeps Copilot-only parts, such as hooks, under this namespace folder.
const hooks = 'com.github.copilot/hooks';

function fetchSource() {
const dir = join(root, '.cache', `pstack-claude-${upstream.sha.slice(0, 12)}`);
Expand Down Expand Up @@ -37,7 +40,7 @@ const skillNames = () =>
.filter((d) => d.isDirectory() && existsSync(join(out, 'skills', d.name, 'SKILL.md')))
.map((d) => d.name);

const removals = ['.codex-plugin', '.claude-plugin', 'hooks/codex-hooks.json', 'agents', 'effort-agents'];
const removals = ['.codex-plugin', '.claude-plugin', 'hooks/codex-hooks.json', 'hooks/hooks.json', 'hooks/session-start.sh', 'agents', 'effort-agents'];

const edits = [
{
Expand Down Expand Up @@ -103,7 +106,7 @@ function unlinkCrossSkillReferences() {
function buildMandate() {
const names = new Set(skillNames());
let rewrites = 0;
const text = read('hooks/session-start-context.md').replace(/pstack:([a-z0-9-]+)/g, (m, name) => {
const text = read(`${hooks}/session-start-context.md`).replace(/pstack:([a-z0-9-]+)/g, (m, name) => {
if (!names.has(name)) return m;
rewrites++;
return name;
Expand All @@ -113,29 +116,30 @@ function buildMandate() {
'\nOn GitHub Copilot, skills load through the `skill` tool by bare name, such as `poteto-mode`. This plugin ships no agents; where a skill names a pstack agent, dispatch `general-purpose` as copilot-tools.md says. When a pstack skill names a Claude tool or model, read the pstack plugin\'s `skills/poteto-mode/references/copilot-tools.md`.\n';
const mandate = text.replace('</EXTREMELY_IMPORTANT>', `${pointer}</EXTREMELY_IMPORTANT>`);
if (mandate === text) throw new Error('mandate: closing tag not found');
write('hooks/session-start-context.md', mandate);
write('hooks/session-start-context.json', `${JSON.stringify({ additionalContext: mandate })}\n`);
write(`${hooks}/session-start-context.md`, mandate);
write(`${hooks}/session-start-context.json`, `${JSON.stringify({ additionalContext: mandate })}\n`);
}

function writeManifests(source) {
const claude = JSON.parse(readFileSync(join(source, 'plugins/pstack/.claude-plugin/plugin.json'), 'utf8'));
const plugin = {
name: 'pstack',
displayName: 'pstack (GitHub Copilot port)',
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
name,
version,
description: `${claude.description.split(' Ported from')[0]} GitHub Copilot port of pstack-claude ${upstream.version}. Original pstack by Lauren Tan (poteto); Claude Code port by Michael Denyer.`,
author: { name: 'hackernotfound', url: 'https://github.com/hackernotfound' },
homepage: 'https://github.com/hackernotfound/pstack-vscode',
repository: 'https://github.com/hackernotfound/pstack-vscode',
license: 'MIT',
keywords: ['pstack', 'poteto-mode', 'copilot', 'vscode', 'skills', 'subagents'],
skills: './skills/',
hooks: './hooks/hooks.json',
extensions: { 'com.github.copilot': {} },
};
write('plugin.json', `${JSON.stringify(plugin, null, 2)}\n`);
const marketplace = {
name: 'pstack-vscode',
owner: { name: 'hackernotfound', url: 'https://github.com/hackernotfound' },
description: 'GitHub Copilot port of pstack for Copilot CLI and the VS Code agents window.',
plugins: [{ name: 'pstack', source: './plugins/pstack', description: plugin.description, version }],
plugins: [{ name, source: './plugins/pstack', description: plugin.description, version }],
};
mkdirSync(join(root, '.github/plugin'), { recursive: true });
writeFileSync(join(root, '.github/plugin/marketplace.json'), `${JSON.stringify(marketplace, null, 2)}\n`);
Expand All @@ -154,10 +158,12 @@ const source = fetchSource();
rmSync(out, { recursive: true, force: true });
cpSync(join(source, 'plugins/pstack'), out, { recursive: true });
for (const rel of removals) rmSync(join(out, rel), { recursive: true, force: true });
mkdirSync(dirname(join(out, hooks)), { recursive: true });
renameSync(join(out, 'hooks'), join(out, hooks));
cpSync(join(root, 'overlay'), out, { recursive: true });
edits.forEach(applyEdit);
unlinkCrossSkillReferences();
buildMandate();
writeManifests(source);
copyLicenses(source);
console.log(`built plugins/pstack ${version} from ${upstream.sha.slice(0, 7)}`);
console.log(`built plugins/pstack (${name}) ${version} from ${upstream.sha.slice(0, 7)}`);
26 changes: 19 additions & 7 deletions tools/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,48 @@ check('build is idempotent (rebuild leaves no diff)', dirty === '');

const plugin = JSON.parse(read('plugin.json'));
const upstream = JSON.parse(readFileSync(join(root, 'upstream.json'), 'utf8'));
check('plugin.json name is pstack', plugin.name === 'pstack');
check('plugin.json name is pstack-copilot', plugin.name === 'pstack-copilot');
const manifestKeys = ['$schema', 'author', 'description', 'extensions', 'homepage', 'keywords', 'license', 'name', 'repository', 'version'];
check(
'plugin.json is Agent Plugins 1.0 (schema URL, only schema properties, Copilot namespace)',
plugin.$schema === 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json' &&
Object.keys(plugin).every((k) => manifestKeys.includes(k)) &&
JSON.stringify(plugin.extensions) === '{"com.github.copilot":{}}',
);
const version = readFileSync(join(root, 'VERSION'), 'utf8').trim();
check('plugin.json version matches VERSION and is MAJOR.MINOR.PATCH', /^\d+\.\d+\.\d+$/.test(version) && plugin.version === version);
const readme = readFileSync(join(root, 'README.md'), 'utf8');
check('README version badge matches VERSION', readme.includes(`version-${version.replaceAll('-', '--')}-`) && readme.includes(`releases/tag/v${version})`));
check('plugin.json names the pinned upstream version', plugin.description.includes(`pstack-claude ${upstream.version}`));
check('no .claude-plugin manifest (VS Code would parse it as Claude format)', !existsSync(join(out, '.claude-plugin')));
check('plugin ships no agents (the VS Code agents window lists each plugin agent twice)', !('agents' in plugin) && !existsSync(join(out, 'agents')) && !readdirSync(out, { recursive: true }).some((rel) => /(^|\/)agents\/[^/]+\.md$/.test(rel) && !rel.startsWith('skills/')));
check('hooks file exists', existsSync(join(out, plugin.hooks)));
const hooks = 'com.github.copilot/hooks';
const hookCommands = JSON.parse(read(`${hooks}/hooks.json`)).hooks.SessionStart.flatMap((g) => g.hooks.map((h) => h.command));
check(
'SessionStart hook runs a script that ships, via ${PLUGIN_ROOT}',
hookCommands.length === 1 && hookCommands.every((c) => c.startsWith('"${PLUGIN_ROOT}/') && existsSync(join(out, c.slice('"${PLUGIN_ROOT}/'.length, -1)))),
);

const market = JSON.parse(readFileSync(join(root, '.github/plugin/marketplace.json'), 'utf8'));
check('marketplace source points at the plugin', market.plugins.some((p) => existsSync(join(root, p.source, 'plugin.json'))));
check('marketplace entry points at the plugin by its name', market.plugins.some((p) => p.name === plugin.name && existsSync(join(root, p.source, 'plugin.json'))));

const skills = readdirSync(join(out, 'skills')).filter((d) => existsSync(join(out, 'skills', d, 'SKILL.md')));
check('every SKILL.md name matches its directory', skills.every((d) => new RegExp(`^name: ${d}$`, 'm').test(read(`skills/${d}/SKILL.md`))));

const mandate = read('hooks/session-start-context.md');
const ctx = JSON.parse(read('hooks/session-start-context.json'));
const mandate = read(`${hooks}/session-start-context.md`);
const ctx = JSON.parse(read(`${hooks}/session-start-context.json`));
check('hook JSON carries the mandate as top-level additionalContext', ctx.additionalContext === mandate);
check('mandate names no pstack:<skill>', !skills.some((s) => mandate.includes(`pstack:${s}`)));
check('mandate under Claude Code 10k cap', mandate.length < 10000);

const hookOut = execFileSync(join(out, 'hooks/session-start.sh'), { env: { ...process.env, COPILOT_HOME: '/nonexistent' }, encoding: 'utf8' });
const hookOut = execFileSync(join(out, hooks, 'session-start.sh'), { env: { ...process.env, COPILOT_HOME: '/nonexistent' }, encoding: 'utf8' });
check('hook script prints the JSON', JSON.parse(hookOut).additionalContext === mandate);

check('poteto-mode links copilot-tools.md', read('skills/poteto-mode/SKILL.md').includes('references/copilot-tools.md'));
check('copilot-tools.md shipped', existsSync(join(out, 'skills/poteto-mode/references/copilot-tools.md')));

const executables = [
'hooks/session-start.sh',
'com.github.copilot/hooks/session-start.sh',
'skills/poteto-mode/scripts/orch/orch.ts',
'skills/poteto-mode/scripts/watch-pr/ship-pr',
'skills/poteto-mode/scripts/watch-pr/watch-pr',
Expand Down
4 changes: 2 additions & 2 deletions tools/eval-vscode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ const rejectedDispatch = (cs) => cs.some((t) => t.t === 'task' && !t.ok);

const agentNames = await run('context', 'Do not run tools. 1) Quote verbatim the sentence in your context that starts with "On GitHub Copilot, skills load". 2) Print the full enum of the agent_type parameter of your task tool, comma separated. 3) Print the number of skills available to you whose name starts with "principle-".', 5 * 60_000);
check('mandate injected by sessionStart hook', final('context').includes('skills load through the'));
check('runtime lists no pstack plugin agents', !agentNames.some((n) => n.startsWith('pstack:')));
check('runtime lists no pstack plugin agents', !agentNames.some((n) => /^pstack(-copilot)?:/.test(n)));

await run('route', 'Explain how main.js gets its output in this repo. Use poteto-mode, and delegate the code reading to one pstack subagent.', 20 * 60_000);
const route = calls('route');
check('poteto-mode loaded via skill tool', route.some((c) => c.t === 'skill' && String(c.a.skill).includes('poteto-mode') && c.ok));
check('no namespaced skill lookups', !route.some((c) => String(c.a.skill ?? '').startsWith('pstack:')));
check('no namespaced skill lookups', !route.some((c) => /^pstack(-copilot)?:/.test(String(c.a.skill ?? ''))));
check('a subagent ran and loaded a pstack skill itself', subagentRanPstack(route));
check('no subagent dispatch was rejected', !rejectedDispatch(route));
check('no Claude-only tool names called', !route.some((c) => /^(Agent|Skill|Task|AskUserQuestion|TodoWrite)$/.test(c.t)));
Expand Down
Loading
Loading