pi-roundtable 0.1.0

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 (315) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/docs/plugins.md +924 -0
  5. package/examples/channels.test.ts +13 -0
  6. package/examples/channels.ts +27 -0
  7. package/examples/dashboard.test.ts +11 -0
  8. package/examples/dashboard.ts +9 -0
  9. package/examples/events.test.ts +26 -0
  10. package/examples/events.ts +25 -0
  11. package/examples/guide.test.ts +52 -0
  12. package/examples/holds.test.ts +16 -0
  13. package/examples/holds.ts +30 -0
  14. package/examples/http.test.ts +12 -0
  15. package/examples/http.ts +17 -0
  16. package/examples/interactions.test.ts +25 -0
  17. package/examples/interactions.ts +28 -0
  18. package/examples/migrations.test.ts +31 -0
  19. package/examples/migrations.ts +42 -0
  20. package/examples/packages.test.ts +9 -0
  21. package/examples/packages.ts +9 -0
  22. package/examples/preflight.test.ts +11 -0
  23. package/examples/preflight.ts +19 -0
  24. package/examples/prompt.test.ts +25 -0
  25. package/examples/prompt.ts +20 -0
  26. package/examples/providers.test.ts +10 -0
  27. package/examples/providers.ts +17 -0
  28. package/examples/seeds.test.ts +12 -0
  29. package/examples/seeds.ts +18 -0
  30. package/examples/selection.test.ts +13 -0
  31. package/examples/selection.ts +14 -0
  32. package/examples/services.test.ts +20 -0
  33. package/examples/services.ts +30 -0
  34. package/examples/session-tools.test.ts +24 -0
  35. package/examples/session-tools.ts +34 -0
  36. package/examples/tools.test.ts +18 -0
  37. package/examples/tools.ts +28 -0
  38. package/package.json +55 -0
  39. package/src/cli/add-plugin.test.ts +107 -0
  40. package/src/cli/add-plugin.ts +75 -0
  41. package/src/cli/checks/basic.test.ts +276 -0
  42. package/src/cli/checks/bun.ts +26 -0
  43. package/src/cli/checks/configuration.ts +28 -0
  44. package/src/cli/checks/database.test.ts +117 -0
  45. package/src/cli/checks/database.ts +100 -0
  46. package/src/cli/checks/discord.test.ts +200 -0
  47. package/src/cli/checks/discord.ts +218 -0
  48. package/src/cli/checks/environment.ts +27 -0
  49. package/src/cli/checks/model.ts +28 -0
  50. package/src/cli/checks/public-url.ts +40 -0
  51. package/src/cli/cli.test.ts +122 -0
  52. package/src/cli/cli.ts +166 -0
  53. package/src/cli/config-edit.test.ts +130 -0
  54. package/src/cli/config-edit.ts +199 -0
  55. package/src/cli/discord-api.ts +96 -0
  56. package/src/cli/doctor.test.ts +184 -0
  57. package/src/cli/doctor.ts +92 -0
  58. package/src/cli/http.ts +23 -0
  59. package/src/cli/init.test.ts +127 -0
  60. package/src/cli/init.ts +82 -0
  61. package/src/cli/main.ts +13 -0
  62. package/src/cli/project.ts +123 -0
  63. package/src/cli/report.ts +68 -0
  64. package/src/cli/roundtable.mjs +22 -0
  65. package/src/cli/runtime.ts +53 -0
  66. package/src/cli/size.test.ts +9 -0
  67. package/src/cli/start.ts +33 -0
  68. package/src/cli/templates.test.ts +129 -0
  69. package/src/cli/templates.ts +119 -0
  70. package/src/cli/testing/fixtures.ts +106 -0
  71. package/src/core/agents/agent-claim.test.ts +142 -0
  72. package/src/core/agents/agent-claim.ts +170 -0
  73. package/src/core/agents/agent-dashboard.test.ts +120 -0
  74. package/src/core/agents/agent-dashboard.ts +185 -0
  75. package/src/core/agents/agent-guild.test.ts +124 -0
  76. package/src/core/agents/agent-messages.ts +189 -0
  77. package/src/core/agents/agent-ports.ts +103 -0
  78. package/src/core/agents/agent-prompt.ts +163 -0
  79. package/src/core/agents/agent-rules.ts +106 -0
  80. package/src/core/agents/agent-schema.ts +115 -0
  81. package/src/core/agents/agent-settings.ts +49 -0
  82. package/src/core/agents/agent-store.test.ts +205 -0
  83. package/src/core/agents/agent-store.ts +384 -0
  84. package/src/core/agents/agent-team.ts +445 -0
  85. package/src/core/agents/agent-tools.ts +386 -0
  86. package/src/core/agents/avatar-studio.test.ts +88 -0
  87. package/src/core/agents/avatar-studio.ts +147 -0
  88. package/src/core/agents/group-messages.ts +99 -0
  89. package/src/core/agents/group-round.test.ts +143 -0
  90. package/src/core/agents/group-round.ts +117 -0
  91. package/src/core/agents/group-turns.ts +132 -0
  92. package/src/core/agents/owner-identity.test.ts +172 -0
  93. package/src/core/agents/team-editing.ts +205 -0
  94. package/src/core/agents/team-keys.ts +40 -0
  95. package/src/core/agents/team-layout.ts +135 -0
  96. package/src/core/agents/team-lifecycle.ts +188 -0
  97. package/src/core/agents/team-options.ts +52 -0
  98. package/src/core/agents/team-status.ts +80 -0
  99. package/src/core/agents/team-text.ts +60 -0
  100. package/src/core/agents/team-turn-types.ts +107 -0
  101. package/src/core/agents/team-turns.test.ts +278 -0
  102. package/src/core/agents/team-turns.ts +323 -0
  103. package/src/core/assets/neutral.png +0 -0
  104. package/src/core/assets/prompts/shared-guest.md +11 -0
  105. package/src/core/assets/prompts/shared.md +11 -0
  106. package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
  107. package/src/core/attachments/attachment-dir.ts +15 -0
  108. package/src/core/attachments/attachment-fetcher.ts +83 -0
  109. package/src/core/attachments/attachments.test.ts +85 -0
  110. package/src/core/attachments/image-prep.ts +50 -0
  111. package/src/core/attachments/prompt-block.ts +37 -0
  112. package/src/core/attachments/turn-attachments.ts +79 -0
  113. package/src/core/boundary.test.ts +45 -0
  114. package/src/core/builtin/agent-server.ts +275 -0
  115. package/src/core/builtin/discord.ts +95 -0
  116. package/src/core/builtin/modules.test.ts +127 -0
  117. package/src/core/builtin/modules.ts +176 -0
  118. package/src/core/builtin/seeds.ts +10 -0
  119. package/src/core/builtin/stores.ts +42 -0
  120. package/src/core/config/config.test.ts +135 -0
  121. package/src/core/config/config.ts +285 -0
  122. package/src/core/config/schema.ts +181 -0
  123. package/src/core/contract/channels.ts +152 -0
  124. package/src/core/contract/discord.ts +33 -0
  125. package/src/core/contract/providers.ts +67 -0
  126. package/src/core/db/guild-scope.ts +53 -0
  127. package/src/core/db/migrations.test.ts +237 -0
  128. package/src/core/db/migrations.ts +37 -0
  129. package/src/core/define-roundtable.test.ts +134 -0
  130. package/src/core/define-roundtable.ts +195 -0
  131. package/src/core/define.test.ts +144 -0
  132. package/src/core/define.ts +154 -0
  133. package/src/core/discord/agent-commands.test.ts +58 -0
  134. package/src/core/discord/agent-commands.ts +385 -0
  135. package/src/core/discord/agent-discord.test.ts +70 -0
  136. package/src/core/discord/agent-discord.ts +367 -0
  137. package/src/core/discord/channel-executor.ts +404 -0
  138. package/src/core/discord/channel-operations.ts +329 -0
  139. package/src/core/discord/discord-surface.ts +431 -0
  140. package/src/core/discord/dispatch-thread-host.test.ts +114 -0
  141. package/src/core/discord/dispatch-thread-host.ts +72 -0
  142. package/src/core/discord/dispatch-threads.test.ts +94 -0
  143. package/src/core/discord/dispatch-threads.ts +180 -0
  144. package/src/core/discord/interaction-module.ts +1 -0
  145. package/src/core/discord/owner-cards.test.ts +435 -0
  146. package/src/core/discord/owner-cards.ts +463 -0
  147. package/src/core/discord/owner-command.ts +126 -0
  148. package/src/core/discord/owner-discord-access.ts +183 -0
  149. package/src/core/discord/owner-discord-threads.ts +160 -0
  150. package/src/core/discord/owner-discord.test.ts +325 -0
  151. package/src/core/discord/owner-discord.ts +282 -0
  152. package/src/core/discord/owner-panel.ts +103 -0
  153. package/src/core/discord/schedule-commands.ts +149 -0
  154. package/src/core/discord/stop-button.ts +20 -0
  155. package/src/core/domain/attachment.ts +39 -0
  156. package/src/core/domain/conversation.ts +42 -0
  157. package/src/core/domain/errors.ts +31 -0
  158. package/src/core/domain/expression.ts +21 -0
  159. package/src/core/domain/owner-prompts.ts +45 -0
  160. package/src/core/domain/ports.ts +91 -0
  161. package/src/core/domain/profile.ts +32 -0
  162. package/src/core/drain.test.ts +46 -0
  163. package/src/core/drain.ts +33 -0
  164. package/src/core/errors.ts +29 -0
  165. package/src/core/events.test.ts +81 -0
  166. package/src/core/events.ts +55 -0
  167. package/src/core/holds.test.ts +54 -0
  168. package/src/core/holds.ts +46 -0
  169. package/src/core/host.test.ts +536 -0
  170. package/src/core/host.ts +297 -0
  171. package/src/core/http/listeners.test.ts +164 -0
  172. package/src/core/http/listeners.ts +151 -0
  173. package/src/core/i18n/agent-panel.ts +96 -0
  174. package/src/core/i18n/cards.ts +64 -0
  175. package/src/core/i18n/channels.ts +28 -0
  176. package/src/core/i18n/conversation.ts +49 -0
  177. package/src/core/i18n/dashboard.ts +72 -0
  178. package/src/core/i18n/discord.ts +23 -0
  179. package/src/core/i18n/en.ts +27 -0
  180. package/src/core/i18n/i18n.test.ts +139 -0
  181. package/src/core/i18n/index.ts +58 -0
  182. package/src/core/i18n/owner.ts +35 -0
  183. package/src/core/i18n/schedules.ts +92 -0
  184. package/src/core/i18n/time-zones.ts +27 -0
  185. package/src/core/i18n/types.ts +7 -0
  186. package/src/core/i18n/zh-tw.ts +26 -0
  187. package/src/core/identity.test.ts +18 -0
  188. package/src/core/identity.ts +31 -0
  189. package/src/core/judging/confirmation-judge.ts +68 -0
  190. package/src/core/judging/effort-judge.test.ts +112 -0
  191. package/src/core/judging/effort-judge.ts +158 -0
  192. package/src/core/judging/model-judge.test.ts +126 -0
  193. package/src/core/judging/model-judge.ts +193 -0
  194. package/src/core/log.ts +45 -0
  195. package/src/core/models.ts +52 -0
  196. package/src/core/modules/background/background-turns.ts +78 -0
  197. package/src/core/modules/delegation/delegate.ts +54 -0
  198. package/src/core/modules/delegation/delegator.test.ts +170 -0
  199. package/src/core/modules/delegation/delegator.ts +191 -0
  200. package/src/core/modules/delegation/sol-worker.ts +78 -0
  201. package/src/core/modules/discord-admin/discord-admin.ts +288 -0
  202. package/src/core/modules/host-shell/shell-policy.ts +386 -0
  203. package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
  204. package/src/core/modules/memory/owner-memory-store.ts +228 -0
  205. package/src/core/modules/memory/owner-memory.ts +153 -0
  206. package/src/core/modules/notify/notify.ts +26 -0
  207. package/src/core/modules/schedules/recurrence.ts +206 -0
  208. package/src/core/modules/schedules/schedule-store.ts +213 -0
  209. package/src/core/modules/schedules/schedule-tools.ts +196 -0
  210. package/src/core/modules/schedules/schedule.test.ts +358 -0
  211. package/src/core/modules/schedules/scheduler.ts +121 -0
  212. package/src/core/modules/schedules/schedules.ts +87 -0
  213. package/src/core/modules/skills/repo-name.ts +11 -0
  214. package/src/core/modules/skills/skill-kind.test.ts +55 -0
  215. package/src/core/modules/skills/skill-link.ts +69 -0
  216. package/src/core/modules/skills/skill-listing.ts +128 -0
  217. package/src/core/modules/skills/skill-registry.test.ts +376 -0
  218. package/src/core/modules/skills/skill-registry.ts +323 -0
  219. package/src/core/modules/skills/skill-rules.ts +120 -0
  220. package/src/core/modules/skills/skill-store.ts +285 -0
  221. package/src/core/modules/skills/skill-tools.ts +207 -0
  222. package/src/core/ops/error-reporter.test.ts +283 -0
  223. package/src/core/ops/error-reporter.ts +271 -0
  224. package/src/core/plugin.ts +200 -0
  225. package/src/core/presentation/card-cadence.ts +41 -0
  226. package/src/core/presentation/headline.ts +34 -0
  227. package/src/core/presentation/presentation.test.ts +166 -0
  228. package/src/core/presentation/quiet-links.ts +36 -0
  229. package/src/core/presentation/reply-splitter.ts +110 -0
  230. package/src/core/presentation/thinking-line.ts +24 -0
  231. package/src/core/public-entry.test.ts +59 -0
  232. package/src/core/registry/contributions.test.ts +215 -0
  233. package/src/core/registry/contributions.ts +249 -0
  234. package/src/core/registry/interactions.test.ts +87 -0
  235. package/src/core/registry/interactions.ts +55 -0
  236. package/src/core/registry/providers.test.ts +103 -0
  237. package/src/core/registry/providers.ts +55 -0
  238. package/src/core/routing/channel-queue.test.ts +31 -0
  239. package/src/core/routing/channel-queue.ts +44 -0
  240. package/src/core/routing/channel-router.test.ts +326 -0
  241. package/src/core/routing/channel-router.ts +157 -0
  242. package/src/core/routing/conversation-kind.ts +14 -0
  243. package/src/core/routing/forward-join.ts +65 -0
  244. package/src/core/routing/message-text.ts +16 -0
  245. package/src/core/routing/settle-turn.test.ts +21 -0
  246. package/src/core/routing/settle-turn.ts +26 -0
  247. package/src/core/runtime/compaction-tiers.test.ts +227 -0
  248. package/src/core/runtime/compaction-tiers.ts +203 -0
  249. package/src/core/runtime/conversation-sessions.ts +162 -0
  250. package/src/core/runtime/extensions/agent-prompt.ts +16 -0
  251. package/src/core/runtime/extensions/ask-user.test.ts +114 -0
  252. package/src/core/runtime/extensions/ask-user.ts +98 -0
  253. package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
  254. package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
  255. package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
  256. package/src/core/runtime/mcp.ts +23 -0
  257. package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
  258. package/src/core/runtime/pending-confirmation-store.ts +75 -0
  259. package/src/core/runtime/pi-agent-runtime.ts +477 -0
  260. package/src/core/runtime/prompt-slot.ts +81 -0
  261. package/src/core/runtime/runtime-types.ts +150 -0
  262. package/src/core/runtime/session-archive.test.ts +20 -0
  263. package/src/core/runtime/session-archive.ts +17 -0
  264. package/src/core/runtime/session-factory.ts +291 -0
  265. package/src/core/runtime/steerable-run.test.ts +264 -0
  266. package/src/core/runtime/steerable-run.ts +109 -0
  267. package/src/core/runtime/text-tools.test.ts +110 -0
  268. package/src/core/runtime/text-tools.ts +66 -0
  269. package/src/core/runtime/turn-answer.test.ts +72 -0
  270. package/src/core/runtime/turn-answer.ts +79 -0
  271. package/src/core/runtime/worker-task.test.ts +82 -0
  272. package/src/core/runtime/worker-task.ts +65 -0
  273. package/src/core/services.test.ts +28 -0
  274. package/src/core/services.ts +107 -0
  275. package/src/core/sessions.test.ts +85 -0
  276. package/src/core/sessions.ts +149 -0
  277. package/src/core/shared/attachment-reader.ts +91 -0
  278. package/src/core/shared/delegate-tool.ts +15 -0
  279. package/src/core/shared/mcp-adapter.ts +58 -0
  280. package/src/core/shared/package-dir.ts +9 -0
  281. package/src/core/shared/profile-tools.ts +16 -0
  282. package/src/core/shared/read-attachment-tool.ts +43 -0
  283. package/src/core/shared/schedule-tools.ts +118 -0
  284. package/src/core/shared/session-messages.ts +50 -0
  285. package/src/core/shared/tool-result.ts +9 -0
  286. package/src/core/shared/unix-server.ts +18 -0
  287. package/src/core/size.test.ts +9 -0
  288. package/src/core/speakers.test.ts +78 -0
  289. package/src/core/speakers.ts +143 -0
  290. package/src/core/testing/database.ts +52 -0
  291. package/src/core/testing/file-size.ts +34 -0
  292. package/src/core/testing/locale.ts +8 -0
  293. package/src/core/testing/modules.ts +97 -0
  294. package/src/core/testing/owner.ts +15 -0
  295. package/src/core/testing/thread-host.ts +60 -0
  296. package/src/core/time.test.ts +166 -0
  297. package/src/core/time.ts +67 -0
  298. package/src/core/tool-tiers.test.ts +117 -0
  299. package/src/core/tool-tiers.ts +127 -0
  300. package/src/entries.test.ts +112 -0
  301. package/src/index.ts +28 -0
  302. package/src/testing.test.ts +156 -0
  303. package/src/testing.ts +182 -0
  304. package/templates/.env.example +22 -0
  305. package/templates/README.md +11 -0
  306. package/templates/_gitignore +3 -0
  307. package/templates/agents.ts +12 -0
  308. package/templates/biome.json.tmpl +7 -0
  309. package/templates/docker-compose.yml +16 -0
  310. package/templates/package.json.tmpl +24 -0
  311. package/templates/persona/shared.md +2 -0
  312. package/templates/plugin/plugin.test.ts.tmpl +13 -0
  313. package/templates/plugin/plugin.ts +18 -0
  314. package/templates/roundtable.config.ts +24 -0
  315. package/templates/tsconfig.json.tmpl +16 -0
