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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AgentError } from "../../domain/errors.ts";
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_SKILLS,
|
|
4
|
+
cut,
|
|
5
|
+
MAX_LIST_DESCRIPTION_CHARS,
|
|
6
|
+
type ResolvedSkill,
|
|
7
|
+
type SkillCatalogEntry,
|
|
8
|
+
type SkillSource,
|
|
9
|
+
} from "./skill-rules.ts";
|
|
10
|
+
import type { SkillStore } from "./skill-store.ts";
|
|
11
|
+
|
|
12
|
+
/** Resolves a skill by name to its file, or says why it cannot. */
|
|
13
|
+
export type SkillResolver = (
|
|
14
|
+
name: string,
|
|
15
|
+
) => ResolvedSkill | { name: string; reason: string };
|
|
16
|
+
|
|
17
|
+
/** Every skill, built-in ones first, with what `list` shows as text (web app spec behavior 15). */
|
|
18
|
+
export function skillCatalog(
|
|
19
|
+
store: SkillStore,
|
|
20
|
+
resolveOne: SkillResolver,
|
|
21
|
+
): SkillCatalogEntry[] {
|
|
22
|
+
const sources: { name: string; source: SkillSource }[] = [
|
|
23
|
+
...BUILTIN_SKILLS.map((name) => ({
|
|
24
|
+
name,
|
|
25
|
+
source: { kind: "builtin" } as const,
|
|
26
|
+
})),
|
|
27
|
+
...store.skills().map((entry) => ({
|
|
28
|
+
name: entry.name,
|
|
29
|
+
source:
|
|
30
|
+
entry.kind === "linked"
|
|
31
|
+
? ({ kind: "linked", repo: entry.repo, path: entry.path } as const)
|
|
32
|
+
: ({ kind: "written" } as const),
|
|
33
|
+
})),
|
|
34
|
+
];
|
|
35
|
+
return sources.map(({ name, source }) => {
|
|
36
|
+
const one = resolveOne(name);
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
source,
|
|
40
|
+
...("reason" in one
|
|
41
|
+
? { missing: one.reason }
|
|
42
|
+
: { description: one.description, file: one.file }),
|
|
43
|
+
groups: store
|
|
44
|
+
.groups()
|
|
45
|
+
.filter((g) => g.skills.includes(name))
|
|
46
|
+
.map((g) => g.name),
|
|
47
|
+
carriers: store.carriers(name),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every skill, or those of a group or matching a query (behavior 14). */
|
|
53
|
+
export function skillListText(
|
|
54
|
+
store: SkillStore,
|
|
55
|
+
resolveOne: SkillResolver,
|
|
56
|
+
filter: { group?: string; query?: string } = {},
|
|
57
|
+
): string {
|
|
58
|
+
const group = filter.group ? store.group(filter.group) : undefined;
|
|
59
|
+
if (filter.group && !group)
|
|
60
|
+
throw new AgentError(
|
|
61
|
+
`There is no skill group "${filter.group}". Groups: ${
|
|
62
|
+
store
|
|
63
|
+
.groups()
|
|
64
|
+
.map((g) => g.name)
|
|
65
|
+
.join(", ") || "none"
|
|
66
|
+
}.`,
|
|
67
|
+
);
|
|
68
|
+
const query = filter.query?.toLowerCase();
|
|
69
|
+
const rows: string[] = [];
|
|
70
|
+
const builtins = BUILTIN_SKILLS.map((name) => ({
|
|
71
|
+
name,
|
|
72
|
+
source: "built in, carried by every agent and worker",
|
|
73
|
+
}));
|
|
74
|
+
const entries = [
|
|
75
|
+
...builtins,
|
|
76
|
+
...store.skills().map((entry) => ({
|
|
77
|
+
name: entry.name,
|
|
78
|
+
source:
|
|
79
|
+
entry.kind === "linked"
|
|
80
|
+
? `linked from ${entry.repo}:${entry.path}`
|
|
81
|
+
: "written by an agent",
|
|
82
|
+
})),
|
|
83
|
+
];
|
|
84
|
+
for (const { name, source } of entries) {
|
|
85
|
+
if (group && !group.skills.includes(name)) continue;
|
|
86
|
+
const one = resolveOne(name);
|
|
87
|
+
const description = "reason" in one ? "" : one.description;
|
|
88
|
+
if (
|
|
89
|
+
query &&
|
|
90
|
+
!name.includes(query) &&
|
|
91
|
+
!description.toLowerCase().includes(query)
|
|
92
|
+
)
|
|
93
|
+
continue;
|
|
94
|
+
const groups = store
|
|
95
|
+
.groups()
|
|
96
|
+
.filter((g) => g.skills.includes(name))
|
|
97
|
+
.map((g) => g.name);
|
|
98
|
+
const carriers = store.carriers(name);
|
|
99
|
+
const missing = "reason" in one ? ` — missing: ${one.reason}` : "";
|
|
100
|
+
rows.push(
|
|
101
|
+
[
|
|
102
|
+
`- ${name} [${source}]${missing}`,
|
|
103
|
+
description
|
|
104
|
+
? ` ${cut(description, MAX_LIST_DESCRIPTION_CHARS)}`
|
|
105
|
+
: undefined,
|
|
106
|
+
groups.length > 0 ? ` groups: ${groups.join(", ")}` : undefined,
|
|
107
|
+
carriers.length > 0
|
|
108
|
+
? ` carried by: ${carriers.join(", ")}`
|
|
109
|
+
: undefined,
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join("\n"),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const groupLines = store
|
|
116
|
+
.groups()
|
|
117
|
+
.map(
|
|
118
|
+
(g) =>
|
|
119
|
+
`- ${g.name}: ${g.description} (${g.skills.join(", ") || "empty"})`,
|
|
120
|
+
);
|
|
121
|
+
return [
|
|
122
|
+
`Skills${group ? ` in group ${group.name}` : ""}${query ? ` matching "${filter.query}"` : ""} (${rows.length}):`,
|
|
123
|
+
rows.join("\n") || "none",
|
|
124
|
+
"",
|
|
125
|
+
"Skill groups:",
|
|
126
|
+
groupLines.join("\n") || "none",
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { SQL } from "bun";
|
|
14
|
+
import { AgentError } from "../../domain/errors.ts";
|
|
15
|
+
import { silentLogger } from "../../log.ts";
|
|
16
|
+
import {
|
|
17
|
+
describeDb,
|
|
18
|
+
openTestStore,
|
|
19
|
+
TEST_GUILD,
|
|
20
|
+
type TestStore,
|
|
21
|
+
testDatabaseUrl,
|
|
22
|
+
} from "../../testing/database.ts";
|
|
23
|
+
import { SkillRegistry } from "./skill-registry.ts";
|
|
24
|
+
import { SkillStore } from "./skill-store.ts";
|
|
25
|
+
|
|
26
|
+
const BUILTIN = join(import.meta.dir, "..", "..", "assets", "skills");
|
|
27
|
+
|
|
28
|
+
// Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
|
|
29
|
+
describeDb("PostgreSQL", () => {
|
|
30
|
+
let store: TestStore<SkillStore>;
|
|
31
|
+
let registry: SkillRegistry;
|
|
32
|
+
let dir: string;
|
|
33
|
+
|
|
34
|
+
const skillFile = (name: string, description: string) =>
|
|
35
|
+
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`;
|
|
36
|
+
|
|
37
|
+
function repoSkill(
|
|
38
|
+
repo: string,
|
|
39
|
+
path: string,
|
|
40
|
+
name: string,
|
|
41
|
+
description = `Use when ${name}.`,
|
|
42
|
+
) {
|
|
43
|
+
mkdirSync(join(dir, "repos", repo, ".git"), { recursive: true });
|
|
44
|
+
mkdirSync(join(dir, "repos", repo, path), { recursive: true });
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(dir, "repos", repo, path, "SKILL.md"),
|
|
47
|
+
skillFile(name, description),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const open = async () => {
|
|
52
|
+
store = await openTestStore(SkillStore, TEST_GUILD);
|
|
53
|
+
registry = new SkillRegistry({
|
|
54
|
+
store,
|
|
55
|
+
reposDir: join(dir, "repos"),
|
|
56
|
+
writtenDir: join(dir, "skills"),
|
|
57
|
+
builtinDir: BUILTIN,
|
|
58
|
+
logger: silentLogger(),
|
|
59
|
+
});
|
|
60
|
+
registry.init();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await store?.close();
|
|
65
|
+
const admin = new SQL(testDatabaseUrl);
|
|
66
|
+
for (const table of ["skills", "skill_groups", "agent_skills"])
|
|
67
|
+
await admin.unsafe(`DROP TABLE IF EXISTS ${table}`);
|
|
68
|
+
await admin.close();
|
|
69
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
dir = mkdtempSync(join(tmpdir(), "roundtable-skills-"));
|
|
71
|
+
repoSkill("acme/skills", "skills/plan-work", "plan-work");
|
|
72
|
+
repoSkill("acme/skills", "skills/write-tests", "write-tests");
|
|
73
|
+
await open();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterAll(async () => {
|
|
77
|
+
await store.close();
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const refuses = async (run: () => Promise<unknown>, text: string) => {
|
|
82
|
+
const error = await run().then(
|
|
83
|
+
() => undefined,
|
|
84
|
+
(e: unknown) => e,
|
|
85
|
+
);
|
|
86
|
+
expect(error).toBeInstanceOf(AgentError);
|
|
87
|
+
expect((error as Error).message).toContain(text);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
describe("built-in skill", () => {
|
|
91
|
+
test("every set carries writing-skills from the release, with a trigger-only description", () => {
|
|
92
|
+
const set = registry.resolve([]);
|
|
93
|
+
expect(set.missing).toEqual([]);
|
|
94
|
+
expect(set.skills.map((s) => s.name)).toEqual(["writing-skills"]);
|
|
95
|
+
expect(set.skills[0]?.description.startsWith("Use when")).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("it cannot be removed, changed, or taken as a name", async () => {
|
|
99
|
+
await refuses(
|
|
100
|
+
() => registry.attach("notes", [], ["writing-skills"]),
|
|
101
|
+
"built in",
|
|
102
|
+
);
|
|
103
|
+
await refuses(
|
|
104
|
+
() => registry.update("writing-skills", { body: "x" }),
|
|
105
|
+
"built in",
|
|
106
|
+
);
|
|
107
|
+
await refuses(
|
|
108
|
+
() =>
|
|
109
|
+
registry.create({
|
|
110
|
+
name: "writing-skills",
|
|
111
|
+
description: "Use when x.",
|
|
112
|
+
body: "x",
|
|
113
|
+
}),
|
|
114
|
+
"already exists",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("linked skills", () => {
|
|
120
|
+
test("a folder of skills links every child with its own name and description", async () => {
|
|
121
|
+
const text = await registry.link("acme/skills", "skills");
|
|
122
|
+
expect(text).toContain("Linked 2 skills");
|
|
123
|
+
const set = registry.resolve(["plan-work", "write-tests"]);
|
|
124
|
+
expect(set.skills.map((s) => s.name)).toEqual([
|
|
125
|
+
"writing-skills",
|
|
126
|
+
"plan-work",
|
|
127
|
+
"write-tests",
|
|
128
|
+
]);
|
|
129
|
+
expect(set.skills[1]?.file).toBe(
|
|
130
|
+
join(dir, "repos/acme/skills/skills/plan-work/SKILL.md"),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a single skill folder links just that skill", async () => {
|
|
135
|
+
await registry.link("acme/skills", "./skills/plan-work/");
|
|
136
|
+
expect(store.skill("plan-work")).toEqual({
|
|
137
|
+
kind: "linked",
|
|
138
|
+
name: "plan-work",
|
|
139
|
+
repo: "acme/skills",
|
|
140
|
+
path: "skills/plan-work",
|
|
141
|
+
});
|
|
142
|
+
expect(store.skill("write-tests")).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a pull that changes the description shows on the next resolve", async () => {
|
|
146
|
+
await registry.link("acme/skills", "skills/plan-work");
|
|
147
|
+
repoSkill(
|
|
148
|
+
"acme/skills",
|
|
149
|
+
"skills/plan-work",
|
|
150
|
+
"plan-work",
|
|
151
|
+
"Use when changed.",
|
|
152
|
+
);
|
|
153
|
+
expect(registry.resolve(["plan-work"]).skills[1]?.description).toBe(
|
|
154
|
+
"Use when changed.",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("one taken name refuses the whole call", async () => {
|
|
159
|
+
await registry.link("acme/skills", "skills/plan-work");
|
|
160
|
+
await refuses(
|
|
161
|
+
() => registry.link("acme/skills", "skills"),
|
|
162
|
+
"Nothing was linked",
|
|
163
|
+
);
|
|
164
|
+
expect(store.skill("write-tests")).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("unmanaged repositories, escaping paths, and folders without skills are refused", async () => {
|
|
168
|
+
await refuses(
|
|
169
|
+
() => registry.link("acme/other", "skills"),
|
|
170
|
+
"not a managed repository",
|
|
171
|
+
);
|
|
172
|
+
await refuses(() => registry.link("acme/skills", "../../x"), "inside");
|
|
173
|
+
mkdirSync(join(dir, "repos/acme/skills/docs"), { recursive: true });
|
|
174
|
+
await refuses(() => registry.link("acme/skills", "docs"), "no SKILL.md");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("linked skills are read-only through the skill tools", async () => {
|
|
178
|
+
await registry.link("acme/skills", "skills/plan-work");
|
|
179
|
+
await refuses(
|
|
180
|
+
() => registry.update("plan-work", { body: "x" }),
|
|
181
|
+
"repo_task",
|
|
182
|
+
);
|
|
183
|
+
await refuses(() => registry.delete("plan-work"), "read-only");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a vanished or renamed file is reported missing, not loaded", async () => {
|
|
187
|
+
await registry.link("acme/skills", "skills");
|
|
188
|
+
rmSync(join(dir, "repos/acme/skills/skills/write-tests"), {
|
|
189
|
+
recursive: true,
|
|
190
|
+
});
|
|
191
|
+
repoSkill("acme/skills", "skills/plan-work", "plan-work-renamed");
|
|
192
|
+
const set = registry.resolve(["plan-work", "write-tests"]);
|
|
193
|
+
expect(set.skills.map((s) => s.name)).toEqual(["writing-skills"]);
|
|
194
|
+
expect(set.missing.map((m) => m.name)).toEqual([
|
|
195
|
+
"plan-work",
|
|
196
|
+
"write-tests",
|
|
197
|
+
]);
|
|
198
|
+
expect(set.missing[0]?.reason).toContain("plan-work-renamed");
|
|
199
|
+
expect(registry.list()).toContain("missing: file not found");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("agent-written skills", () => {
|
|
204
|
+
test("create writes a SKILL.md Pi reads back with the same name and description", async () => {
|
|
205
|
+
await registry.create({
|
|
206
|
+
name: "daily-notes",
|
|
207
|
+
description: 'Use when writing a "daily" note: summaries or logs.',
|
|
208
|
+
body: "# Daily notes\n\n1. Write.",
|
|
209
|
+
});
|
|
210
|
+
const file = join(dir, "skills/daily-notes/SKILL.md");
|
|
211
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(
|
|
212
|
+
readFileSync(file, "utf8"),
|
|
213
|
+
);
|
|
214
|
+
expect(frontmatter.name).toBe("daily-notes");
|
|
215
|
+
expect(frontmatter.description).toBe(
|
|
216
|
+
'Use when writing a "daily" note: summaries or logs.',
|
|
217
|
+
);
|
|
218
|
+
expect(body.trim()).toBe("# Daily notes\n\n1. Write.");
|
|
219
|
+
expect(registry.resolve(["daily-notes"]).skills[1]?.file).toBe(file);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("descriptions must be triggers, bodies bounded, names Pi-valid", async () => {
|
|
223
|
+
await refuses(
|
|
224
|
+
() =>
|
|
225
|
+
registry.create({
|
|
226
|
+
name: "x",
|
|
227
|
+
description: "Writes notes.",
|
|
228
|
+
body: "b",
|
|
229
|
+
}),
|
|
230
|
+
"Use when",
|
|
231
|
+
);
|
|
232
|
+
await refuses(
|
|
233
|
+
() =>
|
|
234
|
+
registry.create({
|
|
235
|
+
name: "x",
|
|
236
|
+
description: "Use when x.",
|
|
237
|
+
body: "b".repeat(12_001),
|
|
238
|
+
}),
|
|
239
|
+
"12000",
|
|
240
|
+
);
|
|
241
|
+
await refuses(
|
|
242
|
+
() =>
|
|
243
|
+
registry.create({
|
|
244
|
+
name: "Bad--name",
|
|
245
|
+
description: "Use when x.",
|
|
246
|
+
body: "b",
|
|
247
|
+
}),
|
|
248
|
+
"not a usable skill name",
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("update rewrites the file; delete removes it and detaches carriers and groups", async () => {
|
|
253
|
+
await registry.create({
|
|
254
|
+
name: "daily-notes",
|
|
255
|
+
description: "Use when a.",
|
|
256
|
+
body: "one",
|
|
257
|
+
});
|
|
258
|
+
await registry.update("daily-notes", { body: "two" });
|
|
259
|
+
expect(
|
|
260
|
+
readFileSync(join(dir, "skills/daily-notes/SKILL.md"), "utf8"),
|
|
261
|
+
).toContain("two");
|
|
262
|
+
await registry.attach("notes", ["daily-notes"], []);
|
|
263
|
+
await registry.setGroup("writing", "Writing skills.", [
|
|
264
|
+
"daily-notes",
|
|
265
|
+
"writing-skills",
|
|
266
|
+
]);
|
|
267
|
+
const text = await registry.delete("daily-notes");
|
|
268
|
+
expect(text).toBe(
|
|
269
|
+
"Removed skill daily-notes; detached from agents notes and groups writing.",
|
|
270
|
+
);
|
|
271
|
+
expect(existsSync(join(dir, "skills/daily-notes"))).toBe(false);
|
|
272
|
+
expect(store.carriedBy("notes")).toEqual([]);
|
|
273
|
+
expect(store.group("writing")?.skills).toEqual(["writing-skills"]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("a restart rewrites files from the database and clears stale folders", async () => {
|
|
277
|
+
await registry.create({
|
|
278
|
+
name: "daily-notes",
|
|
279
|
+
description: "Use when a.",
|
|
280
|
+
body: "one",
|
|
281
|
+
});
|
|
282
|
+
rmSync(join(dir, "skills/daily-notes"), { recursive: true });
|
|
283
|
+
mkdirSync(join(dir, "skills/stale"), { recursive: true });
|
|
284
|
+
await store.close();
|
|
285
|
+
await open();
|
|
286
|
+
expect(existsSync(join(dir, "skills/daily-notes/SKILL.md"))).toBe(true);
|
|
287
|
+
expect(existsSync(join(dir, "skills/stale"))).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("groups and carrying", () => {
|
|
292
|
+
test("attach adds and removes together and survives a restart", async () => {
|
|
293
|
+
await registry.link("acme/skills", "skills");
|
|
294
|
+
expect(
|
|
295
|
+
await registry.attach("infra", ["plan-work", "write-tests"], []),
|
|
296
|
+
).toEqual(["plan-work", "write-tests"]);
|
|
297
|
+
expect(await registry.attach("infra", [], ["write-tests"])).toEqual([
|
|
298
|
+
"plan-work",
|
|
299
|
+
]);
|
|
300
|
+
await store.close();
|
|
301
|
+
await open();
|
|
302
|
+
expect(store.carriedBy("infra")).toEqual(["plan-work"]);
|
|
303
|
+
expect(registry.describeCarried("infra")).toBe(
|
|
304
|
+
"writing-skills (built in), plan-work",
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("an unknown name changes nothing", async () => {
|
|
309
|
+
await registry.link("acme/skills", "skills");
|
|
310
|
+
await refuses(
|
|
311
|
+
() => registry.attach("infra", ["plan-work", "nope"], []),
|
|
312
|
+
"Unknown skills: nope",
|
|
313
|
+
);
|
|
314
|
+
expect(store.carriedBy("infra")).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("the catalog lists every skill with its source, groups, carriers, and missing files", async () => {
|
|
318
|
+
await registry.link("acme/skills", "skills");
|
|
319
|
+
await registry.setGroup("coding", "Coding workflow.", ["plan-work"]);
|
|
320
|
+
await registry.attach("infra", ["plan-work"], []);
|
|
321
|
+
rmSync(join(dir, "repos", "acme/skills", "skills/write-tests"), {
|
|
322
|
+
recursive: true,
|
|
323
|
+
});
|
|
324
|
+
const catalog = registry.catalog();
|
|
325
|
+
expect(catalog.map((entry) => entry.name)).toEqual([
|
|
326
|
+
"writing-skills",
|
|
327
|
+
"plan-work",
|
|
328
|
+
"write-tests",
|
|
329
|
+
]);
|
|
330
|
+
expect(catalog[0]).toMatchObject({
|
|
331
|
+
source: { kind: "builtin" },
|
|
332
|
+
file: join(BUILTIN, "writing-skills", "SKILL.md"),
|
|
333
|
+
});
|
|
334
|
+
expect(catalog[1]).toEqual({
|
|
335
|
+
name: "plan-work",
|
|
336
|
+
source: {
|
|
337
|
+
kind: "linked",
|
|
338
|
+
repo: "acme/skills",
|
|
339
|
+
path: "skills/plan-work",
|
|
340
|
+
},
|
|
341
|
+
description: "Use when plan-work.",
|
|
342
|
+
file: join(dir, "repos", "acme/skills", "skills/plan-work", "SKILL.md"),
|
|
343
|
+
groups: ["coding"],
|
|
344
|
+
carriers: ["infra"],
|
|
345
|
+
});
|
|
346
|
+
expect(catalog[2]?.missing).toBeString();
|
|
347
|
+
expect(catalog[2]?.file).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("groups filter the list; unlinking detaches from groups too", async () => {
|
|
351
|
+
await registry.link("acme/skills", "skills");
|
|
352
|
+
await registry.setGroup("coding", "Coding workflow.", ["plan-work"]);
|
|
353
|
+
await registry.attach("infra", ["plan-work"], []);
|
|
354
|
+
const listed = registry.list({ group: "coding" });
|
|
355
|
+
expect(listed).toContain(
|
|
356
|
+
"- plan-work [linked from acme/skills:skills/plan-work]",
|
|
357
|
+
);
|
|
358
|
+
expect(listed).toContain("carried by: infra");
|
|
359
|
+
expect(listed).not.toContain("- write-tests");
|
|
360
|
+
expect(registry.list({ query: "write-tests" })).toContain(
|
|
361
|
+
"- write-tests",
|
|
362
|
+
);
|
|
363
|
+
expect(await registry.unlink("plan-work")).toBe(
|
|
364
|
+
"Removed skill plan-work; detached from agents infra and groups coding.",
|
|
365
|
+
);
|
|
366
|
+
await refuses(
|
|
367
|
+
() => registry.setGroup("coding", "x", ["nope"]),
|
|
368
|
+
"Unknown skills",
|
|
369
|
+
);
|
|
370
|
+
await refuses(
|
|
371
|
+
async () => registry.list({ group: "nope" }),
|
|
372
|
+
"no skill group",
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|