@soederpop/luca 0.1.3 → 0.2.1

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.
@@ -27,6 +27,10 @@ export interface AssistantEntry {
27
27
  hasHooks: boolean
28
28
  /** Whether a voice.yaml configuration file exists. */
29
29
  hasVoice: boolean
30
+ /** Contents of ABOUT.md if present, undefined otherwise. */
31
+ about?: string
32
+ /** Frontmatter metadata parsed from CORE.md. */
33
+ meta?: Record<string, any>
30
34
  }
31
35
 
32
36
  export const AssistantsManagerEventsSchema = FeatureEventsSchema.extend({
@@ -191,6 +195,25 @@ export class AssistantsManager extends Feature<AssistantsManagerState, Assistant
191
195
 
192
196
  // Don't overwrite earlier entries (home takes precedence for same name)
193
197
  if (!discovered[entry]) {
198
+ const hasAbout = fs.exists(`${folder}/ABOUT.md`)
199
+ let about: string | undefined
200
+ let meta: Record<string, any> | undefined
201
+
202
+ if (hasAbout) {
203
+ about = fs.readFileSync(`${folder}/ABOUT.md`, 'utf8')
204
+ }
205
+
206
+ try {
207
+ const coreContent = fs.readFileSync(`${folder}/CORE.md`, 'utf8')
208
+ const fmMatch = coreContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
209
+ if (fmMatch) {
210
+ const yaml = this.container.feature('yaml')
211
+ meta = yaml.parse(fmMatch[1])
212
+ }
213
+ } catch {
214
+ // CORE.md exists but couldn't be parsed — skip meta
215
+ }
216
+
194
217
  discovered[entry] = {
195
218
  name: entry,
196
219
  folder,
@@ -198,6 +221,8 @@ export class AssistantsManager extends Feature<AssistantsManagerState, Assistant
198
221
  hasTools: fs.exists(`${folder}/tools.ts`),
199
222
  hasHooks: fs.exists(`${folder}/hooks.ts`),
200
223
  hasVoice: fs.exists(`${folder}/voice.yaml`),
224
+ ...(about != null && { about }),
225
+ ...(meta != null && { meta }),
201
226
  }
202
227
  }
203
228
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
3
  import { Feature } from '../feature.js'
4
+ import type { Helper } from '../../helper.js'
4
5
 
5
6
  declare module '@soederpop/luca/feature' {
6
7
  interface AvailableFeatures {
@@ -223,6 +224,35 @@ export class BrowserUse extends Feature<BrowserUseState, BrowserUseOptions> {
223
224
 
224
225
  static { Feature.register(this, 'browserUse') }
225
226
 
227
+ /**
228
+ * When an assistant uses browserUse, inject system prompt guidance
229
+ * about the browser interaction loop.
230
+ */
231
+ override setupToolsConsumer(consumer: Helper) {
232
+ if (typeof (consumer as any).addSystemPromptExtension === 'function') {
233
+ (consumer as any).addSystemPromptExtension('browserUse', [
234
+ '## Browser Automation',
235
+ '',
236
+ '**The core loop:** `browserOpen` → `browserGetState` → interact → `browserGetState` again.',
237
+ '',
238
+ '`browserGetState` is your eyes. It returns all interactive elements with index numbers. You MUST call it:',
239
+ '- After every `browserOpen` or navigation',
240
+ '- After any action that changes the page (click, submit, scroll)',
241
+ '- Before any interaction — to get fresh element indices',
242
+ '',
243
+ 'Element indices change whenever the page updates. Never reuse indices from a previous `browserGetState` call after the page has changed.',
244
+ '',
245
+ '**Interacting with elements:** Use `browserInput` (click + type) for form fields. Use `browserClick` for buttons and links. Use `browserSelect` for dropdowns. All require an element index from `browserGetState`.',
246
+ '',
247
+ '**When things load asynchronously:** Use `browserWaitForSelector` or `browserWaitForText` after actions that trigger page updates (form submissions, AJAX). Then call `browserGetState` to see the updated page.',
248
+ '',
249
+ '**Debugging:** If an interaction doesn\'t work, take a `browserScreenshot` to see the actual page state. Check `browserGetState` to see what elements are available.',
250
+ '',
251
+ '**Cleanup:** Call `browserClose` when you\'re done to free resources.',
252
+ ].join('\n'))
253
+ }
254
+ }
255
+
226
256
  override async afterInitialize() {
227
257
  if (this.options.session) this.state.set('session', this.options.session)
228
258
  if (this.options.headed) this.state.set('headed', true)
@@ -0,0 +1,175 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
3
+ import { Feature } from '../feature.js'
4
+ import type { Helper } from '../../helper.js'
5
+ import type { ChildProcess } from '../../node/features/proc.js'
6
+
7
+ declare module '@soederpop/luca/feature' {
8
+ interface AvailableFeatures {
9
+ codingTools: typeof CodingTools
10
+ }
11
+ }
12
+
13
+ export const CodingToolsStateSchema = FeatureStateSchema.extend({})
14
+ export const CodingToolsOptionsSchema = FeatureOptionsSchema.extend({})
15
+
16
+ /**
17
+ * Shell primitives for AI coding assistants: rg, ls, cat, sed, awk.
18
+ *
19
+ * Wraps standard Unix tools into the assistant tool surface with
20
+ * LLM-optimized descriptions and system prompt guidance. These are
21
+ * the raw, flexible tools for reading, searching, and exploring code.
22
+ *
23
+ * Compose with other features (fileTools, processManager, skillsLibrary)
24
+ * in assistant hooks for a complete coding tool surface.
25
+ *
26
+ * Usage:
27
+ * ```typescript
28
+ * assistant.use(container.feature('codingTools'))
29
+ * ```
30
+ *
31
+ * @extends Feature
32
+ */
33
+ export class CodingTools extends Feature {
34
+ static override shortcut = 'features.codingTools' as const
35
+ static override stateSchema = CodingToolsStateSchema
36
+ static override optionsSchema = CodingToolsOptionsSchema
37
+
38
+ static { Feature.register(this, 'codingTools') }
39
+
40
+ static override tools: Record<string, { schema: z.ZodType; description?: string }> = {
41
+ rg: {
42
+ description: 'ripgrep — fast content search across files. The fastest way to find where something is defined, referenced, or used. Supports regex, file type filtering, context lines, and everything ripgrep supports.',
43
+ schema: z.object({
44
+ args: z.string().describe(
45
+ 'Arguments to pass to rg, exactly as you would on the command line. ' +
46
+ 'Examples: "TODO --type ts", "-n "function handleAuth" src/", ' +
47
+ '"import.*lodash -g "*.ts" --count", "-C 3 "class User" src/models/"'
48
+ ),
49
+ cwd: z.string().optional().describe('Working directory. Defaults to project root.'),
50
+ }).describe('ripgrep — fast content search across files. Supports regex, file type filtering, context lines. Use this as your primary search tool for finding code.'),
51
+ },
52
+ ls: {
53
+ description: 'List files and directories. Use to orient yourself in the project structure, check what exists in a directory, or verify paths before operating on them.',
54
+ schema: z.object({
55
+ args: z.string().optional().describe(
56
+ 'Arguments to pass to ls. Examples: "-la src/", "-R --color=never commands/", "-1 *.ts". ' +
57
+ 'Defaults to listing the project root.'
58
+ ),
59
+ cwd: z.string().optional().describe('Working directory. Defaults to project root.'),
60
+ }).describe('List files and directories. Use to orient yourself, check directory contents, or verify paths.'),
61
+ },
62
+ cat: {
63
+ description: 'Read file contents. Use for reading entire files or specific line ranges. For large files, prefer reading specific ranges with sed or use rg to find the relevant section first.',
64
+ schema: z.object({
65
+ args: z.string().describe(
66
+ 'Arguments to pass to cat. Typically just a file path. ' +
67
+ 'Examples: "src/index.ts", "-n src/index.ts" (with line numbers). ' +
68
+ 'For line ranges, use sed instead: sed -n "10,20p" file.ts'
69
+ ),
70
+ cwd: z.string().optional().describe('Working directory. Defaults to project root.'),
71
+ }).describe('Read file contents. Best for reading entire files or viewing file content with line numbers.'),
72
+ },
73
+ sed: {
74
+ description: 'Stream editor for extracting or transforming text. Use for reading specific line ranges from files, or performing find-and-replace operations.',
75
+ schema: z.object({
76
+ args: z.string().describe(
77
+ 'Arguments to pass to sed. Examples: ' +
78
+ '"-n \\"10,30p\\" src/index.ts" (print lines 10-30), ' +
79
+ '"-n \\"1,5p\\" package.json" (first 5 lines), ' +
80
+ '"s/oldName/newName/g src/config.ts" (find-and-replace)'
81
+ ),
82
+ cwd: z.string().optional().describe('Working directory. Defaults to project root.'),
83
+ }).describe('Stream editor for extracting line ranges or transforming text in files.'),
84
+ },
85
+ awk: {
86
+ description: 'Pattern scanning and text processing. Use for extracting specific fields from structured output, summarizing data, or complex text transformations.',
87
+ schema: z.object({
88
+ args: z.string().describe(
89
+ 'Arguments to pass to awk. Examples: ' +
90
+ '"\'{print $1}\' file.txt" (first column), ' +
91
+ '"-F: \'{print $1, $3}\' /etc/passwd" (colon-delimited fields), ' +
92
+ '"\'/pattern/ {print}\' file.txt" (lines matching pattern)'
93
+ ),
94
+ cwd: z.string().optional().describe('Working directory. Defaults to project root.'),
95
+ }).describe('Pattern scanning and text processing. Extract fields, summarize data, or perform complex text transformations.'),
96
+ },
97
+ }
98
+
99
+ private get proc(): ChildProcess {
100
+ return this.container.feature('proc') as unknown as ChildProcess
101
+ }
102
+
103
+ // -------------------------------------------------------------------------
104
+ // Shell tool implementations
105
+ // -------------------------------------------------------------------------
106
+
107
+ private async _exec(command: string, args: string, cwd?: string): Promise<string> {
108
+ const fullCommand = args ? `${command} ${args}` : command
109
+ const result = await this.proc.execAndCapture(fullCommand, {
110
+ cwd: cwd ?? this.container.cwd,
111
+ })
112
+
113
+ if (result.exitCode !== 0) {
114
+ const parts: string[] = []
115
+ if (result.stdout?.trim()) parts.push(result.stdout.trim())
116
+ if (result.stderr?.trim()) parts.push(`[stderr] ${result.stderr.trim()}`)
117
+ parts.push(`[exit code: ${result.exitCode}]`)
118
+ return parts.join('\n')
119
+ }
120
+
121
+ return result.stdout || '(no output)'
122
+ }
123
+
124
+ async rg(args: { args: string; cwd?: string }): Promise<string> {
125
+ return this._exec('rg', args.args, args.cwd)
126
+ }
127
+
128
+ async ls(args: { args?: string; cwd?: string }): Promise<string> {
129
+ return this._exec('ls', args.args || '', args.cwd)
130
+ }
131
+
132
+ async cat(args: { args: string; cwd?: string }): Promise<string> {
133
+ return this._exec('cat', args.args, args.cwd)
134
+ }
135
+
136
+ async sed(args: { args: string; cwd?: string }): Promise<string> {
137
+ return this._exec('sed', args.args, args.cwd)
138
+ }
139
+
140
+ async awk(args: { args: string; cwd?: string }): Promise<string> {
141
+ return this._exec('awk', args.args, args.cwd)
142
+ }
143
+
144
+ override setupToolsConsumer(consumer: Helper) {
145
+ if (typeof (consumer as any).addSystemPromptExtension === 'function') {
146
+ (consumer as any).addSystemPromptExtension('codingTools', SYSTEM_PROMPT_EXTENSION)
147
+ }
148
+ }
149
+ }
150
+
151
+ // ─── System Prompt Extension ──────────────────────────────────────────────────
152
+
153
+ const SYSTEM_PROMPT_EXTENSION = [
154
+ '## Shell Tools',
155
+ '',
156
+ 'You have direct access to standard Unix tools. These are your primary read/search/explore tools:',
157
+ '',
158
+ '**`rg` (ripgrep) — your most important tool.** Use it before guessing where anything is.',
159
+ '- `rg -n "pattern" --type ts` — search TypeScript files with line numbers',
160
+ '- `rg -C 3 "pattern"` — show 3 lines of context around matches',
161
+ '- `rg -l "pattern"` — list only filenames that match',
162
+ '- `rg "TODO|FIXME" --type ts --count` — count matches per file',
163
+ '',
164
+ '**`cat` — read files.** Use `cat -n` for line numbers. For large files, use `sed -n "10,30p"` to read a range.',
165
+ '',
166
+ '**`ls` — orient yourself.** `ls -la src/` for details, `ls -R` for recursive listing.',
167
+ '',
168
+ '**`sed` — extract line ranges.** `sed -n "50,80p" file.ts` reads lines 50-80.',
169
+ '',
170
+ '**`awk` — structured text processing.** Extract columns, summarize, transform.',
171
+ '',
172
+ '**Workflow:** `rg` to find → `cat -n` to read → `editFile` to change → `runCommand` to verify.',
173
+ ].join('\n')
174
+
175
+ export default CodingTools
@@ -48,20 +48,20 @@ export class FileTools extends Feature {
48
48
  }).describe('Read the contents of a file. Returns the text content. Use offset/limit to read portions of large files.'),
49
49
  },
50
50
  writeFile: {
51
- description: 'Create a new file or overwrite an existing file with the given content. Prefer editFile for modifying existing files.',
51
+ description: 'Create a new file or completely overwrite an existing file. WARNING: this replaces the entire file use editFile instead for modifying existing files. Use writeFile only for creating new files or intentional full rewrites.',
52
52
  schema: z.object({
53
- path: z.string().describe('File path relative to the project root'),
54
- content: z.string().describe('The full content to write'),
55
- }).describe('Create a new file or overwrite an existing file with the given content. Prefer editFile for modifying existing files.'),
53
+ path: z.string().describe('File path relative to the project root. Parent directories are created automatically.'),
54
+ content: z.string().describe('The complete file content. This replaces everything in the file — there is no merge or append.'),
55
+ }).describe('Create a new file or completely overwrite an existing file. WARNING: this replaces the entire file use editFile instead for modifying existing files. Use writeFile only for creating new files or intentional full rewrites.'),
56
56
  },
57
57
  editFile: {
58
- description: 'Make a surgical edit to a file by replacing an exact string match. The oldString must appear exactly once in the file (unless replaceAll is true). This is the preferred way to modify existing files.',
58
+ description: 'Make a surgical edit to a file by replacing an exact string match. The preferred way to modify existing files always use this over writeFile for changes to existing code.',
59
59
  schema: z.object({
60
60
  path: z.string().describe('File path relative to the project root'),
61
- oldString: z.string().describe('The exact text to find and replace'),
62
- newString: z.string().describe('The replacement text'),
63
- replaceAll: z.boolean().optional().describe('Replace all occurrences instead of requiring uniqueness (default: false)'),
64
- }).describe('Make a surgical edit to a file by replacing an exact string match. The oldString must appear exactly once in the file (unless replaceAll is true).'),
61
+ oldString: z.string().describe('The EXACT text to find, copied verbatim from the file — including whitespace and indentation. Must appear exactly once in the file (unless replaceAll is true). If the match fails, read the file again and copy the exact text.'),
62
+ newString: z.string().describe('The replacement text. Preserve the same indentation style as the surrounding code.'),
63
+ replaceAll: z.boolean().optional().describe('Replace all occurrences instead of requiring uniqueness. Use for renaming a variable across a file. Default: false.'),
64
+ }).describe('Make a surgical edit to a file by replacing an exact string match. The preferred way to modify existing files always use this over writeFile for changes to existing code.'),
65
65
  },
