contextos-agents 2.0.0-beta.2 → 2.0.0-beta.3
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/.agents/AGENTS.md +15 -49
- package/.agents/adapters/cursor/export.js +20 -3
- package/.agents/ctx.js +32 -70
- package/.agents/doctor.js +137 -3
- package/.agents/plugins.js +20 -5
- package/.agents/resolver/canonical-resolver.js +1348 -0
- package/.agents/rules/rule-catalog.js +525 -0
- package/.agents/skills-index.json +6 -166
- package/.agents/stats.js +9 -9
- package/package.json +87 -85
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/resolver/canonical-resolver.js
|
|
3
|
+
* ContextOS — Canonical Manifest-Driven Skill Resolver & Budget Planner (Milestone 3)
|
|
4
|
+
*
|
|
5
|
+
* Implements the single unified resolution engine for both CLI and MCP:
|
|
6
|
+
* - Signal-based candidate scoring (Evidence model)
|
|
7
|
+
* - Direct query to compiled registry (registry.v2.json)
|
|
8
|
+
* - Transitive dependency closure with topological sort
|
|
9
|
+
* - Deterministic conflict resolution policy
|
|
10
|
+
* - Token budget planner (removing arbitrary 4-skill hard cap, capability clustering)
|
|
11
|
+
* - Risk classification (routine, standard, high, destructive)
|
|
12
|
+
* - Transparent explainability (--explain breakdown)
|
|
13
|
+
* - 100% parity between CLI and MCP
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const { WorkspaceGraphBuilder } = require('../workspace/workspace-graph');
|
|
22
|
+
|
|
23
|
+
// Prompt budget limits (Section 14.4)
|
|
24
|
+
const BUDGET_TIERS = {
|
|
25
|
+
BOOTSTRAP: 1200, // always-on bootstrap budget
|
|
26
|
+
SKILL_SUMMARY: 150, // single skill summary limit
|
|
27
|
+
SKILL_BODY: 1200, // single skill body limit
|
|
28
|
+
ROUTINE: 2000, // routine context limit
|
|
29
|
+
STANDARD: 4000, // standard normal compiled context limit
|
|
30
|
+
HIGH: 7500, // high-risk compiled context limit
|
|
31
|
+
DESTRUCTIVE: 10000, // destructive compiled context limit
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONTEXT_BUDGET_TOKENS = 8000;
|
|
35
|
+
|
|
36
|
+
// Risk-based workflows (Section 14.3)
|
|
37
|
+
const WORKFLOW_TEMPLATES = {
|
|
38
|
+
routine: {
|
|
39
|
+
name: 'ROUTINE',
|
|
40
|
+
summary: 'Low-risk fast-track: documentation, typo, formatting, low-risk config',
|
|
41
|
+
steps: [
|
|
42
|
+
'1. INSPECT: Read target files and locate exact lines to change',
|
|
43
|
+
'2. CHANGE: Apply targeted edits directly without ceremonial spec/plan',
|
|
44
|
+
'3. TARGETED_VERIFY: Execute specific test, linter, or validation check',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
standard: {
|
|
48
|
+
name: 'STANDARD',
|
|
49
|
+
summary: 'Standard feature development, bugfix, or component refactor',
|
|
50
|
+
steps: [
|
|
51
|
+
'1. SHORT_PLAN: Outline affected files and atomic steps',
|
|
52
|
+
'2. CHANGE: Implement minimal code adhering to ponytail mindset',
|
|
53
|
+
'3. TESTS: Run relevant unit and integration test suites',
|
|
54
|
+
'4. SELF_REVIEW: Verify diff cleanly contains changes to target files only',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
high: {
|
|
58
|
+
name: 'HIGH',
|
|
59
|
+
summary: 'High-risk feature: auth, migration, public API, security, CI, concurrency',
|
|
60
|
+
steps: [
|
|
61
|
+
'1. SPEC: Document explicit in-scope/out-of-scope and acceptance criteria',
|
|
62
|
+
'2. APPROVED_PLAN: Atomic tasks (<2h), risk mitigation, and rollback strategy',
|
|
63
|
+
'3. ISOLATED_CHANGE: Surgical implementation strictly within planned files',
|
|
64
|
+
'4. FULL_VERIFICATION: Global test suite + secret scanner + validator',
|
|
65
|
+
'5. INDEPENDENT_REVIEW: Review through security, database, and accessibility lenses',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
destructive: {
|
|
69
|
+
name: 'DESTRUCTIVE',
|
|
70
|
+
summary: 'Destructive action: file deletion, irreversible migration, history rewrite',
|
|
71
|
+
steps: [
|
|
72
|
+
'1. EXPLICIT_AUTHORITY: Confirm explicit user authorization and bounds',
|
|
73
|
+
'2. ROLLBACK_REHEARSAL: Verify snapshot, backup, or rollback transaction log',
|
|
74
|
+
'3. GUARDED_CHANGE: Execute modification inside journaled transaction with project lock',
|
|
75
|
+
'4. POST_VERIFY: Confirm integrity, state consistency, and absence of data loss',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Streamlined execution modes (Section 14.2)
|
|
81
|
+
const MODES = {
|
|
82
|
+
DISCOVER: 'DISCOVER',
|
|
83
|
+
CHANGE: 'CHANGE',
|
|
84
|
+
VERIFY: 'VERIFY',
|
|
85
|
+
REVIEW: 'REVIEW',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Consolidated deprecated aliases (Section 14.7)
|
|
89
|
+
const DEPRECATED_ALIASES = {
|
|
90
|
+
'context-manager': 'context-os',
|
|
91
|
+
'gstack-roles': 'engineering-workflow',
|
|
92
|
+
'ui-design': 'ui-ux-pro',
|
|
93
|
+
'ux-design': 'ui-ux-pro',
|
|
94
|
+
'react-best-practices': 'react',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Weight constants based on architecture spec
|
|
98
|
+
const WEIGHTS = {
|
|
99
|
+
EXPLICIT_SKILL: 100,
|
|
100
|
+
EXACT_ALIAS: 80,
|
|
101
|
+
FILE_GLOB: 70,
|
|
102
|
+
NEAREST_PACKAGE: 60,
|
|
103
|
+
TASK_KEYWORD_STRONG: 40,
|
|
104
|
+
TASK_KEYWORD_MEDIUM: 20,
|
|
105
|
+
TASK_KEYWORD_WEAK: 5,
|
|
106
|
+
PROFILE_REQUIRED: 1000, // Mandatory
|
|
107
|
+
PROFILE_PREFERRED: 20,
|
|
108
|
+
AMBIENT_CONFIG: 15,
|
|
109
|
+
SYNERGY_PREREQ: 50,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Loads compiled registry or compiles on the fly if missing.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} agentsDir - Path to .agents directory
|
|
116
|
+
* @returns {Object|null}
|
|
117
|
+
*/
|
|
118
|
+
function loadRegistry(agentsDir) {
|
|
119
|
+
const regPath = path.join(agentsDir, 'compiled', 'registry.v2.json');
|
|
120
|
+
if (fs.existsSync(regPath)) {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(fs.readFileSync(regPath, 'utf8'));
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore corrupt, fall through
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fallback: try compiling via ManifestCompiler
|
|
129
|
+
try {
|
|
130
|
+
const { ManifestCompiler } = require('../compiler/manifest-compiler.js');
|
|
131
|
+
const compiler = new ManifestCompiler({ agentsDir });
|
|
132
|
+
const res = compiler.compile();
|
|
133
|
+
if (res.success && res.registry) {
|
|
134
|
+
return res.registry;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves .agents directory, searching upwards or falling back to ContextOS repository root.
|
|
145
|
+
*/
|
|
146
|
+
function findAgentsDir(startDir = process.cwd()) {
|
|
147
|
+
let curr = path.resolve(startDir);
|
|
148
|
+
while (curr) {
|
|
149
|
+
const candidate = path.join(curr, '.agents');
|
|
150
|
+
if (fs.existsSync(path.join(candidate, 'compiled', 'registry.v2.json')) || fs.existsSync(path.join(candidate, 'core', 'skills'))) {
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
const parent = path.dirname(curr);
|
|
154
|
+
if (parent === curr) break;
|
|
155
|
+
curr = parent;
|
|
156
|
+
}
|
|
157
|
+
const fallback = path.resolve(__dirname, '..');
|
|
158
|
+
if (fs.existsSync(fallback)) return fallback;
|
|
159
|
+
return path.join(process.cwd(), '.agents');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolves project root directory containing .agents/
|
|
164
|
+
*/
|
|
165
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
166
|
+
let curr = path.resolve(startDir);
|
|
167
|
+
while (curr) {
|
|
168
|
+
if (fs.existsSync(path.join(curr, '.agents'))) {
|
|
169
|
+
return curr;
|
|
170
|
+
}
|
|
171
|
+
const parent = path.dirname(curr);
|
|
172
|
+
if (parent === curr) break;
|
|
173
|
+
curr = parent;
|
|
174
|
+
}
|
|
175
|
+
return process.cwd();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Analyzes import graph and manifests for project technologies.
|
|
180
|
+
* Exported for backward compatibility with AST tests.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} projectDir
|
|
183
|
+
* @returns {Map<string, number>}
|
|
184
|
+
*/
|
|
185
|
+
function analyzeImportGraph(projectDir = process.cwd()) {
|
|
186
|
+
const signals = new Map();
|
|
187
|
+
if (!projectDir || !fs.existsSync(projectDir)) return signals;
|
|
188
|
+
|
|
189
|
+
const add = (skill, w) => {
|
|
190
|
+
signals.set(skill, (signals.get(skill) || 0) + w);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
194
|
+
if (fs.existsSync(pkgPath)) {
|
|
195
|
+
try {
|
|
196
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
197
|
+
const deps = {
|
|
198
|
+
...(pkg.dependencies || {}),
|
|
199
|
+
...(pkg.devDependencies || {}),
|
|
200
|
+
...(pkg.peerDependencies || {}),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const PKG_MAP = {
|
|
204
|
+
'react': 'react',
|
|
205
|
+
'react-dom': 'react',
|
|
206
|
+
'next': 'nextjs',
|
|
207
|
+
'prisma': 'database',
|
|
208
|
+
'@prisma/client': 'database',
|
|
209
|
+
'drizzle-orm': 'database',
|
|
210
|
+
'drizzle-kit': 'database',
|
|
211
|
+
'typeorm': 'database',
|
|
212
|
+
'mongoose': 'database',
|
|
213
|
+
'pg': 'database',
|
|
214
|
+
'mysql2': 'database',
|
|
215
|
+
'vitest': 'testing',
|
|
216
|
+
'jest': 'testing',
|
|
217
|
+
'playwright': 'testing',
|
|
218
|
+
'@playwright/test': 'testing',
|
|
219
|
+
'cypress': 'testing',
|
|
220
|
+
'express': 'node',
|
|
221
|
+
'fastify': 'node',
|
|
222
|
+
'hono': 'node',
|
|
223
|
+
'@nestjs/core': 'nestjs',
|
|
224
|
+
'typescript': 'typescript',
|
|
225
|
+
'zod': 'typescript',
|
|
226
|
+
'tailwindcss': 'ui-ux-pro',
|
|
227
|
+
'lucide-react': 'ui-ux-pro',
|
|
228
|
+
'@radix-ui/react-dialog': 'web-accessibility',
|
|
229
|
+
'framer-motion': 'impeccable-design',
|
|
230
|
+
'dockerode': 'docker',
|
|
231
|
+
'ioredis': 'system-design',
|
|
232
|
+
'kafkajs': 'microservices',
|
|
233
|
+
'amqplib': 'microservices',
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
for (const [dep, skill] of Object.entries(PKG_MAP)) {
|
|
237
|
+
if (deps[dep]) add(skill, 15);
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// ignore
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const CONFIG_MAP = [
|
|
245
|
+
{ file: 'tsconfig.json', skill: 'typescript', weight: 10 },
|
|
246
|
+
{ file: 'next.config.js', skill: 'nextjs', weight: 15 },
|
|
247
|
+
{ file: 'next.config.mjs', skill: 'nextjs', weight: 15 },
|
|
248
|
+
{ file: 'next.config.ts', skill: 'nextjs', weight: 15 },
|
|
249
|
+
{ file: 'tailwind.config.js', skill: 'ui-ux-pro', weight: 10 },
|
|
250
|
+
{ file: 'tailwind.config.ts', skill: 'ui-ux-pro', weight: 10 },
|
|
251
|
+
{ file: 'nest-cli.json', skill: 'nestjs', weight: 15 },
|
|
252
|
+
{ file: 'prisma/schema.prisma', skill: 'database', weight: 15 },
|
|
253
|
+
{ file: 'drizzle.config.ts', skill: 'database', weight: 15 },
|
|
254
|
+
{ file: 'drizzle.config.js', skill: 'database', weight: 15 },
|
|
255
|
+
{ file: 'Dockerfile', skill: 'docker', weight: 15 },
|
|
256
|
+
{ file: 'docker-compose.yml', skill: 'docker', weight: 15 },
|
|
257
|
+
{ file: 'docker-compose.yaml', skill: 'docker', weight: 15 },
|
|
258
|
+
{ file: 'requirements.txt', skill: 'fastapi', weight: 15 },
|
|
259
|
+
{ file: 'pyproject.toml', skill: 'fastapi', weight: 15 },
|
|
260
|
+
{ file: 'vitest.config.ts', skill: 'testing', weight: 15 },
|
|
261
|
+
{ file: 'vitest.config.js', skill: 'testing', weight: 15 },
|
|
262
|
+
{ file: 'playwright.config.ts', skill: 'testing', weight: 15 },
|
|
263
|
+
{ file: 'playwright.config.js', skill: 'testing', weight: 15 },
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
for (const { file, skill, weight } of CONFIG_MAP) {
|
|
267
|
+
if (fs.existsSync(path.join(projectDir, file))) {
|
|
268
|
+
add(skill, weight);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return signals;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Scans project package.json and config files for ambient signals.
|
|
277
|
+
*/
|
|
278
|
+
function collectWorkspaceEvidence(projectRoot, registry) {
|
|
279
|
+
const evidenceMap = new Map(); // skillId -> Evidence[]
|
|
280
|
+
|
|
281
|
+
const add = (skillId, kind, weight, reason) => {
|
|
282
|
+
if (!evidenceMap.has(skillId)) evidenceMap.set(skillId, []);
|
|
283
|
+
evidenceMap.get(skillId).push({ kind, weight, reason });
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (!projectRoot || !fs.existsSync(projectRoot)) return evidenceMap;
|
|
287
|
+
|
|
288
|
+
// 1. Scan package.json
|
|
289
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
290
|
+
if (fs.existsSync(pkgPath)) {
|
|
291
|
+
try {
|
|
292
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
293
|
+
const deps = {
|
|
294
|
+
...(pkg.dependencies || {}),
|
|
295
|
+
...(pkg.devDependencies || {}),
|
|
296
|
+
...(pkg.peerDependencies || {}),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const packageMap = registry?.packageMap || {
|
|
300
|
+
'npm:next': 'nextjs',
|
|
301
|
+
'npm:react': 'react',
|
|
302
|
+
'npm:prisma': 'database',
|
|
303
|
+
'npm:drizzle-orm': 'database',
|
|
304
|
+
'npm:typescript': 'typescript',
|
|
305
|
+
'npm:tailwindcss': 'ui-ux-pro',
|
|
306
|
+
'npm:@nestjs/core': 'nestjs',
|
|
307
|
+
'pypi:fastapi': 'fastapi',
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
for (const [key, skillId] of Object.entries(packageMap)) {
|
|
311
|
+
const pkgName = key.split(':')[1] || key;
|
|
312
|
+
if (deps[pkgName]) {
|
|
313
|
+
add(skillId, 'package', WEIGHTS.NEAREST_PACKAGE, `Workspace package "${pkgName}" in package.json`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const EXTRA_PACKAGES = {
|
|
318
|
+
'react-dom': 'react',
|
|
319
|
+
'@prisma/client': 'database',
|
|
320
|
+
'drizzle-kit': 'database',
|
|
321
|
+
'typeorm': 'database',
|
|
322
|
+
'mongoose': 'database',
|
|
323
|
+
'pg': 'database',
|
|
324
|
+
'mysql2': 'database',
|
|
325
|
+
'vitest': 'testing',
|
|
326
|
+
'jest': 'testing',
|
|
327
|
+
'playwright': 'testing',
|
|
328
|
+
'@playwright/test': 'testing',
|
|
329
|
+
'cypress': 'testing',
|
|
330
|
+
'express': 'node',
|
|
331
|
+
'fastify': 'node',
|
|
332
|
+
'hono': 'node',
|
|
333
|
+
'zod': 'typescript',
|
|
334
|
+
'lucide-react': 'ui-ux-pro',
|
|
335
|
+
'@radix-ui/react-dialog': 'web-accessibility',
|
|
336
|
+
'framer-motion': 'impeccable-design',
|
|
337
|
+
'dockerode': 'docker',
|
|
338
|
+
'ioredis': 'system-design',
|
|
339
|
+
'kafkajs': 'microservices',
|
|
340
|
+
'amqplib': 'microservices',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
for (const [pName, sId] of Object.entries(EXTRA_PACKAGES)) {
|
|
344
|
+
if (deps[pName]) {
|
|
345
|
+
add(sId, 'package', 15, `Package dependency "${pName}" in package.json`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// ignore
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2. Scan framework config files
|
|
354
|
+
const CONFIG_SIGNALS = [
|
|
355
|
+
{ file: 'tsconfig.json', skill: 'typescript' },
|
|
356
|
+
{ file: 'next.config.js', skill: 'nextjs' },
|
|
357
|
+
{ file: 'next.config.mjs', skill: 'nextjs' },
|
|
358
|
+
{ file: 'next.config.ts', skill: 'nextjs' },
|
|
359
|
+
{ file: 'tailwind.config.js', skill: 'ui-ux-pro' },
|
|
360
|
+
{ file: 'tailwind.config.ts', skill: 'ui-ux-pro' },
|
|
361
|
+
{ file: 'nest-cli.json', skill: 'nestjs' },
|
|
362
|
+
{ file: 'prisma/schema.prisma', skill: 'database' },
|
|
363
|
+
{ file: 'drizzle.config.ts', skill: 'database' },
|
|
364
|
+
{ file: 'drizzle.config.js', skill: 'database' },
|
|
365
|
+
{ file: 'Dockerfile', skill: 'docker' },
|
|
366
|
+
{ file: 'docker-compose.yml', skill: 'docker' },
|
|
367
|
+
{ file: 'docker-compose.yaml', skill: 'docker' },
|
|
368
|
+
{ file: 'requirements.txt', skill: 'fastapi' },
|
|
369
|
+
{ file: 'pyproject.toml', skill: 'fastapi' },
|
|
370
|
+
{ file: 'vitest.config.ts', skill: 'testing' },
|
|
371
|
+
{ file: 'vitest.config.js', skill: 'testing' },
|
|
372
|
+
{ file: 'playwright.config.ts', skill: 'testing' },
|
|
373
|
+
{ file: 'playwright.config.js', skill: 'testing' },
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
for (const { file, skill } of CONFIG_SIGNALS) {
|
|
377
|
+
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
378
|
+
add(skill, 'config', WEIGHTS.AMBIENT_CONFIG, `Config file "${file}" present`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 3. Integrate AST & import graph signals
|
|
383
|
+
const astSignals = analyzeImportGraph(projectRoot);
|
|
384
|
+
for (const [skill, weight] of astSignals.entries()) {
|
|
385
|
+
add(skill, 'package', weight, `Import graph signal for "${skill}" (+${weight})`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return evidenceMap;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Evaluates task risk level according to Milestone 3 spec.
|
|
393
|
+
*/
|
|
394
|
+
function evaluateRisk(taskText, files = []) {
|
|
395
|
+
const reasons = [];
|
|
396
|
+
const text = (taskText || '').toLowerCase();
|
|
397
|
+
let value = 'standard';
|
|
398
|
+
|
|
399
|
+
// Destructive operations
|
|
400
|
+
if (/\b(drop\s*database|drop\s*table|rm\s*-rf|truncate|destroy|delete\s*from|format\s*disk)\b/i.test(text) ||
|
|
401
|
+
/\b(удали\w*\s*(?:базу|таблиц|диск|файл)|очист\w*\s*базу|уничтож\w*)\b/i.test(text)) {
|
|
402
|
+
reasons.push({ kind: 'risk_keyword', weight: 100, reason: 'Mentions destructive database or filesystem operation' });
|
|
403
|
+
value = 'destructive';
|
|
404
|
+
} else if (/\b(secur\w*|auth\w*|jwt|password|token|secret|migration|schema|permission|billing|payment|credit\s*card|crypto)\b/i.test(text) ||
|
|
405
|
+
/\b(безопасн\w*|авториз\w*|аутентифик\w*|парол\w*|токен\w*|миграц\w*|платеж\w*|платёж\w*|доступ\w*)\b/i.test(text) ||
|
|
406
|
+
files.some(f => /\b(auth|security|migration|schema)\b/i.test(f))) {
|
|
407
|
+
// High-risk operations
|
|
408
|
+
reasons.push({ kind: 'risk_keyword', weight: 50, reason: 'Touches security, authentication, migration, or sensitive domain' });
|
|
409
|
+
value = 'high';
|
|
410
|
+
} else if ((/\b(typo|readme|doc|comment|format|lint|prettier)\b/i.test(text) ||
|
|
411
|
+
/\b(опечатк\w*|документац\w*|комментар\w*|форматирован\w*)\b/i.test(text)) &&
|
|
412
|
+
files.every(f => /\.(md|txt|json|ya?ml)$/i.test(f))) {
|
|
413
|
+
// Routine operations
|
|
414
|
+
reasons.push({ kind: 'risk_keyword', weight: 10, reason: 'Routine documentation or formatting change' });
|
|
415
|
+
value = 'routine';
|
|
416
|
+
} else {
|
|
417
|
+
reasons.push({ kind: 'risk_default', weight: 20, reason: 'Standard feature development or modification' });
|
|
418
|
+
value = 'standard';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
value,
|
|
423
|
+
reasons,
|
|
424
|
+
workflow: WORKFLOW_TEMPLATES[value] || WORKFLOW_TEMPLATES.standard,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Maps lifecycle phase and role.
|
|
430
|
+
*/
|
|
431
|
+
function resolvePhaseAndRole(phaseArg, taskText, selectedSkills) {
|
|
432
|
+
let phaseValue = 'Build';
|
|
433
|
+
let phaseSource = 'default';
|
|
434
|
+
|
|
435
|
+
if (phaseArg && typeof phaseArg === 'string') {
|
|
436
|
+
phaseValue = phaseArg.charAt(0).toUpperCase() + phaseArg.slice(1).toLowerCase();
|
|
437
|
+
phaseSource = 'explicit';
|
|
438
|
+
} else {
|
|
439
|
+
const lower = (taskText || '').toLowerCase();
|
|
440
|
+
if (/\b(spec|requirements|user\s*story|критерии|требован)\b/i.test(lower)) {
|
|
441
|
+
phaseValue = 'Define';
|
|
442
|
+
phaseSource = 'prompt';
|
|
443
|
+
} else if (/\b(plan|architecture|design\s*the\s*system|спланируй|архитектур)\b/i.test(lower)) {
|
|
444
|
+
phaseValue = 'Plan';
|
|
445
|
+
phaseSource = 'prompt';
|
|
446
|
+
} else if (/\b(review|audit|check\s*pr|ревью|проверь\s*код)\b/i.test(lower)) {
|
|
447
|
+
phaseValue = 'Review';
|
|
448
|
+
phaseSource = 'prompt';
|
|
449
|
+
} else if (/\b(test|vitest|jest|playwright|tdd|тест)\b/i.test(lower)) {
|
|
450
|
+
phaseValue = 'Verify';
|
|
451
|
+
phaseSource = 'prompt';
|
|
452
|
+
} else if (/\b(deploy|release|ship|production|релиз|деплой)\b/i.test(lower)) {
|
|
453
|
+
phaseValue = 'Ship';
|
|
454
|
+
phaseSource = 'prompt';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let role = 'Senior Developer';
|
|
459
|
+
switch (phaseValue) {
|
|
460
|
+
case 'Define':
|
|
461
|
+
role = 'Product Manager';
|
|
462
|
+
break;
|
|
463
|
+
case 'Plan':
|
|
464
|
+
role = 'Architect';
|
|
465
|
+
break;
|
|
466
|
+
case 'Verify':
|
|
467
|
+
case 'Test':
|
|
468
|
+
role = 'QA Lead';
|
|
469
|
+
break;
|
|
470
|
+
case 'Review':
|
|
471
|
+
role = selectedSkills.some(s => ['ui-ux-pro', 'impeccable-design'].includes(s))
|
|
472
|
+
? 'Staff Engineer + Senior Designer'
|
|
473
|
+
: 'Staff Engineer';
|
|
474
|
+
break;
|
|
475
|
+
case 'Ship':
|
|
476
|
+
role = 'Release Engineer';
|
|
477
|
+
break;
|
|
478
|
+
default:
|
|
479
|
+
if (selectedSkills.every(s => ['ui-ux-pro', 'impeccable-design', 'ui-design'].includes(s))) {
|
|
480
|
+
role = 'Senior Designer';
|
|
481
|
+
} else {
|
|
482
|
+
role = 'Senior Developer';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
phase: { value: phaseValue, source: phaseSource },
|
|
488
|
+
role,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Returns transitive dependencies array for a given skill id.
|
|
494
|
+
*/
|
|
495
|
+
function getTransitiveDeps(skillId, depGraph, excludedSet) {
|
|
496
|
+
const result = [];
|
|
497
|
+
const queue = [skillId];
|
|
498
|
+
const visited = new Set([skillId]);
|
|
499
|
+
|
|
500
|
+
while (queue.length > 0) {
|
|
501
|
+
const curr = queue.shift();
|
|
502
|
+
const reqs = depGraph[curr] || [];
|
|
503
|
+
for (const req of reqs) {
|
|
504
|
+
if (excludedSet.has(req)) continue;
|
|
505
|
+
if (!visited.has(req)) {
|
|
506
|
+
visited.add(req);
|
|
507
|
+
result.push(req);
|
|
508
|
+
queue.push(req);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Main CanonicalResolver class.
|
|
518
|
+
*/
|
|
519
|
+
class CanonicalResolver {
|
|
520
|
+
constructor(options = {}) {
|
|
521
|
+
this.rootDir = options.rootDir || process.cwd();
|
|
522
|
+
this.agentsDir = options.agentsDir || findAgentsDir(this.rootDir);
|
|
523
|
+
this.registry = options.registry || loadRegistry(this.agentsDir);
|
|
524
|
+
this.workspaceGraph = options.workspaceGraph || null;
|
|
525
|
+
this.workspaceGraphBuilder = new WorkspaceGraphBuilder();
|
|
526
|
+
this._workspaceGraphCache = new Map();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
getWorkspaceGraph(projectDir) {
|
|
530
|
+
if (this.workspaceGraph) return this.workspaceGraph;
|
|
531
|
+
const root = this.workspaceGraphBuilder.findRepositoryRoot(projectDir);
|
|
532
|
+
if (!this._workspaceGraphCache.has(root)) {
|
|
533
|
+
this._workspaceGraphCache.set(root, this.workspaceGraphBuilder.build(root));
|
|
534
|
+
}
|
|
535
|
+
return this._workspaceGraphCache.get(root);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_mapDependencyOrConfigToSkill(target, registry) {
|
|
539
|
+
if (!target) return null;
|
|
540
|
+
const clean = target.toLowerCase();
|
|
541
|
+
|
|
542
|
+
const packageMap = registry?.packageMap || {};
|
|
543
|
+
if (packageMap[`npm:${clean}`]) return packageMap[`npm:${clean}`];
|
|
544
|
+
if (packageMap[`pypi:${clean}`]) return packageMap[`pypi:${clean}`];
|
|
545
|
+
if (packageMap[clean]) return packageMap[clean];
|
|
546
|
+
|
|
547
|
+
const PKG_MAP = {
|
|
548
|
+
'next': 'nextjs',
|
|
549
|
+
'react': 'react',
|
|
550
|
+
'react-dom': 'react',
|
|
551
|
+
'typescript': 'typescript',
|
|
552
|
+
'tailwindcss': 'ui-ux-pro',
|
|
553
|
+
'@tailwindcss/postcss': 'ui-ux-pro',
|
|
554
|
+
'lucide-react': 'ui-ux-pro',
|
|
555
|
+
'@nestjs/core': 'nestjs',
|
|
556
|
+
'@nestjs/common': 'nestjs',
|
|
557
|
+
'fastapi': 'fastapi',
|
|
558
|
+
'pydantic': 'fastapi',
|
|
559
|
+
'uvicorn': 'fastapi',
|
|
560
|
+
'prisma': 'database',
|
|
561
|
+
'@prisma/client': 'database',
|
|
562
|
+
'drizzle-orm': 'database',
|
|
563
|
+
'drizzle-kit': 'database',
|
|
564
|
+
'typeorm': 'database',
|
|
565
|
+
'vitest': 'testing',
|
|
566
|
+
'jest': 'testing',
|
|
567
|
+
'playwright': 'testing',
|
|
568
|
+
'@playwright/test': 'testing',
|
|
569
|
+
'dockerode': 'docker',
|
|
570
|
+
'ioredis': 'system-design',
|
|
571
|
+
'kafkajs': 'microservices',
|
|
572
|
+
'amqplib': 'microservices',
|
|
573
|
+
'zod': 'typescript',
|
|
574
|
+
};
|
|
575
|
+
if (PKG_MAP[clean]) return PKG_MAP[clean];
|
|
576
|
+
|
|
577
|
+
const base = path.basename(clean);
|
|
578
|
+
if (base.startsWith('next.config.')) return 'nextjs';
|
|
579
|
+
if (base.startsWith('tailwind.config.')) return 'ui-ux-pro';
|
|
580
|
+
if (base.startsWith('vitest.config.') || base.startsWith('playwright.config.') || base.startsWith('jest.config.')) return 'testing';
|
|
581
|
+
if (base === 'tsconfig.json') return 'typescript';
|
|
582
|
+
if (base === 'nest-cli.json') return 'nestjs';
|
|
583
|
+
if (base.includes('docker-compose') || base === 'dockerfile') return 'docker';
|
|
584
|
+
if (base === 'schema.prisma' || base.startsWith('drizzle.config.')) return 'database';
|
|
585
|
+
if (base === 'requirements.txt' || base === 'pyproject.toml') return 'fastapi';
|
|
586
|
+
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Resolves minimal skills required for a given request.
|
|
592
|
+
*
|
|
593
|
+
* @param {Object} request - Resolution request parameters
|
|
594
|
+
* @returns {Object} Structured ResolutionResult
|
|
595
|
+
*/
|
|
596
|
+
resolve(request = {}) {
|
|
597
|
+
const task = request.task || request.prompt || '';
|
|
598
|
+
const files = (request.files || []).map(f => f.replace(/\\/g, '/'));
|
|
599
|
+
const projectRoot = request.projectDir ? path.resolve(request.projectDir) : this.rootDir;
|
|
600
|
+
const explicitPhase = request.explicitPhase || request.phase;
|
|
601
|
+
|
|
602
|
+
// Evaluate risk early to establish tiered prompt budget (Section 14.4)
|
|
603
|
+
const risk = evaluateRisk(task, files);
|
|
604
|
+
const defaultTierBudget = (risk.value === 'high' || risk.value === 'destructive')
|
|
605
|
+
? BUDGET_TIERS.HIGH
|
|
606
|
+
: BUDGET_TIERS.STANDARD;
|
|
607
|
+
const budgetTokens = (typeof request.contextBudgetTokens === 'number' && request.contextBudgetTokens > 0)
|
|
608
|
+
? request.contextBudgetTokens
|
|
609
|
+
: defaultTierBudget;
|
|
610
|
+
|
|
611
|
+
const maxSkillsLimit = request.maxSkills ? Math.max(1, request.maxSkills) : Infinity;
|
|
612
|
+
|
|
613
|
+
const taskLower = task.toLowerCase();
|
|
614
|
+
const warnings = [];
|
|
615
|
+
|
|
616
|
+
// Ensure registry is loaded
|
|
617
|
+
if (!this.registry) {
|
|
618
|
+
this.registry = loadRegistry(findAgentsDir(projectRoot));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 1. Load active profile (supports monorepo package overrides for touched files)
|
|
622
|
+
let activeProfile = null;
|
|
623
|
+
try {
|
|
624
|
+
const profilesModule = require('../profiles.js');
|
|
625
|
+
activeProfile = profilesModule.getActiveProfile(projectRoot, files[0]);
|
|
626
|
+
} catch {
|
|
627
|
+
// ignore
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const profileExcluded = new Set(activeProfile?.exclude_skills || []);
|
|
631
|
+
const profilePreferred = new Set(activeProfile?.prefer_skills || []);
|
|
632
|
+
const profileRequired = new Set(activeProfile?.require_skills || []);
|
|
633
|
+
const profileEnforce = activeProfile?.enforce || {};
|
|
634
|
+
|
|
635
|
+
// 2. Collect Evidence
|
|
636
|
+
const evidenceBySkill = new Map(); // skillId -> Evidence[]
|
|
637
|
+
const deprecatedMap = { ...DEPRECATED_ALIASES, ...(this.registry?.deprecatedAliases || {}) };
|
|
638
|
+
|
|
639
|
+
const addEvidence = (id, kind, weight, reason) => {
|
|
640
|
+
let resolvedId = id;
|
|
641
|
+
if (deprecatedMap[id]) {
|
|
642
|
+
resolvedId = deprecatedMap[id];
|
|
643
|
+
if (!warnings.some(w => w.code === 'CTX_SKILL_DEPRECATED_ALIAS' && w.deprecatedSkill === id)) {
|
|
644
|
+
warnings.push({
|
|
645
|
+
code: 'CTX_SKILL_DEPRECATED_ALIAS',
|
|
646
|
+
deprecatedSkill: id,
|
|
647
|
+
canonicalSkill: resolvedId,
|
|
648
|
+
message: `Skill '${id}' is a deprecated alias. Redirected to canonical skill '${resolvedId}' with 0 duplicate tokens.`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (profileExcluded.has(resolvedId)) return;
|
|
654
|
+
if (!evidenceBySkill.has(resolvedId)) evidenceBySkill.set(resolvedId, []);
|
|
655
|
+
evidenceBySkill.get(resolvedId).push({ kind, weight, reason });
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Workspace Evidence Graph
|
|
659
|
+
const workspaceGraph = request.workspaceGraph || this.getWorkspaceGraph(projectRoot);
|
|
660
|
+
if (workspaceGraph) {
|
|
661
|
+
if (files.length > 0) {
|
|
662
|
+
// Collect nearest-package evidence for specific touched files
|
|
663
|
+
const touchedPackages = new Set();
|
|
664
|
+
for (const file of files) {
|
|
665
|
+
const nearestPkg = this.workspaceGraphBuilder.findNearestPackage(file, workspaceGraph);
|
|
666
|
+
if (nearestPkg && !touchedPackages.has(nearestPkg.id)) {
|
|
667
|
+
touchedPackages.add(nearestPkg.id);
|
|
668
|
+
const pkgEvidences = this.workspaceGraphBuilder.extractPackageEvidence(file, workspaceGraph);
|
|
669
|
+
for (const pe of pkgEvidences) {
|
|
670
|
+
const skillId = this._mapDependencyOrConfigToSkill(pe.target, this.registry);
|
|
671
|
+
if (skillId) {
|
|
672
|
+
addEvidence(skillId, pe.source, pe.weight, pe.description);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
// Ambient workspace evidence from root package
|
|
679
|
+
const rootPkg = workspaceGraph.packages.find(p => p.root === '.');
|
|
680
|
+
if (rootPkg) {
|
|
681
|
+
for (const dep of rootPkg.dependencies) {
|
|
682
|
+
const skillId = this._mapDependencyOrConfigToSkill(dep, this.registry);
|
|
683
|
+
if (skillId) {
|
|
684
|
+
addEvidence(skillId, 'nearest_package', WEIGHTS.NEAREST_PACKAGE, `Root package dependency "${dep}" (+${WEIGHTS.NEAREST_PACKAGE})`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const cfg of rootPkg.configs) {
|
|
688
|
+
const skillId = this._mapDependencyOrConfigToSkill(cfg, this.registry);
|
|
689
|
+
if (skillId) {
|
|
690
|
+
addEvidence(skillId, 'package_config', 50, `Root package config "${cfg}" (+50)`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
// Fallback to ambient workspace scan
|
|
697
|
+
const workspaceEvidence = collectWorkspaceEvidence(projectRoot, this.registry);
|
|
698
|
+
for (const [sId, evList] of workspaceEvidence.entries()) {
|
|
699
|
+
for (const ev of evList) {
|
|
700
|
+
addEvidence(sId, ev.kind, ev.weight, ev.reason);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Profile preferences & requirements
|
|
706
|
+
for (const sId of profilePreferred) {
|
|
707
|
+
addEvidence(sId, 'profile_preferred', WEIGHTS.PROFILE_PREFERRED, `Preferred by active profile "${activeProfile?.name || 'custom'}" (+${WEIGHTS.PROFILE_PREFERRED})`);
|
|
708
|
+
}
|
|
709
|
+
for (const sId of profileRequired) {
|
|
710
|
+
addEvidence(sId, 'profile_required', WEIGHTS.PROFILE_REQUIRED, `Required by active profile "${activeProfile?.name || 'custom'}"`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Profile enforcement rules
|
|
714
|
+
const normPhase = (explicitPhase || 'Build').toLowerCase();
|
|
715
|
+
if (profileEnforce.testing === true && ['build', 'verify', 'test'].includes(normPhase)) {
|
|
716
|
+
addEvidence('testing', 'profile_required', WEIGHTS.PROFILE_REQUIRED, 'Enforced by active profile (enforce.testing)');
|
|
717
|
+
}
|
|
718
|
+
if (profileEnforce.adr === true && normPhase === 'plan') {
|
|
719
|
+
addEvidence('decisions', 'profile_required', WEIGHTS.PROFILE_REQUIRED, 'Enforced by active profile (enforce.adr)');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Explicit skills requested via prompt (@skill) or args
|
|
723
|
+
const explicitSkills = new Set(request.explicitSkills || []);
|
|
724
|
+
const explicitMatches = task.match(/@([a-z0-9-]+)/g);
|
|
725
|
+
if (explicitMatches) {
|
|
726
|
+
for (const m of explicitMatches) explicitSkills.add(m.slice(1));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
for (const sId of explicitSkills) {
|
|
730
|
+
addEvidence(sId, 'explicit', WEIGHTS.EXPLICIT_SKILL, `Explicitly requested skill "${sId}"`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Scan registry skills signals (or built-in fallback)
|
|
734
|
+
const skillsDict = this.registry?.skills || {};
|
|
735
|
+
const skillIds = Object.keys(skillsDict);
|
|
736
|
+
|
|
737
|
+
for (const id of skillIds) {
|
|
738
|
+
const skill = skillsDict[id];
|
|
739
|
+
const signals = skill.signals || {};
|
|
740
|
+
|
|
741
|
+
// Exact alias match
|
|
742
|
+
for (const alias of signals.aliases || []) {
|
|
743
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
744
|
+
const re = new RegExp(`\\b${escaped}\\b`, 'i');
|
|
745
|
+
if (re.test(taskLower)) {
|
|
746
|
+
addEvidence(id, 'alias', WEIGHTS.EXACT_ALIAS, `Exact alias match "${alias}" in task description`);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Keyword matches
|
|
752
|
+
for (const kw of signals.keywords || []) {
|
|
753
|
+
const escaped = kw.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
754
|
+
const re = new RegExp(`\\b${escaped}\\b`, 'i');
|
|
755
|
+
if (re.test(taskLower)) {
|
|
756
|
+
const w = kw.weight || WEIGHTS.TASK_KEYWORD_STRONG;
|
|
757
|
+
addEvidence(id, 'keyword', w, `Task keyword "${kw.value}" (+${w})`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// File pattern matches
|
|
762
|
+
for (const file of files) {
|
|
763
|
+
for (const fg of signals.fileGlobs || []) {
|
|
764
|
+
const pattern = fg.value;
|
|
765
|
+
const rePattern = pattern
|
|
766
|
+
.replace(/\./g, '\\.')
|
|
767
|
+
.replace(/\*\*\//g, '.*')
|
|
768
|
+
.replace(/\*/g, '[^/]*')
|
|
769
|
+
.replace(/\{([^}]+)\}/g, (_, group) => `(${group.replace(/,/g, '|')})`);
|
|
770
|
+
const re = new RegExp(rePattern, 'i');
|
|
771
|
+
if (re.test(file)) {
|
|
772
|
+
const w = fg.weight || WEIGHTS.FILE_GLOB;
|
|
773
|
+
addEvidence(id, 'file_glob', w, `Touched file "${file}" matches glob "${pattern}" (+${w})`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Built-in keyword triggers & bilingual support (including Russian patterns)
|
|
780
|
+
const BUILTIN_TRIGGERS = [
|
|
781
|
+
{ id: 'nextjs', re: /\b(next|next\.js|app\s*router|server\s*actions?|rsc)\b|некст/i, w: 40, kw: 'next.js' },
|
|
782
|
+
{ id: 'react', re: /\b(react|reactjs|useOptimistic)\b|компонент|хук|модал\w*/i, w: 40, kw: 'react' },
|
|
783
|
+
{ id: 'typescript', re: /\b(typescript|type-?safe|generics?|tsconfig)\b|тайпскрипт|типизац/i, w: 40, kw: 'typescript' },
|
|
784
|
+
{ id: 'ui-ux-pro', re: /\b(ui|ux|tailwind|styling)\b|дизайн|верстк|макет|интерфейс|модал\w*/i, w: 35, kw: 'ui/ux' },
|
|
785
|
+
{ id: 'security', re: /\b(security|auth|jwt|login|csrf|xss|rate\s*limit)\b|авториз|аутентифик|парол|безопасност/i, w: 40, kw: 'security' },
|
|
786
|
+
{ id: 'database', re: /\b(database|sql|postgres|prisma|drizzle|migration|orm)\b|баз.*данн|миграц|таблиц/i, w: 40, kw: 'database' },
|
|
787
|
+
{ id: 'testing', re: /\b(vitest|jest|playwright|tdd|bdd|e2e)\b|тестирован|покрыти|юнит|тест/i, w: 40, kw: 'testing' },
|
|
788
|
+
{ id: 'performance', re: /\b(performance|latency|lcp|cls|inp|core\s*web\s*vitals)\b|производительн|ускор|быстр|throughput/i, w: 40, kw: 'performance' },
|
|
789
|
+
{ id: 'docker', re: /\b(docker|dockerfile|compose)\b|докер/i, w: 40, kw: 'docker' },
|
|
790
|
+
{ id: 'fastapi', re: /\b(fastapi|pydantic|uvicorn)\b|питон/i, w: 40, kw: 'fastapi' },
|
|
791
|
+
{ id: 'nestjs', re: /\b(nestjs|@nestjs)\b|нест/i, w: 40, kw: 'nestjs' },
|
|
792
|
+
{ id: 'node', re: /\b(node|node\.js|nodejs|express|fastify)\b|бэкенд|эндпоинт/i, w: 35, kw: 'node' },
|
|
793
|
+
{ id: 'ddd', re: /\b(ddd|domain-?driven|bounded\s*context)\b|агрегат|домен/i, w: 40, kw: 'ddd' },
|
|
794
|
+
{ id: 'microservices', re: /\b(microservices?|grpc|rabbitmq|kafka)\b|микросервис/i, w: 40, kw: 'microservices' },
|
|
795
|
+
{ id: 'system-design', re: /\b(system\s*design|architecture)\b|архитектур|масштабируем/i, w: 40, kw: 'system-design' },
|
|
796
|
+
{ id: 'graphify', re: /\b(graphify|knowledge\s*graph|codebase\s*graph)\b|граф\s*проект|граф\s*зависимост/i, w: 45, kw: 'graphify' },
|
|
797
|
+
{ id: 'architecture-diagrams', re: /\b(diagram|flowchart|sequence\s*diagram)\b|диаграмм|схем/i, w: 40, kw: 'diagram' },
|
|
798
|
+
{ id: 'interview-me', re: /\b(interview)\b|интервью|расспроси|уточни/i, w: 40, kw: 'interview' },
|
|
799
|
+
{ id: 'web-accessibility', re: /\b(accessib\w*|a11y|wcag|aria|focus\s*trap|screen\s*reader)\b|доступност|скринридер/i, w: 40, kw: 'accessibility' },
|
|
800
|
+
{ id: 'impeccable-design', re: /\b(impeccable|visual\s*qa|micro-?animation)\b|анимац|полировк/i, w: 40, kw: 'impeccable' },
|
|
801
|
+
{ id: 'state-management', re: /\b(zustand|tanstack\s*query|redux)\b|стейт|хранилищ/i, w: 40, kw: 'state-management' },
|
|
802
|
+
{ id: 'brutalist-design', re: /\b(brutalist|brutalism)\b|брутализм/i, w: 40, kw: 'brutalist' },
|
|
803
|
+
{ id: 'minimalist-design', re: /\b(minimalist|minimalism)\b|минимализм/i, w: 40, kw: 'minimalist' },
|
|
804
|
+
{ id: 'soft-design', re: /\b(soft-design)\b|мягкий\s*дизайн/i, w: 40, kw: 'soft-design' },
|
|
805
|
+
{ id: 'redesign-audit', re: /\b(redesign-audit|audit\s*ui)\b|аудит\s*интерфейс/i, w: 40, kw: 'redesign-audit' },
|
|
806
|
+
{ id: 'vercel-optimize', re: /\b(vercel-optimize|vercel\s*edge)\b/i, w: 40, kw: 'vercel-optimize' },
|
|
807
|
+
];
|
|
808
|
+
|
|
809
|
+
for (const trig of BUILTIN_TRIGGERS) {
|
|
810
|
+
if (trig.re.test(taskLower)) {
|
|
811
|
+
addEvidence(trig.id, 'keyword', trig.w, `Task keyword "${trig.kw}" (+${trig.w})`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Built-in file glob signals
|
|
816
|
+
const BUILTIN_FILE_GLOBS = [
|
|
817
|
+
{ id: 'nextjs', re: /app\/.*\.(tsx|jsx|ts|js)$|next\.config\./i, w: 70 },
|
|
818
|
+
{ id: 'react', re: /\.(tsx|jsx)$/i, w: 60 },
|
|
819
|
+
{ id: 'typescript', re: /\.tsx?$|tsconfig\.json$/i, w: 60 },
|
|
820
|
+
{ id: 'ui-ux-pro', re: /\.(css|scss|sass)$|tailwind\.config\./i, w: 60 },
|
|
821
|
+
{ id: 'database', re: /\.prisma$|drizzle\.config\.|migrations?\/.*\.sql$/i, w: 70 },
|
|
822
|
+
{ id: 'security', re: /\bauth\b|\bsecurity\b/i, w: 60 },
|
|
823
|
+
{ id: 'testing', re: /\.(test|spec)\.(ts|js|tsx|jsx|py)$|vitest\.config\.|playwright\.config\./i, w: 70 },
|
|
824
|
+
{ id: 'docker', re: /Dockerfile|docker-compose\./i, w: 70 },
|
|
825
|
+
{ id: 'fastapi', re: /\.py$|requirements\.txt$|pyproject\.toml$/i, w: 60 },
|
|
826
|
+
{ id: 'nestjs', re: /nest-cli\.json$/i, w: 70 },
|
|
827
|
+
{ id: 'graphify', re: /graph\.json$|GRAPH_REPORT\.md$/i, w: 70 },
|
|
828
|
+
{ id: 'vercel-optimize', re: /vercel\.json$/i, w: 70 },
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
for (const file of files) {
|
|
832
|
+
for (const fg of BUILTIN_FILE_GLOBS) {
|
|
833
|
+
if (fg.re.test(file)) {
|
|
834
|
+
addEvidence(fg.id, 'file_glob', fg.w, `Touched file "${file}" matches pattern (+${fg.w})`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 3. Compute Candidate Scores & Intent Precedence
|
|
840
|
+
const candidates = [];
|
|
841
|
+
for (const [id, evList] of evidenceBySkill.entries()) {
|
|
842
|
+
if (profileExcluded.has(id)) continue;
|
|
843
|
+
|
|
844
|
+
let score = 0;
|
|
845
|
+
let hasDirectTaskIntent = false;
|
|
846
|
+
let isMandatory = false;
|
|
847
|
+
|
|
848
|
+
for (const e of evList) {
|
|
849
|
+
score += e.weight;
|
|
850
|
+
if ((e.kind === 'keyword' || e.kind === 'alias') && e.weight >= 20) {
|
|
851
|
+
hasDirectTaskIntent = true;
|
|
852
|
+
}
|
|
853
|
+
if (e.kind === 'explicit' || e.kind === 'profile_required') {
|
|
854
|
+
isMandatory = true;
|
|
855
|
+
hasDirectTaskIntent = true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Intent Precedence: direct prompt/task intent receives a priority boost over ambient stack noise
|
|
860
|
+
const priorityScore = (hasDirectTaskIntent ? 1000 : 0) + score;
|
|
861
|
+
candidates.push({ id, score, priorityScore, hasDirectTaskIntent, isMandatory, reasons: evList });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Sort by priorityScore descending, then score descending, then lexical ID
|
|
865
|
+
candidates.sort((a, b) => b.priorityScore - a.priorityScore || b.score - a.score || a.id.localeCompare(b.id));
|
|
866
|
+
|
|
867
|
+
// 4. Intent Slots & Context Suppression
|
|
868
|
+
const hasPureInfraIntent = candidates.some(c => c.id === 'docker' && c.hasDirectTaskIntent);
|
|
869
|
+
const hasBackendOnlyIntent = candidates.some(c => ['database', 'fastapi', 'nestjs', 'ddd'].includes(c.id) && c.hasDirectTaskIntent) &&
|
|
870
|
+
!files.some(f => /\.(tsx|jsx|css|scss|html)$/.test(f)) &&
|
|
871
|
+
!/\b(ui|react|css|tailwind|frontend|макет|дизайн|кнопк|стил|компонент)/i.test(taskLower);
|
|
872
|
+
|
|
873
|
+
const suppressAmbientUI = hasPureInfraIntent || hasBackendOnlyIntent;
|
|
874
|
+
|
|
875
|
+
const initialSelection = [];
|
|
876
|
+
const excluded = [];
|
|
877
|
+
|
|
878
|
+
// Qualification filter (score >= 25 or explicit/required or direct task intent with score >= 20)
|
|
879
|
+
for (const cand of candidates) {
|
|
880
|
+
if (suppressAmbientUI && ['ui-ux-pro', 'impeccable-design', 'ui-design', 'web-accessibility'].includes(cand.id) && !cand.hasDirectTaskIntent) {
|
|
881
|
+
excluded.push({ id: cand.id, reasonCode: 'suppressed_by_intent', details: 'Ambient UI skill suppressed for pure backend/infrastructure task' });
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (cand.isMandatory || cand.score >= 25 || (cand.hasDirectTaskIntent && cand.score >= 20)) {
|
|
886
|
+
initialSelection.push(cand);
|
|
887
|
+
} else {
|
|
888
|
+
excluded.push({ id: cand.id, reasonCode: 'score_threshold', details: `Score ${cand.score} below activation threshold (25)` });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Architectural synergy rules (e.g. database, microservices, ddd, graphify -> system-design)
|
|
893
|
+
const needsSystemDesign = initialSelection.some(c => ['database', 'microservices', 'ddd', 'graphify'].includes(c.id));
|
|
894
|
+
if (needsSystemDesign && !initialSelection.some(c => c.id === 'system-design') && !profileExcluded.has('system-design')) {
|
|
895
|
+
initialSelection.push({
|
|
896
|
+
id: 'system-design',
|
|
897
|
+
score: WEIGHTS.SYNERGY_PREREQ,
|
|
898
|
+
priorityScore: 500,
|
|
899
|
+
hasDirectTaskIntent: false,
|
|
900
|
+
isMandatory: false,
|
|
901
|
+
reasons: [{ kind: 'synergy', weight: WEIGHTS.SYNERGY_PREREQ, reason: 'Architectural synergy prerequisite for database/microservices/ddd/graphify' }],
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Secondary profile enforce check (e.g. enforce.adr when system-design or microservices is present)
|
|
906
|
+
if (profileEnforce.adr === true && initialSelection.some(c => c.id === 'system-design' || c.id === 'microservices')) {
|
|
907
|
+
if (!initialSelection.some(c => c.id === 'decisions') && !profileExcluded.has('decisions')) {
|
|
908
|
+
initialSelection.push({
|
|
909
|
+
id: 'decisions',
|
|
910
|
+
score: 100,
|
|
911
|
+
priorityScore: 1000,
|
|
912
|
+
hasDirectTaskIntent: true,
|
|
913
|
+
isMandatory: true,
|
|
914
|
+
reasons: [{ kind: 'profile_required', weight: 100, reason: 'Enforced by active profile (enforce.adr with system-design/microservices)' }],
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 5. Dependency Graph Setup
|
|
920
|
+
const depGraph = this.registry?.dependencyGraph || {
|
|
921
|
+
react: ['typescript'],
|
|
922
|
+
nextjs: ['react', 'typescript'],
|
|
923
|
+
nestjs: ['node', 'typescript'],
|
|
924
|
+
node: ['typescript'],
|
|
925
|
+
microservices: ['system-design'],
|
|
926
|
+
'vercel-optimize': ['nextjs', 'react', 'typescript'],
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const closureMap = new Map();
|
|
930
|
+
for (const item of initialSelection) {
|
|
931
|
+
const rawEst = skillsDict[item.id]?.estimatedTokens || 1000;
|
|
932
|
+
const estTokens = Math.min(rawEst, BUDGET_TIERS.SKILL_BODY);
|
|
933
|
+
closureMap.set(item.id, {
|
|
934
|
+
id: item.id,
|
|
935
|
+
displayName: skillsDict[item.id]?.displayName || item.id,
|
|
936
|
+
score: item.score,
|
|
937
|
+
priorityScore: item.priorityScore,
|
|
938
|
+
reasons: item.reasons,
|
|
939
|
+
requiredBy: [],
|
|
940
|
+
estimatedTokens: estTokens,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Populate required dependencies in closureMap
|
|
945
|
+
for (const cand of initialSelection) {
|
|
946
|
+
const reqs = getTransitiveDeps(cand.id, depGraph, profileExcluded);
|
|
947
|
+
for (const reqId of reqs) {
|
|
948
|
+
if (closureMap.has(reqId)) {
|
|
949
|
+
const entry = closureMap.get(reqId);
|
|
950
|
+
if (!entry.requiredBy.includes(cand.id)) {
|
|
951
|
+
entry.requiredBy.push(cand.id);
|
|
952
|
+
entry.reasons.push({ kind: 'dependency', weight: 100, reason: `Required by "${cand.id}"` });
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
const rawEst = skillsDict[reqId]?.estimatedTokens || 1000;
|
|
956
|
+
const estTokens = Math.min(rawEst, BUDGET_TIERS.SKILL_BODY);
|
|
957
|
+
closureMap.set(reqId, {
|
|
958
|
+
id: reqId,
|
|
959
|
+
displayName: skillsDict[reqId]?.displayName || reqId,
|
|
960
|
+
score: 100,
|
|
961
|
+
priorityScore: 800,
|
|
962
|
+
reasons: [{ kind: 'dependency', weight: 100, reason: `Required by "${cand.id}"` }],
|
|
963
|
+
requiredBy: [cand.id],
|
|
964
|
+
estimatedTokens: estTokens,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// 6. Conflict Resolution Policy
|
|
971
|
+
const conflicts = [];
|
|
972
|
+
for (const [id, item] of closureMap.entries()) {
|
|
973
|
+
const skillConflicts = skillsDict[id]?.dependencies?.conflicts || [];
|
|
974
|
+
for (const confId of skillConflicts) {
|
|
975
|
+
if (closureMap.has(confId)) {
|
|
976
|
+
const other = closureMap.get(confId);
|
|
977
|
+
const itemExplicit = item.reasons.some(r => r.kind === 'explicit');
|
|
978
|
+
const otherExplicit = other.reasons.some(r => r.kind === 'explicit');
|
|
979
|
+
|
|
980
|
+
if (itemExplicit && otherExplicit) {
|
|
981
|
+
conflicts.push({ skillA: id, skillB: confId, resolution: 'error', reason: `Both conflicting skills "${id}" and "${confId}" were explicitly requested` });
|
|
982
|
+
} else if (itemExplicit && !otherExplicit) {
|
|
983
|
+
closureMap.delete(confId);
|
|
984
|
+
excluded.push({ id: confId, reasonCode: 'conflict', details: `Conflicted with explicitly requested skill "${id}"` });
|
|
985
|
+
} else if (!itemExplicit && otherExplicit) {
|
|
986
|
+
closureMap.delete(id);
|
|
987
|
+
excluded.push({ id, reasonCode: 'conflict', details: `Conflicted with explicitly requested skill "${confId}"` });
|
|
988
|
+
} else {
|
|
989
|
+
// Compare priority/score
|
|
990
|
+
if (item.priorityScore >= other.priorityScore) {
|
|
991
|
+
closureMap.delete(confId);
|
|
992
|
+
excluded.push({ id: confId, reasonCode: 'conflict', details: `Conflicted with higher-priority skill "${id}"` });
|
|
993
|
+
} else {
|
|
994
|
+
closureMap.delete(id);
|
|
995
|
+
excluded.push({ id, reasonCode: 'conflict', details: `Conflicted with higher-priority skill "${confId}"` });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 7. Token Budget Planner & Capability Clustering
|
|
1003
|
+
const BASE_SKILLS = ['ponytail-mindset', 'engineering-workflow'];
|
|
1004
|
+
let baseTokens = 0;
|
|
1005
|
+
|
|
1006
|
+
for (const baseId of BASE_SKILLS) {
|
|
1007
|
+
if (!profileExcluded.has(baseId)) {
|
|
1008
|
+
baseTokens += skillsDict[baseId]?.estimatedTokens || 800;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let allocatedDynamicTokens = 0;
|
|
1013
|
+
const admitted = new Map(); // id -> candidate entry
|
|
1014
|
+
|
|
1015
|
+
// Separate mandatory vs optional candidates
|
|
1016
|
+
const mandatoryCandidates = [];
|
|
1017
|
+
const optionalCandidates = [];
|
|
1018
|
+
|
|
1019
|
+
for (const cand of closureMap.values()) {
|
|
1020
|
+
const isMandatory = cand.reasons.some(r => r.kind === 'explicit' || r.kind === 'profile_required');
|
|
1021
|
+
if (isMandatory) {
|
|
1022
|
+
mandatoryCandidates.push(cand);
|
|
1023
|
+
} else {
|
|
1024
|
+
optionalCandidates.push(cand);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Sort optional candidates by priorityScore descending, then score descending, then lexical ID
|
|
1029
|
+
optionalCandidates.sort((a, b) => b.priorityScore - a.priorityScore || b.score - a.score || a.id.localeCompare(b.id));
|
|
1030
|
+
|
|
1031
|
+
// Phase 1: Admit mandatory candidates and their dependency closures unconditionally
|
|
1032
|
+
for (const cand of mandatoryCandidates) {
|
|
1033
|
+
if (admitted.has(cand.id)) continue;
|
|
1034
|
+
|
|
1035
|
+
const reqs = getTransitiveDeps(cand.id, depGraph, profileExcluded);
|
|
1036
|
+
const cluster = [cand.id, ...reqs.filter(d => !admitted.has(d))];
|
|
1037
|
+
|
|
1038
|
+
for (const sId of cluster) {
|
|
1039
|
+
const entry = closureMap.get(sId) || {
|
|
1040
|
+
id: sId,
|
|
1041
|
+
displayName: skillsDict[sId]?.displayName || sId,
|
|
1042
|
+
score: 100,
|
|
1043
|
+
priorityScore: 1000,
|
|
1044
|
+
reasons: [{ kind: 'dependency', weight: 100, reason: `Required by "${cand.id}"` }],
|
|
1045
|
+
requiredBy: [cand.id],
|
|
1046
|
+
estimatedTokens: closureMap.get(sId)?.estimatedTokens || Math.min(skillsDict[sId]?.estimatedTokens || 1000, BUDGET_TIERS.SKILL_BODY),
|
|
1047
|
+
};
|
|
1048
|
+
admitted.set(sId, entry);
|
|
1049
|
+
if (!BASE_SKILLS.includes(sId)) {
|
|
1050
|
+
allocatedDynamicTokens += entry.estimatedTokens;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (allocatedDynamicTokens > budgetTokens) {
|
|
1055
|
+
warnings.push({
|
|
1056
|
+
code: 'CTX_RESOLVER_BUDGET_EXCEEDED',
|
|
1057
|
+
message: `Context budget (${budgetTokens} tokens) exceeded by mandatory skill "${cand.id}" and dependencies (total: ${allocatedDynamicTokens} tokens)`,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Phase 2: Admit optional candidates with their dependency clusters within budget and maxSkills
|
|
1063
|
+
for (const cand of optionalCandidates) {
|
|
1064
|
+
if (admitted.has(cand.id)) continue;
|
|
1065
|
+
|
|
1066
|
+
if (admitted.size >= maxSkillsLimit) {
|
|
1067
|
+
excluded.push({
|
|
1068
|
+
id: cand.id,
|
|
1069
|
+
reasonCode: 'max_skills_limit',
|
|
1070
|
+
details: `Admitting skill would exceed maxSkills limit (${maxSkillsLimit})`,
|
|
1071
|
+
});
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const reqs = getTransitiveDeps(cand.id, depGraph, profileExcluded);
|
|
1076
|
+
const unadmittedCluster = [cand.id, ...reqs.filter(d => !admitted.has(d))];
|
|
1077
|
+
|
|
1078
|
+
if (maxSkillsLimit !== Infinity && admitted.size + unadmittedCluster.length > maxSkillsLimit) {
|
|
1079
|
+
excluded.push({
|
|
1080
|
+
id: cand.id,
|
|
1081
|
+
reasonCode: 'max_skills_limit',
|
|
1082
|
+
details: `Admitting cluster (${unadmittedCluster.length} skills) would exceed maxSkills limit (${maxSkillsLimit})`,
|
|
1083
|
+
});
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const clusterCost = unadmittedCluster.reduce((sum, sId) => {
|
|
1088
|
+
if (BASE_SKILLS.includes(sId)) return sum;
|
|
1089
|
+
return sum + (closureMap.get(sId)?.estimatedTokens || Math.min(skillsDict[sId]?.estimatedTokens || 1000, BUDGET_TIERS.SKILL_BODY));
|
|
1090
|
+
}, 0);
|
|
1091
|
+
|
|
1092
|
+
if (allocatedDynamicTokens + clusterCost <= budgetTokens) {
|
|
1093
|
+
for (const sId of unadmittedCluster) {
|
|
1094
|
+
const entry = closureMap.get(sId) || {
|
|
1095
|
+
id: sId,
|
|
1096
|
+
displayName: skillsDict[sId]?.displayName || sId,
|
|
1097
|
+
score: cand.score,
|
|
1098
|
+
priorityScore: cand.priorityScore,
|
|
1099
|
+
reasons: sId === cand.id ? cand.reasons : [{ kind: 'dependency', weight: 100, reason: `Required by "${cand.id}"` }],
|
|
1100
|
+
requiredBy: sId === cand.id ? [] : [cand.id],
|
|
1101
|
+
estimatedTokens: closureMap.get(sId)?.estimatedTokens || Math.min(skillsDict[sId]?.estimatedTokens || 1000, BUDGET_TIERS.SKILL_BODY),
|
|
1102
|
+
};
|
|
1103
|
+
admitted.set(sId, entry);
|
|
1104
|
+
}
|
|
1105
|
+
allocatedDynamicTokens += clusterCost;
|
|
1106
|
+
} else {
|
|
1107
|
+
excluded.push({
|
|
1108
|
+
id: cand.id,
|
|
1109
|
+
reasonCode: 'budget_exceeded',
|
|
1110
|
+
details: `Adding skill and required dependencies (${clusterCost} tokens) would exceed budget (${budgetTokens} tokens)`,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// 8. Foundational skills integration
|
|
1116
|
+
const finalSelected = Array.from(admitted.values());
|
|
1117
|
+
const allSelectedIds = finalSelected.map(s => s.id);
|
|
1118
|
+
|
|
1119
|
+
for (const baseId of BASE_SKILLS) {
|
|
1120
|
+
if (!profileExcluded.has(baseId) && !allSelectedIds.includes(baseId)) {
|
|
1121
|
+
allSelectedIds.unshift(baseId);
|
|
1122
|
+
finalSelected.unshift({
|
|
1123
|
+
id: baseId,
|
|
1124
|
+
displayName: skillsDict[baseId]?.displayName || baseId,
|
|
1125
|
+
score: 100,
|
|
1126
|
+
priorityScore: 1000,
|
|
1127
|
+
reasons: [{ kind: 'foundation', weight: 100, reason: 'Core skill' }],
|
|
1128
|
+
requiredBy: [],
|
|
1129
|
+
estimatedTokens: skillsDict[baseId]?.estimatedTokens || 800,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// 9. Domain, Phase & Role
|
|
1135
|
+
let domain = request.domain || '';
|
|
1136
|
+
if (!domain) {
|
|
1137
|
+
const isFrontend = allSelectedIds.includes('react') || allSelectedIds.includes('nextjs') || allSelectedIds.includes('ui-ux-pro');
|
|
1138
|
+
const isBackend = allSelectedIds.includes('database') || allSelectedIds.includes('fastapi') || allSelectedIds.includes('nestjs') || allSelectedIds.includes('node') || allSelectedIds.includes('system-design');
|
|
1139
|
+
if (isFrontend && isBackend) domain = 'Full-Stack';
|
|
1140
|
+
else if (isFrontend) domain = 'Frontend';
|
|
1141
|
+
else if (isBackend) domain = 'Backend';
|
|
1142
|
+
else if (allSelectedIds.includes('docker')) domain = 'DevOps';
|
|
1143
|
+
else domain = 'Architecture';
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const { phase, role } = resolvePhaseAndRole(explicitPhase, task, allSelectedIds);
|
|
1147
|
+
|
|
1148
|
+
// Streamlined execution modes (Section 14.2)
|
|
1149
|
+
let mode = request.mode || request.explicitMode || null;
|
|
1150
|
+
if (!mode) {
|
|
1151
|
+
const pVal = typeof phase === 'object' ? phase.value : phase;
|
|
1152
|
+
if (pVal === 'Define' || pVal === 'Plan') mode = MODES.DISCOVER;
|
|
1153
|
+
else if (pVal === 'Build') mode = MODES.CHANGE;
|
|
1154
|
+
else if (pVal === 'Verify') mode = MODES.VERIFY;
|
|
1155
|
+
else if (pVal === 'Review' || pVal === 'Ship') mode = MODES.REVIEW;
|
|
1156
|
+
else mode = MODES.CHANGE;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Review lenses
|
|
1160
|
+
const reviewLenses = [];
|
|
1161
|
+
if (allSelectedIds.includes('security') || risk.value === 'high' || risk.value === 'destructive') {
|
|
1162
|
+
reviewLenses.push('security');
|
|
1163
|
+
}
|
|
1164
|
+
if (allSelectedIds.includes('database')) {
|
|
1165
|
+
reviewLenses.push('database');
|
|
1166
|
+
}
|
|
1167
|
+
if (allSelectedIds.includes('web-accessibility') || allSelectedIds.includes('ui-ux-pro')) {
|
|
1168
|
+
reviewLenses.push('accessibility');
|
|
1169
|
+
}
|
|
1170
|
+
if (allSelectedIds.includes('performance') || allSelectedIds.includes('vercel-optimize')) {
|
|
1171
|
+
reviewLenses.push('performance');
|
|
1172
|
+
}
|
|
1173
|
+
if (allSelectedIds.includes('system-design') || allSelectedIds.includes('ddd') || allSelectedIds.includes('microservices')) {
|
|
1174
|
+
reviewLenses.push('architecture');
|
|
1175
|
+
}
|
|
1176
|
+
if (reviewLenses.length === 0) {
|
|
1177
|
+
reviewLenses.push('general-code-quality');
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Rule catalog integration (Section 14.1 & 14.6)
|
|
1181
|
+
let applicableRules = [];
|
|
1182
|
+
try {
|
|
1183
|
+
const { RuleCatalog } = require('../rules/rule-catalog.js');
|
|
1184
|
+
const catalog = new RuleCatalog({ rootDir: projectRoot });
|
|
1185
|
+
if (this.registry) {
|
|
1186
|
+
catalog.loadFromRegistry(this.registry);
|
|
1187
|
+
}
|
|
1188
|
+
applicableRules = catalog.getAllRules().filter(r =>
|
|
1189
|
+
allSelectedIds.includes(r.sourceSkill) || r.applicability.includes('all')
|
|
1190
|
+
);
|
|
1191
|
+
} catch {
|
|
1192
|
+
// ignore
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
registryFingerprint: this.registry?.sourceGraphHash || 'none',
|
|
1197
|
+
workspaceFingerprint: workspaceGraph?.fingerprint || crypto.createHash('md5').update(projectRoot).digest('hex'),
|
|
1198
|
+
workspaceGraph: workspaceGraph || null,
|
|
1199
|
+
domain,
|
|
1200
|
+
phase,
|
|
1201
|
+
mode,
|
|
1202
|
+
reviewLenses,
|
|
1203
|
+
role,
|
|
1204
|
+
risk,
|
|
1205
|
+
workflow: risk.workflow,
|
|
1206
|
+
rules: applicableRules,
|
|
1207
|
+
selected: finalSelected,
|
|
1208
|
+
excluded,
|
|
1209
|
+
conflicts,
|
|
1210
|
+
warnings,
|
|
1211
|
+
totalEstimatedTokens: baseTokens + allocatedDynamicTokens,
|
|
1212
|
+
// Legacy compatibility properties
|
|
1213
|
+
skills: allSelectedIds,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Formats execution declaration header matching AGENTS.md standard.
|
|
1219
|
+
*/
|
|
1220
|
+
formatDeclaration(res) {
|
|
1221
|
+
const phaseStr = typeof res.phase === 'object' ? res.phase.value : res.phase;
|
|
1222
|
+
const modeStr = res.mode || 'CHANGE';
|
|
1223
|
+
const lensesStr = Array.isArray(res.reviewLenses) && res.reviewLenses.length > 0
|
|
1224
|
+
? res.reviewLenses.join(', ')
|
|
1225
|
+
: 'general';
|
|
1226
|
+
const riskStr = res.risk?.value || 'standard';
|
|
1227
|
+
const skillsList = (res.skills || []).join(', ');
|
|
1228
|
+
|
|
1229
|
+
const lines = [
|
|
1230
|
+
`[DOMAIN: ${res.domain}] [PHASE: ${phaseStr}] [ROLE: ${res.role}] [MODE: ${modeStr}] [LENSES: ${lensesStr}] [RISK: ${riskStr}]`,
|
|
1231
|
+
`Skills loaded: ${skillsList}`,
|
|
1232
|
+
];
|
|
1233
|
+
if (res.workflow && res.workflow.steps && res.workflow.steps.length > 0) {
|
|
1234
|
+
lines.push(`Workflow (${res.workflow.name}): ${res.workflow.steps[0]}`);
|
|
1235
|
+
}
|
|
1236
|
+
return lines.join('\n');
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Formats detailed explainability report for --explain flag.
|
|
1241
|
+
*/
|
|
1242
|
+
formatExplanation(res) {
|
|
1243
|
+
const lines = [];
|
|
1244
|
+
const phaseStr = typeof res.phase === 'object' ? res.phase.value : res.phase;
|
|
1245
|
+
lines.push('══════════════════════════════════════════');
|
|
1246
|
+
lines.push(' ContextOS — Dynamic Skill Resolution');
|
|
1247
|
+
lines.push('══════════════════════════════════════════');
|
|
1248
|
+
lines.push(`[DOMAIN: ${res.domain}] [PHASE: ${phaseStr}] [ROLE: ${res.role}] [MODE: ${res.mode}] [LENSES: ${(res.reviewLenses || []).join(', ')}] [RISK: ${res.risk?.value || 'standard'}]`);
|
|
1249
|
+
lines.push(`Registry: ${(res.registryFingerprint || '').slice(0, 19)}... | Estimated Budget: ~${res.totalEstimatedTokens} tokens\n`);
|
|
1250
|
+
|
|
1251
|
+
if (res.workflow) {
|
|
1252
|
+
lines.push(`Workflow: ${res.workflow.name} — ${res.workflow.summary}`);
|
|
1253
|
+
for (const st of res.workflow.steps) {
|
|
1254
|
+
lines.push(` → ${st}`);
|
|
1255
|
+
}
|
|
1256
|
+
lines.push('');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
lines.push('Selected Skills:');
|
|
1260
|
+
for (const item of res.selected) {
|
|
1261
|
+
const reasonsStr = (item.reasons || []).map(r => `${r.kind}: ${r.reason}`).join('; ');
|
|
1262
|
+
lines.push(` ✓ ${item.id.padEnd(22)} score: ${String(item.score).padStart(3)} tokens: ~${item.estimatedTokens} (${reasonsStr})`);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (res.excluded && res.excluded.length > 0) {
|
|
1266
|
+
lines.push('\nExcluded Skills:');
|
|
1267
|
+
for (const ex of res.excluded) {
|
|
1268
|
+
lines.push(` ✗ ${ex.id.padEnd(22)} [${ex.reasonCode}] ${ex.details}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (res.rules && res.rules.length > 0) {
|
|
1273
|
+
lines.push('\nApplicable Rules:');
|
|
1274
|
+
for (const r of res.rules.slice(0, 10)) {
|
|
1275
|
+
const enf = r.enforcement === 'ENFORCED' ? `[ENFORCED: ${r.checker}]` : `[${r.enforcement}]`;
|
|
1276
|
+
lines.push(` • ${r.id.padEnd(10)} ${enf.padEnd(25)} ${r.summary.slice(0, 60)}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (res.rules.length > 10) {
|
|
1279
|
+
lines.push(` ... and ${res.rules.length - 10} more rules`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (res.conflicts && res.conflicts.length > 0) {
|
|
1284
|
+
lines.push('\nConflicts:');
|
|
1285
|
+
for (const c of res.conflicts) {
|
|
1286
|
+
lines.push(` ⚠ ${c.skillA} <-> ${c.skillB}: ${c.reason}`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (res.warnings && res.warnings.length > 0) {
|
|
1291
|
+
lines.push('\nWarnings:');
|
|
1292
|
+
for (const w of res.warnings) {
|
|
1293
|
+
lines.push(` ▲ [${w.code}] ${w.message}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
lines.push('──────────────────────────────────────────');
|
|
1298
|
+
return lines.join('\n');
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Builds progressive skills index for quick searching.
|
|
1303
|
+
*/
|
|
1304
|
+
buildSkillIndex(projectDir = this.rootDir) {
|
|
1305
|
+
const skills = [];
|
|
1306
|
+
const skillsDict = this.registry?.skills || {};
|
|
1307
|
+
|
|
1308
|
+
if (Object.keys(skillsDict).length > 0) {
|
|
1309
|
+
for (const [id, s] of Object.entries(skillsDict)) {
|
|
1310
|
+
skills.push({
|
|
1311
|
+
name: id,
|
|
1312
|
+
description: s.description || `ContextOS skill for ${s.displayName || id}`,
|
|
1313
|
+
path: s.source ? `${s.source.replace(/\\/g, '/')}/${s.entrypoint || 'SKILL.md'}` : `.agents/core/skills/${id}/SKILL.md`,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Fallback: directory traversal
|
|
1320
|
+
const coreDir = path.join(projectDir, '.agents', 'core', 'skills');
|
|
1321
|
+
if (fs.existsSync(coreDir)) {
|
|
1322
|
+
const dirs = fs.readdirSync(coreDir, { withFileTypes: true });
|
|
1323
|
+
for (const d of dirs) {
|
|
1324
|
+
if (d.isDirectory()) {
|
|
1325
|
+
skills.push({
|
|
1326
|
+
name: d.name,
|
|
1327
|
+
description: `ContextOS skill for ${d.name}`,
|
|
1328
|
+
path: `.agents/core/skills/${d.name}/SKILL.md`,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
module.exports = {
|
|
1338
|
+
CanonicalResolver,
|
|
1339
|
+
DEFAULT_CONTEXT_BUDGET_TOKENS,
|
|
1340
|
+
BUDGET_TIERS,
|
|
1341
|
+
WORKFLOW_TEMPLATES,
|
|
1342
|
+
MODES,
|
|
1343
|
+
DEPRECATED_ALIASES,
|
|
1344
|
+
WEIGHTS,
|
|
1345
|
+
analyzeImportGraph,
|
|
1346
|
+
evaluateRisk,
|
|
1347
|
+
resolvePhaseAndRole,
|
|
1348
|
+
};
|