Skip to content

Commit 5aee627

Browse files
fix(layers): audit skills/rules/commands/hooks and harden install paths
Add audit-layers gate, fix backend skill matrix path drift, align login/diagnose with local plugin root, and DRY capability-refresh auth headers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 409d5e7 commit 5aee627

10 files changed

Lines changed: 212 additions & 52 deletions

File tree

.github/workflows/validate.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
node-version: '20'
1616
- name: Validate (strict screenshots)
1717
run: node scripts/validate-plugin.mjs --strict-screenshots
18+
- name: Layer audit (skills/rules/commands/hooks)
19+
run: node scripts/audit-layers.mjs
1820
- name: Hook contract tests
1921
run: node scripts/test-hooks-contract.mjs
2022
- name: Intent eval

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to the AgentStack Cursor plugin are documented here. Format:
1515
- Standalone CI: vendor `scripts/lib/stale-actions.mjs` + `docs/CAPABILITY_MATRIX.md` (validate no longer imports monorepo-only paths).
1616
- Cursor 2.6+ install: restore schema-valid `.cursor-plugin/marketplace.json` + nest package under `plugins/agentstack/` (bare `source`, `pluginRoot: plugins`). Keep `listing.json` as AgentStack publisher SoT.
1717
- Lean MCP config: strip `tools` extras; sessionStart seeds capability snapshot with Bearer **or** `X-API-Key`; `scripts/diagnose-local.mjs` (`--fix`, `--seed-snapshot`).
18+
- Layer audit: `scripts/audit-layers.mjs` (skills/rules/commands/agents/hooks) wired into smoke + CI.
1819