66
66
  listDirectory: {
67
67
  description: 'List files and directories at a path. Returns arrays of file and directory names.',
@@ -73,23 +73,23 @@ export class FileTools extends Feature {
73
73
  }).describe('List files and directories at a path. Returns arrays of file and directory names.'),
74
74
  },
75
75
  searchFiles: {
76
- description: 'Search file contents for a pattern using ripgrep. Returns structured matches with file, line number, and content.',
76
+ description: 'Search inside file contents for a pattern (like grep/ripgrep). Returns matching lines with file path and line number. Use this to find where code is defined, where a function is called, or any text pattern across the codebase.',
77
77
  schema: z.object({
78
- pattern: z.string().describe('Search pattern (regex supported)'),
79
- path: z.string().optional().describe('Directory to search in (defaults to project root)'),
80
- include: z.string().optional().describe('Glob pattern to filter files (e.g. "*.ts")'),
81
- exclude: z.string().optional().describe('Glob pattern to exclude (e.g. "node_modules")'),
82
- ignoreCase: z.boolean().optional().describe('Case insensitive search'),
83
- maxResults: z.number().optional().describe('Maximum number of results to return'),
84
- }).describe('Search file contents for a pattern using ripgrep. Returns structured matches with file, line number, and content.'),
78
+ pattern: z.string().describe('Search pattern — supports regex. Examples: "function handleAuth", "TODO|FIXME", "import.*from.*react"'),
79
+ path: z.string().optional().describe('Directory to search in (defaults to project root). Narrow this to avoid searching node_modules or irrelevant directories.'),
80
+ include: z.string().optional().describe('Glob to filter which files to search (e.g. "*.ts", "*.tsx"). Use this to scope to specific file types.'),
81
+ exclude: z.string().optional().describe('Glob to exclude files (e.g. "node_modules", "dist"). node_modules is excluded by default.'),
82
+ ignoreCase: z.boolean().optional().describe('Case insensitive search. Default: false.'),
83
+ maxResults: z.number().optional().describe('Maximum results to return. Default: 50. Use a smaller number for broad patterns.'),
84
+ }).describe('Search inside file contents for a pattern (like grep/ripgrep). Returns matching lines with file path and line number. Use this to find where code is defined, where a function is called, or any text pattern across the codebase.'),
85
85
  },
