@ngockhoale/ukit 2.2.16 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/manifests/platform.full.yaml +11 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +130 -0
- package/src/core/runtimeConfig.js +13 -50
- package/src/index/taskRouting.js +10 -103
- package/templates/.claude/agents/ukit-vision-analyst.md +32 -21
- package/templates/.claude/hooks/context-window-guard.sh +66 -14
- package/templates/.claude/hooks/protect-files.sh +1 -0
- package/templates/.claude/hooks/sensitive-data-guard.sh +269 -0
- package/templates/.claude/hooks/skill-router.sh +33 -0
- package/templates/.claude/hooks/vision-router.sh +63 -37
- package/templates/.claude/settings.json +17 -1
- package/templates/.claude/ukit/index/extract-image.mjs +18 -8
- package/templates/.claude/ukit/index/route-task.mjs +53 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +180 -22
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +8 -4
- package/templates/.omp/hooks/pre/ukit-bridge.js +15 -5
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/storage/config.json +8 -7
- package/src/core/router/advisor.js +0 -42
- package/src/core/router/router.js +0 -180
- package/src/core/validation/confidence.js +0 -89
- package/src/core/validation/validator.js +0 -165
- package/templates/docs/INSTALL.md +0 -115
- package/templates/docs/STATUS.md +0 -81
- package/templates/docs/TASKS.md +0 -79
- package/templates/docs/UKIT_USAGE_GUIDE.md +0 -163
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
"affectVerification": true,
|
|
7
7
|
"affectDelegation": true
|
|
8
8
|
},
|
|
9
|
+
"security": {
|
|
10
|
+
"sensitiveDataGate": true,
|
|
11
|
+
"allowlistPath": ".ukit/storage/security/allowlist.json"
|
|
12
|
+
},
|
|
9
13
|
"compact": {
|
|
10
14
|
"enabled": true,
|
|
11
15
|
"tokenThreshold": 150000,
|
|
@@ -91,7 +95,6 @@
|
|
|
91
95
|
"advisorEnabled": true,
|
|
92
96
|
"contracts": {
|
|
93
97
|
"tiny-fix": {
|
|
94
|
-
"modelTier": "lite",
|
|
95
98
|
"maxReadPasses": 0,
|
|
96
99
|
"maxContextPulls": 0,
|
|
97
100
|
"verificationPolicy": "minimal-or-targeted",
|
|
@@ -99,7 +102,6 @@
|
|
|
99
102
|
"delegationPolicy": "disallow"
|
|
100
103
|
},
|
|
101
104
|
"local-fix": {
|
|
102
|
-
"modelTier": "code",
|
|
103
105
|
"maxReadPasses": 1,
|
|
104
106
|
"maxContextPulls": 1,
|
|
105
107
|
"verificationPolicy": "targeted-if-covered",
|
|
@@ -107,7 +109,6 @@
|
|
|
107
109
|
"delegationPolicy": "disallow"
|
|
108
110
|
},
|
|
109
111
|
"local-build": {
|
|
110
|
-
"modelTier": "code",
|
|
111
112
|
"maxReadPasses": 2,
|
|
112
113
|
"maxContextPulls": 1,
|
|
113
114
|
"verificationPolicy": "targeted-if-covered",
|
|
@@ -116,14 +117,12 @@
|
|
|
116
117
|
"postEditReviewPolicy": "sidecar-non-blocking"
|
|
117
118
|
},
|
|
118
119
|
"find-cause": {
|
|
119
|
-
"modelTier": "code",
|
|
120
120
|
"maxReadPassesBeforeReassess": 3,
|
|
121
121
|
"verificationPolicy": "root-cause-then-targeted",
|
|
122
122
|
"completionRule": "never-claim-fixed-without-write-and-verification",
|
|
123
123
|
"delegationPolicy": "allow-specialized-debug-lane"
|
|
124
124
|
},
|
|
125
125
|
"shared-edit": {
|
|
126
|
-
"modelTier": "code",
|
|
127
126
|
"maxReadPasses": 2,
|
|
128
127
|
"maxContextPulls": 2,
|
|
129
128
|
"verificationPolicy": "targeted-then-widen-on-risk",
|
|
@@ -132,7 +131,6 @@
|
|
|
132
131
|
"postEditReviewPolicy": "sidecar-non-blocking"
|
|
133
132
|
},
|
|
134
133
|
"map-impact": {
|
|
135
|
-
"modelTier": "code",
|
|
136
134
|
"maxReadPasses": 3,
|
|
137
135
|
"maxContextPulls": 3,
|
|
138
136
|
"verificationPolicy": "impact-first-then-targeted-then-widen-on-risk",
|
|
@@ -140,7 +138,6 @@
|
|
|
140
138
|
"delegationPolicy": "allow-impact-sidecar"
|
|
141
139
|
},
|
|
142
140
|
"review-release": {
|
|
143
|
-
"modelTier": "smart",
|
|
144
141
|
"verificationPolicy": "evidence-first",
|
|
145
142
|
"completionRule": "report-findings-not-implementation",
|
|
146
143
|
"delegationPolicy": "allow-review-sidecar"
|
|
@@ -392,6 +389,10 @@
|
|
|
392
389
|
"affectVerification": "Nếu true, autonomy.level ảnh hưởng hành vi verification plan.",
|
|
393
390
|
"affectDelegation": "Nếu true, autonomy.level ảnh hưởng ngưỡng delegation."
|
|
394
391
|
},
|
|
392
|
+
"security": {
|
|
393
|
+
"sensitiveDataGate": "Bật sensitive-data gate: chặn key/private data/secret (đọc file .env, *.pem, id_rsa; lệnh bash dump secret; prompt chứa token) trước khi chúng tới AI. Chặn cực gắt theo yêu cầu: chỉ cần nghi ngờ là chặn và hỏi user. Tắt chỉ khi debug gate này.",
|
|
394
|
+
"allowlistPath": "File allowlist JSON do USER tự tạo để phê duyệt tường minh giá trị secret (sha256) hoặc đường dẫn file được phép gửi. File này được protect-files.sh chặn AI tự sửa."
|
|
395
|
+
},
|
|
395
396
|
"compact": {
|
|
396
397
|
"enabled": "Bật/tắt toàn bộ helper compact của UKit.",
|
|
397
398
|
"tokenThreshold": "Ngưỡng token chung cho runtime compact dùng chung.",
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export function shouldEscalate(trigger) {
|
|
2
|
-
if (!trigger || typeof trigger !== 'object') {
|
|
3
|
-
return false;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
if (trigger.type === 'user_requested') {
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (trigger.type === 'complexity_detected') {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (trigger.type === 'retry_exceeded') {
|
|
15
|
-
return (trigger.attempts ?? 0) >= 2;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (trigger.type === 'validation_failed') {
|
|
19
|
-
return (trigger.attempts ?? 0) >= 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function askAdvisor(request) {
|
|
26
|
-
const strategy = request?.question
|
|
27
|
-
? `Break the task into verifiable steps, focusing on: ${request.question}`
|
|
28
|
-
: 'Break the task into smaller verifiable steps.';
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
strategy,
|
|
32
|
-
steps: [
|
|
33
|
-
'Restate the goal in one short sentence.',
|
|
34
|
-
'List the smallest changes that can be verified locally.',
|
|
35
|
-
'Run focused checks before expanding the scope.',
|
|
36
|
-
],
|
|
37
|
-
warnings: [
|
|
38
|
-
'Fallback advisor response used because no external advisor integration is wired in this local runtime.',
|
|
39
|
-
],
|
|
40
|
-
confidence: 55,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
const HIGH_COMPLEXITY_KEYWORDS = [
|
|
2
|
-
'architecture',
|
|
3
|
-
'architect',
|
|
4
|
-
'design',
|
|
5
|
-
'trade-off',
|
|
6
|
-
'tradeoff',
|
|
7
|
-
'security',
|
|
8
|
-
'compare',
|
|
9
|
-
'why',
|
|
10
|
-
'reasoning',
|
|
11
|
-
'scalable',
|
|
12
|
-
'system design',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const LOW_COMPLEXITY_KEYWORDS = [
|
|
16
|
-
'rename',
|
|
17
|
-
'add field',
|
|
18
|
-
'change text',
|
|
19
|
-
'format',
|
|
20
|
-
'typo',
|
|
21
|
-
'update label',
|
|
22
|
-
'refactor name',
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const REVIEW_KEYWORDS = ['review', 'code review', 'audit'];
|
|
26
|
-
const DEBUG_KEYWORDS = ['debug', 'bug', 'stack trace', 'error', 'crash', 'retry', 'failed attempt', 'failure'];
|
|
27
|
-
const CODING_HINTS = ['src/', 'package.json', 'function', 'class', 'implement', 'file', '.js', '.ts', '.tsx', '.jsx', '```'];
|
|
28
|
-
|
|
29
|
-
function normalize(text) {
|
|
30
|
-
return String(text ?? '').toLowerCase();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function escapeRegExp(value) {
|
|
34
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function matchesKeyword(text, keyword) {
|
|
38
|
-
if (keyword.startsWith('.')) {
|
|
39
|
-
return new RegExp(`(?:^|[\\s/\\\\])[^\\s/\\\\]+${escapeRegExp(keyword)}(?:$|[\\s)\\],.;:])`).test(text);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (keyword.includes(' ')) {
|
|
43
|
-
return new RegExp(`\\b${keyword.split(/\\s+/).map(escapeRegExp).join('\\\\s+')}\\b`).test(text);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return new RegExp(`\\b${escapeRegExp(keyword)}\\b`).test(text);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function countFileHints(text) {
|
|
50
|
-
const explicitCount = Number.parseInt(normalize(text).match(/\b(\d+)\s+files?\b/)?.[1] ?? '0', 10);
|
|
51
|
-
const pathMatches = normalize(text).match(/\b[\w./-]+\.(?:js|ts|tsx|jsx|json|md|yaml|yml)\b/g) ?? [];
|
|
52
|
-
return Math.max(explicitCount, new Set(pathMatches).size);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function detectComplexity(message, context = '') {
|
|
56
|
-
const combined = normalize(`${message}\n${context}`);
|
|
57
|
-
const wordCount = combined.split(/\s+/).filter(Boolean).length;
|
|
58
|
-
const fileHints = countFileHints(combined);
|
|
59
|
-
const hasRetryPattern = /\b(retry|failed|failure|attempt)\b/.test(combined);
|
|
60
|
-
const highSignals = HIGH_COMPLEXITY_KEYWORDS.filter((keyword) => matchesKeyword(combined, keyword)).length;
|
|
61
|
-
const lowSignals = LOW_COMPLEXITY_KEYWORDS.filter((keyword) => matchesKeyword(combined, keyword)).length;
|
|
62
|
-
|
|
63
|
-
if (highSignals > 0 || fileHints > 5 || (hasRetryPattern && /\b(debug|error|crash|stack trace)\b/.test(combined))) {
|
|
64
|
-
return 'high';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (lowSignals > 0 || (fileHints <= 1 && wordCount <= 24 && !hasRetryPattern)) {
|
|
68
|
-
return 'low';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return 'medium';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function detectTaskType(message, context = '', complexity = 'medium') {
|
|
75
|
-
const combined = normalize(`${message}\n${context}`);
|
|
76
|
-
|
|
77
|
-
if (DEBUG_KEYWORDS.some((keyword) => matchesKeyword(combined, keyword)) && complexity === 'high') {
|
|
78
|
-
return 'debug_hard';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (HIGH_COMPLEXITY_KEYWORDS.some((keyword) => matchesKeyword(combined, keyword))) {
|
|
82
|
-
return 'reasoning';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (REVIEW_KEYWORDS.some((keyword) => matchesKeyword(combined, keyword))) {
|
|
86
|
-
return 'review';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (CODING_HINTS.some((keyword) => matchesKeyword(combined, keyword))) {
|
|
90
|
-
return 'coding';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return 'simple_chat';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function recommendTier(type, complexity) {
|
|
97
|
-
if (type === 'simple_chat' && complexity === 'low') {
|
|
98
|
-
return 'fast';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (type === 'reasoning') {
|
|
102
|
-
return 'powerful';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (type === 'debug_hard' && complexity === 'high') {
|
|
106
|
-
return 'powerful';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (type === 'review') {
|
|
110
|
-
return complexity === 'high' ? 'powerful' : 'balanced';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (type === 'coding') {
|
|
114
|
-
return complexity === 'high' ? 'powerful' : 'balanced';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return complexity === 'low' ? 'fast' : 'balanced';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function buildReason(type, complexity) {
|
|
121
|
-
if (type === 'simple_chat') {
|
|
122
|
-
return 'Short conversational request without strong coding or reasoning signals.';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (type === 'debug_hard') {
|
|
126
|
-
return 'Debugging request includes repeated failures or stack-trace style signals.';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (type === 'reasoning') {
|
|
130
|
-
return 'Request asks for architecture, comparison, trade-offs, or deeper analysis.';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (type === 'review') {
|
|
134
|
-
return complexity === 'high'
|
|
135
|
-
? 'Review request touches higher-risk or cross-cutting areas.'
|
|
136
|
-
: 'Review request is scoped enough for the balanced tier.';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (type === 'coding') {
|
|
140
|
-
return complexity === 'high'
|
|
141
|
-
? 'Implementation spans multiple modules or difficult trade-offs.'
|
|
142
|
-
: 'Normal implementation work is best handled by the balanced tier.';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return 'Fallback routing decision.';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function classifyTask(userMessage, context = '') {
|
|
149
|
-
const complexity = detectComplexity(userMessage, context);
|
|
150
|
-
const type = detectTaskType(userMessage, context, complexity);
|
|
151
|
-
const recommendedTier = recommendTier(type, complexity);
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
type,
|
|
155
|
-
complexity,
|
|
156
|
-
recommendedTier,
|
|
157
|
-
reason: buildReason(type, complexity),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function selectModel(classification, config) {
|
|
162
|
-
const tier = classification?.recommendedTier ?? 'balanced';
|
|
163
|
-
if (config?.[tier]) {
|
|
164
|
-
return config[tier];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (tier === 'powerful' && config?.advisorModel) {
|
|
168
|
-
return config.advisorModel;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (tier === 'balanced' && config?.defaultModel) {
|
|
172
|
-
return config.defaultModel;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (tier === 'fast' && config?.fast) {
|
|
176
|
-
return config.fast;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return config?.defaultModel ?? config?.balanced ?? config?.fast ?? config?.powerful ?? null;
|
|
180
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const HEDGING_WORDS = [
|
|
2
|
-
'maybe',
|
|
3
|
-
'might',
|
|
4
|
-
'i think',
|
|
5
|
-
'probably',
|
|
6
|
-
'possibly',
|
|
7
|
-
'could be',
|
|
8
|
-
'perhaps',
|
|
9
|
-
'có thể',
|
|
10
|
-
'chắc là',
|
|
11
|
-
'hình như',
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const VERIFICATION_WORDS = [
|
|
15
|
-
'verified',
|
|
16
|
-
'tested',
|
|
17
|
-
'passes',
|
|
18
|
-
'passed',
|
|
19
|
-
'checked',
|
|
20
|
-
'confirmed',
|
|
21
|
-
'validated',
|
|
22
|
-
'đã test',
|
|
23
|
-
'đã kiểm tra',
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
function normalize(text) {
|
|
27
|
-
return String(text ?? '').toLowerCase();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buildResult(score, factors) {
|
|
31
|
-
const clamped = Math.max(0, Math.min(100, Math.round(score)));
|
|
32
|
-
const level = clamped >= 80 ? 'high' : clamped >= 50 ? 'medium' : 'low';
|
|
33
|
-
const action = clamped >= 80 ? 'respond' : clamped >= 50 ? 'warn' : 'clarify';
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
score: clamped,
|
|
37
|
-
level,
|
|
38
|
-
factors,
|
|
39
|
-
action,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function scoreConfidence(output, taskType, context = '') {
|
|
44
|
-
const normalizedOutput = normalize(output);
|
|
45
|
-
const normalizedContext = normalize(context);
|
|
46
|
-
const factors = [];
|
|
47
|
-
let score = 50;
|
|
48
|
-
|
|
49
|
-
if (normalizedContext.trim().length >= 24) {
|
|
50
|
-
score += 15;
|
|
51
|
-
factors.push({ name: 'context_availability', impact: 'positive', reason: 'Enough task context is available.' });
|
|
52
|
-
} else {
|
|
53
|
-
score -= 20;
|
|
54
|
-
factors.push({ name: 'context_availability', impact: 'negative', reason: 'Very little context is available.' });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (normalizedOutput.length >= 60 && !HEDGING_WORDS.some((word) => normalizedOutput.includes(word))) {
|
|
58
|
-
score += 15;
|
|
59
|
-
factors.push({ name: 'task_clarity', impact: 'positive', reason: 'Output is specific and direct.' });
|
|
60
|
-
} else if (normalizedOutput.length < 30) {
|
|
61
|
-
score -= 10;
|
|
62
|
-
factors.push({ name: 'task_clarity', impact: 'negative', reason: 'Output is too short to inspire confidence.' });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const hedges = HEDGING_WORDS.filter((word) => normalizedOutput.includes(word));
|
|
66
|
-
if (hedges.length > 0) {
|
|
67
|
-
score -= 30;
|
|
68
|
-
factors.push({ name: 'model_certainty', impact: 'negative', reason: `Hedging language detected: ${hedges.join(', ')}.` });
|
|
69
|
-
} else {
|
|
70
|
-
score += 10;
|
|
71
|
-
factors.push({ name: 'model_certainty', impact: 'positive', reason: 'Language is direct rather than hedged.' });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const verificationHits = VERIFICATION_WORDS.filter((word) => normalizedOutput.includes(word));
|
|
75
|
-
if (verificationHits.length > 0) {
|
|
76
|
-
score += 20;
|
|
77
|
-
factors.push({ name: 'verification', impact: 'positive', reason: `Verification signals found: ${verificationHits.join(', ')}.` });
|
|
78
|
-
} else if (taskType === 'coding' || taskType === 'debug' || taskType === 'debug_hard') {
|
|
79
|
-
score -= 10;
|
|
80
|
-
factors.push({ name: 'verification', impact: 'negative', reason: 'No verification evidence was provided for a technical task.' });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (/\b(previous|existing|again|recall|memory)\b/.test(normalizedContext)) {
|
|
84
|
-
score += 5;
|
|
85
|
-
factors.push({ name: 'prior_knowledge', impact: 'positive', reason: 'Context suggests continuity with prior knowledge.' });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return buildResult(score, factors);
|
|
89
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
const REQUIREMENT_PREFIX = /^\s*(?:[-*•]|\d+[.)])\s+/;
|
|
2
|
-
const STOPWORDS = new Set([
|
|
3
|
-
'the', 'a', 'an', 'and', 'or', 'to', 'for', 'of', 'with', 'in', 'on', 'is', 'are',
|
|
4
|
-
'be', 'this', 'that', 'it', 'as', 'by', 'run', 'add', 'use', 'please', 'giúp', 'hãy',
|
|
5
|
-
'và', 'là', 'cho', 'cần', 'một', 'những',
|
|
6
|
-
]);
|
|
7
|
-
|
|
8
|
-
function normalize(text) {
|
|
9
|
-
return String(text ?? '')
|
|
10
|
-
.toLowerCase()
|
|
11
|
-
.replace(/```[\w-]*\n?/g, ' ')
|
|
12
|
-
.replace(/```/g, ' ')
|
|
13
|
-
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
14
|
-
.replace(/\s+/g, ' ')
|
|
15
|
-
.trim();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function unwrapCodeFence(output) {
|
|
19
|
-
const match = String(output ?? '').match(/```(?:[\w-]+)?\n([\s\S]*?)```/);
|
|
20
|
-
return match ? match[1].trim() : String(output ?? '').trim();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function tokenize(text) {
|
|
24
|
-
return normalize(text)
|
|
25
|
-
.split(/\s+/)
|
|
26
|
-
.filter((token) => token && !STOPWORDS.has(token) && token.length > 1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseRequirements(spec) {
|
|
30
|
-
if (!spec) {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (Array.isArray(spec?.requirements)) {
|
|
35
|
-
return spec.requirements.filter(Boolean);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof spec === 'string') {
|
|
39
|
-
const lines = spec.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
40
|
-
const bulletLines = lines.filter((line) => REQUIREMENT_PREFIX.test(line));
|
|
41
|
-
if (bulletLines.length > 0) {
|
|
42
|
-
return bulletLines.map((line) => line.replace(REQUIREMENT_PREFIX, '').trim());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return lines.length > 0 ? [lines.join(' ')] : [];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function requirementCovered(requirement, output) {
|
|
52
|
-
const outputTokens = new Set(tokenize(output));
|
|
53
|
-
const requirementTokens = tokenize(requirement);
|
|
54
|
-
if (requirementTokens.length === 0) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const matches = requirementTokens.filter((token) => outputTokens.has(token)).length;
|
|
59
|
-
const threshold = requirementTokens.length <= 2 ? requirementTokens.length : Math.max(2, Math.ceil(requirementTokens.length * 0.6));
|
|
60
|
-
return matches >= threshold;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildCompletenessCheck(output, spec) {
|
|
64
|
-
const requirements = parseRequirements(spec);
|
|
65
|
-
if (requirements.length === 0) {
|
|
66
|
-
return { name: 'completeness', passed: true };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const missing = requirements.filter((requirement) => !requirementCovered(requirement, output));
|
|
70
|
-
return {
|
|
71
|
-
name: 'completeness',
|
|
72
|
-
passed: missing.length === 0,
|
|
73
|
-
details: missing.length === 0 ? undefined : `Missing requirement coverage: ${missing.join('; ')}`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function countListItems(output) {
|
|
78
|
-
return String(output ?? '')
|
|
79
|
-
.split('\n')
|
|
80
|
-
.filter((line) => /^\s*(?:[-*•]|\d+[.)])\s+/.test(line))
|
|
81
|
-
.length;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildConsistencyCheck(output) {
|
|
85
|
-
const text = String(output ?? '');
|
|
86
|
-
const numericClaim = text.match(/\b(\d+)\s+(?:steps?|items?|checks?|requirements?|bước|mục)\b/i);
|
|
87
|
-
if (!numericClaim) {
|
|
88
|
-
return { name: 'consistency', passed: true };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const expectedCount = Number.parseInt(numericClaim[1], 10);
|
|
92
|
-
const actualCount = countListItems(text);
|
|
93
|
-
const passed = actualCount === expectedCount;
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
name: 'consistency',
|
|
97
|
-
passed,
|
|
98
|
-
details: passed ? undefined : `Declared ${expectedCount} items but found ${actualCount}.`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function detectRequestedFormat(taskType, spec) {
|
|
103
|
-
if (typeof spec === 'object' && typeof spec?.format === 'string') {
|
|
104
|
-
return spec.format.toLowerCase();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const combined = `${taskType ?? ''}\n${typeof spec === 'string' ? spec : ''}`.toLowerCase();
|
|
108
|
-
if (combined.includes('json')) return 'json';
|
|
109
|
-
if (combined.includes('code')) return 'code';
|
|
110
|
-
if (combined.includes('list')) return 'list';
|
|
111
|
-
return 'text';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function buildFormatCheck(output, taskType, spec) {
|
|
115
|
-
const format = detectRequestedFormat(taskType, spec);
|
|
116
|
-
const raw = String(output ?? '').trim();
|
|
117
|
-
|
|
118
|
-
if (format === 'json') {
|
|
119
|
-
try {
|
|
120
|
-
JSON.parse(unwrapCodeFence(raw));
|
|
121
|
-
return { name: 'format', passed: true };
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return { name: 'format', passed: false, details: `Invalid JSON output: ${error.message}` };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (format === 'code') {
|
|
128
|
-
const passed = /```/.test(raw);
|
|
129
|
-
return {
|
|
130
|
-
name: 'format',
|
|
131
|
-
passed,
|
|
132
|
-
details: passed ? undefined : 'Expected a code block in the output.',
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (format === 'list') {
|
|
137
|
-
const passed = countListItems(raw) > 0;
|
|
138
|
-
return {
|
|
139
|
-
name: 'format',
|
|
140
|
-
passed,
|
|
141
|
-
details: passed ? undefined : 'Expected a list-style response.',
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return { name: 'format', passed: true };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function validateOutput(output, taskType, spec) {
|
|
149
|
-
const checks = [
|
|
150
|
-
buildCompletenessCheck(output, spec),
|
|
151
|
-
buildConsistencyCheck(output),
|
|
152
|
-
buildFormatCheck(output, taskType, spec),
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
const failedChecks = checks.filter((check) => !check.passed);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
passed: failedChecks.length === 0,
|
|
159
|
-
checks,
|
|
160
|
-
shouldRetry: failedChecks.length > 0,
|
|
161
|
-
retryHint: failedChecks.length === 0
|
|
162
|
-
? undefined
|
|
163
|
-
: `Previous output failed validation: ${failedChecks.map((check) => `${check.name}${check.details ? ` (${check.details})` : ''}`).join('; ')}`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# Installation Guide
|
|
2
|
-
|
|
3
|
-
## Team Rule
|
|
4
|
-
|
|
5
|
-
**For this project, the only UKit command teammates should need to remember is `ukit install`.**
|
|
6
|
-
|
|
7
|
-
After it runs, the normal workflow is:
|
|
8
|
-
1. fill the docs baseline
|
|
9
|
-
2. open the AI tool
|
|
10
|
-
3. work in natural language
|
|
11
|
-
|
|
12
|
-
Do not turn normal onboarding into a list of UKit subcommands.
|
|
13
|
-
How the CLI binary itself gets installed or updated can stay a maintainer/platform concern; inside projects, the human-facing workflow still centers on `ukit install`.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Initial Installation
|
|
18
|
-
|
|
19
|
-
### 1) Install the UKit CLI
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install -g @ngockhoale/ukit
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### 2) Install UKit into this project
|
|
26
|
-
|
|
27
|
-
Run from the project root:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
ukit install
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
This creates or refreshes the UKit workspace (`.claude/`, adapters, metadata, and `docs/`).
|
|
34
|
-
|
|
35
|
-
It also provisions the shared runtime in `.ukit/storage/`, including:
|
|
36
|
-
- memory state
|
|
37
|
-
- prompt/output caches
|
|
38
|
-
- compact history
|
|
39
|
-
- threshold-based compact pressure tracking
|
|
40
|
-
|
|
41
|
-
If the repo still has a legacy visible `ukit/` runtime from older installs, rerunning `ukit install` now migrates that shared runtime into hidden `.ukit/` when it is safe to do so.
|
|
42
|
-
|
|
43
|
-
End users do not need to manage any of that manually.
|
|
44
|
-
|
|
45
|
-
### 3) Fill in the docs baseline
|
|
46
|
-
|
|
47
|
-
Complete these files before first serious use:
|
|
48
|
-
- `docs/PROJECT.md`
|
|
49
|
-
- `docs/MEMORY.md`
|
|
50
|
-
- `docs/AI_HANDOFF/`
|
|
51
|
-
- `docs/WORKLOG.md`
|
|
52
|
-
|
|
53
|
-
### 4) Open your AI tool
|
|
54
|
-
|
|
55
|
-
After install, give natural-language requests such as:
|
|
56
|
-
- review this change
|
|
57
|
-
- fix this bug
|
|
58
|
-
- implement this feature
|
|
59
|
-
- follow the existing pattern
|
|
60
|
-
|
|
61
|
-
No slash command is required.
|
|
62
|
-
Teammates also should not need to know skill names — Claude Code / Codex should auto-detect and use the right project-local skill from the prompt plus the files/tools involved.
|
|
63
|
-
The workspace should also lean on indexed source code to find files/tests fast and prefer targeted verification before broad blanket checks.
|
|
64
|
-
When long sessions grow large, the shared runtime should compact old safe-zone context automatically near its configured threshold without changing the human workflow.
|
|
65
|
-
By default, the soft threshold comes from the configured compact token threshold and the hard threshold is about 20% above that; UKit should compact logs/history first while preserving the active task, rules, decisions, and current code focus.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Updating
|
|
70
|
-
|
|
71
|
-
To refresh the workspace after UKit changes, rerun the same command:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
ukit install
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
UKit is designed so first install and later refreshes use the same command.
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## Troubleshooting
|
|
82
|
-
|
|
83
|
-
### `ukit` command not found
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npm install -g @ngockhoale/ukit
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Workspace files seem stale
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
ukit install
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### The AI lacks project context
|
|
96
|
-
|
|
97
|
-
Check that the docs baseline files exist and are filled in:
|
|
98
|
-
- `docs/PROJECT.md`
|
|
99
|
-
- `docs/MEMORY.md`
|
|
100
|
-
- `docs/AI_HANDOFF/`
|
|
101
|
-
- `docs/WORKLOG.md`
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Maintainer / Debug Note
|
|
106
|
-
|
|
107
|
-
UKit may expose additional subcommands for maintainers and debugging. Upstream skill catalogs, GitHub skill repos, and awesome lists are also maintainer inputs — not the default team workflow.
|
|
108
|
-
|
|
109
|
-
If maintainers pull ideas from official skill ecosystems or curated lists, they should package those improvements into UKit and have teammates rerun the same install command.
|
|
110
|
-
|
|
111
|
-
Those sources should not become a new onboarding burden.
|
|
112
|
-
|
|
113
|
-
When in doubt, keep the guidance simple:
|
|
114
|
-
|
|
115
|
-
> rerun `ukit install`, then work in natural language inside the AI tool.
|