feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
import { describe , it , expect , vi , beforeEach } from 'vitest' ;
import { McpRouter } from '../src/router.js' ;
import type { UpstreamConnection , JsonRpcRequest , JsonRpcResponse , JsonRpcNotification } from '../src/types.js' ;
import type { McpdClient } from '../src/http/mcpd-client.js' ;
import { ProviderRegistry } from '../src/providers/registry.js' ;
import type { LlmProvider , CompletionResult } from '../src/providers/types.js' ;
function mockUpstream (
name : string ,
opts : { tools? : Array < { name : string ; description? : string } > } = { } ,
) : UpstreamConnection {
return {
name ,
isAlive : vi.fn ( ( ) = > true ) ,
close : vi.fn ( async ( ) = > { } ) ,
onNotification : vi.fn ( ) ,
send : vi.fn ( async ( req : JsonRpcRequest ) : Promise < JsonRpcResponse > = > {
if ( req . method === 'tools/list' ) {
return { jsonrpc : '2.0' , id : req.id , result : { tools : opts.tools ? ? [ ] } } ;
}
if ( req . method === 'tools/call' ) {
return {
jsonrpc : '2.0' ,
id : req.id ,
result : { content : [ { type : 'text' , text : ` Called ${ ( req . params as Record < string , unknown > ) ? . name } ` } ] } ,
} ;
}
if ( req . method === 'resources/list' ) {
return { jsonrpc : '2.0' , id : req.id , result : { resources : [ ] } } ;
}
if ( req . method === 'prompts/list' ) {
return { jsonrpc : '2.0' , id : req.id , result : { prompts : [ ] } } ;
}
return { jsonrpc : '2.0' , id : req.id , error : { code : - 32601 , message : 'Not found' } } ;
} ) ,
} as UpstreamConnection ;
}
function mockMcpdClient ( prompts : Array < { name : string ; priority : number ; summary : string | null ; chapters : string [ ] | null ; content : string ; type ? : string } > = [ ] ) : McpdClient {
return {
get : vi . fn ( async ( path : string ) = > {
if ( path . includes ( '/prompts/visible' ) ) {
return prompts . map ( ( p ) = > ( { . . . p , type : p . type ? ? 'prompt' } ) ) ;
}
if ( path . includes ( '/prompt-index' ) ) {
return prompts . map ( ( p ) = > ( {
name : p.name ,
priority : p.priority ,
summary : p.summary ,
chapters : p.chapters ,
} ) ) ;
}
return [ ] ;
} ) ,
post : vi.fn ( async ( ) = > ( { } ) ) ,
put : vi.fn ( async ( ) = > ( { } ) ) ,
delete : vi . fn ( async ( ) = > { } ) ,
forward : vi.fn ( async ( ) = > ( { status : 200 , body : { } } ) ) ,
withHeaders : vi.fn ( function ( this : McpdClient ) { return this ; } ) ,
} as unknown as McpdClient ;
}
const samplePrompts = [
{ name : 'common-mistakes' , priority : 10 , summary : 'Critical safety rules everyone must follow' , chapters : null , content : 'NEVER do X. ALWAYS do Y.' } ,
{ name : 'zigbee-pairing' , priority : 7 , summary : 'How to pair Zigbee devices with the hub' , chapters : [ 'Setup' , 'Troubleshooting' ] , content : 'Step 1: Put device in pairing mode...' } ,
{ name : 'mqtt-config' , priority : 5 , summary : 'MQTT broker configuration guide' , chapters : [ 'Broker Setup' , 'Authentication' ] , content : 'Configure the MQTT broker at...' } ,
{ name : 'security-policy' , priority : 8 , summary : 'Security policies for production deployments' , chapters : [ 'Network' , 'Auth' ] , content : 'All connections must use TLS...' } ,
] ;
function setupGatedRouter (
opts : {
gated? : boolean ;
prompts? : typeof samplePrompts ;
withLlm? : boolean ;
llmResponse? : string ;
2026-02-27 17:05:05 +00:00
byteBudget? : number ;
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
} = { } ,
) : { router : McpRouter ; mcpdClient : McpdClient } {
const router = new McpRouter ( ) ;
const prompts = opts . prompts ? ? samplePrompts ;
const mcpdClient = mockMcpdClient ( prompts ) ;
router . setPromptConfig ( mcpdClient , 'test-project' ) ;
let providerRegistry : ProviderRegistry | null = null ;
if ( opts . withLlm ) {
providerRegistry = new ProviderRegistry ( ) ;
const mockProvider : LlmProvider = {
name : 'mock-heavy' ,
complete : vi.fn ( ) . mockResolvedValue ( {
content : opts.llmResponse ? ? '{ "selectedNames": ["zigbee-pairing"], "reasoning": "User is working with zigbee" }' ,
toolCalls : [ ] ,
usage : { promptTokens : 100 , completionTokens : 50 , totalTokens : 150 } ,
finishReason : 'stop' ,
} satisfies CompletionResult ) ,
listModels : vi.fn ( ) . mockResolvedValue ( [ ] ) ,
isAvailable : vi.fn ( ) . mockResolvedValue ( true ) ,
} ;
providerRegistry . register ( mockProvider ) ;
providerRegistry . assignTier ( mockProvider . name , 'heavy' ) ;
}
router . setGateConfig ( {
gated : opts.gated !== false ,
providerRegistry ,
2026-02-27 17:05:05 +00:00
byteBudget : opts.byteBudget ,
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
} ) ;
return { router , mcpdClient } ;
}
describe ( 'McpRouter gating' , ( ) = > {
describe ( 'initialize with gating' , ( ) = > {
it ( 'creates gated session on initialize' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
expect ( res . result ) . toBeDefined ( ) ;
// The session should be gated now
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools ;
expect ( tools ) . toHaveLength ( 1 ) ;
expect ( tools [ 0 ] ! . name ) . toBe ( 'begin_session' ) ;
} ) ;
it ( 'creates ungated session when project is not gated' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools ;
const names = tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
expect ( names ) . toContain ( 'read_prompts' ) ;
expect ( names ) . not . toContain ( 'begin_session' ) ;
} ) ;
} ) ;
describe ( 'tools/list gating' , ( ) = > {
it ( 'shows only begin_session when session is gated' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( res . result as { tools : Array < { name : string } > } ) . tools ;
expect ( tools ) . toHaveLength ( 1 ) ;
expect ( tools [ 0 ] ! . name ) . toBe ( 'begin_session' ) ;
} ) ;
it ( 'shows all tools plus read_prompts after ungating' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// Ungate via begin_session
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools ;
const names = tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
expect ( names ) . toContain ( 'propose_prompt' ) ;
expect ( names ) . toContain ( 'read_prompts' ) ;
expect ( names ) . not . toContain ( 'begin_session' ) ;
} ) ;
} ) ;
describe ( 'begin_session' , ( ) = > {
it ( 'returns matched prompts with keyword matching' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' , 'pairing' ] } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeUndefined ( ) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
// Should include priority 10 prompt
expect ( text ) . toContain ( 'common-mistakes' ) ;
expect ( text ) . toContain ( 'NEVER do X' ) ;
// Should include zigbee-pairing (matches both tags)
expect ( text ) . toContain ( 'zigbee-pairing' ) ;
expect ( text ) . toContain ( 'pairing mode' ) ;
// Should include encouragement
expect ( text ) . toContain ( 'read_prompts' ) ;
} ) ;
it ( 'includes priority 10 prompts even without matching tags' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'unrelated-keyword' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'common-mistakes' ) ;
expect ( text ) . toContain ( 'NEVER do X' ) ;
} ) ;
it ( 'uses LLM selection when provider is available' , async ( ) = > {
const { router } = setupGatedRouter ( {
withLlm : true ,
llmResponse : '{ "selectedNames": ["zigbee-pairing", "security-policy"], "reasoning": "Zigbee pairing needs security awareness" }' ,
} ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'Zigbee pairing needs security awareness' ) ;
expect ( text ) . toContain ( 'zigbee-pairing' ) ;
expect ( text ) . toContain ( 'security-policy' ) ;
expect ( text ) . toContain ( 'common-mistakes' ) ; // priority 10 always included
} ) ;
it ( 'rejects empty tags' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ ] } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeDefined ( ) ;
expect ( res . error ! . code ) . toBe ( - 32602 ) ;
} ) ;
it ( 'returns message when session is already ungated' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// First call ungates
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
// Second call tells user to use read_prompts
const res = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'mqtt' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'already started' ) ;
expect ( text ) . toContain ( 'read_prompts' ) ;
} ) ;
it ( 'lists remaining prompts for awareness' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
// Non-matching prompts should be listed as "other available prompts"
// security-policy doesn't match 'zigbee' in keyword mode
expect ( text ) . toContain ( 'security-policy' ) ;
} ) ;
} ) ;
describe ( 'read_prompts' , ( ) = > {
it ( 'returns prompts matching keywords' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ 'mqtt' , 'broker' ] } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeUndefined ( ) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'mqtt-config' ) ;
expect ( text ) . toContain ( 'Configure the MQTT broker' ) ;
} ) ;
it ( 'filters out already-sent prompts' , async ( ) = > {
2026-02-27 17:05:05 +00:00
// Use a tight byte budget so begin_session only sends the top-scoring prompts
const { router } = setupGatedRouter ( { byteBudget : 80 } ) ;
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
2026-02-27 17:05:05 +00:00
// begin_session with ['zigbee'] sends common-mistakes (priority 10, Inf) and
// zigbee-pairing (7+7=14) within 80 bytes. Lower-scored prompts overflow.
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
2026-02-27 17:05:05 +00:00
// read_prompts for mqtt should find mqtt-config (wasn't fully sent), not re-send common-mistakes
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
const res = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ 'mqtt' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'mqtt-config' ) ;
// common-mistakes was already sent, should not appear again
expect ( text ) . not . toContain ( 'NEVER do X' ) ;
} ) ;
it ( 'returns message when no new prompts match' , async ( ) = > {
const { router } = setupGatedRouter ( { prompts : [ ] } ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ 'nonexistent' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ) ;
expect ( text ) . toContain ( 'No new matching prompts' ) ;
} ) ;
it ( 'rejects empty tags' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ ] } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeDefined ( ) ;
expect ( res . error ! . code ) . toBe ( - 32602 ) ;
} ) ;
} ) ;
describe ( 'gated intercept' , ( ) = > {
it ( 'auto-ungates when gated session calls a real tool' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const ha = mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ;
router . addUpstream ( ha ) ;
await router . discoverTools ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// Call a real tool while gated — should intercept, extract keywords, and route
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'ha/get_entities' , arguments : { domain : 'light' } } } ,
{ sessionId : 's1' } ,
) ;
// Response should include the tool result
expect ( res . error ) . toBeUndefined ( ) ;
const result = res . result as { content : Array < { type : string ; text : string } > } ;
// Should have briefing prepended
expect ( result . content . length ) . toBeGreaterThanOrEqual ( 1 ) ;
// Session should now be ungated
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools ;
expect ( tools . map ( ( t ) = > t . name ) ) . toContain ( 'ha/get_entities' ) ;
} ) ;
it ( 'includes project context in intercepted response' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const ha = mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ;
router . addUpstream ( ha ) ;
await router . discoverTools ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'ha/get_entities' , arguments : { domain : 'light' } } } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { content : Array < { type : string ; text : string } > } ;
// First content block should be the briefing (priority 10 at minimum)
const briefing = result . content [ 0 ] ! . text ;
expect ( briefing ) . toContain ( 'common-mistakes' ) ;
expect ( briefing ) . toContain ( 'NEVER do X' ) ;
} ) ;
} ) ;
describe ( 'initialize instructions for gated projects' , ( ) = > {
it ( 'includes gate message and prompt index in instructions' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { instructions? : string } ;
expect ( result . instructions ) . toBeDefined ( ) ;
expect ( result . instructions ) . toContain ( 'begin_session' ) ;
expect ( result . instructions ) . toContain ( 'gated session' ) ;
// Should list available prompts
expect ( result . instructions ) . toContain ( 'common-mistakes' ) ;
expect ( result . instructions ) . toContain ( 'zigbee-pairing' ) ;
} ) ;
it ( 'does not include gate message for non-gated projects' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
router . setInstructions ( 'Base project instructions' ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { instructions? : string } ;
expect ( result . instructions ) . toBe ( 'Base project instructions' ) ;
expect ( result . instructions ) . not . toContain ( 'gated session' ) ;
} ) ;
it ( 'preserves base instructions and appends gate message' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
router . setInstructions ( 'You are a helpful assistant.' ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { instructions? : string } ;
expect ( result . instructions ) . toContain ( 'You are a helpful assistant.' ) ;
expect ( result . instructions ) . toContain ( 'begin_session' ) ;
} ) ;
it ( 'sorts prompt index by priority descending' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { instructions : string } ;
const lines = result . instructions . split ( '\n' ) ;
// Find the prompt index lines
const promptLines = lines . filter ( ( l ) = > l . startsWith ( '- ' ) && l . includes ( 'priority' ) ) ;
// priority 10 should come first
expect ( promptLines [ 0 ] ) . toContain ( 'common-mistakes' ) ;
expect ( promptLines [ 0 ] ) . toContain ( 'priority 10' ) ;
} ) ;
} ) ;
describe ( 'session cleanup' , ( ) = > {
it ( 'cleanupSession removes gate state' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// Session is gated
let toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
expect ( ( toolsRes . result as { tools : Array < { name : string } > } ) . tools [ 0 ] ! . name ) . toBe ( 'begin_session' ) ;
// Cleanup
router . cleanupSession ( 's1' ) ;
// After cleanup, session is treated as unknown (ungated)
toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const tools = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools ;
expect ( tools . map ( ( t ) = > t . name ) ) . not . toContain ( 'begin_session' ) ;
} ) ;
} ) ;
2026-02-27 17:05:05 +00:00
describe ( 'tool inventory' , ( ) = > {
it ( 'includes tool names but NOT descriptions in gated initialize instructions' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' , description : 'Get all entities' } ] } ) ) ;
router . addUpstream ( mockUpstream ( 'node-red' , { tools : [ { name : 'get_flows' , description : 'Get all flows' } ] } ) ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 1 , method : 'initialize' } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { instructions : string } ;
expect ( result . instructions ) . toContain ( 'ha/get_entities' ) ;
expect ( result . instructions ) . toContain ( 'node-red/get_flows' ) ;
expect ( result . instructions ) . toContain ( 'after begin_session' ) ;
// Descriptions should NOT be in init instructions (names only)
expect ( result . instructions ) . not . toContain ( 'Get all entities' ) ;
expect ( result . instructions ) . not . toContain ( 'Get all flows' ) ;
} ) ;
it ( 'includes tool names but NOT descriptions in begin_session response' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' , description : 'Get all entities' } ] } ) ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
expect ( text ) . toContain ( 'ha/get_entities' ) ;
expect ( text ) . not . toContain ( 'Get all entities' ) ;
} ) ;
it ( 'includes retry instruction in begin_session response' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
expect ( text ) . toContain ( 'Proceed with' ) ;
} ) ;
it ( 'includes tool names but NOT descriptions in gated intercept briefing' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const ha = mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' , description : 'Get all entities' } ] } ) ;
router . addUpstream ( ha ) ;
await router . discoverTools ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'ha/get_entities' , arguments : { } } } ,
{ sessionId : 's1' } ,
) ;
const result = res . result as { content : Array < { type : string ; text : string } > } ;
const briefing = result . content [ 0 ] ! . text ;
expect ( briefing ) . toContain ( 'ha/get_entities' ) ;
expect ( briefing ) . not . toContain ( 'Get all entities' ) ;
} ) ;
} ) ;
describe ( 'notifications after ungating' , ( ) = > {
it ( 'queues tools/list_changed after begin_session ungating' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const notifications = router . consumeNotifications ( 's1' ) ;
expect ( notifications ) . toHaveLength ( 1 ) ;
expect ( notifications [ 0 ] ! . method ) . toBe ( 'notifications/tools/list_changed' ) ;
} ) ;
it ( 'queues tools/list_changed after gated intercept' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
const ha = mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ;
router . addUpstream ( ha ) ;
await router . discoverTools ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'ha/get_entities' , arguments : { } } } ,
{ sessionId : 's1' } ,
) ;
const notifications = router . consumeNotifications ( 's1' ) ;
expect ( notifications ) . toHaveLength ( 1 ) ;
expect ( notifications [ 0 ] ! . method ) . toBe ( 'notifications/tools/list_changed' ) ;
} ) ;
it ( 'consumeNotifications clears the queue' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
// First consume returns the notification
expect ( router . consumeNotifications ( 's1' ) ) . toHaveLength ( 1 ) ;
// Second consume returns empty
expect ( router . consumeNotifications ( 's1' ) ) . toHaveLength ( 0 ) ;
} ) ;
} ) ;
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
describe ( 'prompt index caching' , ( ) = > {
it ( 'caches prompt index for 60 seconds' , async ( ) = > {
const { router , mcpdClient } = setupGatedRouter ( { gated : false } ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// First read_prompts call fetches from mcpd
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ 'mqtt' ] } } } ,
{ sessionId : 's1' } ,
) ;
// Second call should use cache
await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/call' , params : { name : 'read_prompts' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
// mcpdClient.get should have been called only once for prompts/visible
const getCalls = vi . mocked ( mcpdClient . get ) . mock . calls . filter ( ( c ) = > ( c [ 0 ] as string ) . includes ( '/prompts/visible' ) ) ;
expect ( getCalls ) . toHaveLength ( 1 ) ;
} ) ;
} ) ;
2026-02-27 17:05:05 +00:00
describe ( 'begin_session description field' , ( ) = > {
it ( 'accepts description and tokenizes to keywords' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { description : 'I want to pair a zigbee device with mqtt' } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeUndefined ( ) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
// Should match zigbee-pairing and mqtt-config via tokenized keywords
expect ( text ) . toContain ( 'zigbee-pairing' ) ;
expect ( text ) . toContain ( 'mqtt-config' ) ;
} ) ;
it ( 'prefers tags over description when both provided' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'mqtt' ] , description : 'zigbee pairing' } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeUndefined ( ) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
// Tags take priority — mqtt-config should match, zigbee-pairing should not
expect ( text ) . toContain ( 'mqtt-config' ) ;
} ) ;
it ( 'rejects calls with neither tags nor description' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeDefined ( ) ;
expect ( res . error ! . code ) . toBe ( - 32602 ) ;
expect ( res . error ! . message ) . toContain ( 'tags or description' ) ;
} ) ;
it ( 'rejects empty description with no tags' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { description : ' ' } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeDefined ( ) ;
expect ( res . error ! . code ) . toBe ( - 32602 ) ;
} ) ;
} ) ;
describe ( 'gate config refresh' , ( ) = > {
it ( 'new sessions pick up gate config change (gated → ungated)' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : true } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
// First session is gated
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
let toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
expect ( ( toolsRes . result as { tools : Array < { name : string } > } ) . tools [ 0 ] ! . name ) . toBe ( 'begin_session' ) ;
// Project config changes: gated → ungated
router . setGateConfig ( { gated : false , providerRegistry : null } ) ;
// New session should be ungated
await router . route ( { jsonrpc : '2.0' , id : 3 , method : 'initialize' } , { sessionId : 's2' } ) ;
toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 4 , method : 'tools/list' } ,
{ sessionId : 's2' } ,
) ;
const names = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
expect ( names ) . not . toContain ( 'begin_session' ) ;
} ) ;
it ( 'new sessions pick up gate config change (ungated → gated)' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
// First session is ungated
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
let toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
let names = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
// Project config changes: ungated → gated
router . setGateConfig ( { gated : true , providerRegistry : null } ) ;
// New session should be gated
await router . route ( { jsonrpc : '2.0' , id : 3 , method : 'initialize' } , { sessionId : 's2' } ) ;
toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 4 , method : 'tools/list' } ,
{ sessionId : 's2' } ,
) ;
names = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toHaveLength ( 1 ) ;
expect ( names [ 0 ] ) . toBe ( 'begin_session' ) ;
} ) ;
it ( 'existing sessions retain gate state after config change' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : true } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
// Session created while gated
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// Config changes to ungated
router . setGateConfig ( { gated : false , providerRegistry : null } ) ;
// Existing session s1 should STILL be gated (session state is immutable after creation)
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
expect ( ( toolsRes . result as { tools : Array < { name : string } > } ) . tools [ 0 ] ! . name ) . toBe ( 'begin_session' ) ;
} ) ;
it ( 'already-ungated sessions remain ungated after config changes to gated' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : false } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
// Session created while ungated
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
// Config changes to gated
router . setGateConfig ( { gated : true , providerRegistry : null } ) ;
// Existing session s1 should remain ungated
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const names = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
expect ( names ) . not . toContain ( 'begin_session' ) ;
} ) ;
it ( 'config refresh does not reset sessions that ungated via begin_session' , async ( ) = > {
const { router } = setupGatedRouter ( { gated : true } ) ;
router . addUpstream ( mockUpstream ( 'ha' , { tools : [ { name : 'get_entities' } ] } ) ) ;
// Session starts gated and ungates
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
// Config refreshes (still gated)
router . setGateConfig ( { gated : true , providerRegistry : null } ) ;
// Session should remain ungated — begin_session already completed
const toolsRes = await router . route (
{ jsonrpc : '2.0' , id : 3 , method : 'tools/list' } ,
{ sessionId : 's1' } ,
) ;
const names = ( toolsRes . result as { tools : Array < { name : string } > } ) . tools . map ( ( t ) = > t . name ) ;
expect ( names ) . toContain ( 'ha/get_entities' ) ;
expect ( names ) . not . toContain ( 'begin_session' ) ;
} ) ;
} ) ;
describe ( 'response size cap' , ( ) = > {
it ( 'truncates begin_session response over 24K chars' , async ( ) = > {
// Create prompts with very large content to exceed 24K
// Use byteBudget large enough so content is included in fullContent
const largePrompts = [
{ name : 'huge-prompt' , priority : 10 , summary : 'A very large prompt' , chapters : null , content : 'x' . repeat ( 30 _000 ) } ,
] ;
const { router } = setupGatedRouter ( { prompts : largePrompts , byteBudget : 50_000 } ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'huge' ] } } } ,
{ sessionId : 's1' } ,
) ;
expect ( res . error ) . toBeUndefined ( ) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
expect ( text . length ) . toBeLessThanOrEqual ( 24 _000 + 100 ) ; // allow for truncation message
expect ( text ) . toContain ( '[Response truncated' ) ;
} ) ;
it ( 'does not truncate responses under 24K chars' , async ( ) = > {
const { router } = setupGatedRouter ( ) ;
await router . route ( { jsonrpc : '2.0' , id : 1 , method : 'initialize' } , { sessionId : 's1' } ) ;
const res = await router . route (
{ jsonrpc : '2.0' , id : 2 , method : 'tools/call' , params : { name : 'begin_session' , arguments : { tags : [ 'zigbee' ] } } } ,
{ sessionId : 's1' } ,
) ;
const text = ( res . result as { content : Array < { text : string } > } ) . content [ 0 ] ! . text ;
expect ( text ) . not . toContain ( '[Response truncated' ) ;
} ) ;
} ) ;
feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system:
- Prisma schema: add gated, priority, summary, chapters, linkTarget fields
- Session gate: state machine (gated → begin_session → ungated) with LLM-powered
tool selection based on prompt index
- Tag matcher: intelligent prompt-to-tool matching with project/server/action tags
- LLM selector: tiered provider selection (fast for gating, heavy for complex tasks)
- Link resolver: cross-project MCP resource references (project/server:uri format)
- Prompt summary service: LLM-generated summaries and chapter extraction
- System project bootstrap: ensures default project exists on startup
- Structural link health checks: enrichWithLinkStatus on prompt GET endpoints
- CLI: create prompt --priority/--link, create project --gated/--no-gated,
describe project shows prompts section, get prompts shows PRI/LINK/STATUS
- Apply/edit: priority, linkTarget, gated fields supported
- Shell completions: fish updated with new flags
- 1,253 tests passing across all packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:22:42 +00:00
} ) ;