86
86
  findFiles: {
87
- description: 'Find files by name/glob pattern. Returns matching file paths.',
87
+ description: 'Find files by name or glob pattern. Returns file paths, not contents. Use this to locate files ("where is the config?"), not to search inside them (use searchFiles for that).',
88
88
  schema: z.object({
89
- pattern: z.string().describe('Glob pattern to match (e.g. "**/*.test.ts", "src/**/*.tsx")'),
89
+ pattern: z.string().describe('Glob pattern to match file names. Examples: "**/*.test.ts", "src/**/*.tsx", "**/config.*"'),
90
90
  path: z.string().optional().describe('Directory to search from (defaults to project root)'),
91
- exclude: z.string().optional().describe('Glob pattern to exclude'),
92
- }).describe('Find files by name/glob pattern. Returns matching file paths.'),
91
+ exclude: z.string().optional().describe('Glob pattern to exclude. node_modules and .git are excluded by default.'),
92
+ }).describe('Find files by name or glob pattern. Returns file paths, not contents. Use this to locate files ("where is the config?"), not to search inside them (use searchFiles for that).'),
93
93
  },
94
94
  fileInfo: {
95
95
  description: 'Get information about a file or directory: whether it exists, its type (file/directory), size, and modification time.',
@@ -273,11 +273,18 @@ export class FileTools extends Feature {
273
273
  if (typeof (consumer as any).addSystemPromptExtension === 'function') {
274
274
  (consumer as any).addSystemPromptExtension('fileTools', [
275
275
  '## File Tools',
276
- '- All file paths are relative to the project root unless they start with /',
277
- '- Use `searchFiles` to understand code before modifying it',
278
- '- Use `editFile` for surgical changes to existing files — prefer it over `writeFile`',
279
- '- Use `listDirectory` to explore before assuming paths exist',
280
- '- Use `readFile` with offset/limit for large files instead of reading the entire file',
276
+ '',
277
+ 'All paths are relative to the project root unless they start with /.',
278
+ '',
279
+ '**Before modifying code:** Always read the file first. Use `searchFiles` to find where something is defined before making changes. Use `listDirectory` to verify paths exist before assuming.',
280
+ '',
281
+ '**Editing files:** Prefer `editFile` over `writeFile` for existing files — it makes surgical replacements. The `oldString` must appear exactly once in the file (unless using `replaceAll`). If your match isn\'t unique, include more surrounding context to make it unique. Never guess at file contents — read first, then edit.',
282
+ '',
283
+ '**Finding things:** Use `findFiles` to locate files by name or glob pattern (e.g. "**/*.test.ts"). Use `searchFiles` to find specific content inside files (grep). These are different tools for different questions: "where is the file?" vs "where is this code?".',
284
+ '',
285
+ '**Large files:** Use `readFile` with `offset` and `limit` to read portions. Don\'t load a 5000-line file when you only need lines 100-150.',
286
+ '',
287
+ '**`writeFile` is destructive** — it overwrites the entire file. Only use it for creating new files or when you intend a complete rewrite.',
281
288
  ].join('\n'))
282
289
  }
283
290
  }
@@ -76,19 +76,19 @@ export class SkillsLibrary extends Feature<SkillsLibraryState, SkillsLibraryOpti
76
76
  static override tools: Record<string, { schema: z.ZodType; handler?: Function }> = {
77
77
  searchAvailableSkills: {
78
78
  schema: z.object({
79
- query: z.string().optional().describe('Optional search term to filter skills by name or description'),
80
- }).describe('Search for available skills in the library. Returns matching skill names and descriptions.'),
79
+ query: z.string().optional().describe('A keyword or phrase to filter skills by name or description. Omit to list all available skills.'),
80
+ }).describe('Discover what skills are available. Call this first when you need specialized knowledge — skills are curated guides and reference material for specific domains (frameworks, tools, patterns). Returns skill names and descriptions so you can decide which to load.'),
81
81
  },
