@ngockhoale/ukit 1.1.6 → 1.1.8
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 +22 -0
- package/README.md +9 -4
- package/manifests/platform.full.yaml +55 -0
- package/package.json +1 -1
- package/src/cli/commands/doctor.js +2 -0
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/uninstall.js +1 -1
- package/src/core/runtimeConfig.js +1 -1
- package/src/core/uninstall.js +1 -1
- package/src/index/buildIndex.js +88 -2
- package/src/index/impactCatalog.js +126 -0
- package/src/index/impactContext.js +232 -0
- package/src/index/paths.js +1 -0
- package/src/index/resolveContext.js +1 -0
- package/src/index/routeCatalog.js +24 -2
- package/src/index/taskRouting.js +147 -4
- package/src/index/verificationPlan.js +18 -1
- package/templates/.claude/hooks/skill-router.sh +1 -1
- package/templates/.claude/hooks/verification-guard.sh +150 -12
- package/templates/.claude/skills/docs-quality/SKILL.md +9 -1
- package/templates/.claude/skills/next-step/SKILL.md +78 -0
- package/templates/.claude/skills/update-status/SKILL.md +88 -0
- package/templates/.claude/ukit/index/impact-context.mjs +122 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +352 -2
- package/templates/.claude/ukit/index/route-catalog.mjs +24 -2
- package/templates/.claude/ukit/index/route-task.mjs +166 -4
- package/templates/.codex/README.md +6 -1
- package/templates/.codex/settings.json +8 -1
- package/templates/AGENTS.md +12 -1
- package/templates/CLAUDE.md +12 -1
- package/templates/docs/INSTALL.md +2 -0
- package/templates/docs/PROJECT.md +5 -4
- package/templates/docs/STATUS.md +81 -0
- package/templates/docs/UKIT_USAGE_GUIDE.md +16 -0
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getArtifactPath, INDEX_ARTIFACTS } from './paths.js';
|
|
5
|
+
import { classifyImpactRisk, findMirrorCounterparts } from './impactCatalog.js';
|
|
6
|
+
import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from './relatedTests.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_IMPACT_LIMITS = {
|
|
9
|
+
maxCallers: 8,
|
|
10
|
+
maxCallees: 8,
|
|
11
|
+
maxImporters: 8,
|
|
12
|
+
maxTests: 8,
|
|
13
|
+
maxMirrors: 4,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const RISK_TEST_RECOMMENDATIONS = {
|
|
17
|
+
'shared-index-runtime': ['tests/index/indexing.test.js', 'tests/index/taskRouting.test.js'],
|
|
18
|
+
'shared-install-runtime': ['tests/integration/installPipeline.test.js'],
|
|
19
|
+
'installed-hook-runtime': ['tests/hooks/skillRouterHook.test.js', 'tests/integration/installPipeline.test.js'],
|
|
20
|
+
'published-template-surface': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
21
|
+
'install-manifest': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function readArtifact(rootDir, artifactName) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(getArtifactPath(path.resolve(rootDir), artifactName), 'utf8');
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return { items: [] };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePath(filePath) {
|
|
34
|
+
return String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unique(values = []) {
|
|
38
|
+
return [...new Set(values.filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveImportTarget(fromFilePath, specifier) {
|
|
42
|
+
const from = normalizePath(fromFilePath);
|
|
43
|
+
const to = String(specifier ?? '').trim();
|
|
44
|
+
if (!from || !to || !to.startsWith('.')) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fromDir = path.posix.dirname(from);
|
|
49
|
+
const base = path.posix.normalize(path.posix.join(fromDir, to));
|
|
50
|
+
const candidates = [
|
|
51
|
+
base,
|
|
52
|
+
`${base}.js`,
|
|
53
|
+
`${base}.mjs`,
|
|
54
|
+
`${base}.cjs`,
|
|
55
|
+
`${base}.ts`,
|
|
56
|
+
`${base}.tsx`,
|
|
57
|
+
`${base}.jsx`,
|
|
58
|
+
`${base}.vue`,
|
|
59
|
+
path.posix.join(base, 'index.js'),
|
|
60
|
+
path.posix.join(base, 'index.ts'),
|
|
61
|
+
path.posix.join(base, 'index.mjs'),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return unique(candidates);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveImpactContext({
|
|
68
|
+
rootDir = process.cwd(),
|
|
69
|
+
changedFiles = [],
|
|
70
|
+
changedSymbols = [],
|
|
71
|
+
limits = {},
|
|
72
|
+
} = {}) {
|
|
73
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
74
|
+
const budget = { ...DEFAULT_IMPACT_LIMITS, ...(limits ?? {}) };
|
|
75
|
+
const normalizedChangedFiles = unique(changedFiles.map(normalizePath));
|
|
76
|
+
const normalizedChangedSymbols = unique(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
77
|
+
|
|
78
|
+
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
79
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
80
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
81
|
+
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const callItems = callsArtifact.items ?? [];
|
|
85
|
+
const importItems = importsArtifact.items ?? [];
|
|
86
|
+
const risk = classifyImpactRisk(normalizedChangedFiles);
|
|
87
|
+
const mirrorCounterparts = findMirrorCounterparts(normalizedChangedFiles).slice(0, budget.maxMirrors);
|
|
88
|
+
|
|
89
|
+
const changedFileSet = new Set(normalizedChangedFiles);
|
|
90
|
+
const changedSymbolSet = new Set(normalizedChangedSymbols);
|
|
91
|
+
|
|
92
|
+
const changedRecords = callItems.filter((item) => {
|
|
93
|
+
const fileMatched = changedFileSet.has(normalizePath(item.filePath));
|
|
94
|
+
if (!fileMatched) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (changedSymbolSet.size === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return changedSymbolSet.has(item.symbol);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const callees = [];
|
|
104
|
+
for (const record of changedRecords) {
|
|
105
|
+
for (const symbol of record.calls ?? []) {
|
|
106
|
+
callees.push({ filePath: record.filePath, fromSymbol: record.symbol, symbol });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const callers = [];
|
|
111
|
+
const callerTargets = new Set([
|
|
112
|
+
...normalizedChangedSymbols,
|
|
113
|
+
...changedRecords.map((record) => record.symbol),
|
|
114
|
+
]);
|
|
115
|
+
if (callerTargets.size > 0) {
|
|
116
|
+
for (const record of callItems) {
|
|
117
|
+
const matchedSymbol = (record.calls ?? []).find((name) => callerTargets.has(name));
|
|
118
|
+
if (!matchedSymbol) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
changedFileSet.has(normalizePath(record.filePath))
|
|
124
|
+
&& changedSymbolSet.has(record.symbol)
|
|
125
|
+
) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
callers.push({ filePath: record.filePath, symbol: record.symbol, reason: `calls ${matchedSymbol}` });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const importers = [];
|
|
134
|
+
const dependencies = [];
|
|
135
|
+
for (const imp of importItems) {
|
|
136
|
+
const from = normalizePath(imp.from);
|
|
137
|
+
const targets = resolveImportTarget(from, imp.to) ?? [];
|
|
138
|
+
const matched = targets.find((candidate) => changedFileSet.has(normalizePath(candidate)));
|
|
139
|
+
|
|
140
|
+
if (matched) {
|
|
141
|
+
importers.push(from);
|
|
142
|
+
dependencies.push(matched);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const candidateFiles = unique([
|
|
147
|
+
...normalizedChangedFiles,
|
|
148
|
+
...importers,
|
|
149
|
+
...callers.map((item) => item.filePath),
|
|
150
|
+
...mirrorCounterparts.map((item) => item.filePath),
|
|
151
|
+
]);
|
|
152
|
+
const relatedTests = inferRelatedTestsFromArtifacts({
|
|
153
|
+
candidateFiles,
|
|
154
|
+
analogsMap: relatedArtifacts.analogsMap,
|
|
155
|
+
relationsMap: relatedArtifacts.relationsMap,
|
|
156
|
+
limit: budget.maxTests,
|
|
157
|
+
}).map((entry) => entry.filePath);
|
|
158
|
+
|
|
159
|
+
const recommendedTestFiles = unique([
|
|
160
|
+
...relatedTests,
|
|
161
|
+
...risk.riskLabels.flatMap((label) => RISK_TEST_RECOMMENDATIONS[label] ?? []),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
changedFiles: normalizedChangedFiles,
|
|
166
|
+
changedSymbols: normalizedChangedSymbols,
|
|
167
|
+
riskLabels: risk.riskLabels,
|
|
168
|
+
requiresImpactCheck: risk.requiresImpactCheck,
|
|
169
|
+
callees: unique(callees.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallees),
|
|
170
|
+
callers: unique(callers.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallers),
|
|
171
|
+
importers: unique(importers).slice(0, budget.maxImporters),
|
|
172
|
+
dependencies: unique(dependencies).slice(0, budget.maxImporters),
|
|
173
|
+
mirrorCounterparts,
|
|
174
|
+
relatedTests,
|
|
175
|
+
recommendedTestFiles,
|
|
176
|
+
explanations: {
|
|
177
|
+
callees: changedRecords.map((record) => `${record.filePath}:${record.symbol}`),
|
|
178
|
+
callers: callers.map((entry) => `${entry.filePath}:${entry.symbol}`),
|
|
179
|
+
importers,
|
|
180
|
+
relatedTests,
|
|
181
|
+
mirrorCounterparts: mirrorCounterparts.map((entry) => `${entry.source} -> ${entry.filePath}`),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatImpactConfidenceSummary(impact = {}) {
|
|
187
|
+
const lines = ['Impact checked:'];
|
|
188
|
+
|
|
189
|
+
if ((impact.changedFiles ?? []).length > 0) {
|
|
190
|
+
lines.push(`- Changed: ${(impact.changedFiles ?? []).slice(0, 3).join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
if ((impact.changedSymbols ?? []).length > 0) {
|
|
193
|
+
lines.push(`- Symbols: ${(impact.changedSymbols ?? []).slice(0, 5).join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
if ((impact.riskLabels ?? []).length > 0) {
|
|
196
|
+
lines.push(`- Risk: ${(impact.riskLabels ?? []).join(', ')}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const calleeSymbols = (impact.callees ?? []).map((item) => item?.symbol).filter(Boolean).slice(0, 5);
|
|
200
|
+
if (calleeSymbols.length > 0) {
|
|
201
|
+
lines.push(`- Callees: ${calleeSymbols.join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const callerFiles = (impact.callers ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 5);
|
|
205
|
+
if (callerFiles.length > 0) {
|
|
206
|
+
lines.push(`- Callers: ${callerFiles.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const importers = (impact.importers ?? []).slice(0, 5);
|
|
210
|
+
if (importers.length > 0) {
|
|
211
|
+
lines.push(`- Importers: ${importers.join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const mirrors = (impact.mirrorCounterparts ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 3);
|
|
215
|
+
if (mirrors.length > 0) {
|
|
216
|
+
lines.push(`- Mirrors: ${mirrors.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const relatedTests = (impact.relatedTests ?? []).slice(0, 4);
|
|
220
|
+
if (relatedTests.length > 0) {
|
|
221
|
+
lines.push(`- Related tests: ${relatedTests.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const recommendedTests = (impact.recommendedTestFiles ?? []).slice(0, 4);
|
|
225
|
+
if (recommendedTests.length > 0) {
|
|
226
|
+
lines.push(`- Recommended tests: ${recommendedTests.join(', ')}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { DEFAULT_IMPACT_LIMITS };
|
package/src/index/paths.js
CHANGED
|
@@ -4,6 +4,7 @@ import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from './rela
|
|
|
4
4
|
const TASK_TYPE_BUDGETS = {
|
|
5
5
|
trivial: { minFiles: 1, maxFiles: 2 },
|
|
6
6
|
simple: { minFiles: 2, maxFiles: 5 },
|
|
7
|
+
'shared-simple': { minFiles: 3, maxFiles: 8 },
|
|
7
8
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -223,8 +223,30 @@ export const ROUTE_CATALOG = [
|
|
|
223
223
|
path: '.claude/skills/docs-quality/SKILL.md',
|
|
224
224
|
order: 12,
|
|
225
225
|
signals: [
|
|
226
|
-
{ type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map)\b/i, score: 4 },
|
|
227
|
-
{ type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
|
|
226
|
+
{ type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map|status\.md)\b/i, score: 4 },
|
|
227
|
+
{ type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|status\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'next-step',
|
|
232
|
+
path: '.claude/skills/next-step/SKILL.md',
|
|
233
|
+
order: 12.1,
|
|
234
|
+
contextMode: 'standalone',
|
|
235
|
+
signals: [
|
|
236
|
+
{ type: 'prompt', regex: /\b(what(?:'s| is)? next|next steps?|project status|current status|where are we|continue(?: from)?(?: last session)?|roadmap|status\.md)\b/i, score: 7 },
|
|
237
|
+
{ type: 'prompt', regex: /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project)\b/i, score: 7 },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'update-status',
|
|
242
|
+
path: '.claude/skills/update-status/SKILL.md',
|
|
243
|
+
order: 12.2,
|
|
244
|
+
contextMode: 'standalone',
|
|
245
|
+
signals: [
|
|
246
|
+
{ type: 'prompt', regex: /\b(update|refresh|write|sync|record|capture|summari[sz]e).{0,48}\b(status\.md|project status|current state|next candidates?|session state)\b/i, score: 9 },
|
|
247
|
+
{ type: 'prompt', regex: /\b(status\.md|project status).{0,48}\b(update|refresh|write|sync|record|capture|summari[sz]e)\b/i, score: 9 },
|
|
248
|
+
{ type: 'prompt', regex: /\b(wrap up|handoff|end(?:ing)? this session|session summary|before final)\b/i, score: 7 },
|
|
249
|
+
{ type: 'prompt', regex: /\b(cập nhật|ghi lại|tổng kết|chốt session|bàn giao).{0,48}\b(status|trạng thái|việc tiếp theo)\b/i, score: 9 },
|
|
228
250
|
],
|
|
229
251
|
},
|
|
230
252
|
{
|
package/src/index/taskRouting.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ROUTE_CATALOG } from './routeCatalog.js';
|
|
|
5
5
|
import { buildRouteSignalText } from './languageTools.js';
|
|
6
6
|
import { resolveContext } from './resolveContext.js';
|
|
7
7
|
import { deriveVerificationPlan } from './verificationPlan.js';
|
|
8
|
+
import { isSharedImpactFile } from './impactCatalog.js';
|
|
8
9
|
|
|
9
10
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
10
11
|
|
|
@@ -21,11 +22,17 @@ export async function deriveTaskRoute({
|
|
|
21
22
|
const normalizedPrompt = String(promptText || '').trim();
|
|
22
23
|
const normalizedCommand = String(commandText || '').trim();
|
|
23
24
|
const normalizedTarget = normalizeRelativeFile(absoluteRoot, targetFile);
|
|
25
|
+
const intentMode = deriveIntentMode({
|
|
26
|
+
promptText: normalizedPrompt,
|
|
27
|
+
commandText: normalizedCommand,
|
|
28
|
+
targetFile: normalizedTarget,
|
|
29
|
+
});
|
|
24
30
|
const activeSkills = await selectActiveSkills({
|
|
25
31
|
rootDir: absoluteRoot,
|
|
26
32
|
promptText: normalizedPrompt,
|
|
27
33
|
commandText: normalizedCommand,
|
|
28
34
|
targetFile: normalizedTarget,
|
|
35
|
+
intentMode,
|
|
29
36
|
});
|
|
30
37
|
const useIndexedContext = shouldUseIndexedContext({
|
|
31
38
|
activeSkills,
|
|
@@ -42,6 +49,7 @@ export async function deriveTaskRoute({
|
|
|
42
49
|
promptText: normalizedPrompt,
|
|
43
50
|
commandText: normalizedCommand,
|
|
44
51
|
selectedIds,
|
|
52
|
+
targetFile: normalizedTarget,
|
|
45
53
|
});
|
|
46
54
|
const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
|
|
47
55
|
const contextResult = useIndexedContext && (contextIntent || normalizedTarget)
|
|
@@ -100,6 +108,7 @@ export async function deriveTaskRoute({
|
|
|
100
108
|
targetFile: normalizedTarget,
|
|
101
109
|
contextIntent,
|
|
102
110
|
taskType: inferredTaskType,
|
|
111
|
+
intentMode,
|
|
103
112
|
},
|
|
104
113
|
contextRecommendation,
|
|
105
114
|
verificationRecommendation,
|
|
@@ -115,6 +124,7 @@ export async function deriveTaskRoute({
|
|
|
115
124
|
targetFile: normalizedTarget,
|
|
116
125
|
contextIntent,
|
|
117
126
|
taskType: inferredTaskType,
|
|
127
|
+
intentMode,
|
|
118
128
|
},
|
|
119
129
|
contextRecommendation,
|
|
120
130
|
verificationRecommendation,
|
|
@@ -173,6 +183,7 @@ export function buildRouteSummary({
|
|
|
173
183
|
fallbackCommands,
|
|
174
184
|
preferredOrder,
|
|
175
185
|
policyMode,
|
|
186
|
+
intentMode: routingContext.intentMode ?? null,
|
|
176
187
|
delegateHint: delegationRecommendation?.hint ?? null,
|
|
177
188
|
nextActionType: nextAction?.type ?? null,
|
|
178
189
|
nextActionCommand,
|
|
@@ -181,7 +192,7 @@ export function buildRouteSummary({
|
|
|
181
192
|
};
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
async function selectActiveSkills({ rootDir, promptText, commandText, targetFile }) {
|
|
195
|
+
async function selectActiveSkills({ rootDir, promptText, commandText, targetFile, intentMode = null }) {
|
|
185
196
|
const routeSignals = {
|
|
186
197
|
promptRawText: String(promptText || '').toLowerCase(),
|
|
187
198
|
promptNormalizedText: buildRouteSignalText(promptText),
|
|
@@ -192,6 +203,7 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
|
|
|
192
203
|
const scoredEntries = ROUTE_CATALOG
|
|
193
204
|
.map((entry) => scoreSkillRouteEntry(entry, routeSignals))
|
|
194
205
|
.filter((entry) => entry.score > 0)
|
|
206
|
+
.filter((entry) => shouldKeepRouteEntryForIntent(entry, intentMode))
|
|
195
207
|
.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
196
208
|
const active = [];
|
|
197
209
|
|
|
@@ -207,6 +219,22 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
|
|
|
207
219
|
return active.map(({ order, ...rest }) => rest);
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
function shouldKeepRouteEntryForIntent(entry, intentMode) {
|
|
223
|
+
if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific'].includes(intentMode)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (entry.id === 'update-status' && intentMode === 'docs-specific') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (entry.id === 'docs-quality' && ['open-ended-status', 'status-update'].includes(intentMode)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
function scoreSkillRouteEntry(entry, routeSignals = {}) {
|
|
211
239
|
let score = 0;
|
|
212
240
|
const reasons = [];
|
|
@@ -280,6 +308,116 @@ function getSignalTexts(signalType, routeSignals = {}) {
|
|
|
280
308
|
};
|
|
281
309
|
}
|
|
282
310
|
|
|
311
|
+
function deriveIntentMode({ promptText = '', commandText = '', targetFile = null } = {}) {
|
|
312
|
+
const lower = buildRouteSignalText(promptText, commandText);
|
|
313
|
+
const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
|
|
314
|
+
const docsSpecific = hasDocsSpecificTaskSignal(lower, raw, targetFile);
|
|
315
|
+
const statusUpdate = hasStatusUpdateSignal(lower, raw);
|
|
316
|
+
const openEndedStatus = hasOpenEndedStatusSignal(lower, raw);
|
|
317
|
+
const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile);
|
|
318
|
+
|
|
319
|
+
if (docsSpecific) {
|
|
320
|
+
return 'docs-specific';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (statusUpdate) {
|
|
324
|
+
return 'status-update';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (concreteTask && openEndedStatus) {
|
|
328
|
+
return 'scoped-advice';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (openEndedStatus) {
|
|
332
|
+
return 'open-ended-status';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (concreteTask) {
|
|
336
|
+
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|login|timeout)\b/.test(lower)) {
|
|
337
|
+
return 'debug-specific';
|
|
338
|
+
}
|
|
339
|
+
if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(lower)) {
|
|
340
|
+
return 'review-specific';
|
|
341
|
+
}
|
|
342
|
+
if (/\b(implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature)\b/.test(lower)) {
|
|
343
|
+
return 'implement-specific';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function hasStatusUpdateSignal(lower, raw) {
|
|
351
|
+
return /\b(update|refresh|write|sync|record|capture|summarize|summarise).{0,64}\b(status\.md|project status|current state|next candidates|session state)\b/.test(lower)
|
|
352
|
+
|| /\b(status\.md|project status).{0,64}\b(update|refresh|write|sync|record|capture|summarize|summarise)\b/.test(lower)
|
|
353
|
+
|| /\b(wrap up|handoff|end this session|ending this session|session summary|before final)\b/.test(lower)
|
|
354
|
+
|| /\b(cap nhat|ghi lai|tong ket|chot session|ban giao).{0,64}\b(status|trang thai|viec tiep theo)\b/.test(lower)
|
|
355
|
+
|| /\b(cập nhật|ghi lại|tổng kết|chốt session|bàn giao).{0,64}\b(status|trạng thái|việc tiếp theo)\b/.test(raw);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hasOpenEndedStatusSignal(lower, raw) {
|
|
359
|
+
return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md)\b/.test(lower)
|
|
360
|
+
|| /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project)\b/.test(lower)
|
|
361
|
+
|| /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project)\b/.test(raw);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function hasConcreteTaskSignal(lower, raw, targetFile) {
|
|
365
|
+
if (targetFile && !isStatusFileTarget(targetFile)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
|
|
370
|
+
|| /\b(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)\b/.test(raw);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function hasDocsSpecificTaskSignal(lower, raw, targetFile) {
|
|
374
|
+
if (!targetFile || !isDocsTarget(targetFile)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const targetName = path.posix.basename(String(targetFile || '').replaceAll('\\', '/')).toLowerCase();
|
|
379
|
+
const explicitStatusUpdate = hasExplicitStatusUpdateSignal(lower, raw);
|
|
380
|
+
|
|
381
|
+
if (!isStatusFileTarget(targetFile)) {
|
|
382
|
+
if (explicitStatusUpdate && !mentionsTargetDoc(lower, targetName)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return mentionsTargetDoc(lower, targetName)
|
|
387
|
+
|| /\b(edit|write|improve|document|docs?|readme|changelog|worklog|memory|code map|template|wording|copy|grammar|format|structure|heading|section|handoff notes?)\b/.test(lower)
|
|
388
|
+
|| /\b(câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu|ghi chú bàn giao)\b/.test(raw);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return /\b(template|wording|edit|copy|grammar|format|structure|heading|section|docs?)\b/.test(lower)
|
|
392
|
+
|| /\b(mẫu|câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu)\b/.test(raw);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function hasExplicitStatusUpdateSignal(lower, raw) {
|
|
396
|
+
return /\b(update|refresh|write|sync|record|capture|summarize|summarise).{0,64}\b(status\.md|project status|current state|next candidates|session state)\b/.test(lower)
|
|
397
|
+
|| /\b(status\.md|project status).{0,64}\b(update|refresh|write|sync|record|capture|summarize|summarise)\b/.test(lower)
|
|
398
|
+
|| /\b(cap nhat|ghi lai|tong ket|chot session|ban giao).{0,64}\b(status|trang thai|viec tiep theo)\b/.test(lower)
|
|
399
|
+
|| /\b(cập nhật|ghi lại|tổng kết|chốt session|bàn giao).{0,64}\b(status|trạng thái|việc tiếp theo)\b/.test(raw);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function mentionsTargetDoc(lower, targetName) {
|
|
403
|
+
if (!targetName) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const escaped = targetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
408
|
+
const withoutExt = escaped.replace(/\\\.md$/i, '');
|
|
409
|
+
return new RegExp(`\\b(?:${escaped}|${withoutExt})\\b`, 'i').test(lower);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isStatusFileTarget(targetFile) {
|
|
413
|
+
return /(?:^|\/)docs\/STATUS\.md$|(?:^|\/)STATUS\.md$/i.test(String(targetFile || ''));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isDocsTarget(targetFile) {
|
|
417
|
+
const normalized = String(targetFile || '').replaceAll('\\', '/');
|
|
418
|
+
return /(?:^|\/)docs\/.+\.md$|(?:^|\/)(?:README|CHANGELOG|AGENTS|CLAUDE|STATUS)\.md$/i.test(normalized);
|
|
419
|
+
}
|
|
420
|
+
|
|
283
421
|
function shouldUseIndexedContext({ activeSkills = [], targetFile = null } = {}) {
|
|
284
422
|
if (activeSkills.length === 0) {
|
|
285
423
|
return Boolean(targetFile);
|
|
@@ -531,7 +669,7 @@ function deriveContextIntent({ promptText, commandText, targetFile, selectedIds
|
|
|
531
669
|
return '';
|
|
532
670
|
}
|
|
533
671
|
|
|
534
|
-
function inferTaskType({ promptText, commandText, selectedIds }) {
|
|
672
|
+
function inferTaskType({ promptText, commandText, selectedIds, targetFile = null }) {
|
|
535
673
|
const lower = buildRouteSignalText(promptText, commandText);
|
|
536
674
|
|
|
537
675
|
if (
|
|
@@ -542,11 +680,16 @@ function inferTaskType({ promptText, commandText, selectedIds }) {
|
|
|
542
680
|
return 'non-trivial';
|
|
543
681
|
}
|
|
544
682
|
|
|
683
|
+
let inferred = 'simple';
|
|
545
684
|
if (/\b(typo|label|text|rename|color|spacing|toggle|comment)\b/.test(lower)) {
|
|
546
|
-
|
|
685
|
+
inferred = 'trivial';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if ((inferred === 'simple' || inferred === 'trivial') && isSharedImpactFile(targetFile)) {
|
|
689
|
+
return 'shared-simple';
|
|
547
690
|
}
|
|
548
691
|
|
|
549
|
-
return
|
|
692
|
+
return inferred;
|
|
550
693
|
}
|
|
551
694
|
|
|
552
695
|
function shellEscape(str) {
|
|
@@ -23,6 +23,7 @@ export async function deriveVerificationPlan({
|
|
|
23
23
|
targetFile = null,
|
|
24
24
|
taskType = null,
|
|
25
25
|
contextResult = null,
|
|
26
|
+
impactContext = null,
|
|
26
27
|
skillIds = [],
|
|
27
28
|
} = {}) {
|
|
28
29
|
const absoluteRoot = path.resolve(rootDir);
|
|
@@ -72,7 +73,15 @@ export async function deriveVerificationPlan({
|
|
|
72
73
|
const notes = [];
|
|
73
74
|
const docsOnly = primaryTargets.length > 0
|
|
74
75
|
&& primaryTargets.every((filePath) => filePath.startsWith('docs/'));
|
|
76
|
+
const impactTests = unique(impactContext?.recommendedTestFiles ?? []);
|
|
77
|
+
const impactRisk = Boolean(
|
|
78
|
+
impactContext?.requiresImpactCheck
|
|
79
|
+
|| (impactContext?.riskLabels ?? []).length > 0,
|
|
80
|
+
);
|
|
81
|
+
const sharedSimple = effectiveTaskType === 'shared-simple' || taskType === 'shared-simple';
|
|
75
82
|
const risky = effectiveTaskType === 'non-trivial'
|
|
83
|
+
|| sharedSimple
|
|
84
|
+
|| impactRisk
|
|
76
85
|
|| skillIds.some((skillId) => RISKY_SKILL_IDS.has(skillId));
|
|
77
86
|
|
|
78
87
|
let mode = 'minimal';
|
|
@@ -98,6 +107,14 @@ export async function deriveVerificationPlan({
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
if (impactTests.length > 0 && scripts.test) {
|
|
111
|
+
mode = relatedTests.length > 0 ? mode : 'impact-tests-first';
|
|
112
|
+
reasons.push('impactTests');
|
|
113
|
+
for (const testFile of impactTests.slice(0, 2)) {
|
|
114
|
+
commands.push(buildScriptCommand(packageManager, 'test', [testFile]));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
if (risky) {
|
|
102
119
|
reasons.push('riskySkillOrTask');
|
|
103
120
|
if (scripts.test) {
|
|
@@ -345,7 +362,7 @@ function deriveExecutionPolicy({
|
|
|
345
362
|
|| reasons.includes('riskySkillOrTask')
|
|
346
363
|
|| reasons.includes('highRiskFallback');
|
|
347
364
|
const hasPrimaryCommands = commands.length > 0;
|
|
348
|
-
const hasTargetedPrimaryCommands = mode === 'targeted-tests-first' && hasPrimaryCommands;
|
|
365
|
+
const hasTargetedPrimaryCommands = (mode === 'targeted-tests-first' || mode === 'impact-tests-first') && hasPrimaryCommands;
|
|
349
366
|
const sharedScope = sharedAbstractions.length > 0;
|
|
350
367
|
const noRelatedTests = relatedTests.length === 0;
|
|
351
368
|
const broadOnlyPrimary = hasPrimaryCommands && !hasTargetedPrimaryCommands && noRelatedTests;
|
|
@@ -386,7 +386,7 @@ const { pathToFileURL } = require('url');
|
|
|
386
386
|
return '';
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
function inferTaskType({ promptText, commandText, selectedIds, buildRouteSignalText }) {
|
|
389
|
+
function inferTaskType({ promptText, commandText, selectedIds, buildRouteSignalText, targetFile = null }) {
|
|
390
390
|
const lower = buildRouteSignalText(promptText, commandText);
|
|
391
391
|
|
|
392
392
|
if (
|