1+ import { mkdtemp , readdir , rm , mkdir } from "node:fs/promises" ;
12import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
23import fs from "node:fs/promises" ;
34import os from "node:os" ;
@@ -8,7 +9,7 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async (importOriginal) =>
89 return { ...actual , runAdapterExecutionTargetProcess : vi . fn ( ) } ;
910} ) ;
1011
11- import { ensureRemoteOpenCodeModelConfiguredAndAvailable , execute } from "./execute.js" ;
12+ import { buildOpenCodeSkillsDir , ensureRemoteOpenCodeModelConfiguredAndAvailable , execute } from "./execute.js" ;
1213import { runAdapterExecutionTargetProcess } from "@paperclipai/adapter-utils/execution-target" ;
1314
1415const runProcessMock = vi . mocked ( runAdapterExecutionTargetProcess ) ;
@@ -33,6 +34,109 @@ function probeResult(overrides: Record<string, unknown>) {
3334 } as never ;
3435}
3536
37+ describe ( "buildOpenCodeSkillsDir create-agent inclusion" , ( ) => {
38+ const cleanupDirs : string [ ] = [ ] ;
39+
40+ afterEach ( async ( ) => {
41+ while ( cleanupDirs . length > 0 ) {
42+ const dir = cleanupDirs . pop ( ) ;
43+ if ( ! dir ) continue ;
44+ await rm ( dir , { recursive : true , force : true } ) . catch ( ( ) => undefined ) ;
45+ }
46+ } ) ;
47+
48+ async function makeConfigWithSkills ( ) {
49+ const root = await mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-opencode-skilltest-" ) ) ;
50+ cleanupDirs . push ( root ) ;
51+ const createAgentSource = path . join ( root , "paperclip-create-agent" ) ;
52+ const coordinationSource = path . join ( root , "paperclip" ) ;
53+ const memorySource = path . join ( root , "para-memory-files" ) ;
54+ await mkdir ( createAgentSource , { recursive : true } ) ;
55+ await mkdir ( coordinationSource , { recursive : true } ) ;
56+ await mkdir ( memorySource , { recursive : true } ) ;
57+ // Runtime skills are configured directly on the adapter config so the helper
58+ // resolves them without touching the packaged skills directory.
59+ return {
60+ paperclipRuntimeSkills : [
61+ {
62+ key : "paperclipai/paperclip/paperclip-create-agent" ,
63+ runtimeName : "paperclip-create-agent" ,
64+ source : createAgentSource ,
65+ } ,
66+ {
67+ key : "paperclipai/paperclip/paperclip" ,
68+ runtimeName : "paperclip" ,
69+ source : coordinationSource ,
70+ } ,
71+ {
72+ key : "paperclipai/paperclip/para-memory-files" ,
73+ runtimeName : "para-memory-files" ,
74+ source : memorySource ,
75+ } ,
76+ ] ,
77+ } as Record < string , unknown > ;
78+ }
79+
80+ it ( "includes the paperclip-create-agent skill when the agent can hire" , async ( ) => {
81+ const config = await makeConfigWithSkills ( ) ;
82+ const dir = await buildOpenCodeSkillsDir ( config , { canCreateAgents : true } ) ;
83+ cleanupDirs . push ( path . dirname ( dir ) ) ;
84+ const entries = await readdir ( dir ) ;
85+ expect ( entries ) . toContain ( "paperclip-create-agent" ) ;
86+ } ) ;
87+
88+ it ( "excludes the paperclip-create-agent skill when the agent cannot hire" , async ( ) => {
89+ const config = await makeConfigWithSkills ( ) ;
90+ const dir = await buildOpenCodeSkillsDir ( config , { canCreateAgents : false } ) ;
91+ cleanupDirs . push ( path . dirname ( dir ) ) ;
92+ const entries = await readdir ( dir ) ;
93+ expect ( entries ) . not . toContain ( "paperclip-create-agent" ) ;
94+ } ) ;
95+
96+ // Managed agents run instruction bundles (ceo/AGENTS.md, HEARTBEAT.md) that
97+ // MANDATE the coordination (`paperclip`) and memory (`para-memory-files`)
98+ // skills. Those skills are never in a managed agent's explicit desiredSkills,
99+ // so they must be force-included whenever the agent is managed.
100+ it ( "includes coordination + memory + create-agent skills for a managed agent that can hire" , async ( ) => {
101+ const config = await makeConfigWithSkills ( ) ;
102+ const dir = await buildOpenCodeSkillsDir ( config , {
103+ canCreateAgents : true ,
104+ managed : true ,
105+ } ) ;
106+ cleanupDirs . push ( path . dirname ( dir ) ) ;
107+ const entries = await readdir ( dir ) ;
108+ expect ( entries ) . toContain ( "paperclip" ) ;
109+ expect ( entries ) . toContain ( "para-memory-files" ) ;
110+ expect ( entries ) . toContain ( "paperclip-create-agent" ) ;
111+ } ) ;
112+
113+ it ( "includes coordination + memory but NOT create-agent for a managed agent that cannot hire" , async ( ) => {
114+ const config = await makeConfigWithSkills ( ) ;
115+ const dir = await buildOpenCodeSkillsDir ( config , {
116+ canCreateAgents : false ,
117+ managed : true ,
118+ } ) ;
119+ cleanupDirs . push ( path . dirname ( dir ) ) ;
120+ const entries = await readdir ( dir ) ;
121+ expect ( entries ) . toContain ( "paperclip" ) ;
122+ expect ( entries ) . toContain ( "para-memory-files" ) ;
123+ expect ( entries ) . not . toContain ( "paperclip-create-agent" ) ;
124+ } ) ;
125+
126+ it ( "does NOT force coordination/memory skills on a non-managed (BYO) agent" , async ( ) => {
127+ const config = await makeConfigWithSkills ( ) ;
128+ const dir = await buildOpenCodeSkillsDir ( config , {
129+ canCreateAgents : false ,
130+ managed : false ,
131+ } ) ;
132+ cleanupDirs . push ( path . dirname ( dir ) ) ;
133+ const entries = await readdir ( dir ) ;
134+ expect ( entries ) . not . toContain ( "paperclip" ) ;
135+ expect ( entries ) . not . toContain ( "para-memory-files" ) ;
136+ expect ( entries ) . not . toContain ( "paperclip-create-agent" ) ;
137+ } ) ;
138+ } ) ;
139+
36140describe ( "OpenCode local skill injection" , ( ) => {
37141 it ( "injects runtime skills into the configured child HOME" , async ( ) => {
38142 const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-opencode-configured-home-" ) ) ;
0 commit comments