@@ -10,6 +10,7 @@ import { ProviderTransform } from "@/provider/transform"
1010import PROMPT_GENERATE from "./generate.txt"
1111import PROMPT_COMPACTION from "./prompt/compaction.txt"
1212import PROMPT_EXPLORE from "./prompt/explore.txt"
13+ import PROMPT_SCOUT from "./prompt/scout.txt"
1314import PROMPT_SUMMARY from "./prompt/summary.txt"
1415import PROMPT_TITLE from "./prompt/title.txt"
1516import { Permission } from "@/permission"
@@ -25,6 +26,9 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
2526import { zod } from "@/util/effect-zod"
2627import { withStatics , type DeepMutable } from "@/util/schema"
2728
29+ type ReferenceEntry = NonNullable < Config . Info [ "reference" ] > [ string ]
30+ type ResolvedReference = { kind : "git" ; repository : string ; branch ?: string } | { kind : "local" ; path : string }
31+
2832export const Info = Schema . Struct ( {
2933 name : Schema . String ,
3034 description : Schema . optional ( Schema . String ) ,
@@ -86,6 +90,10 @@ export const layer = Layer.effect(
8690 path . join ( Global . Path . tmp , "*" ) ,
8791 ...skillDirs . map ( ( dir ) => path . join ( dir , "*" ) ) ,
8892 ]
93+ const readonlyExternalDirectory = {
94+ "*" : "ask" ,
95+ ...Object . fromEntries ( whitelistedDirs . map ( ( dir ) => [ dir , "allow" ] ) ) ,
96+ } satisfies Record < string , "allow" | "ask" | "deny" >
8997
9098 const defaults = Permission . fromConfig ( {
9199 "*" : "allow" ,
@@ -98,6 +106,8 @@ export const layer = Layer.effect(
98106 plan_enter : "deny" ,
99107 plan_exit : "deny" ,
100108 edit : "ask" ,
109+ repo_clone : "deny" ,
110+ repo_overview : "deny" ,
101111 // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
102112 read : {
103113 "*" : "allow" ,
@@ -175,10 +185,7 @@ export const layer = Layer.effect(
175185 webfetch : "allow" ,
176186 websearch : "allow" ,
177187 read : "allow" ,
178- external_directory : {
179- "*" : "ask" ,
180- ...Object . fromEntries ( whitelistedDirs . map ( ( dir ) => [ dir , "allow" ] ) ) ,
181- } ,
188+ external_directory : readonlyExternalDirectory ,
182189 } ) ,
183190 user ,
184191 ) ,
@@ -188,6 +195,33 @@ export const layer = Layer.effect(
188195 mode : "subagent" ,
189196 native : true ,
190197 } ,
198+ scout : {
199+ name : "scout" ,
200+ permission : Permission . merge (
201+ defaults ,
202+ Permission . fromConfig ( {
203+ "*" : "deny" ,
204+ grep : "allow" ,
205+ glob : "allow" ,
206+ webfetch : "allow" ,
207+ websearch : "allow" ,
208+ codesearch : "allow" ,
209+ read : "allow" ,
210+ repo_clone : "allow" ,
211+ repo_overview : "allow" ,
212+ external_directory : {
213+ ...readonlyExternalDirectory ,
214+ [ path . join ( Global . Path . repos , "*" ) ] : "allow" ,
215+ } ,
216+ } ) ,
217+ user ,
218+ ) ,
219+ description : `Docs and dependency-source specialist. Use this when you need to inspect external documentation, clone dependency repositories into the managed cache, and research library implementation details without modifying the user's workspace.` ,
220+ prompt : PROMPT_SCOUT ,
221+ options : { } ,
222+ mode : "subagent" ,
223+ native : true ,
224+ } ,
191225 compaction : {
192226 name : "compaction" ,
193227 mode : "primary" ,
@@ -265,6 +299,73 @@ export const layer = Layer.effect(
265299 item . permission = Permission . merge ( item . permission , Permission . fromConfig ( value . permission ?? { } ) )
266300 }
267301
302+ function referencePath ( value : string ) {
303+ if ( value . startsWith ( "~/" ) ) return path . join ( Global . Path . home , value . slice ( 2 ) )
304+ return path . isAbsolute ( value )
305+ ? value
306+ : path . resolve ( ctx . worktree === "/" ? ctx . directory : ctx . worktree , value )
307+ }
308+
309+ function resolveReference ( reference : ReferenceEntry ) : ResolvedReference {
310+ if ( typeof reference === "string" ) {
311+ if ( reference . startsWith ( "." ) || reference . startsWith ( "/" ) || reference . startsWith ( "~" ) ) {
312+ return { kind : "local" , path : referencePath ( reference ) }
313+ }
314+ return { kind : "git" , repository : reference }
315+ }
316+ if ( "path" in reference ) return { kind : "local" , path : referencePath ( reference . path ) }
317+ return { kind : "git" , repository : reference . repository , branch : reference . branch }
318+ }
319+
320+ function referencePrompt ( name : string , reference : ResolvedReference ) {
321+ if ( reference . kind === "local" ) {
322+ return [
323+ PROMPT_SCOUT ,
324+ `You are Scout reference @${ name } . This reference points to a local directory outside or alongside the current workspace.` ,
325+ `Local directory: ${ reference . path } ` ,
326+ `When invoked, inspect this directory as the primary reference source. Prefer repo_overview with path ${ JSON . stringify ( reference . path ) } before broader searches. Do not edit files.` ,
327+ ] . join ( "\n\n" )
328+ }
329+
330+ return [
331+ PROMPT_SCOUT ,
332+ `You are Scout reference @${ name } . This reference points to a git repository.` ,
333+ `Repository: ${ reference . repository } ` ,
334+ ...( reference . branch ? [ `Branch/ref: ${ reference . branch } ` ] : [ ] ) ,
335+ `When invoked, clone or refresh this repository with repo_clone, then inspect the cached repository as the primary reference source. Do not edit files.` ,
336+ ] . join ( "\n\n" )
337+ }
338+
339+ for ( const [ name , reference ] of Object . entries ( cfg . reference ?? { } ) ) {
340+ if ( agents [ name ] ) continue
341+ const resolved = resolveReference ( reference )
342+ const localPath = resolved . kind === "local" ? resolved . path : undefined
343+ agents [ name ] = {
344+ name,
345+ description :
346+ resolved . kind === "local"
347+ ? `Scout reference for local directory ${ resolved . path } `
348+ : `Scout reference for repository ${ resolved . repository } ` ,
349+ permission : Permission . merge (
350+ agents . scout . permission ,
351+ Permission . fromConfig (
352+ localPath
353+ ? {
354+ external_directory : {
355+ [ localPath ] : "allow" ,
356+ [ path . join ( localPath , "*" ) ] : "allow" ,
357+ } ,
358+ }
359+ : { } ,
360+ ) ,
361+ ) ,
362+ prompt : referencePrompt ( name , resolved ) ,
363+ options : { reference } ,
364+ mode : "subagent" ,
365+ native : false ,
366+ }
367+ }
368+
268369 // Ensure Truncate.GLOB is allowed unless explicitly configured
269370 for ( const name in agents ) {
270371 const agent = agents [ name ]
0 commit comments