82
82
  loadSkill: {
83
83
  schema: z.object({
84
- skillName: z.string().describe('The name of the skill to load'),
85
- }).describe('Load a skill by name and return its full SKILL.md content and metadata.'),
84
+ skillName: z.string().describe('The exact skill name as returned by searchAvailableSkills'),
85
+ }).describe('Load a skill\'s full reference content (SKILL.md). This gives you detailed guidance, examples, and best practices for that domain. Load a skill before attempting work in an unfamiliar area — the content is curated to prevent common mistakes.'),
86
86
  },
87
87
  askSkillBasedQuestion: {
88
88
  schema: z.object({
89
- skillName: z.string().describe('The name of the skill to query'),
90
- question: z.string().describe('The question to ask about the skill'),
91
- }).describe('Ask a question about a specific skill using AI-assisted document reading.'),
89
+ skillName: z.string().describe('The exact skill name to query'),
90
+ question: z.string().describe('A specific question about the skill\'s domain. Be precise — "how do I add a new feature to the container?" is better than "tell me about features".'),
91
+ }).describe('Ask a focused question about a skill\'s domain using AI-assisted document reading. Use this when you need a specific answer from a skill rather than reading the whole thing. More efficient than loadSkill for targeted lookups.'),
92
92
  },
93
93
  }