1920
### Added (local verify)
2021

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ scripts/ ← validate / smoke / install-local (repo tool
5151

5252
Ready-to-paste form fields (especially **Description**): **[SUBMIT_FORM.md](SUBMIT_FORM.md)**.
5353
Demo script: [MARKETPLACE_DEMO.md](MARKETPLACE_DEMO.md). Local verify: [LOCAL_INSTALL.md](LOCAL_INSTALL.md). Ship checklist: [SHIP_TODO.md](SHIP_TODO.md).
54-
Diagnose: `node scripts/diagnose-local.mjs` (`--fix` / `--seed-snapshot`).
54+
Diagnose: `node scripts/diagnose-local.mjs` (`--fix` / `--seed-snapshot`).
55+
Layer audit: `node scripts/audit-layers.mjs` (skills / rules / commands / hooks).
5556

5657
## First 5 minutes
5758

SHIP_TODO.md

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,69 @@
55

66
---
77

8-
## A. Install layout
9-
10-
- [x] `.cursor-plugin/marketplace.json` + `plugins/agentstack/` (Cursor 2.6+)
11-
- [x] Local junction → `plugins/agentstack` (`install-local.mjs`)
12-
- [x] Local link verified OK on this machine
13-
- [ ] Reload Window after latest hook/mcpConfig changes
8+
## A. Layer inventory (skills / rules / commands / hooks)
9+
10+
| Layer | Count | Gate | Status |
11+
|-------|------:|------|--------|
12+
| Skills | 24 | frontmatter + live catalog pointer | [x] `audit-layers.mjs` |
13+
| Rules | 9 | exactly 1 `alwaysApply` (prefer) | [x] |
14+
| Commands | 13 | name/description + init/login/diagnose | [x] |
15+
| Agents | 5 | name/description | [x] |
16+
| Hooks | 7 events | scripts resolve under package | [x] |
17+
| Device Code | 1 script | not a hook event; install path | [x] |
18+
19+
- [x] Fix backend skill: no `docs/MCP_CAPABILITY_MATRIX.md` (use live `GET /mcp/actions`)
20+
- [x] login command: resolve plugin root before `device-code.mjs`
21+
- [x] capability-refresh: shared `agentstackAuthHeaders` (Bearer or API key)
22+
- [x] CI + smoke run `audit-layers.mjs`
1423

1524
---
1625

17-
## B. Call / data flow (verified)
26+
## B. Call / data flow
1827

1928
| Step | Contract | Status |
2029
|------|----------|--------|
2130
| Marketplace → package | `pluginRoot=plugins` / `source=agentstack` | [x] |
22-
| Local package load | `~/.cursor/plugins/local/agentstack` → SoT | [x] |
23-
| Device Code | authorize → poll 400+pending → tokens | [x] code |
24-
| MCP write | lean `streamable-http` + Bearer (strips `tools`, drops API key on OAuth) | [x] |
25-
| MCP normalize | `--fix` / sessionStart strips non-lean keys | [x] |
26-
| Snapshot | flat `actions[]` from `GET /mcp/actions` | [x] seeded via API key |
27-
| Cap hint | `beforeMCPExecution` reads flat snapshot | [x] |
28-
| Session start | auth via Bearer **or** X-API-Key for catalog; Bearer refresh only | [x] |
29-
30-
**This machine (diagnose):**
31-
32-
- [x] Plugin linked
33-
- [x] mcp lean after `--fix`
34-
- [x] Snapshot flat (live catalog via `--seed-snapshot`)
35-
- [ ] Auth still **X-API-Key** — run `/agentstack-init` for Device Code + refresh file
36-
- [ ] `/agentstack-diagnose` + `/agentstack-capability-matrix` in Cursor
31+
| Local link | `~/.cursor/plugins/local/agentstack` | [x] |
32+
| Device Code | authorize → poll pending → tokens | [x] |
33+
| MCP lean write | no `tools` key; Bearer or API key | [x] |
34+
| Snapshot | flat `actions[]` | [x] |
35+
| beforeMCPExecution | cap hint from snapshot | [x] |
36+
| sessionStart | normalize + seed catalog | [x] |
37+
| afterFileEdit mcp.json | cache clear + snapshot | [x] |
38+
| Telemetry | opt-in only | [x] |
3739

3840
---
3941

40-
## C. Commands for Lance (now)
42+
## C. Human e2e (this machine)
4143

4244
```bash
4345
cd provided_plugins/cursor-plugin
44-
node scripts/diagnose-local.mjs # status
45-
node scripts/diagnose-local.mjs --fix # lean mcp.json
46-
node scripts/diagnose-local.mjs --seed-snapshot
46+
node scripts/diagnose-local.mjs
47+
node scripts/audit-layers.mjs
48+
node scripts/smoke-local.mjs
4749
```
4850

49-
In Cursor after Reload:
51+
In Cursor after **Reload Window**:
5052

51-
1. `/agentstack-init` (upgrade to Device Code)
52-
2. `/agentstack-diagnose`
53-
3. `/agentstack-capability-matrix`
54-
4. Optional `/agentstack-host-site`
53+
- [ ] `/agentstack-init` (upgrade X-API-Key → Device Code)
54+
- [ ] `/agentstack-diagnose`
55+
- [ ] `/agentstack-capability-matrix`
56+
- [ ] Spot-check one skill route (e.g. hosting or auth)
5557

56-
Form paste: [`SUBMIT_FORM.md`](SUBMIT_FORM.md)
58+
Form: [`SUBMIT_FORM.md`](SUBMIT_FORM.md)
5759

5860
---
5961

6062
## D. Publish
6163

62-
- [ ] `git push origin master` + `v0.4.14` (`agentstacktech`)
64+
- [ ] `git push origin master` + `v0.4.14`
6365
- [ ] https://cursor.com/marketplace/publish
64-
- [ ] Post-release checklist (monorepo)
66+
- [ ] Post-release checklist
6567

6668
---
6769

6870
## E. Optional
6971

70-
- [~] Live screenshots replace mocks
71-
- [~] Prefer Device Code over long-lived API key in local mcp.json
72+
- [~] Live screenshots
73+
- [~] Explicit `alwaysApply: false` on all non-T0 rules (cosmetic)

plugins/agentstack/commands/agentstack-diagnose.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ Run these in order and present results as a single Markdown table.
1616
5. **Project**`projects.get_stats` with the active project id (from `~/.cursor/agentstack-project`).
1717
6. **API keys**`apikeys.list` (print label, prefix, scopes, ttl).
1818
7. **Recent errors** — last 20 lines from `~/.cursor/agentstack-telemetry.jsonl` where `success=false`; show `trace_id` + action.
19-
8. **Hooks** — verify `hooks/hooks.json` lists sessionStart, beforeShellExecution, beforeMCPExecution, postToolUse, postToolUseFailure, sessionEnd, afterFileEdit; scripts under `hooks/scripts/` resolve from plugin root.
20-
9. **Capability snapshot** — age of `~/.cursor/agentstack-capabilities.json` (mtime); if missing or >24h, refresh via session-start or `GET /mcp/actions`.
19+
8. **Hooks** — verify `hooks/hooks.json` lists sessionStart, beforeShellExecution, beforeMCPExecution, postToolUse, postToolUseFailure, sessionEnd, afterFileEdit; scripts under `hooks/scripts/` resolve from plugin root (`~/.cursor/plugins/local/agentstack` or marketplace install).
20+
9. **Capability snapshot** — age of `~/.cursor/agentstack-capabilities.json` (mtime); if missing or >24h, refresh via session-start or `GET /mcp/actions`. Confirm `actions` is a **flat array** (not nested `domains`).
2121
10. **MCP cache**`POST /mcp/cache/clear` (expected 200, `cleared: true`).
22+
11. **Local layout (optional)** — from the publish checkout: `node scripts/diagnose-local.mjs` and `node scripts/audit-layers.mjs`.
2223

2324
## Output
2425

plugins/agentstack/commands/agentstack-login.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ Use when:
1818
- `--scope-preset=readonly` for inspection.
1919
- `--scope-preset=builder` for app/data/rules/RAG/storage work.
2020
- `--scope-preset=full` for full platform work including Agents Fleet/support.
21-
2. Run `node ./hooks/scripts/device-code.mjs --scope-preset=builder` or `node ./hooks/scripts/device-code.mjs --scopes="<space-separated>"`.
21+
2. Resolve the plugin root (directory with `hooks/scripts/device-code.mjs` — typically `~/.cursor/plugins/local/agentstack`). Then run:
22+
`node ./hooks/scripts/device-code.mjs --scope-preset=builder`
23+
or `node ./hooks/scripts/device-code.mjs --scopes="<space-separated>"`
24+
(If cwd is not the plugin root, pass an absolute path to the script.)
2225
3. After success, if the token grants access to more than one project, ask the user to pick one:
2326
- Call `projects.get_projects` via MCP (alias `projects.list`).
2427
- Persist the picked project id to `~/.cursor/agentstack-project` (plain text, 0600).

plugins/agentstack/hooks/scripts/capability-refresh.mjs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,24 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
66
import { join } from 'node:path';
77
import { homedir } from 'node:os';
88
import { flattenMcpActionsCatalog } from '../../lib/plugin-kernel/mcpActionsCatalog.mjs';
9+
import { agentstackAuthHeaders } from '../../lib/plugin-kernel/mcpConfig.mjs';
910

1011
const BASE_URL = process.env.AGENTSTACK_BASE_URL || 'https://agentstack.tech';
1112
const CURSOR_DIR = join(homedir(), '.cursor');
1213
const MCP_PATH = join(CURSOR_DIR, 'mcp.json');
1314
const SNAPSHOT_PATH = join(CURSOR_DIR, 'agentstack-capabilities.json');
1415

15-
async function getAuthHeader() {
16+
async function getAuthHeaders() {
1617
try {
1718
const cfg = JSON.parse(await readFile(MCP_PATH, 'utf8'));
18-
const h = cfg?.mcpServers?.agentstack?.headers || {};
19-
if (h.Authorization) return { Authorization: h.Authorization };
20-
if (h['X-API-Key']) return { 'X-API-Key': h['X-API-Key'] };
19+
return agentstackAuthHeaders(cfg);
2120
} catch {
22-
/* none */
21+
return null;
2322
}
24-
return null;
2523
}
2624

2725
async function main() {
28-
const auth = await getAuthHeader();
26+
const auth = await getAuthHeaders();
2927
if (!auth) return;
3028

3129
try {

plugins/agentstack/skills/agentstack-backend/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Entry point for AgentStack backend ecosystem. Use WHENEVER the user
55

66
# AgentStack Backend — Master Router
77

8-
AgentStack is a **full backend ecosystem** exposed through ONE MCP tool: `agentstack.execute`. Capabilities come from `GET /mcp/actions` and `docs/MCP_CAPABILITY_MATRIX.md` — do not hard-code action counts in skills.
8+
AgentStack is a **full backend ecosystem** exposed through ONE MCP tool: `agentstack.execute`. Capabilities come from live `GET /mcp/actions` (and `/agentstack-capability-matrix`) — do not hard-code action counts in skills.
99

1010
## Quick router — pick the sub-skill
1111

scripts/audit-layers.mjs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Deep audit of plugin layers: skills, rules, commands, agents, hooks.
4+
* Run from publish repo root: node scripts/audit-layers.mjs
5+
*/
6+
import fs from 'fs';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11+
const PLUGIN = path.join(ROOT, 'plugins', 'agentstack');
12+
let fails = 0;
13+
let warns = 0;
14+
15+
function ok(m) {
16+
console.log(`OK ${m}`);
17+
}
18+
function warn(m) {
19+
console.warn(`WARN ${m}`);
20+
warns += 1;
21+
}
22+
function fail(m) {
23+
console.error(`FAIL ${m}`);
24+
fails += 1;
25+
}
26+
27+
function listDirs(dir) {
28+
if (!fs.existsSync(dir)) return [];
29+
return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
30+
}
31+
32+
function listFiles(dir, ext) {
33+
if (!fs.existsSync(dir)) return [];
34+
return fs.readdirSync(dir).filter((f) => f.endsWith(ext));
35+
}
36+
37+
function parseFm(content) {
38+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
39+
if (!m) return null;
40+
const fields = {};
41+
for (const line of m[1].split(/\r?\n/)) {
42+
const kv = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
43+
if (kv) fields[kv[1]] = kv[2].trim();
44+
}
45+
return fields;
46+
}
47+
48+
console.log(`Layer audit — ${PLUGIN}\n`);
49+
50+
// Skills
51+
const skillDirs = listDirs(path.join(PLUGIN, 'skills'));
52+
const missingLive = [];
53+
const skillNames = new Set();
54+
for (const d of skillDirs) {
55+
const p = path.join(PLUGIN, 'skills', d, 'SKILL.md');
56+
if (!fs.existsSync(p)) {
57+
fail(`skills/${d}: missing SKILL.md`);
58+
continue;
59+
}
60+
const t = fs.readFileSync(p, 'utf8');
61+
const fm = parseFm(t);
62+
if (!fm?.name || !fm?.description) fail(`skills/${d}: frontmatter needs name + description`);
63+
else {
64+
skillNames.add(fm.name);
65+
ok(`skill ${fm.name}`);
66+
}
67+
if (!/\/mcp\/actions|live catalog/i.test(t)) missingLive.push(d);
68+
if (/docs\/MCP_CAPABILITY_MATRIX\.md/.test(t)) {
69+
fail(`skills/${d}: references docs/MCP_CAPABILITY_MATRIX.md (not in package) — use GET /mcp/actions`);
70+
}
71+
}
72+
if (missingLive.length) {
73+
for (const d of missingLive) warn(`skills/${d}: add live catalog pointer (GET /mcp/actions)`);
74+
} else {
75+
ok('all skills mention live catalog /mcp/actions');
76+
}
77+
78+
// Rules
79+
const rules = listFiles(path.join(PLUGIN, 'rules'), '.mdc');
80+
let always = 0;
81+
for (const f of rules) {
82+
const t = fs.readFileSync(path.join(PLUGIN, 'rules', f), 'utf8');
83+
const fm = parseFm(t);
84+
if (!fm?.description) fail(`rules/${f}: missing description frontmatter`);
85+
if (/alwaysApply:\s*true/.test(t)) always += 1;
86+
ok(`rule ${f}`);
87+
}
88+
if (always !== 1) fail(`expected exactly 1 alwaysApply rule, got ${always}`);
89+
else ok('exactly 1 alwaysApply rule (agentstack-prefer)');
90+
91+
// Commands
92+
const cmds = listFiles(path.join(PLUGIN, 'commands'), '.md');
93+
for (const f of cmds) {
94+
const t = fs.readFileSync(path.join(PLUGIN, 'commands', f), 'utf8');
95+
const fm = parseFm(t);
96+
if (!fm?.name || !fm?.description) fail(`commands/${f}: frontmatter needs name + description`);
97+
else ok(`command ${fm.name}`);
98+
}
99+
for (const need of ['agentstack-init.md', 'agentstack-login.md', 'agentstack-diagnose.md']) {
100+
if (!cmds.includes(need)) fail(`missing command ${need}`);
101+
}
102+
103+
// Agents
104+
const agents = listFiles(path.join(PLUGIN, 'agents'), '.md');
105+
for (const f of agents) {
106+
const t = fs.readFileSync(path.join(PLUGIN, 'agents', f), 'utf8');
107+
const fm = parseFm(t);
108+
if (!fm?.name || !fm?.description) fail(`agents/${f}: frontmatter needs name + description`);
109+
else ok(`agent ${fm.name}`);
110+
}
111+
112+
// Hooks
113+
const hooksPath = path.join(PLUGIN, 'hooks/hooks.json');
114+
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
115+
const requiredEvents = [
116+
'sessionStart',
117+
'sessionEnd',
118+
'beforeShellExecution',
119+
'beforeMCPExecution',
120+
'postToolUse',
121+
'postToolUseFailure',
122+
'afterFileEdit',
123+
];
124+
for (const ev of requiredEvents) {
125+
const list = hooks.hooks?.[ev];
126+
if (!Array.isArray(list) || !list.length) fail(`hooks.json missing ${ev}`);
127+
else {
128+
for (const entry of list) {
129+
const m = String(entry.command || '').match(/\.\/(.+\.mjs)/);
130+
if (!m || !fs.existsSync(path.join(PLUGIN, m[1]))) fail(`hooks ${ev}: missing ${entry.command}`);
131+
else ok(`hook ${ev}${m[1]}`);
132+
}
133+
}
134+
}
135+
136+
// device-code is not a hook event but required script
137+
if (!fs.existsSync(path.join(PLUGIN, 'hooks/scripts/device-code.mjs'))) {
138+
fail('hooks/scripts/device-code.mjs missing');
139+
} else ok('device-code.mjs present (install path)');
140+
141+
// Backend router covers skill folders (name match or known alias)
142+
const backend = fs.readFileSync(path.join(PLUGIN, 'skills/agentstack-backend/SKILL.md'), 'utf8');
143+
const optional = new Set(['solana']);
144+
for (const d of skillDirs) {
145+
if (d === 'agentstack-backend') continue;
146+
if (optional.has(d)) continue;
147+
if (!backend.includes(d) && !backend.includes(`\`${d}\``)) {
148+
warn(`backend router may omit skill folder ${d}`);
149+
}
150+
}
151+
152+
console.log(`\nsummary: failed=${fails} warnings=${warns}`);
153+
console.log(`counts: skills=${skillDirs.length} rules=${rules.length} commands=${cmds.length} agents=${agents.length}`);
154+
process.exit(fails ? 1 : 0);

scripts/smoke-local.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ if (chk.status === 0) {
5555
console.log('NOTE not linked yet — run: node scripts/install-local.mjs');
5656
}
5757

58-
run(
59-
'validate-plugin',
60-
['scripts/validate-plugin.mjs', ...(strict ? ['--strict-screenshots'] : [])],
61-
);
58+
run('validate-plugin', ['scripts/validate-plugin.mjs', ...(strict ? ['--strict-screenshots'] : [])]);
59+
run('audit-layers', ['scripts/audit-layers.mjs']);
6260
run('test-hooks-contract', ['scripts/test-hooks-contract.mjs']);
6361
run('test-kernel-catalog', ['scripts/test-kernel-catalog.mjs']);
6462
run('device-code --help', ['plugins/agentstack/hooks/scripts/device-code.mjs', '--help']);

0 commit comments

Comments
 (0)