@@ -0,0 +1,323 @@
1
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { AgentError } from "../../domain/errors.ts";
4
+ import { planLinkedSkills } from "./skill-link.ts";
5
+ import { skillCatalog, skillListText } from "./skill-listing.ts";
6
+ import {
7
+ BUILTIN_SKILLS,
8
+ checkBody,
9
+ checkDescription,
10
+ checkSkillName,
11
+ type ResolvedSkill,
12
+ readSkillFile,
13
+ type SkillCatalogEntry,
14
+ type SkillRegistryOptions,
15
+ type SkillSet,
16
+ skillFileContent,
17
+ } from "./skill-rules.ts";
18
+ import type { SkillEntry } from "./skill-store.ts";
19
+
20
+ export {
21
+ BUILTIN_SKILLS,
22
+ checkSkillName,
23
+ MAX_SKILL_BODY_CHARS,
24
+ type ResolvedSkill,
25
+ type SkillCatalogEntry,
26
+ type SkillRegistryOptions,
27
+ type SkillSet,
28
+ type SkillSource,
29
+ } from "./skill-rules.ts";
30
+
31
+ /** The skill registry and what each agent carries (repos-and-skills spec behaviors 13–22). */
32
+ export class SkillRegistry {
33
+ readonly #options: SkillRegistryOptions;
34
+
35
+ constructor(options: SkillRegistryOptions) {
36
+ this.#options = options;
37
+ }
38
+
39
+ /** Writes every agent-written skill for Pi and removes folders left by deleted ones. */
40
+ init(): void {
41
+ const { store, writtenDir } = this.#options;
42
+ mkdirSync(writtenDir, { recursive: true });
43
+ const written = new Set<string>();
44
+ for (const entry of store.skills()) {
45
+ if (entry.kind !== "written") continue;
46
+ this.#write(entry);
47
+ written.add(entry.name);
48
+ }
49
+ for (const name of readdirSync(writtenDir))
50
+ if (!written.has(name))
51
+ rmSync(join(writtenDir, name), { recursive: true, force: true });
52
+ }
53
+
54
+ // ── Reading ────────────────────────────────────────────────────────────
55
+
56
+ /** The resolved built-in skills plus the named registered ones. */
57
+ resolve(names: readonly string[]): SkillSet {
58
+ const set: SkillSet = { skills: [], missing: [] };
59
+ for (const name of new Set([...BUILTIN_SKILLS, ...names])) {
60
+ const one = this.#resolveOne(name);
61
+ if ("reason" in one) set.missing.push(one);
62
+ else set.skills.push(one);
63
+ }
64
+ return set;
65
+ }
66
+
67
+ /** What an agent carries, built-in skills included. */
68
+ carried(agent: string): SkillSet {
69
+ const set = this.resolve(this.#options.store.carriedBy(agent));
70
+ for (const { name, reason } of set.missing)
71
+ this.#options.logger.warn(
72
+ { agent, skill: name, reason },
73
+ "carried skill missing",
74
+ );
75
+ return set;
76
+ }
77
+
78
+ /** The skills linked from a managed repository, as repo_list shows them. */
79
+ linkedFrom(repo: string): string[] {
80
+ return this.#options.store
81
+ .skills()
82
+ .filter((entry) => entry.kind === "linked" && entry.repo === repo)
83
+ .map((entry) => entry.name);
84
+ }
85
+
86
+ /** The registered skills an agent carries, as the dashboard lists them. */
87
+ carriedNames(agent: string): string[] {
88
+ return this.#options.store.carriedBy(agent);
89
+ }
90
+
91
+ /** An agent's skills as agent_get shows them (behavior 19). */
92
+ describeCarried(agent: string): string {
93
+ const set = this.resolve(this.#options.store.carriedBy(agent));
94
+ const lines = set.skills.map((skill) =>
95
+ (BUILTIN_SKILLS as readonly string[]).includes(skill.name)
96
+ ? `${skill.name} (built in)`
97
+ : skill.name,
98
+ );
99
+ for (const { name, reason } of set.missing)
100
+ lines.push(`${name} (missing: ${reason})`);
101
+ return lines.join(", ");
102
+ }
103
+
104
+ /** Every skill, built-in ones first, with what `list` shows as text (web app spec behavior 15). */
105
+ catalog(): SkillCatalogEntry[] {
106
+ return skillCatalog(this.#options.store, (name) => this.#resolveOne(name));
107
+ }
108
+
109
+ /** Every skill, or those of a group or matching a query (behavior 14). */
110
+ list(filter: { group?: string; query?: string } = {}): string {
111
+ return skillListText(
112
+ this.#options.store,
113
+ (name) => this.#resolveOne(name),
114
+ filter,
115
+ );
116
+ }
117
+
118
+ /** Throws when a name is not registered; built-in names are carried already. */
119
+ checkRegistered(names: readonly string[]): void {
120
+ const unknown = names.filter((name) => !this.#options.store.skill(name));
121
+ if (unknown.length > 0)
122
+ throw new AgentError(
123
+ `Unknown skills: ${unknown.join(", ")}. Find names with skill_list.${unknown.some((n) => (BUILTIN_SKILLS as readonly string[]).includes(n)) ? " Built-in skills are always carried." : ""}`,
124
+ );
125
+ }
126
+
127
+ // ── Changing ───────────────────────────────────────────────────────────
128
+
129
+ /** Links the skill at a repository path, or every child folder with a SKILL.md (behavior 15). */
130
+ async link(repo: string, path: string): Promise<string> {
131
+ const entries = planLinkedSkills(
132
+ this.#options.reposDir,
133
+ repo,
134
+ path,
135
+ (name) => this.#checkFreeName(name),
136
+ );
137
+ await this.#options.store.addLinked(entries);
138
+ const names = entries.map((e) => e.name);
139
+ return `Linked ${entries.length} skill${entries.length === 1 ? "" : "s"} from ${repo}: ${names.join(", ")}. Attach them with agent_skills.`;
140
+ }
141
+
142
+ async unlink(name: string): Promise<string> {
143
+ const entry = this.#entry(name);
144
+ if (entry.kind !== "linked")
145
+ throw new AgentError(
146
+ `${name} is written by an agent; remove it with skill_delete.`,
147
+ );
148
+ return this.#removed(name, await this.#options.store.remove(name));
149
+ }
150
+
151
+ async create(skill: {
152
+ name: string;
153
+ description: string;
154
+ body: string;
155
+ }): Promise<string> {
156
+ checkSkillName(skill.name);
157
+ this.#checkFreeName(skill.name);
158
+ checkDescription(skill.description);
159
+ checkBody(skill.body);
160
+ const entry = {
161
+ name: skill.name,
162
+ description: skill.description.trim(),
163
+ body: skill.body.trim(),
164
+ };
165
+ await this.#options.store.addWritten(entry);
166
+ this.#write(entry);
167
+ return `Created skill ${skill.name}. Attach it with agent_skills; it loads from each carrier's next turn.`;
168
+ }
169
+
170
+ async update(
171
+ name: string,
172
+ change: { description?: string; body?: string },
173
+ ): Promise<string> {
174
+ const entry = this.#writable(name);
175
+ if (change.description !== undefined) checkDescription(change.description);
176
+ if (change.body !== undefined) checkBody(change.body);
177
+ const next = {
178
+ name,
179
+ description: change.description?.trim() ?? entry.description,
180
+ body: change.body?.trim() ?? entry.body,
181
+ };
182
+ await this.#options.store.updateWritten(next);
183
+ this.#write(next);
184
+ return `Updated skill ${name}; its carriers read the new version from their next turn.`;
185
+ }
186
+
187
+ async delete(name: string): Promise<string> {
188
+ this.#writable(name);
189
+ const detached = await this.#options.store.remove(name);
190
+ rmSync(join(this.#options.writtenDir, name), {
191
+ recursive: true,
192
+ force: true,
193
+ });
194
+ return this.#removed(name, detached);
195
+ }
196
+
197
+ async setGroup(
198
+ name: string,
199
+ description: string,
200
+ skills: readonly string[],
201
+ ): Promise<string> {
202
+ checkSkillName(name);
203
+ if (!description.trim())
204
+ throw new AgentError("A skill group needs a description.");
205
+ const unknown = skills.filter(
206
+ (skill) =>
207
+ !this.#options.store.skill(skill) &&
208
+ !(BUILTIN_SKILLS as readonly string[]).includes(skill),
209
+ );
210
+ if (unknown.length > 0)
211
+ throw new AgentError(
212
+ `Unknown skills: ${unknown.join(", ")}. Nothing was changed.`,
213
+ );
214
+ const existed = this.#options.store.group(name) !== undefined;
215
+ await this.#options.store.setGroup({
216
+ name,
217
+ description: description.trim(),
218
+ skills: [...new Set(skills)],
219
+ });
220
+ return `${existed ? "Replaced" : "Created"} skill group ${name}: ${[...new Set(skills)].join(", ") || "empty"}.`;
221
+ }
222
+
223
+ async deleteGroup(name: string): Promise<string> {
224
+ if (!this.#options.store.group(name))
225
+ throw new AgentError(`There is no skill group "${name}".`);
226
+ await this.#options.store.removeGroup(name);
227
+ return `Deleted skill group ${name}; its skills are untouched.`;
228
+ }
229
+
230
+ /** Adds and removes an agent's skills together, or changes nothing (behavior 19). */
231
+ async attach(
232
+ agent: string,
233
+ add: readonly string[],
234
+ remove: readonly string[],
235
+ ): Promise<string[]> {
236
+ const builtin = remove.filter((name) =>
237
+ (BUILTIN_SKILLS as readonly string[]).includes(name),
238
+ );
239
+ if (builtin.length > 0)
240
+ throw new AgentError(
241
+ `${builtin.join(", ")} is built in and stays with every agent. Nothing was changed.`,
242
+ );
243
+ this.checkRegistered(add);
244
+ const current = this.#options.store.carriedBy(agent);
245
+ const next = [...new Set([...current, ...add])].filter(
246
+ (name) => !remove.includes(name),
247
+ );
248
+ await this.#options.store.setCarried(agent, next);
249
+ return this.#options.store.carriedBy(agent);
250
+ }
251
+
252
+ // ── Internals ──────────────────────────────────────────────────────────
253
+
254
+ #resolveOne(name: string): ResolvedSkill | { name: string; reason: string } {
255
+ const file = this.#fileOf(name);
256
+ if (!file) return { name, reason: "not registered" };
257
+ const read = readSkillFile(file);
258
+ if ("error" in read) return { name, reason: read.error };
259
+ if (read.name !== name)
260
+ return {
261
+ name,
262
+ reason: `its file is now named "${read.name}"; unlink it and link it again`,
263
+ };
264
+ return { name, description: read.description, file };
265
+ }
266
+
267
+ #fileOf(name: string): string | undefined {
268
+ const { store, reposDir, writtenDir, builtinDir } = this.#options;
269
+ if ((BUILTIN_SKILLS as readonly string[]).includes(name))
270
+ return join(builtinDir, name, "SKILL.md");
271
+ const entry = store.skill(name);
272
+ if (!entry) return undefined;
273
+ return entry.kind === "linked"
274
+ ? join(reposDir, entry.repo, entry.path, "SKILL.md")
275
+ : join(writtenDir, name, "SKILL.md");
276
+ }
277
+
278
+ #entry(name: string): SkillEntry {
279
+ const entry = this.#options.store.skill(name);
280
+ if (!entry)
281
+ throw new AgentError(
282
+ (BUILTIN_SKILLS as readonly string[]).includes(name)
283
+ ? `${name} is built in and cannot be changed.`
284
+ : `There is no skill "${name}"; see skill_list.`,
285
+ );
286
+ return entry;
287
+ }
288
+
289
+ #writable(name: string): Extract<SkillEntry, { kind: "written" }> {
290
+ const entry = this.#entry(name);
291
+ if (entry.kind !== "written")
292
+ throw new AgentError(
293
+ `${name} is linked from ${entry.repo}:${entry.path} and is read-only here. Change it in the repository with repo_task, then ship it with repo_change_report and repo_push.`,
294
+ );
295
+ return entry;
296
+ }
297
+
298
+ #checkFreeName(name: string): void {
299
+ if (
300
+ (BUILTIN_SKILLS as readonly string[]).includes(name) ||
301
+ this.#options.store.skill(name)
302
+ )
303
+ throw new AgentError(`A skill named "${name}" already exists.`);
304
+ }
305
+
306
+ #write(entry: { name: string; description: string; body: string }): void {
307
+ const dir = join(this.#options.writtenDir, entry.name);
308
+ mkdirSync(dir, { recursive: true });
309
+ writeFileSync(join(dir, "SKILL.md"), skillFileContent(entry));
310
+ }
311
+
312
+ #removed(name: string, detached: { agents: string[]; groups: string[] }) {
313
+ const from = [
314
+ detached.agents.length > 0
315
+ ? `agents ${detached.agents.join(", ")}`
316
+ : undefined,
317
+ detached.groups.length > 0
318
+ ? `groups ${detached.groups.join(", ")}`
319
+ : undefined,
320
+ ].filter(Boolean);
321
+ return `Removed skill ${name}${from.length > 0 ? `; detached from ${from.join(" and ")}` : ""}.`;
322
+ }
323
+ }
@@ -0,0 +1,120 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
3
+ import { AgentError } from "../../domain/errors.ts";
4
+ import type { Logger } from "../../log.ts";
5
+ import type { SkillStore } from "./skill-store.ts";
6
+
7
+ /** Carried by every agent and worker, shipped in the release (behavior 20). */
8
+ export const BUILTIN_SKILLS = ["writing-skills"] as const;
9
+
10
+ export const MAX_SKILL_BODY_CHARS = 12_000;
11
+ const MAX_DESCRIPTION_CHARS = 1_024;
12
+ export const MAX_LIST_DESCRIPTION_CHARS = 200;
13
+
14
+ /** A skill as a session loads it. */
15
+ export interface ResolvedSkill {
16
+ name: string;
17
+ description: string;
18
+ /** Its SKILL.md. */
19
+ file: string;
20
+ }
21
+
22
+ export interface SkillSet {
23
+ skills: ResolvedSkill[];
24
+ /** Carried skills whose file is gone or unreadable, with the reason. */
25
+ missing: { name: string; reason: string }[];
26
+ }
27
+
28
+ export type SkillSource =
29
+ | { kind: "builtin" }
30
+ | { kind: "linked"; repo: string; path: string }
31
+ | { kind: "written" };
32
+
33
+ /** One skill as the web app lists it; a skill whose file cannot be read has `missing` instead. */
34
+ export interface SkillCatalogEntry {
35
+ name: string;
36
+ source: SkillSource;
37
+ description?: string;
38
+ /** Its SKILL.md. */
39
+ file?: string;
40
+ missing?: string;
41
+ groups: string[];
42
+ /** Agents carrying it; built-in skills are carried by every agent without being listed. */
43
+ carriers: string[];
44
+ }
45
+
46
+ export interface SkillRegistryOptions {
47
+ store: SkillStore;
48
+ /** Managed repositories, as `<dir>/<owner>/<repo>`. */
49
+ reposDir: string;
50
+ /** Where agent-written skills are written for Pi to read. */
51
+ writtenDir: string;
52
+ /** The release's built-in skills, as `<dir>/<name>/SKILL.md`. */
53
+ builtinDir: string;
54
+ logger: Logger;
55
+ }
56
+
57
+ /** Pi's skill-name rule, so every registered name also loads. */
58
+ export function checkSkillName(name: string): void {
59
+ if (
60
+ !/^[a-z0-9-]{1,64}$/.test(name) ||
61
+ name.startsWith("-") ||
62
+ name.endsWith("-") ||
63
+ name.includes("--")
64
+ )
65
+ throw new AgentError(
66
+ `"${name}" is not a usable skill name: 1 to 64 lowercase letters, digits, and single hyphens, not at either end.`,
67
+ );
68
+ }
69
+
70
+ export function checkDescription(description: string): void {
71
+ if (!description.trim().startsWith("Use when"))
72
+ throw new AgentError(
73
+ 'A description states only when to use the skill and starts with "Use when".',
74
+ );
75
+ if (description.length > MAX_DESCRIPTION_CHARS)
76
+ throw new AgentError(
77
+ `A description has at most ${MAX_DESCRIPTION_CHARS} characters; this one has ${description.length}.`,
78
+ );
79
+ }
80
+
81
+ export function checkBody(body: string): void {
82
+ if (!body.trim() || body.length > MAX_SKILL_BODY_CHARS)
83
+ throw new AgentError(
84
+ `A skill body has 1 to ${MAX_SKILL_BODY_CHARS} characters; this one has ${body.length}.`,
85
+ );
86
+ }
87
+
88
+ /** A skill file's name and description as Pi reads them: the folder name stands in for a missing name. */
89
+ export function readSkillFile(
90
+ file: string,
91
+ ): { name: string; description: string } | { error: string } {
92
+ if (!existsSync(file)) return { error: "file not found" };
93
+ try {
94
+ const { frontmatter } = parseFrontmatter<Record<string, unknown>>(
95
+ readFileSync(file, "utf8"),
96
+ );
97
+ const description =
98
+ typeof frontmatter.description === "string"
99
+ ? frontmatter.description.trim()
100
+ : "";
101
+ if (!description) return { error: "no description in its frontmatter" };
102
+ const name =
103
+ typeof frontmatter.name === "string" && frontmatter.name
104
+ ? frontmatter.name
105
+ : (file.split("/").at(-2) ?? "");
106
+ return { name, description };
107
+ } catch (error) {
108
+ return { error: error instanceof Error ? error.message : String(error) };
109
+ }
110
+ }
111
+
112
+ export const skillFileContent = (entry: {
113
+ name: string;
114
+ description: string;
115
+ body: string;
116
+ }) =>
117
+ `---\nname: ${entry.name}\ndescription: ${JSON.stringify(entry.description.trim())}\n---\n\n${entry.body.trim()}\n`;
118
+
119
+ export const cut = (text: string, max: number) =>
120
+ text.length > max ? `${text.slice(0, max - 1)}…` : text;