prjct-cli 1.5.1 → 1.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.1] - 2026-02-06
4
+
5
+ ### Bug Fixes
6
+
7
+ - replace console.error with logger in bus.ts (PRJ-72) (#122)
8
+
9
+
10
+ ## [1.6.1] - 2026-02-06
11
+
12
+ ### Bug Fixes
13
+
14
+ - **Replace console.error with logger in bus.ts**: Event bus now uses the centralized `log` module instead of raw `console.error` for error logging. Silent catch block in `logEvent()` now logs at debug level with `getErrorMessage()` context. Production remains quiet by default; enable with `PRJCT_DEBUG=1`.
15
+
16
+ ### Test Plan
17
+
18
+ #### For QA
19
+ 1. Run `PRJCT_DEBUG=1 bun test` — verify bus errors appear with `[prjct:error]` prefix
20
+ 2. Run `bun test` (no debug) — verify bus errors are silent by default
21
+ 3. Trigger a listener error — verify it logs via logger, not console.error
22
+
23
+ #### For Users
24
+ - **What changed:** Error logging in event bus uses centralized logger
25
+ - **How to use:** `PRJCT_DEBUG=1` to see bus errors; quiet by default
26
+ - **Breaking changes:** None
27
+
28
+ ## [1.6.0] - 2026-02-06
29
+
30
+ ### Features
31
+
32
+ - super context for agents — skills.sh, proactive codebase context, effort/model (#121)
33
+
34
+
35
+ ## [1.6.0] - 2026-02-06
36
+
37
+ ### Features
38
+
39
+ - **Skills.sh auto-install**: During `prjct sync`, skills from skills.sh are automatically installed for generated agents. Real packages like `anthropics/skills/frontend-design`, `obra/superpowers/systematic-debugging`, and `obra/superpowers/test-driven-development` are mapped per agent domain.
40
+ - **Proactive codebase context**: The orchestrator now gathers real context before agent execution — git state, relevant files (scored by task relevance), code signatures from top files, and recently changed files. Agents start with a complete briefing instead of exploring first.
41
+ - **Effort/model metadata wiring**: Agent frontmatter `effort` and `model` fields are now extracted and injected into prompts, enabling per-agent reasoning depth control.
42
+
43
+ ### Improved
44
+
45
+ - **Skill loading warnings**: Missing skills now log visible warnings with the agent that needs them and a hint to run `prjct sync`
46
+ - **Skill content in prompts**: Increased skill content truncation from 1000 to 2000 characters for richer context
47
+ - **Skill mappings v3**: Updated `skill-mappings.json` from generic names to real installable skills.sh packages
48
+
49
+ ### Implementation Details
50
+
51
+ - `sync-service.ts`: New `autoInstallSkills()` method reads `skill-mappings.json`, checks if each skill is installed, and calls `skillInstaller.install()` for missing ones
52
+ - `orchestrator-executor.ts`: New `gatherRealContext()` calls `findRelevantFiles()`, `getRecentFiles()`, and `extractSignatures()` in parallel to build a proactive briefing
53
+ - `prompt-builder.ts`: New "CODEBASE CONTEXT" section with git state, relevant files table, code signatures, and recently changed files; plus effort/model per agent
54
+ - `agent-loader.ts`: New `extractFrontmatterMeta()` parses YAML frontmatter for effort/model fields
55
+ - `agentic.ts`: New `RealCodebaseContext` interface; `LoadedAgent` extended with `effort?` and `model?`
56
+
57
+ ### Test Plan
58
+
59
+ #### For QA
60
+ 1. Run `prjct sync` — verify skills auto-install (check `~/.claude/skills/`)
61
+ 2. Run `p. task "test"` — verify prompt includes git state, relevant files, signatures, effort/model
62
+ 3. Verify warnings for missing skills
63
+ 4. Build and all 416 tests pass
64
+
65
+ #### For Users
66
+ **What changed:** Agents receive proactive codebase context before starting work. Skills auto-install during sync.
67
+ **How to use:** No action needed — automatic during `prjct sync` and `p. task`.
68
+ **Breaking changes:** None
69
+
70
+ ## [1.5.2] - 2026-02-06
71
+
72
+ ### Improved
73
+
74
+ - **TTY detection for CLI spinners**: Spinner, step, and progress animations now detect non-TTY environments (CI/CD, Claude Code, piped output) and print a single static line instead of animating
75
+
76
+ ### Implementation Details
77
+
78
+ - Added `process.stdout.isTTY` guard to `spin()`, `step()`, and `progress()` in `core/utils/output.ts`
79
+ - Non-TTY environments get a single line with `\n` instead of `setInterval` with `\r` carriage returns
80
+ - Made `clear()` a no-op in non-TTY (the `\r` + spaces trick doesn't work outside terminals)
81
+ - Updated test for `stop()` to handle non-TTY behavior in test runner
82
+
83
+ ### Learnings
84
+
85
+ - `process.stdout.isTTY` is the reliable built-in way to detect interactive terminals in Node.js
86
+ - Test suites (bun test) also run as non-TTY, so test assertions need to account for both paths
87
+
88
+ ### Test Plan
89
+
90
+ #### For QA
91
+ 1. Run `prjct sync --yes` in an interactive terminal — spinner should animate normally
92
+ 2. Run `prjct sync --yes > out.txt` — output should show a single static line, no repeated frames
93
+ 3. Run inside Claude Code Bash tool — output should be clean, no spinner noise
94
+ 4. Verify `step()` and `progress()` behave the same way in both environments
95
+
96
+ #### For Users
97
+ **What changed:** CLI spinners no longer produce garbage output in non-interactive terminals
98
+ **How to use:** No action needed — automatic TTY detection
99
+ **Breaking changes:** None
100
+
3
101
  ## [1.5.1] - 2026-02-06
4
102
 
5
103
  ### Refactoring
@@ -124,7 +124,13 @@ describe('Output Module', () => {
124
124
  stdoutWriteSpy.mockClear()
125
125
  out.stop()
126
126
 
127
- expect(stdoutWriteSpy).toHaveBeenCalled()
127
+ // In TTY, stop() writes a clear sequence; in non-TTY, spinner doesn't
128
+ // use setInterval so stop() is a no-op (clear skips in non-TTY)
129
+ if (process.stdout.isTTY) {
130
+ expect(stdoutWriteSpy).toHaveBeenCalled()
131
+ } else {
132
+ expect(stdoutWriteSpy).not.toHaveBeenCalled()
133
+ }
128
134
  })
129
135
 
130
136
  it('should be safe to call multiple times', () => {
@@ -13,16 +13,29 @@
13
13
  * @version 1.0.0
14
14
  */
15
15
 
16
+ import { exec as execCallback } from 'node:child_process'
16
17
  import fs from 'node:fs/promises'
17
18
  import os from 'node:os'
18
19
  import path from 'node:path'
20
+ import { promisify } from 'node:util'
21
+ import { findRelevantFiles } from '../context-tools/files-tool'
22
+ import { getRecentFiles } from '../context-tools/recent-tool'
23
+ import { extractSignatures } from '../context-tools/signatures-tool'
19
24
  import configManager from '../infrastructure/config-manager'
20
25
  import pathManager from '../infrastructure/path-manager'
21
26
  import { stateStorage } from '../storage'
22
- import type { LoadedAgent, LoadedSkill, OrchestratorContext, OrchestratorSubtask } from '../types'
27
+ import type {
28
+ LoadedAgent,
29
+ LoadedSkill,
30
+ OrchestratorContext,
31
+ OrchestratorSubtask,
32
+ RealCodebaseContext,
33
+ } from '../types'
23
34
  import { isNotFoundError } from '../types/fs'
24
35
  import { parseFrontmatter } from './template-loader'
25
36
 
37
+ const execAsync = promisify(execCallback)
38
+
26
39
  // =============================================================================
27
40
  // Domain Detection Keywords
28
41
  // =============================================================================
@@ -201,10 +214,13 @@ export class OrchestratorExecutor {
201
214
  // Step 5: Load skills from agent frontmatter
202
215
  const skills = await this.loadSkills(agents)
203
216
 
204
- // Step 6: Determine if fragmentation is needed
217
+ // Step 6: Gather real codebase context proactively
218
+ const realContext = await this.gatherRealContext(taskDescription, projectPath)
219
+
220
+ // Step 7: Determine if fragmentation is needed
205
221
  const requiresFragmentation = this.shouldFragment(domains, taskDescription)
206
222
 
207
- // Step 7: Create subtasks if fragmentation is required
223
+ // Step 8: Create subtasks if fragmentation is required
208
224
  let subtasks: OrchestratorSubtask[] | null = null
209
225
  if (requiresFragmentation && command === 'task') {
210
226
  subtasks = await this.createSubtasks(taskDescription, domains, agents, projectId)
@@ -222,6 +238,100 @@ export class OrchestratorExecutor {
222
238
  ecosystem: repoAnalysis?.ecosystem || 'unknown',
223
239
  conventions: repoAnalysis?.conventions || [],
224
240
  },
241
+ realContext,
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Gather real codebase context proactively.
247
+ *
248
+ * Calls existing context tools (files-tool, recent-tool, signatures-tool)
249
+ * to build a briefing so the agent doesn't need to explore first.
250
+ */
251
+ private async gatherRealContext(
252
+ taskDescription: string,
253
+ projectPath: string
254
+ ): Promise<RealCodebaseContext | undefined> {
255
+ try {
256
+ // Run git state + relevant files + recent files in parallel
257
+ const [gitResult, filesResult, recentResult] = await Promise.all([
258
+ this.getGitState(projectPath),
259
+ findRelevantFiles(taskDescription, projectPath, { maxFiles: 10, minScore: 0.15 }),
260
+ getRecentFiles(projectPath, { commits: 10, maxFiles: 10 }),
261
+ ])
262
+
263
+ // Extract signatures from top 3 relevant files
264
+ const topFiles = filesResult.files.slice(0, 3)
265
+ const signatureResults = await Promise.all(
266
+ topFiles.map(async (f) => {
267
+ try {
268
+ const result = await extractSignatures(f.path, projectPath)
269
+ if (result.signatures.length === 0) return null
270
+ const sigContent = result.signatures
271
+ .map((s) => `${s.exported ? 'export ' : ''}${s.type} ${s.name}: ${s.signature}`)
272
+ .join('\n')
273
+ return { path: f.path, content: sigContent }
274
+ } catch {
275
+ return null
276
+ }
277
+ })
278
+ )
279
+
280
+ return {
281
+ gitBranch: gitResult.branch,
282
+ gitStatus: gitResult.status,
283
+ relevantFiles: filesResult.files.map((f) => ({
284
+ path: f.path,
285
+ score: Math.round(f.score * 100),
286
+ reason: f.reasons.join(', '),
287
+ })),
288
+ recentFiles: recentResult.hotFiles.slice(0, 5).map((f) => ({
289
+ path: f.path,
290
+ lastChanged: f.lastChanged,
291
+ changes: f.changes,
292
+ })),
293
+ signatures: signatureResults.filter(
294
+ (s): s is { path: string; content: string } => s !== null
295
+ ),
296
+ }
297
+ } catch {
298
+ // Non-critical — return undefined if context gathering fails
299
+ return undefined
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Get current git state (branch + short status)
305
+ */
306
+ private async getGitState(projectPath: string): Promise<{ branch: string; status: string }> {
307
+ try {
308
+ const [branchResult, statusResult] = await Promise.all([
309
+ execAsync('git branch --show-current', { cwd: projectPath }),
310
+ execAsync('git status --porcelain', { cwd: projectPath }),
311
+ ])
312
+
313
+ const branch = branchResult.stdout.trim() || 'main'
314
+ const lines = statusResult.stdout.trim().split('\n').filter(Boolean)
315
+
316
+ let modified = 0
317
+ let untracked = 0
318
+ let staged = 0
319
+ for (const line of lines) {
320
+ const code = line.substring(0, 2)
321
+ if (code.startsWith('??')) untracked++
322
+ else if (code[0] !== ' ' && code[0] !== '?') staged++
323
+ else modified++
324
+ }
325
+
326
+ const parts: string[] = []
327
+ if (staged > 0) parts.push(`${staged} staged`)
328
+ if (modified > 0) parts.push(`${modified} modified`)
329
+ if (untracked > 0) parts.push(`${untracked} untracked`)
330
+ const status = parts.length > 0 ? parts.join(', ') : 'clean'
331
+
332
+ return { branch, status }
333
+ } catch {
334
+ return { branch: 'unknown', status: 'git unavailable' }
225
335
  }
226
336
  }
227
337
 
@@ -365,6 +475,8 @@ export class OrchestratorExecutor {
365
475
  content: body,
366
476
  skills: frontmatter.skills || [],
367
477
  filePath,
478
+ effort: frontmatter.effort as LoadedAgent['effort'],
479
+ model: frontmatter.model as string | undefined,
368
480
  }
369
481
  } catch {
370
482
  // Try next variation
@@ -409,16 +521,18 @@ export class OrchestratorExecutor {
409
521
  async loadSkills(agents: LoadedAgent[]): Promise<LoadedSkill[]> {
410
522
  const skillsDir = path.join(os.homedir(), '.claude', 'skills')
411
523
 
412
- // Collect unique skill names from all agents
413
- const uniqueSkillNames = new Set<string>()
524
+ // Collect unique skill names from all agents, tracking which agents need them
525
+ const skillToAgents = new Map<string, string[]>()
414
526
  for (const agent of agents) {
415
527
  for (const skillName of agent.skills) {
416
- uniqueSkillNames.add(skillName)
528
+ const existing = skillToAgents.get(skillName) || []
529
+ existing.push(agent.name)
530
+ skillToAgents.set(skillName, existing)
417
531
  }
418
532
  }
419
533
 
420
534
  // Load all skills in parallel
421
- const skillPromises = Array.from(uniqueSkillNames).map(
535
+ const skillPromises = Array.from(skillToAgents.keys()).map(
422
536
  async (skillName): Promise<LoadedSkill | null> => {
423
537
  // Check both patterns: flat file and subdirectory (ecosystem standard)
424
538
  const flatPath = path.join(skillsDir, `${skillName}.md`)
@@ -434,7 +548,11 @@ export class OrchestratorExecutor {
434
548
  const content = await fs.readFile(flatPath, 'utf-8')
435
549
  return { name: skillName, content, filePath: flatPath }
436
550
  } catch {
437
- // Skill not found - not an error, just skip
551
+ // Skill not found log warning with agent context
552
+ const agentNames = skillToAgents.get(skillName) || []
553
+ console.warn(
554
+ `⚠ Skill "${skillName}" not installed (needed by: ${agentNames.join(', ')}). Run \`prjct sync\` to auto-install.`
555
+ )
438
556
  return null
439
557
  }
440
558
  }
@@ -459,6 +459,8 @@ class PromptBuilder {
459
459
  parts.push('### LOADED AGENTS (Project-Specific Specialists)\n\n')
460
460
  for (const agent of orchestratorContext.agents) {
461
461
  parts.push(`#### Agent: ${agent.name} (${agent.domain})\n`)
462
+ if (agent.effort) parts.push(`Effort: ${agent.effort}\n`)
463
+ if (agent.model) parts.push(`Model: ${agent.model}\n`)
462
464
  if (agent.skills.length > 0) {
463
465
  parts.push(`Skills: ${agent.skills.join(', ')}\n`)
464
466
  }
@@ -476,15 +478,50 @@ class PromptBuilder {
476
478
  parts.push('### LOADED SKILLS (From Agent Frontmatter)\n\n')
477
479
  for (const skill of orchestratorContext.skills) {
478
480
  parts.push(`#### Skill: ${skill.name}\n`)
479
- // Include first 1000 chars of skill content
481
+ // Include first 2000 chars of skill content
480
482
  const truncatedContent =
481
- skill.content.length > 1000
482
- ? `${skill.content.substring(0, 1000)}\n... (truncated)`
483
+ skill.content.length > 2000
484
+ ? `${skill.content.substring(0, 2000)}\n... (truncated)`
483
485
  : skill.content
484
486
  parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
485
487
  }
486
488
  }
487
489
 
490
+ // Inject real codebase context (proactively gathered)
491
+ if (orchestratorContext.realContext) {
492
+ const rc = orchestratorContext.realContext
493
+ parts.push('### CODEBASE CONTEXT (Real — gathered proactively)\n\n')
494
+
495
+ parts.push(`**Git State**: Branch \`${rc.gitBranch}\` | ${rc.gitStatus}\n\n`)
496
+
497
+ if (rc.relevantFiles.length > 0) {
498
+ parts.push('**Relevant Files** (scored by task relevance):\n')
499
+ parts.push('| Score | File | Why |\n')
500
+ parts.push('|-------|------|-----|\n')
501
+ for (const f of rc.relevantFiles.slice(0, 8)) {
502
+ parts.push(`| ${f.score} | ${f.path} | ${f.reason} |\n`)
503
+ }
504
+ parts.push('\n')
505
+ }
506
+
507
+ if (rc.signatures.length > 0) {
508
+ parts.push('**Code Signatures** (top files):\n')
509
+ for (const sig of rc.signatures) {
510
+ parts.push(`\`\`\`typescript\n// ${sig.path}\n${sig.content}\n\`\`\`\n`)
511
+ }
512
+ parts.push('\n')
513
+ }
514
+
515
+ if (rc.recentFiles.length > 0) {
516
+ parts.push('**Recently Changed**: ')
517
+ const recentSummary = rc.recentFiles
518
+ .slice(0, 5)
519
+ .map((f) => `${f.path} (${f.lastChanged})`)
520
+ .join(', ')
521
+ parts.push(`${recentSummary}\n\n`)
522
+ }
523
+ }
524
+
488
525
  // Inject subtasks if fragmented
489
526
  if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
490
527
  parts.push('### SUBTASKS (Execute in Order)\n\n')
@@ -36,7 +36,7 @@ export const AI_TOOLS: Record<string, AIToolConfig> = {
36
36
  name: 'Claude Code',
37
37
  outputFile: 'CLAUDE.md',
38
38
  outputPath: 'global',
39
- maxTokens: 3000,
39
+ maxTokens: 6000,
40
40
  format: 'detailed',
41
41
  description: 'Anthropic Claude Code CLI',
42
42
  },
package/core/bus/bus.ts CHANGED
@@ -9,8 +9,10 @@
9
9
 
10
10
  import fs from 'node:fs/promises'
11
11
  import path from 'node:path'
12
+ import { getErrorMessage } from '../errors'
12
13
  import pathManager from '../infrastructure/path-manager'
13
14
  import { type EventCallback, type EventData, EventTypes } from '../types'
15
+ import log from '../utils/logger'
14
16
 
15
17
  class EventBus {
16
18
  private listeners: Map<string, Set<EventCallback>>
@@ -108,7 +110,7 @@ class EventBus {
108
110
  // Log any errors
109
111
  results.forEach((result) => {
110
112
  if (result.status === 'rejected') {
111
- console.error(`Event listener error for ${event}:`, result.reason)
113
+ log.error(`Event listener error for ${event}:`, result.reason)
112
114
  }
113
115
  })
114
116
 
@@ -169,7 +171,7 @@ class EventBus {
169
171
  await result
170
172
  }
171
173
  } catch (error) {
172
- console.error('Event callback error:', error)
174
+ log.error('Event callback error:', error)
173
175
  throw error
174
176
  }
175
177
  }
@@ -188,8 +190,8 @@ class EventBus {
188
190
  // Append event
189
191
  const line = `${JSON.stringify(eventData)}\n`
190
192
  await fs.appendFile(eventsPath, line)
191
- } catch (_error) {
192
- // Silently fail - logging should not break functionality
193
+ } catch (error) {
194
+ log.debug('Failed to log event:', getErrorMessage(error))
193
195
  }
194
196
  }
195
197
 
@@ -516,6 +516,11 @@ export class AnalysisCommands extends PrjctCommandsBase {
516
516
  const skillWord = result.skills.length === 1 ? 'skill' : 'skills'
517
517
  generatedItems.push(`${result.skills.length} ${skillWord}`)
518
518
  }
519
+ const installed = result.skillsInstalled?.filter((s) => s.status === 'installed') || []
520
+ if (installed.length > 0) {
521
+ const word = installed.length === 1 ? 'skill' : 'skills'
522
+ generatedItems.push(`${installed.length} ${word} auto-installed`)
523
+ }
519
524
 
520
525
  out.section('Generated')
521
526
  out.list(generatedItems, { bullet: '✓' })
@@ -40,6 +40,7 @@ const MODEL_PRICING = {
40
40
  'claude-sonnet-4.5': { input: 0.003, output: 0.015 }, // $3/$15 per M
41
41
  'claude-haiku-4.5': { input: 0.001, output: 0.005 }, // $1/$5 per M
42
42
  'claude-opus-4': { input: 0.015, output: 0.075 }, // $15/$75 per M (legacy)
43
+ 'claude-opus-4-6': { input: 0.015, output: 0.075 }, // $15/$75 per M
43
44
  // OpenAI
44
45
  'gpt-4o': { input: 0.0025, output: 0.01 }, // $2.50/$10 per M
45
46
  'gpt-4-turbo': { input: 0.01, output: 0.03 }, // $10/$30 per M
@@ -78,6 +79,7 @@ export function countTokens(text: string): number {
78
79
  const BREAKDOWN_MODELS: ModelName[] = [
79
80
  'claude-sonnet-4.5',
80
81
  'claude-opus-4.5',
82
+ 'claude-opus-4-6',
81
83
  'gpt-4o',
82
84
  'gemini-1.5-pro',
83
85
  ]
@@ -25,6 +25,8 @@ interface Agent {
25
25
  role: string | null
26
26
  domain: string
27
27
  skills: string[]
28
+ effort?: 'low' | 'medium' | 'high' | 'max'
29
+ model?: string
28
30
  modified: Date
29
31
  /** Source of this agent: 'file' (generated) or 'hierarchical' (AGENTS.md) */
30
32
  source?: 'file' | 'hierarchical'
@@ -69,7 +71,8 @@ class AgentLoader {
69
71
  const agentPath = path.join(this.agentsDir, `${agentName}.md`)
70
72
  const content = await fs.readFile(agentPath, 'utf-8')
71
73
 
72
- // Parse agent metadata from content
74
+ // Parse agent metadata from content (including frontmatter)
75
+ const { effort, model } = this.extractFrontmatterMeta(content)
73
76
  const agent: Agent = {
74
77
  name: agentName,
75
78
  content,
@@ -77,6 +80,8 @@ class AgentLoader {
77
80
  role: this.extractRole(content),
78
81
  domain: this.extractDomain(content),
79
82
  skills: this.extractSkills(content),
83
+ effort,
84
+ model,
80
85
  modified: (await fs.stat(agentPath)).mtime,
81
86
  }
82
87
 
@@ -203,6 +208,35 @@ class AgentLoader {
203
208
  return skills
204
209
  }
205
210
 
211
+ /**
212
+ * Extract effort and model from YAML frontmatter
213
+ */
214
+ private extractFrontmatterMeta(content: string): {
215
+ effort?: 'low' | 'medium' | 'high' | 'max'
216
+ model?: string
217
+ } {
218
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/)
219
+ if (!frontmatterMatch) return {}
220
+
221
+ const fm = frontmatterMatch[1]
222
+ const result: { effort?: 'low' | 'medium' | 'high' | 'max'; model?: string } = {}
223
+
224
+ const effortMatch = fm.match(/^effort:\s*(.+)$/m)
225
+ if (effortMatch) {
226
+ const val = effortMatch[1].trim().replace(/['"]/g, '') as 'low' | 'medium' | 'high' | 'max'
227
+ if (['low', 'medium', 'high', 'max'].includes(val)) {
228
+ result.effort = val
229
+ }
230
+ }
231
+
232
+ const modelMatch = fm.match(/^model:\s*(.+)$/m)
233
+ if (modelMatch) {
234
+ result.model = modelMatch[1].trim().replace(/['"]/g, '')
235
+ }
236
+
237
+ return result
238
+ }
239
+
206
240
  /**
207
241
  * Get agents directory path
208
242
  */
@@ -29,11 +29,14 @@ export interface SelectedContext {
29
29
  }
30
30
  }
31
31
 
32
+ /** Default token budget for context selection (increased for 200K+ context models) */
33
+ const DEFAULT_TOKEN_BUDGET = 80_000
34
+
32
35
  export interface ContextSelectionOptions {
33
36
  maxFiles?: number // Max files to return (default: 50)
34
37
  minScore?: number // Min relevance score (default: 30)
35
38
  includeGeneral?: boolean // Include 'general' domain files (default: true)
36
- tokenBudget?: number // Max estimated tokens (default: 50000)
39
+ tokenBudget?: number // Max estimated tokens (default: 80000)
37
40
  }
38
41
 
39
42
  // ============================================================================
@@ -173,7 +176,7 @@ export class ContextSelector {
173
176
  const maxFiles = options.maxFiles || 50
174
177
  const minScore = options.minScore || 30
175
178
  const includeGeneral = options.includeGeneral !== false
176
- const tokenBudget = options.tokenBudget || 50000
179
+ const tokenBudget = options.tokenBudget || DEFAULT_TOKEN_BUDGET
177
180
 
178
181
  // Load index and categories
179
182
  const [index, domainsData, categoriesCache] = await Promise.all([
@@ -351,7 +354,7 @@ export class ContextSelector {
351
354
  ): SelectedContext {
352
355
  const maxFiles = options.maxFiles || 50
353
356
  const minScore = options.minScore || 30
354
- const tokenBudget = options.tokenBudget || 50000
357
+ const tokenBudget = options.tokenBudget || DEFAULT_TOKEN_BUDGET
355
358
 
356
359
  // Filter and sort by score
357
360
  const filteredFiles = files.filter((f) => f.score >= minScore).sort((a, b) => b.score - a.score)
@@ -36,6 +36,8 @@ export interface HierarchicalAgent {
36
36
  sources: string[]
37
37
  /** Whether this agent was overridden at some level */
38
38
  wasOverridden: boolean
39
+ /** Effort level hint for Claude's adaptive reasoning depth */
40
+ effort?: 'low' | 'medium' | 'high' | 'max'
39
41
  }
40
42
 
41
43
  export interface AgentResolutionResult {