chapterhouse 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +363 -0
  3. package/agents/chapterhouse.agent.md +40 -0
  4. package/agents/coder.agent.md +38 -0
  5. package/agents/designer.agent.md +43 -0
  6. package/agents/general-purpose.agent.md +30 -0
  7. package/dist/api/auth.js +159 -0
  8. package/dist/api/auth.test.js +463 -0
  9. package/dist/api/errors.js +95 -0
  10. package/dist/api/errors.test.js +89 -0
  11. package/dist/api/rate-limit.js +85 -0
  12. package/dist/api/server-runtime.js +62 -0
  13. package/dist/api/server.js +651 -0
  14. package/dist/api/server.test.js +385 -0
  15. package/dist/api/sse.integration.test.js +270 -0
  16. package/dist/api/sse.js +7 -0
  17. package/dist/api/team.js +196 -0
  18. package/dist/api/team.test.js +466 -0
  19. package/dist/cli.js +102 -0
  20. package/dist/config.js +299 -0
  21. package/dist/config.phase3.test.js +20 -0
  22. package/dist/config.test.js +148 -0
  23. package/dist/copilot/agents.js +447 -0
  24. package/dist/copilot/agents.squad.test.js +72 -0
  25. package/dist/copilot/classifier.js +72 -0
  26. package/dist/copilot/client.js +32 -0
  27. package/dist/copilot/client.test.js +100 -0
  28. package/dist/copilot/episode-writer.js +219 -0
  29. package/dist/copilot/episode-writer.test.js +41 -0
  30. package/dist/copilot/mcp-config.js +22 -0
  31. package/dist/copilot/okr-mapper.js +196 -0
  32. package/dist/copilot/okr-mapper.test.js +114 -0
  33. package/dist/copilot/orchestrator.js +685 -0
  34. package/dist/copilot/orchestrator.test.js +523 -0
  35. package/dist/copilot/router.js +142 -0
  36. package/dist/copilot/router.test.js +119 -0
  37. package/dist/copilot/skills.js +125 -0
  38. package/dist/copilot/standup.js +138 -0
  39. package/dist/copilot/standup.test.js +132 -0
  40. package/dist/copilot/system-message.js +143 -0
  41. package/dist/copilot/system-message.test.js +17 -0
  42. package/dist/copilot/tools.js +1212 -0
  43. package/dist/copilot/tools.okr.test.js +260 -0
  44. package/dist/copilot/tools.squad.test.js +168 -0
  45. package/dist/daemon.js +235 -0
  46. package/dist/home-path.js +12 -0
  47. package/dist/home-path.test.js +11 -0
  48. package/dist/integrations/ado-analytics.js +178 -0
  49. package/dist/integrations/ado-analytics.test.js +284 -0
  50. package/dist/integrations/ado-client.js +227 -0
  51. package/dist/integrations/ado-client.test.js +176 -0
  52. package/dist/integrations/ado-schema.js +25 -0
  53. package/dist/integrations/ado-schema.test.js +39 -0
  54. package/dist/integrations/ado-skill.js +55 -0
  55. package/dist/integrations/report-generator.js +114 -0
  56. package/dist/integrations/report-generator.test.js +62 -0
  57. package/dist/integrations/team-push.js +144 -0
  58. package/dist/integrations/team-push.test.js +178 -0
  59. package/dist/integrations/teams-notify.js +108 -0
  60. package/dist/integrations/teams-notify.test.js +135 -0
  61. package/dist/paths.js +41 -0
  62. package/dist/setup.js +149 -0
  63. package/dist/shutdown-signals.js +13 -0
  64. package/dist/shutdown-signals.test.js +33 -0
  65. package/dist/squad/charter.js +108 -0
  66. package/dist/squad/charter.test.js +89 -0
  67. package/dist/squad/context.js +48 -0
  68. package/dist/squad/context.test.js +59 -0
  69. package/dist/squad/discovery.js +280 -0
  70. package/dist/squad/discovery.test.js +93 -0
  71. package/dist/squad/index.js +7 -0
  72. package/dist/squad/mirror.js +81 -0
  73. package/dist/squad/mirror.scheduler.js +78 -0
  74. package/dist/squad/mirror.scheduler.test.js +197 -0
  75. package/dist/squad/mirror.test.js +172 -0
  76. package/dist/squad/registry.js +162 -0
  77. package/dist/squad/registry.test.js +31 -0
  78. package/dist/squad/squad-coordinator-system-message.test.js +190 -0
  79. package/dist/squad/squad-session-routing.test.js +260 -0
  80. package/dist/squad/types.js +4 -0
  81. package/dist/status.js +25 -0
  82. package/dist/status.test.js +22 -0
  83. package/dist/store/db.js +290 -0
  84. package/dist/store/db.test.js +126 -0
  85. package/dist/store/squad-sessions.test.js +341 -0
  86. package/dist/test/setup-env.js +3 -0
  87. package/dist/update.js +112 -0
  88. package/dist/update.test.js +25 -0
  89. package/dist/wiki/context.js +138 -0
  90. package/dist/wiki/fs.js +195 -0
  91. package/dist/wiki/fs.test.js +39 -0
  92. package/dist/wiki/index-manager.js +359 -0
  93. package/dist/wiki/index-manager.test.js +129 -0
  94. package/dist/wiki/lock.js +26 -0
  95. package/dist/wiki/lock.test.js +30 -0
  96. package/dist/wiki/log-manager.js +20 -0
  97. package/dist/wiki/migrate.js +306 -0
  98. package/dist/wiki/okr.test.js +101 -0
  99. package/dist/wiki/path-utils.js +4 -0
  100. package/dist/wiki/path-utils.test.js +8 -0
  101. package/dist/wiki/seed-team-wiki.js +296 -0
  102. package/dist/wiki/seed-team-wiki.test.js +69 -0
  103. package/dist/wiki/team-sync.js +212 -0
  104. package/dist/wiki/team-sync.test.js +185 -0
  105. package/dist/wiki/templates/okr.js +98 -0
  106. package/package.json +72 -0
  107. package/skills/.gitkeep +0 -0
  108. package/skills/find-skills/SKILL.md +161 -0
  109. package/skills/find-skills/_meta.json +4 -0
  110. package/skills/frontend-design/LICENSE.txt +177 -0
  111. package/skills/frontend-design/SKILL.md +42 -0
  112. package/skills/squad/SKILL.md +76 -0
  113. package/web/dist/assets/index-D-e7K-fT.css +10 -0
  114. package/web/dist/assets/index-DAg9IrpO.js +142 -0
  115. package/web/dist/assets/index-DAg9IrpO.js.map +1 -0
  116. package/web/dist/chapterhouse-icon.png +0 -0
  117. package/web/dist/chapterhouse-icon.svg +42 -0
  118. package/web/dist/chapterhouse-logo.svg +46 -0
  119. package/web/dist/index.html +15 -0
