prjct-cli 1.5.1 → 1.6.0
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 +73 -0
- package/core/__tests__/utils/output.test.ts +7 -1
- package/core/agentic/orchestrator-executor.ts +126 -8
- package/core/agentic/prompt-builder.ts +40 -3
- package/core/ai-tools/registry.ts +1 -1
- package/core/commands/analysis.ts +5 -0
- package/core/context-tools/token-counter.ts +2 -0
- package/core/domain/agent-loader.ts +35 -1
- package/core/services/context-selector.ts +6 -3
- package/core/services/hierarchical-agent-resolver.ts +2 -0
- package/core/services/sync-service.ts +112 -0
- package/core/session/compaction.ts +1 -1
- package/core/types/agentic.ts +22 -0
- package/core/types/agents.ts +5 -0
- package/core/types/index.ts +1 -0
- package/core/utils/output.ts +15 -1
- package/dist/bin/prjct.mjs +996 -358
- package/package.json +1 -1
- package/templates/config/skill-mappings.json +23 -60
- package/templates/subagents/domain/backend.md +1 -0
- package/templates/subagents/domain/database.md +1 -0
- package/templates/subagents/domain/devops.md +1 -0
- package/templates/subagents/domain/frontend.md +1 -0
- package/templates/subagents/domain/testing.md +1 -0
- package/templates/subagents/workflow/chief-architect.md +2 -1
- package/templates/subagents/workflow/prjct-planner.md +2 -1
- package/templates/subagents/workflow/prjct-shipper.md +1 -0
- package/templates/subagents/workflow/prjct-workflow.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-02-06
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- super context for agents — skills.sh, proactive codebase context, effort/model (#121)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.6.0] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **Effort/model metadata wiring**: Agent frontmatter `effort` and `model` fields are now extracted and injected into prompts, enabling per-agent reasoning depth control.
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
|
|
20
|
+
- **Skill loading warnings**: Missing skills now log visible warnings with the agent that needs them and a hint to run `prjct sync`
|
|
21
|
+
- **Skill content in prompts**: Increased skill content truncation from 1000 to 2000 characters for richer context
|
|
22
|
+
- **Skill mappings v3**: Updated `skill-mappings.json` from generic names to real installable skills.sh packages
|
|
23
|
+
|
|
24
|
+
### Implementation Details
|
|
25
|
+
|
|
26
|
+
- `sync-service.ts`: New `autoInstallSkills()` method reads `skill-mappings.json`, checks if each skill is installed, and calls `skillInstaller.install()` for missing ones
|
|
27
|
+
- `orchestrator-executor.ts`: New `gatherRealContext()` calls `findRelevantFiles()`, `getRecentFiles()`, and `extractSignatures()` in parallel to build a proactive briefing
|
|
28
|
+
- `prompt-builder.ts`: New "CODEBASE CONTEXT" section with git state, relevant files table, code signatures, and recently changed files; plus effort/model per agent
|
|
29
|
+
- `agent-loader.ts`: New `extractFrontmatterMeta()` parses YAML frontmatter for effort/model fields
|
|
30
|
+
- `agentic.ts`: New `RealCodebaseContext` interface; `LoadedAgent` extended with `effort?` and `model?`
|
|
31
|
+
|
|
32
|
+
### Test Plan
|
|
33
|
+
|
|
34
|
+
#### For QA
|
|
35
|
+
1. Run `prjct sync` — verify skills auto-install (check `~/.claude/skills/`)
|
|
36
|
+
2. Run `p. task "test"` — verify prompt includes git state, relevant files, signatures, effort/model
|
|
37
|
+
3. Verify warnings for missing skills
|
|
38
|
+
4. Build and all 416 tests pass
|
|
39
|
+
|
|
40
|
+
#### For Users
|
|
41
|
+
**What changed:** Agents receive proactive codebase context before starting work. Skills auto-install during sync.
|
|
42
|
+
**How to use:** No action needed — automatic during `prjct sync` and `p. task`.
|
|
43
|
+
**Breaking changes:** None
|
|
44
|
+
|
|
45
|
+
## [1.5.2] - 2026-02-06
|
|
46
|
+
|
|
47
|
+
### Improved
|
|
48
|
+
|
|
49
|
+
- **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
|
|
50
|
+
|
|
51
|
+
### Implementation Details
|
|
52
|
+
|
|
53
|
+
- Added `process.stdout.isTTY` guard to `spin()`, `step()`, and `progress()` in `core/utils/output.ts`
|
|
54
|
+
- Non-TTY environments get a single line with `\n` instead of `setInterval` with `\r` carriage returns
|
|
55
|
+
- Made `clear()` a no-op in non-TTY (the `\r` + spaces trick doesn't work outside terminals)
|
|
56
|
+
- Updated test for `stop()` to handle non-TTY behavior in test runner
|
|
57
|
+
|
|
58
|
+
### Learnings
|
|
59
|
+
|
|
60
|
+
- `process.stdout.isTTY` is the reliable built-in way to detect interactive terminals in Node.js
|
|
61
|
+
- Test suites (bun test) also run as non-TTY, so test assertions need to account for both paths
|
|
62
|
+
|
|
63
|
+
### Test Plan
|
|
64
|
+
|
|
65
|
+
#### For QA
|
|
66
|
+
1. Run `prjct sync --yes` in an interactive terminal — spinner should animate normally
|
|
67
|
+
2. Run `prjct sync --yes > out.txt` — output should show a single static line, no repeated frames
|
|
68
|
+
3. Run inside Claude Code Bash tool — output should be clean, no spinner noise
|
|
69
|
+
4. Verify `step()` and `progress()` behave the same way in both environments
|
|
70
|
+
|
|
71
|
+
#### For Users
|
|
72
|
+
**What changed:** CLI spinners no longer produce garbage output in non-interactive terminals
|
|
73
|
+
**How to use:** No action needed — automatic TTY detection
|
|
74
|
+
**Breaking changes:** None
|
|
75
|
+
|
|
3
76
|
## [1.5.1] - 2026-02-06
|
|
4
77
|
|
|
5
78
|
### Refactoring
|
|
@@ -124,7 +124,13 @@ describe('Output Module', () => {
|
|
|
124
124
|
stdoutWriteSpy.mockClear()
|
|
125
125
|
out.stop()
|
|
126
126
|
|
|
127
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
481
|
+
// Include first 2000 chars of skill content
|
|
480
482
|
const truncatedContent =
|
|
481
|
-
skill.content.length >
|
|
482
|
-
? `${skill.content.substring(0,
|
|
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')
|
|
@@ -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:
|
|
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 ||
|
|
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 ||
|
|
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 {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { exec } from 'node:child_process'
|
|
19
19
|
import fs from 'node:fs/promises'
|
|
20
|
+
import os from 'node:os'
|
|
20
21
|
import path from 'node:path'
|
|
21
22
|
import { promisify } from 'node:util'
|
|
22
23
|
import {
|
|
@@ -26,15 +27,18 @@ import {
|
|
|
26
27
|
type ProjectContext,
|
|
27
28
|
resolveToolIds,
|
|
28
29
|
} from '../ai-tools'
|
|
30
|
+
import { getErrorMessage } from '../errors'
|
|
29
31
|
import commandInstaller from '../infrastructure/command-installer'
|
|
30
32
|
import configManager from '../infrastructure/config-manager'
|
|
31
33
|
import pathManager from '../infrastructure/path-manager'
|
|
32
34
|
import { metricsStorage } from '../storage/metrics-storage'
|
|
33
35
|
import { type ContextSources, defaultSources, type SourceInfo } from '../utils/citations'
|
|
34
36
|
import dateHelper from '../utils/date-helper'
|
|
37
|
+
import log from '../utils/logger'
|
|
35
38
|
import { ContextFileGenerator } from './context-generator'
|
|
36
39
|
import type { SyncDiff } from './diff-generator'
|
|
37
40
|
import { localStateGenerator } from './local-state-generator'
|
|
41
|
+
import { skillInstaller } from './skill-installer'
|
|
38
42
|
import { type StackDetection, StackDetector } from './stack-detector'
|
|
39
43
|
import { syncVerifier, type VerificationReport } from './sync-verifier'
|
|
40
44
|
|
|
@@ -105,6 +109,7 @@ interface SyncResult {
|
|
|
105
109
|
stack: StackDetection
|
|
106
110
|
agents: AgentInfo[]
|
|
107
111
|
skills: { agent: string; skill: string }[]
|
|
112
|
+
skillsInstalled: { name: string; agent: string; status: 'installed' | 'skipped' | 'error' }[]
|
|
108
113
|
contextFiles: string[]
|
|
109
114
|
aiTools: AIToolResult[]
|
|
110
115
|
syncMetrics?: SyncMetrics
|
|
@@ -176,6 +181,7 @@ class SyncService {
|
|
|
176
181
|
stack: this.emptyStack(),
|
|
177
182
|
agents: [],
|
|
178
183
|
skills: [],
|
|
184
|
+
skillsInstalled: [],
|
|
179
185
|
contextFiles: [],
|
|
180
186
|
aiTools: [],
|
|
181
187
|
error: 'No prjct project. Run p. init first.',
|
|
@@ -203,6 +209,7 @@ class SyncService {
|
|
|
203
209
|
// 4. Generate all files (depends on gathered data)
|
|
204
210
|
const agents = await this.generateAgents(stack, stats)
|
|
205
211
|
const skills = this.configureSkills(agents)
|
|
212
|
+
const skillsInstalled = await this.autoInstallSkills(agents)
|
|
206
213
|
const sources = this.buildSources(stats, commands)
|
|
207
214
|
const contextFiles = await this.generateContextFiles(git, stats, commands, agents, sources)
|
|
208
215
|
|
|
@@ -274,6 +281,7 @@ class SyncService {
|
|
|
274
281
|
stack,
|
|
275
282
|
agents,
|
|
276
283
|
skills,
|
|
284
|
+
skillsInstalled,
|
|
277
285
|
contextFiles,
|
|
278
286
|
aiTools: aiToolResults.map((r) => ({
|
|
279
287
|
toolId: r.toolId,
|
|
@@ -294,6 +302,7 @@ class SyncService {
|
|
|
294
302
|
stack: this.emptyStack(),
|
|
295
303
|
agents: [],
|
|
296
304
|
skills: [],
|
|
305
|
+
skillsInstalled: [],
|
|
297
306
|
contextFiles: [],
|
|
298
307
|
aiTools: [],
|
|
299
308
|
error: (error as Error).message,
|
|
@@ -800,6 +809,109 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
800
809
|
return skills
|
|
801
810
|
}
|
|
802
811
|
|
|
812
|
+
// ==========================================================================
|
|
813
|
+
// SKILL AUTO-INSTALLATION
|
|
814
|
+
// ==========================================================================
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Auto-install skills from skill-mappings.json for generated agents.
|
|
818
|
+
* Reads the mapping, checks which packages are needed, and installs missing ones.
|
|
819
|
+
*/
|
|
820
|
+
private async autoInstallSkills(
|
|
821
|
+
agents: AgentInfo[]
|
|
822
|
+
): Promise<{ name: string; agent: string; status: 'installed' | 'skipped' | 'error' }[]> {
|
|
823
|
+
const results: { name: string; agent: string; status: 'installed' | 'skipped' | 'error' }[] = []
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
// Load skill mappings
|
|
827
|
+
const mappingsPath = path.join(
|
|
828
|
+
__dirname,
|
|
829
|
+
'..',
|
|
830
|
+
'..',
|
|
831
|
+
'templates',
|
|
832
|
+
'config',
|
|
833
|
+
'skill-mappings.json'
|
|
834
|
+
)
|
|
835
|
+
const mappingsContent = await fs.readFile(mappingsPath, 'utf-8')
|
|
836
|
+
const mappings = JSON.parse(mappingsContent)
|
|
837
|
+
const agentToSkillMap = mappings.agentToSkillMap || {}
|
|
838
|
+
|
|
839
|
+
// Collect all packages to install, grouped by agent
|
|
840
|
+
const packagesToInstall: { pkg: string; agent: string }[] = []
|
|
841
|
+
for (const agent of agents) {
|
|
842
|
+
const mapping = agentToSkillMap[agent.name]
|
|
843
|
+
if (mapping?.packages) {
|
|
844
|
+
for (const pkg of mapping.packages) {
|
|
845
|
+
packagesToInstall.push({ pkg, agent: agent.name })
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (packagesToInstall.length === 0) return results
|
|
851
|
+
|
|
852
|
+
// Install each package (check if already installed first)
|
|
853
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills')
|
|
854
|
+
for (const { pkg, agent } of packagesToInstall) {
|
|
855
|
+
// Extract skill name from package path (e.g., "anthropics/skills/frontend-design" -> "frontend-design")
|
|
856
|
+
const skillName = pkg.split('/').pop() || pkg
|
|
857
|
+
|
|
858
|
+
// Check if already installed
|
|
859
|
+
const subdirPath = path.join(skillsDir, skillName, 'SKILL.md')
|
|
860
|
+
const flatPath = path.join(skillsDir, `${skillName}.md`)
|
|
861
|
+
|
|
862
|
+
let alreadyInstalled = false
|
|
863
|
+
try {
|
|
864
|
+
await fs.access(subdirPath)
|
|
865
|
+
alreadyInstalled = true
|
|
866
|
+
} catch {
|
|
867
|
+
try {
|
|
868
|
+
await fs.access(flatPath)
|
|
869
|
+
alreadyInstalled = true
|
|
870
|
+
} catch {
|
|
871
|
+
// Not installed
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (alreadyInstalled) {
|
|
876
|
+
results.push({ name: skillName, agent, status: 'skipped' })
|
|
877
|
+
continue
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Install via skillInstaller (supports owner/repo format)
|
|
881
|
+
try {
|
|
882
|
+
// Parse package as owner/repo or owner/repo@skill format
|
|
883
|
+
// "anthropics/skills/frontend-design" -> owner=anthropics, repo=skills, skill=frontend-design
|
|
884
|
+
const parts = pkg.split('/')
|
|
885
|
+
let installSource: string
|
|
886
|
+
if (parts.length === 3) {
|
|
887
|
+
// owner/repo/skill -> owner/repo@skill
|
|
888
|
+
installSource = `${parts[0]}/${parts[1]}@${parts[2]}`
|
|
889
|
+
} else {
|
|
890
|
+
installSource = pkg
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const installResult = await skillInstaller.install(installSource)
|
|
894
|
+
if (installResult.installed.length > 0) {
|
|
895
|
+
results.push({ name: skillName, agent, status: 'installed' })
|
|
896
|
+
log.info(`Installed skill: ${skillName} for agent: ${agent}`)
|
|
897
|
+
} else if (installResult.errors.length > 0) {
|
|
898
|
+
results.push({ name: skillName, agent, status: 'error' })
|
|
899
|
+
log.debug(`Failed to install skill ${skillName}`, { errors: installResult.errors })
|
|
900
|
+
} else {
|
|
901
|
+
results.push({ name: skillName, agent, status: 'skipped' })
|
|
902
|
+
}
|
|
903
|
+
} catch (error) {
|
|
904
|
+
results.push({ name: skillName, agent, status: 'error' })
|
|
905
|
+
log.debug(`Skill install error for ${skillName}`, { error: getErrorMessage(error) })
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
log.debug('Skill auto-installation failed (non-critical)', { error: getErrorMessage(error) })
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return results
|
|
913
|
+
}
|
|
914
|
+
|
|
803
915
|
// ==========================================================================
|
|
804
916
|
// CONTEXT FILE GENERATION
|
|
805
917
|
// ==========================================================================
|