94
94
 
@@ -107,8 +107,23 @@ export class SkillsLibrary extends Feature<SkillsLibraryState, SkillsLibraryOpti
107
107
  if (!(assistant instanceof Assistant)) {
108
108
  throw new Error('Skills library tools require an Assistant instance (including subclasses).')
109
109
  }
110
-
110
+
111
111
  const a : Assistant = assistant as Assistant
112
+
113
+ a.addSystemPromptExtension('skillsLibrary', [
114
+ '## Skills Library',
115
+ '',
116
+ 'You have access to a library of curated skills — domain-specific reference guides with examples, patterns, and best practices.',
117
+ '',
118
+ '**When to use skills:**',
119
+ '- When working in an unfamiliar domain or framework — load the skill before writing code',
120
+ '- When the user asks about a topic that might have a matching skill — search first',
121
+ '- When you see "Required Skills" in a message — load those skills immediately with `loadSkill` before answering',
122
+ '',
123
+ '**Workflow:** `searchAvailableSkills` → find relevant skill → `loadSkill` to get the full guide → follow its patterns. Use `askSkillBasedQuestion` for targeted lookups when you don\'t need the whole guide.',
124
+ '',
125
+ '**Skills are authoritative.** When a loaded skill contradicts your general knowledge, follow the skill — it reflects project-specific conventions and decisions.',
126
+ ].join('\n'))
112
127
 
113
128
  const { container } = a
114
129
 
@@ -451,10 +466,12 @@ Return only the skill names that are directly relevant. Return an empty array if
451
466
 
452
467
  const fork = assistant.conversation.fork()
453
468
  const result = await fork.ask(prompt, { schema: responseSchema }) as unknown as { skills: string[] }
454
-
455
- console.log('Got a result', result)
456
469
 
457
- return result.skills.filter(name => this.find(name) !== undefined)
470
+ const found = result.skills.filter(name => this.find(name) !== undefined)
471
+
472
+ this.emit('foundSkills', found, assistant, userQuery)
473
+
474
+ return found
458
475
  }
459
476
  }
460
477
 
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-04-03T01:24:53.975Z
2
+ // Generated at: 2026-04-05T06:58:07.990Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*, docs/examples/*.md, docs/tutorials/*.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -1,4 +1,4 @@
1
1
  // Generated at compile time — do not edit manually
2
- export const BUILD_SHA = '2d0d67e'
2
+ export const BUILD_SHA = '8323521'
3
3
  export const BUILD_BRANCH = 'main'
4
- export const BUILD_DATE = '2026-04-03T01:24:54Z'
4
+ export const BUILD_DATE = '2026-04-05T06:58:08Z'