@@ -0,0 +1,185 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ const tmpRoot = join(process.cwd(), ".test-work", `team-sync-${process.pid}`);
6
+ process.env.CHAPTERHOUSE_HOME = tmpRoot;
7
+ function makeWikiDir(name) {
8
+ const wikiDir = join(tmpRoot, name, ".chapterhouse", "wiki");
9
+ mkdirSync(wikiDir, { recursive: true });
10
+ return wikiDir;
11
+ }
12
+ async function loadTeamSyncModule() {
13
+ try {
14
+ return await import(new URL(`./team-sync.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ test.after(() => {
21
+ rmSync(tmpRoot, { recursive: true, force: true });
22
+ });
23
+ test("uses fresh cache when TTL has not expired", async () => {
24
+ const teamSyncModule = await loadTeamSyncModule();
25
+ assert.ok(teamSyncModule, "team sync module should exist");
26
+ const wikiDir = makeWikiDir("fresh-cache");
27
+ const cacheRoot = join(wikiDir, ".team-cache");
28
+ const pagePath = "pages/team/roadmap.md";
29
+ mkdirSync(join(cacheRoot, "pages", "team"), { recursive: true });
30
+ writeFileSync(join(cacheRoot, pagePath), "# Cached roadmap\n");
31
+ writeFileSync(join(cacheRoot, "manifest.json"), JSON.stringify({
32
+ [pagePath]: {
33
+ fetchedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
34
+ etag: '"fresh-etag"',
35
+ },
36
+ }));
37
+ let fetchCalls = 0;
38
+ const sync = new teamSyncModule.TeamWikiSync({
39
+ teamChapterhouseUrl: "https://team.example.com",
40
+ standaloneMode: false,
41
+ cacheTtlMinutes: 60,
42
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis"],
43
+ wikiDir,
44
+ fetchImpl: async () => {
45
+ fetchCalls += 1;
46
+ throw new Error("network should not be used for fresh cache");
47
+ },
48
+ });
49
+ const content = await sync.fetchPage(pagePath);
50
+ assert.equal(content, "# Cached roadmap\n");
51
+ assert.equal(fetchCalls, 0);
52
+ });
53
+ test("re-fetches when cache is stale", async () => {
54
+ const teamSyncModule = await loadTeamSyncModule();
55
+ assert.ok(teamSyncModule, "team sync module should exist");
56
+ const wikiDir = makeWikiDir("stale-cache");
57
+ const cacheRoot = join(wikiDir, ".team-cache");
58
+ const pagePath = "pages/okrs/q2.md";
59
+ mkdirSync(join(cacheRoot, "pages", "okrs"), { recursive: true });
60
+ writeFileSync(join(cacheRoot, pagePath), "# Stale\n");
61
+ writeFileSync(join(cacheRoot, "manifest.json"), JSON.stringify({
62
+ [pagePath]: {
63
+ fetchedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
64
+ },
65
+ }));
66
+ const sync = new teamSyncModule.TeamWikiSync({
67
+ teamChapterhouseUrl: "https://team.example.com",
68
+ standaloneMode: false,
69
+ cacheTtlMinutes: 60,
70
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis"],
71
+ wikiDir,
72
+ fetchImpl: async (input) => {
73
+ assert.equal(String(input), "https://team.example.com/api/team/wiki/pages%2Fokrs%2Fq2.md");
74
+ return new Response(JSON.stringify({
75
+ path: pagePath,
76
+ content: "# Fresh Q2\n",
77
+ exists: true,
78
+ }), {
79
+ status: 200,
80
+ headers: {
81
+ "content-type": "application/json",
82
+ etag: '"fresh-q2"',
83
+ },
84
+ });
85
+ },
86
+ });
87
+ const content = await sync.fetchPage(pagePath);
88
+ assert.equal(content, "# Fresh Q2\n");
89
+ assert.equal(readFileSync(join(cacheRoot, pagePath), "utf-8"), "# Fresh Q2\n");
90
+ const manifest = JSON.parse(readFileSync(join(cacheRoot, "manifest.json"), "utf-8"));
91
+ assert.equal(manifest[pagePath]?.etag, '"fresh-q2"');
92
+ });
93
+ test("returns stale cache on network failure with warning", async () => {
94
+ const teamSyncModule = await loadTeamSyncModule();
95
+ assert.ok(teamSyncModule, "team sync module should exist");
96
+ const wikiDir = makeWikiDir("stale-fallback");
97
+ const cacheRoot = join(wikiDir, ".team-cache");
98
+ const pagePath = "pages/kpis/reliability.md";
99
+ mkdirSync(join(cacheRoot, "pages", "kpis"), { recursive: true });
100
+ writeFileSync(join(cacheRoot, pagePath), "# Cached KPI\n");
101
+ writeFileSync(join(cacheRoot, "manifest.json"), JSON.stringify({
102
+ [pagePath]: {
103
+ fetchedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
104
+ },
105
+ }));
106
+ const warnings = [];
107
+ const sync = new teamSyncModule.TeamWikiSync({
108
+ teamChapterhouseUrl: "https://team.example.com",
109
+ standaloneMode: false,
110
+ cacheTtlMinutes: 60,
111
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis"],
112
+ wikiDir,
113
+ fetchImpl: async () => {
114
+ throw new Error("network unavailable");
115
+ },
116
+ warn: (message) => warnings.push(message),
117
+ });
118
+ const content = await sync.fetchPage(pagePath);
119
+ assert.equal(content, "# Cached KPI\n");
120
+ assert.equal(warnings.length, 1);
121
+ assert.match(warnings[0] ?? "", /stale cache/i);
122
+ });
123
+ test("returns null when no cache exists and network fails", async () => {
124
+ const teamSyncModule = await loadTeamSyncModule();
125
+ assert.ok(teamSyncModule, "team sync module should exist");
126
+ const wikiDir = makeWikiDir("cold-miss");
127
+ const sync = new teamSyncModule.TeamWikiSync({
128
+ teamChapterhouseUrl: "https://team.example.com",
129
+ standaloneMode: false,
130
+ cacheTtlMinutes: 60,
131
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis"],
132
+ wikiDir,
133
+ fetchImpl: async () => {
134
+ throw new Error("offline");
135
+ },
136
+ });
137
+ const content = await sync.fetchPage("pages/team/vision.md");
138
+ assert.equal(content, null);
139
+ });
140
+ test("isTeamPath matches configured team wiki prefixes", async () => {
141
+ const teamSyncModule = await loadTeamSyncModule();
142
+ assert.ok(teamSyncModule, "team sync module should exist");
143
+ const sync = new teamSyncModule.TeamWikiSync({
144
+ teamChapterhouseUrl: "https://team.example.com",
145
+ standaloneMode: false,
146
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis", "pages/shared"],
147
+ wikiDir: makeWikiDir("team-paths"),
148
+ });
149
+ assert.equal(sync.isTeamPath("pages/team/vision.md"), true);
150
+ assert.equal(sync.isTeamPath("pages/okrs/2026-q2.md"), true);
151
+ assert.equal(sync.isTeamPath("pages/kpis/reliability.md"), true);
152
+ assert.equal(sync.isTeamPath("pages/shared/runbooks/deploy.md"), true);
153
+ assert.equal(sync.isTeamPath("pages/private/notes.md"), false);
154
+ });
155
+ test("pushUpdate silently no-ops when team wiki sync is disabled", async () => {
156
+ const teamSyncModule = await loadTeamSyncModule();
157
+ assert.ok(teamSyncModule, "team sync module should exist");
158
+ let called = false;
159
+ const sync = new teamSyncModule.TeamWikiSync({
160
+ teamChapterhouseUrl: "",
161
+ standaloneMode: false,
162
+ wikiDir: makeWikiDir("disabled-push"),
163
+ fetchImpl: async () => {
164
+ called = true;
165
+ throw new Error("fetch should not run");
166
+ },
167
+ });
168
+ assert.deepEqual(await sync.pushUpdate({
169
+ engineerId: "eng-1",
170
+ activity: "shipped standalone mode",
171
+ krId: "O1-KR2",
172
+ delta: 5,
173
+ }), {
174
+ ok: false,
175
+ disabled: true,
176
+ payload: {
177
+ engineerId: "eng-1",
178
+ activity: "shipped standalone mode",
179
+ krId: "O1-KR2",
180
+ delta: 5,
181
+ },
182
+ });
183
+ assert.equal(called, false);
184
+ });
185
+ //# sourceMappingURL=team-sync.test.js.map
@@ -0,0 +1,98 @@
1
+ function formatQuarterLabel(quarter) {
2
+ const match = quarter.match(/^(\d{4})-Q([1-4])$/);
3
+ if (!match) {
4
+ throw new Error(`Quarter must be in YYYY-QN format. Received: ${quarter}`);
5
+ }
6
+ return `${match[1]} Q${match[2]}`;
7
+ }
8
+ function getQuarterPeriod(quarter) {
9
+ const match = quarter.match(/^(\d{4})-Q([1-4])$/);
10
+ if (!match) {
11
+ throw new Error(`Quarter must be in YYYY-QN format. Received: ${quarter}`);
12
+ }
13
+ const year = Number(match[1]);
14
+ const quarterNumber = Number(match[2]);
15
+ const startMonth = (quarterNumber - 1) * 3;
16
+ const start = new Date(Date.UTC(year, startMonth, 1));
17
+ const end = new Date(Date.UTC(year, startMonth + 3, 0));
18
+ return {
19
+ start: start.toISOString().slice(0, 10),
20
+ end: end.toISOString().slice(0, 10),
21
+ };
22
+ }
23
+ function formatMetric(value, unit) {
24
+ return unit === "%" ? `${value}%` : `${value} ${unit}`;
25
+ }
26
+ export function generateOKRQuarterPage(quarter, objectives) {
27
+ const label = formatQuarterLabel(quarter);
28
+ const period = getQuarterPeriod(quarter);
29
+ const lines = [
30
+ `# OKRs — ${label}`,
31
+ "",
32
+ `> Period: ${period.start} to ${period.end}`,
33
+ "",
34
+ ];
35
+ objectives.forEach((objective, objectiveIndex) => {
36
+ lines.push(`## ${objective.id}: ${objective.title}`);
37
+ lines.push(`**Owner**: ${objective.owner}`);
38
+ lines.push("");
39
+ objective.keyResults.forEach((keyResult) => {
40
+ lines.push(`### ${keyResult.id}: ${keyResult.title}`);
41
+ lines.push(`- **Owner**: ${keyResult.owner}`);
42
+ lines.push(`- **Target**: ${formatMetric(keyResult.targetValue, keyResult.unit)}`);
43
+ lines.push(`- **Current**: ${formatMetric(keyResult.currentValue, keyResult.unit)}`);
44
+ lines.push(`- **Unit**: ${keyResult.unit}`);
45
+ lines.push(`- **Due**: ${keyResult.dueDate}`);
46
+ lines.push("- **ADO Work Item**: <!-- fill in after ADO setup -->");
47
+ lines.push("");
48
+ });
49
+ if (objectiveIndex < objectives.length - 1) {
50
+ lines.push("---");
51
+ lines.push("");
52
+ }
53
+ });
54
+ return `${lines.join("\n").trimEnd()}\n`;
55
+ }
56
+ export function generateKPIPage(kpis) {
57
+ const lines = [
58
+ "# Team KPIs",
59
+ "",
60
+ "| KPI | Owner | Target | Current | Unit | Frequency |",
61
+ "|-----|-------|--------|---------|------|-----------|",
62
+ ...kpis.map((kpi) => `| ${kpi.name} | ${kpi.owner} | ${kpi.target} | ${kpi.current} | ${kpi.unit} | ${kpi.frequency} |`),
63
+ "",
64
+ ];
65
+ return lines.join("\n");
66
+ }
67
+ export function generateTeamMemberPage(member) {
68
+ const ownership = member.okrOwnership.length > 0
69
+ ? member.okrOwnership.map((krId) => `- ${krId}`)
70
+ : ["- _No KR ownership assigned yet._"];
71
+ return [
72
+ `# ${member.name}`,
73
+ "",
74
+ `**Email**: ${member.email}`,
75
+ `**Role**: ${member.role}`,
76
+ `**Entra Object ID**: ${member.entraObjectId}`,
77
+ "",
78
+ "## OKR Ownership",
79
+ ...ownership,
80
+ "",
81
+ ].join("\n");
82
+ }
83
+ export function generateTeamIndexPage(members) {
84
+ return [
85
+ "# Team Directory",
86
+ "",
87
+ "| Name | Role | Email | Entra Object ID | OKR Ownership |",
88
+ "|------|------|-------|-----------------|---------------|",
89
+ ...members.map((member) => {
90
+ const ownership = member.okrOwnership.length > 0
91
+ ? member.okrOwnership.join(", ")
92
+ : "_Unassigned_";
93
+ return `| ${member.name} | ${member.role} | ${member.email} | ${member.entraObjectId} | ${ownership} |`;
94
+ }),
95
+ "",
96
+ ].join("\n");
97
+ }
98
+ //# sourceMappingURL=okr.js.map
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "chapterhouse",
3
+ "version": "0.1.1",
4
+ "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
+ "bin": {
6
+ "chapterhouse": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist/**/*.js",
10
+ "agents/",
11
+ "skills/",
12
+ "web/dist/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc && npm --prefix web run build",
18
+ "build:server": "tsc",
19
+ "build:web": "npm --prefix web run build",
20
+ "clean": "rm -rf dist",
21
+ "daemon": "tsx src/daemon.ts",
22
+ "dev:server": "tsx --watch src/daemon.ts",
23
+ "dev:web": "npm --prefix web run dev",
24
+ "dev": "tsx --watch src/daemon.ts",
25
+ "test": "npm run clean && npm run build:server && node --experimental-test-module-mocks --import ./dist/test/setup-env.js --test 'dist/**/*.test.js'",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.5.0"
30
+ },
31
+ "keywords": [
32
+ "copilot",
33
+ "orchestrator",
34
+ "ai",
35
+ "cli",
36
+ "web"
37
+ ],
38
+ "author": "Brian Ketelsen",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/bketelsen/chapterhouse.git"
43
+ },
44
+ "homepage": "https://github.com/bketelsen/chapterhouse#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/bketelsen/chapterhouse/issues"
47
+ },
48
+ "type": "module",
49
+ "dependencies": {
50
+ "@bradygaster/squad-sdk": "0.9.4",
51
+ "@github/copilot-sdk": "^0.3.0",
52
+ "azure-devops-node-api": "^15.1.2",
53
+ "better-sqlite3": "^12.6.2",
54
+ "cors": "^2.8.6",
55
+ "dotenv": "^17.3.1",
56
+ "express": "^5.2.1",
57
+ "helmet": "^8.1.0",
58
+ "jsonwebtoken": "^9.0.3",
59
+ "jwks-rsa": "^4.0.1",
60
+ "zod": "^4.3.6"
61
+ },
62
+ "devDependencies": {
63
+ "@bradygaster/squad-cli": "^0.9.4",
64
+ "@types/better-sqlite3": "^7.6.13",
65
+ "@types/cors": "^2.8.19",
66
+ "@types/express": "^5.0.6",
67
+ "@types/jsonwebtoken": "^9.0.10",
68
+ "@types/node": "^25.6.0",
69
+ "tsx": "^4.21.0",
70
+ "typescript": "^5.9.3"
71
+ }
72
+ }
File without changes
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: find-skills
3
+ description: Helps users discover agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. Always ask the user for permission before installing any skill, and flag security risks.
4
+ ---
5
+
6
+ # Find Skills
7
+
8
+ Discover and install skills from the open agent skills ecosystem at https://skills.sh/.
9
+
10
+ ## When to Use
11
+
12
+ Use this skill when the user:
13
+
14
+ - Asks "how do I do X" where X might be a common task with an existing skill
15
+ - Says "find a skill for X" or "is there a skill for X"
16
+ - Asks "can you do X" where X is a specialized capability
17
+ - Expresses interest in extending agent capabilities
18
+ - Wants to search for tools, templates, or workflows
19
+
20
+ ## Search & Present
21
+
22
+ Do these two steps in a worker session — they can run in parallel:
23
+
24
+ ### 1. Search the API
25
+
26
+ ```bash
27
+ curl -s "https://skills.sh/api/search?q=QUERY"
28
+ ```
29
+
30
+ Replace `QUERY` with a URL-encoded search term (e.g., `react`, `email`, `pr+review`). The response is JSON with skills sorted by installs (most popular first):
31
+
32
+ ```json
33
+ {
34
+ "skills": [
35
+ {
36
+ "id": "vercel-labs/agent-skills/vercel-react-best-practices",
37
+ "skillId": "vercel-react-best-practices",
38
+ "name": "vercel-react-best-practices",
39
+ "installs": 174847,
40
+ "source": "vercel-labs/agent-skills"
41
+ }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ ### 2. Fetch Security Audits
47
+
48
+ **Required — do not skip.** Use the `web_fetch` tool to get the audits page:
49
+
50
+ ```
51
+ web_fetch url="https://skills.sh/audits"
52
+ ```
53
+
54
+ If `web_fetch` fails or returns unexpected content, still present the search results but show "⚠️ Audit unavailable" for all security columns and include a link to https://skills.sh/audits so the user can check manually.
55
+
56
+ This returns markdown where each skill has a heading (`### skill-name`) followed by its source, then three security scores:
57
+
58
+ - **Gen Agent Trust Hub**: Safe / Med Risk / Critical
59
+ - **Socket**: Number of alerts (0 is best)
60
+ - **Snyk**: Low Risk / Med Risk / High Risk / Critical
61
+
62
+ Scan the returned markdown to find scores for each skill from your search results. Match by both **skill name** and **full source** (`owner/repo`) to avoid misattribution — different repos can have skills with the same name.
63
+
64
+ ### 3. Present Combined Results
65
+
66
+ Cross-reference the search results with the audit data and format as a numbered table. Show the top 6-8 results sorted by installs:
67
+
68
+ ```
69
+ # Skill Publisher Installs Gen Socket Snyk
70
+ ─ ───────────────────────────── ───────────── ──────── ───── ────── ────────
71
+ 1 vercel-react-best-practices vercel-labs 175.3K ✅Safe ✅ 0 ✅Low
72
+ 2 web-design-guidelines vercel-labs 135.8K ✅Safe ✅ 0 ⚠️Med
73
+ 3 frontend-design anthropics 122.6K ✅Safe ✅ 0 ✅Low
74
+ 4 remotion-best-practices remotion-dev 125.2K ✅Safe ✅ 0 ⚠️Med
75
+ 5 browser-use browser-use 45.0K ⚠️Med 🔴 1 🔴High
76
+ ```
77
+
78
+ **Formatting:**
79
+ - Sort by installs descending
80
+ - Format counts: 1000+ → "1.0K", 1000000+ → "1.0M"
81
+ - ✅ for Safe / Low Risk / 0 alerts, ⚠️ for Med Risk, 🔴 for High Risk / Critical / 1+ alerts
82
+ - If a skill has no audit data, show "⚠️ N/A" — never leave security blank
83
+ - Publisher = first part of `source` field (before `/`)
84
+
85
+ After the table:
86
+
87
+ ```
88
+ 🔗 Browse all: https://skills.sh/
89
+
90
+ Pick a number to install (or "none")
91
+ ```
92
+
93
+ ## Install
94
+
95
+ **NEVER install without the user picking a number first.**
96
+
97
+ When the user picks a skill:
98
+
99
+ ### Security Gate
100
+
101
+ If ANY of its three audit scores is not green (Safe / 0 alerts / Low Risk), warn before proceeding:
102
+
103
+ ```
104
+ ⚠️ "{skill-name}" has security concerns:
105
+ • Gen Agent Trust Hub: {score}
106
+ • Socket: {count} alerts
107
+ • Snyk: {score}
108
+
109
+ Want to proceed anyway, or pick a different skill?
110
+ ```
111
+
112
+ Wait for explicit confirmation. Do not install if the user says no.
113
+
114
+ ### Fetch & Install
115
+
116
+ 1. **Fetch the SKILL.md** from GitHub. The `source` field is `owner/repo` and `skillId` is the directory:
117
+
118
+ ```bash
119
+ curl -fsSL "https://raw.githubusercontent.com/{source}/main/{skillId}/SKILL.md" || \
120
+ curl -fsSL "https://raw.githubusercontent.com/{source}/master/{skillId}/SKILL.md"
121
+ ```
122
+
123
+ If both fail, tell the user and link to `https://github.com/{source}`.
124
+
125
+ 2. **Validate** the fetched content: it must not be empty and should contain meaningful instructions (more than just a title). If the content is empty, an HTML error page, or clearly not a SKILL.md, do NOT install — tell the user it couldn't be fetched properly.
126
+
127
+ 3. **Install** using the `learn_skill` tool:
128
+ - `slug`: the `skillId` from the API
129
+ - `name`: from the SKILL.md frontmatter `name:` field (between `---` markers). If no frontmatter, use `skillId`.
130
+ - `description`: from the SKILL.md frontmatter `description:` field. If none, use the first sentence.
131
+ - `instructions`: if frontmatter exists, use the content after the closing `---`. If no frontmatter, use the full fetched content as instructions.
132
+
133
+ **Always install to ~/.chapterhouse/skills/ via learn_skill. Never install globally.**
134
+
135
+ ## Behavioral Security Review
136
+
137
+ In addition to audit scores, review the fetched SKILL.md content before installing. Flag concerns if the skill:
138
+
139
+ - **Runs arbitrary shell commands** or executes code on the user's machine
140
+ - **Accesses sensitive data** — credentials, API keys, SSH keys, personal files
141
+ - **Makes network requests** to external services (data exfiltration risk)
142
+ - **Comes from an unknown or unverified source** with no audit data
143
+
144
+ If any of these apply, warn the user with specifics even if audit scores are green:
145
+
146
+ ```
147
+ ⚠️ Note: "{skill-name}" requests shell access and reads files from your home directory.
148
+ This is common for CLI-integration skills, but worth knowing. Proceed?
149
+ ```
150
+
151
+ ## When No Skills Are Found
152
+
153
+ If the API returns no results:
154
+
155
+ 1. Tell the user no existing skill was found
156
+ 2. Offer to help directly with your general capabilities
157
+ 3. Suggest building a custom skill if the task is worth automating
158
+
159
+ ## Uninstalling
160
+
161
+ Use the `uninstall_skill` tool with the skill's slug to remove it from `~/.chapterhouse/skills/`.
@@ -0,0 +1,4 @@
1
+ {
2
+ "slug": "find-skills",
3
+ "version": "1.0.0"
4
+ }