@@ -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" ,
@@ -97,6 +105,8 @@ export const layer = Layer.effect(
97105 question : "deny" ,
98106 plan_enter : "deny" ,
99107 plan_exit : "deny" ,
108+ repo_clone : "deny" ,
109+ repo_overview : "deny" ,
100110 // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
101111 read : {
102112 "*" : "allow" ,
@@ -174,10 +184,7 @@ export const layer = Layer.effect(
174184 webfetch : "allow" ,
175185 websearch : "allow" ,
176186 read : "allow" ,
177- external_directory : {
178- "*" : "ask" ,
179- ...Object . fromEntries ( whitelistedDirs . map ( ( dir ) => [ dir , "allow" ] ) ) ,
180- } ,
187+ external_directory : readonlyExternalDirectory ,
181188 } ) ,
182189 user ,
183190 ) ,
@@ -187,6 +194,33 @@ export const layer = Layer.effect(
187194 mode : "subagent" ,
188195 native : true ,
189196 } ,
197+ scout : {
198+ name : "scout" ,
199+ permission : Permission . merge (
200+ defaults ,
201+ Permission . fromConfig ( {
202+ "*" : "deny" ,
203+ grep : "allow" ,
204+ glob : "allow" ,
205+ webfetch : "allow" ,
206+ websearch : "allow" ,
207+ codesearch : "allow" ,
208+ read : "allow" ,
209+ repo_clone : "allow" ,
210+ repo_overview : "allow" ,
211+ external_directory : {
212+ ...readonlyExternalDirectory ,
213+ [ path . join ( Global . Path . repos , "*" ) ] : "allow" ,
214+ } ,
215+ } ) ,
216+ user ,
217+ ) ,
218+ 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.` ,
219+ prompt : PROMPT_SCOUT ,
220+ options : { } ,
221+ mode : "subagent" ,
222+ native : true ,
223+ } ,
190224 compaction : {
191225 name : "compaction" ,
192226 mode : "primary" ,
@@ -264,6 +298,73 @@ export const layer = Layer.effect(
264298 item . permission = Permission . merge ( item . permission , Permission . fromConfig ( value . permission ?? { } ) )
265299 }
266300
301+ function referencePath ( value : string ) {
302+ if ( value . startsWith ( "~/" ) ) return path . join ( Global . Path . home , value . slice ( 2 ) )
303+ return path . isAbsolute ( value )
304+ ? value
305+ : path . resolve ( ctx . worktree === "/" ? ctx . directory : ctx . worktree , value )
306+ }
307+
308+ function resolveReference ( reference : ReferenceEntry ) : ResolvedReference {
309+ if ( typeof reference === "string" ) {
310+ if ( reference . startsWith ( "." ) || reference . startsWith ( "/" ) || reference . startsWith ( "~" ) ) {
311+ return { kind : "local" , path : referencePath ( reference ) }
312+ }
313+ return { kind : "git" , repository : reference }
314+ }
315+ if ( "path" in reference ) return { kind : "local" , path : referencePath ( reference . path ) }
316+ return { kind : "git" , repository : reference . repository , branch : reference . branch }
317+ }
318+
319+ function referencePrompt ( name : string , reference : ResolvedReference ) {
320+ if ( reference . kind === "local" ) {
321+ return [
322+ PROMPT_SCOUT ,
323+ `You are Scout reference @${ name } . This reference points to a local directory outside or alongside the current workspace.` ,
324+ `Local directory: ${ reference . path } ` ,
325+ `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.` ,
326+ ] . join ( "\n\n" )
327+ }
328+
329+ return [
330+ PROMPT_SCOUT ,
331+ `You are Scout reference @${ name } . This reference points to a git repository.` ,
332+ `Repository: ${ reference . repository } ` ,
333+ ...( reference . branch ? [ `Branch/ref: ${ reference . branch } ` ] : [ ] ) ,
334+ `When invoked, clone or refresh this repository with repo_clone, then inspect the cached repository as the primary reference source. Do not edit files.` ,
335+ ] . join ( "\n\n" )
336+ }
337+
338+ for ( const [ name , reference ] of Object . entries ( cfg . reference ?? { } ) ) {
339+ if ( agents [ name ] ) continue
340+ const resolved = resolveReference ( reference )
341+ const localPath = resolved . kind === "local" ? resolved . path : undefined
342+ agents [ name ] = {
343+ name,
344+ description :
345+ resolved . kind === "local"
346+ ? `Scout reference for local directory ${ resolved . path } `
347+ : `Scout reference for repository ${ resolved . repository } ` ,
348+ permission : Permission . merge (
349+ agents . scout . permission ,
350+ Permission . fromConfig (
351+ localPath
352+ ? {
353+ external_directory : {
354+ [ localPath ] : "allow" ,
355+ [ path . join ( localPath , "*" ) ] : "allow" ,
356+ } ,
357+ }
358+ : { } ,
359+ ) ,
360+ ) ,
361+ prompt : referencePrompt ( name , resolved ) ,
362+ options : { reference } ,
363+ mode : "subagent" ,
364+ native : false ,
365+ }
366+ }
367+
267368 // Ensure Truncate.GLOB is allowed unless explicitly configured
268369 for ( const name in agents ) {
269370 const agent = agents [ name ]
0 commit comments