@ngockhoale/ukit 2.2.12 → 2.2.16
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 +75 -0
- package/manifests/platform.full.yaml +0 -13
- package/package.json +1 -1
- package/src/index/buildIndex.js +15 -2
- package/src/index/paths.js +1 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/taskRouting.js +5 -5
- package/templates/.claude/hooks/context-hardcap-gate.sh +20 -4
- package/templates/.claude/hooks/context-window-guard.sh +1 -1
- package/templates/.claude/hooks/handoff-model-guard.sh +99 -29
- package/templates/.claude/hooks/skill-router.sh +54 -6
- package/templates/.claude/hooks/vision-router.sh +8 -14
- package/templates/.claude/settings.json +0 -5
- package/templates/.claude/ukit/index/lib/index-core.mjs +35 -23
- package/templates/.claude/ukit/index/provision-worktree.mjs +1 -1
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.claude/ukit/index/route-task.mjs +52 -6
- package/templates/.claude/ukit/index/unic-gateway.mjs +1 -1
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +97 -2
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +0 -1
- package/templates/.omp/README.md +3 -3
- package/templates/.omp/hooks/pre/ukit-bridge.js +0 -2
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/storage/config.json +2 -2
- package/templates/.claude/hooks/vision-gate.sh +0 -230
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# UserPromptSubmit hook: detect image input (pasted / local path / URL) and
|
|
3
|
-
#
|
|
2
|
+
# UserPromptSubmit hook: detect image input (pasted / local path / URL) and mark it for
|
|
3
|
+
# the vision analyst by writing pending-<sha>.json markers via extract-image.mjs.
|
|
4
4
|
#
|
|
5
5
|
# FAILS OPEN, unconditionally: this hook must never block the user's prompt. Any
|
|
6
6
|
# failure (malformed stdin, missing extractor, node crash) degrades to "no hint,
|
|
7
|
-
# no markers" and exit 0.
|
|
8
|
-
# which fails closed on Edit/Write.
|
|
7
|
+
# no markers" and exit 0. Advisory only — nothing is ever blocked.
|
|
9
8
|
#
|
|
10
9
|
# Detection only — never decodes an image, never writes an image file. Marker
|
|
11
10
|
# writing is delegated entirely to `extract-image.mjs --mark-pending`; this hook
|
|
@@ -44,7 +43,7 @@ const { pathToFileURL } = require('url');
|
|
|
44
43
|
|
|
45
44
|
const promptText = extractPromptText(payload);
|
|
46
45
|
|
|
47
|
-
// Tri-state
|
|
46
|
+
// Tri-state: true | false | null(unknown). Only a positive false short-circuits.
|
|
48
47
|
async function detectUnicModeSafe() {
|
|
49
48
|
try {
|
|
50
49
|
const gatewayPath = path.join(projectRoot, '.claude', 'ukit', 'index', 'unic-gateway.mjs');
|
|
@@ -118,14 +117,12 @@ const { pathToFileURL } = require('url');
|
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
// `cases` reflects what the PROMPT mentioned
|
|
122
|
-
//
|
|
123
|
-
// — still worth a hint, but the gate is not armed and must not be claimed to be.
|
|
120
|
+
// `cases` reflects what the PROMPT mentioned. Named paths that do not resolve from
|
|
121
|
+
// the project root still get the unresolved-path note below.
|
|
124
122
|
const cases = [];
|
|
125
123
|
if (markedPasted > 0) cases.push('pasted image');
|
|
126
124
|
if (localMatches.length > 0) cases.push('local file path');
|
|
127
125
|
if (urlMatches.length > 0) cases.push('image URL');
|
|
128
|
-
const armed = markedPasted + markedPath + markedUrl;
|
|
129
126
|
|
|
130
127
|
if (cases.length === 0) {
|
|
131
128
|
process.exit(0);
|
|
@@ -135,8 +132,7 @@ const { pathToFileURL } = require('url');
|
|
|
135
132
|
// Only unicMode true or null reach here; off-gateway already exited. Both remaining
|
|
136
133
|
// cases get the same strict `unic-vision` remedy, so there is no fallback-model branch
|
|
137
134
|
// to advertise. There must never be one: the analyst self-reports the model it really
|
|
138
|
-
// ran on, so telling it to claim some other ID is asking it to falsify the receipt
|
|
139
|
-
// which the gate is entitled to reject, and which defeats the point of having a gate.
|
|
135
|
+
// ran on, so telling it to claim some other ID is asking it to falsify the receipt.
|
|
140
136
|
let unicNote = '';
|
|
141
137
|
if (unicMode === true) {
|
|
142
138
|
try {
|
|
@@ -151,9 +147,7 @@ const { pathToFileURL } = require('url');
|
|
|
151
147
|
}
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
const reasonLine =
|
|
155
|
-
? 'unic-code / unic-smart cannot read images on this gateway, and Edit/Write is now GATED\nuntil a vision analysis exists. Do this before anything else:'
|
|
156
|
-
: 'unic-code / unic-smart cannot read images on this gateway. Do this before anything else:';
|
|
150
|
+
const reasonLine = 'unic-code / unic-smart cannot read images on this gateway — never guess at\nimage contents. Do this before relying on them:';
|
|
157
151
|
const agentLine = ' 2. Agent(subagent_type: "ukit-vision-analyst") [model: unic-vision]';
|
|
158
152
|
|
|
159
153
|
const lines = [
|
|
@@ -84,11 +84,6 @@
|
|
|
84
84
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\"",
|
|
85
85
|
"timeout": 8
|
|
86
86
|
},
|
|
87
|
-
{
|
|
88
|
-
"type": "command",
|
|
89
|
-
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/vision-gate.sh\"",
|
|
90
|
-
"timeout": 8
|
|
91
|
-
},
|
|
92
87
|
{
|
|
93
88
|
"type": "command",
|
|
94
89
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\"",
|
|
@@ -24,11 +24,11 @@ const EXCLUDED_DIR_NAMES = new Set([
|
|
|
24
24
|
'.venv',
|
|
25
25
|
'venv',
|
|
26
26
|
]);
|
|
27
|
-
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue']);
|
|
27
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.sql']);
|
|
28
28
|
const STYLE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
29
|
-
const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.yaml', '.yml', '.md', '.sh']);
|
|
29
|
+
const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.sql', '.json', '.yaml', '.yml', '.md', '.sh']);
|
|
30
30
|
const DISCOVERED_EXTENSIONS = new Set([...TRACKED_EXTENSIONS, ...STYLE_EXTENSIONS]);
|
|
31
|
-
export const INDEX_SCHEMA_VERSION =
|
|
31
|
+
export const INDEX_SCHEMA_VERSION = 8;
|
|
32
32
|
export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
33
33
|
const INDEX_PARSE_BATCH_SIZE = 8;
|
|
34
34
|
const MAX_IMPORTER_HOPS = 2;
|
|
@@ -1810,26 +1810,38 @@ export function extractSymbols(filePath, content) {
|
|
|
1810
1810
|
}
|
|
1811
1811
|
};
|
|
1812
1812
|
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1813
|
+
if (filePath.endsWith('.sql')) {
|
|
1814
|
+
// Capture the object name, skipping an optional schema qualifier (qas.fn_rpt_billing → fn_rpt_billing).
|
|
1815
|
+
const sqlIdent = '(?:(?:["`]?[A-Za-z_][A-Za-z0-9_$]*["`]?)\\s*\\.\\s*)?["`]?([A-Za-z_][A-Za-z0-9_$]*)["`]?';
|
|
1816
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'table');
|
|
1817
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?VIEW\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'view');
|
|
1818
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'function');
|
|
1819
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?PROCEDURE\\s+${sqlIdent}`, 'gi'), 'procedure');
|
|
1820
|
+
addMatches(new RegExp(`CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'index');
|
|
1821
|
+
addMatches(new RegExp(`CREATE\\s+(?:TYPE|DOMAIN|SEQUENCE|MATERIALIZED\\s+VIEW)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'type');
|
|
1822
|
+
addMatches(new RegExp(`ALTER\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'table');
|
|
1823
|
+
} else {
|
|
1824
|
+
addMatches(/export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'function');
|
|
1825
|
+
addMatches(/export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'class');
|
|
1826
|
+
addMatches(/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, 'callable');
|
|
1827
|
+
addMatches(/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/g, 'callable');
|
|
1828
|
+
addMatches(/export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'default-function');
|
|
1829
|
+
addMatches(/export\s+default\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'default-class');
|
|
1830
|
+
addMatches(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:;|\n|$)/g, 'default-reference');
|
|
1831
|
+
addMatches(/defineOptions\(\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
|
|
1832
|
+
addMatches(/defineComponent\(\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
|
|
1833
|
+
addMatches(/export\s+default\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
|
|
1834
|
+
|
|
1835
|
+
if (/\bexport\s+default\b/.test(content)) {
|
|
1836
|
+
addSymbol('default', 'default-export');
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
for (const match of content.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
1840
|
+
for (const part of (match[1] ?? '').split(',').map((x) => x.trim()).filter(Boolean)) {
|
|
1841
|
+
const parsed = part.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/);
|
|
1842
|
+
if (!parsed) continue;
|
|
1843
|
+
addSymbol(parsed[2] ?? parsed[1], 'named-export', match.index);
|
|
1844
|
+
}
|
|
1833
1845
|
}
|
|
1834
1846
|
}
|
|
1835
1847
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - .claude/ -> REAL COPY preserving file modes (never a symlink: a worktree
|
|
8
8
|
* edit must not write through into the live main-tree mirror).
|
|
9
9
|
* Mode preservation matters: every .claude/hooks/*.sh is 755 and
|
|
10
|
-
* tests/handoff/
|
|
10
|
+
* tests/handoff/cycle7/provision-worktree.test.mjs asserts the exec bit.
|
|
11
11
|
* - .ukit/storage/config.json -> copy
|
|
12
12
|
* - .cache/index/ -> copy
|
|
13
13
|
*
|
|
@@ -234,7 +234,7 @@ export const ROUTE_CATALOG = [
|
|
|
234
234
|
contextMode: 'standalone',
|
|
235
235
|
signals: [
|
|
236
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|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/i, score: 7 },
|
|
237
|
-
{ type: 'prompt', regex:
|
|
237
|
+
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(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|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/i, score: 7 },
|
|
238
238
|
],
|
|
239
239
|
},
|
|
240
240
|
{
|
|
@@ -1044,7 +1044,7 @@ function hasHandoffSignal(lower, raw) {
|
|
|
1044
1044
|
|| /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
|
|
1045
1045
|
|| /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
|
|
1046
1046
|
|| /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
|
|
1047
|
-
||
|
|
1047
|
+
|| /(?<![A-Za-z0-9_])(gom|chia|tach|tách|lên|len|xóa|clear|dọn).{0,80}(?<![A-Za-z0-9_])(idea|ý tưởng|y tuong|task|công việc|cong viec|handoff)(?![A-Za-z0-9_])/.test(raw)
|
|
1048
1048
|
|| /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
@@ -1056,7 +1056,7 @@ function hasHandoffClearSignal(lower, raw) {
|
|
|
1056
1056
|
function hasOpenEndedStatusSignal(lower, raw) {
|
|
1057
1057
|
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|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/.test(lower)
|
|
1058
1058
|
|| /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project|task tiep theo|viec tiep theo trong tasks)\b/.test(lower)
|
|
1059
|
-
||
|
|
1059
|
+
|| /(?<![A-Za-z0-9_])(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|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/.test(raw);
|
|
1060
1060
|
}
|
|
1061
1061
|
|
|
1062
1062
|
function hasTaskQueueNextSignal(lower, raw) {
|
|
@@ -1070,7 +1070,7 @@ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false }
|
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
|
|
1073
|
-
||
|
|
1073
|
+
|| /(?<![A-Za-z0-9_])(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)(?![A-Za-z0-9_])/.test(raw);
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
1076
|
function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
|
|
@@ -1093,11 +1093,11 @@ function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = fal
|
|
|
1093
1093
|
|
|
1094
1094
|
return mentionsTargetDoc(lower, targetName)
|
|
1095
1095
|
|| /\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)
|
|
1096
|
-
||
|
|
1096
|
+
|| /(?<![A-Za-z0-9_])(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)(?![A-Za-z0-9_])/.test(raw);
|
|
1097
1097
|
}
|
|
1098
1098
|
|
|
1099
1099
|
return /\b(template|wording|edit|copy|grammar|format|structure|heading|section|docs?)\b/.test(lower)
|
|
1100
|
-
||
|
|
1100
|
+
|| /(?<![A-Za-z0-9_])(mẫu|câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu)(?![A-Za-z0-9_])/.test(raw);
|
|
1101
1101
|
}
|
|
1102
1102
|
|
|
1103
1103
|
function hasExplicitStatusUpdateSignal(lower, raw) {
|
|
@@ -1644,6 +1644,48 @@ function deriveExecutionScores({
|
|
|
1644
1644
|
};
|
|
1645
1645
|
}
|
|
1646
1646
|
|
|
1647
|
+
function isInformationalPrompt({
|
|
1648
|
+
promptText = '',
|
|
1649
|
+
commandText = '',
|
|
1650
|
+
scores = null,
|
|
1651
|
+
} = {}) {
|
|
1652
|
+
if (String(commandText || '').trim()) return false;
|
|
1653
|
+
const raw = String(promptText || '').toLowerCase().trim();
|
|
1654
|
+
if (!raw) return false;
|
|
1655
|
+
const questionSignal = raw.includes('?')
|
|
1656
|
+
|| /\b(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
1657
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu|nghĩa là|được không|không\b)/.test(raw);
|
|
1658
|
+
// '?' or a leading/Vietnamese interrogative — required to overrule error wording.
|
|
1659
|
+
const strongQuestion = raw.includes('?')
|
|
1660
|
+
|| /^(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
1661
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu)/.test(raw);
|
|
1662
|
+
if (scores) {
|
|
1663
|
+
if (
|
|
1664
|
+
scores.implementSignal
|
|
1665
|
+
|| scores.reviewSignal
|
|
1666
|
+
|| scores.debugSignal
|
|
1667
|
+
|| scores.impactSignal
|
|
1668
|
+
|| scores.smallFixSignal
|
|
1669
|
+
|| scores.directTransformSignal
|
|
1670
|
+
) {
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
if (scores.failureSignal && !strongQuestion) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
const implementWords = /(?<![A-Za-z0-9_])(implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|sửa|thêm|tạo|xóa|đổi|thay thế|cập nhật|viết|build|chạy|cài)(?![A-Za-z0-9_])/.test(raw);
|
|
1678
|
+
if (implementWords) return false;
|
|
1679
|
+
const investigationWords = /\b(why|debug|triage|root cause|investigate|tại sao)\b/.test(raw);
|
|
1680
|
+
if (investigationWords) return false;
|
|
1681
|
+
// \b never matches around 'lỗi' (diacritics are not \w), so test it as a plain
|
|
1682
|
+
// substring: an error report is informational only when phrased as a question.
|
|
1683
|
+
if (raw.includes('lỗi') && !strongQuestion) return false;
|
|
1684
|
+
const reviewWords = /\b(review|audit|verify)\b/.test(raw);
|
|
1685
|
+
if (reviewWords) return false;
|
|
1686
|
+
return questionSignal;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1647
1689
|
function deriveExecutionMode({
|
|
1648
1690
|
promptText = '',
|
|
1649
1691
|
commandText = '',
|
|
@@ -1666,6 +1708,10 @@ function deriveExecutionMode({
|
|
|
1666
1708
|
&& (scores.impactSignal || /\b(check all affected|map all affected|across all affected)\b/.test(raw));
|
|
1667
1709
|
const boundedLocalBuildCandidate = scores.buildSignal && scores.boundedEditSignal && scores.explicitTarget && !scores.sharedRisk;
|
|
1668
1710
|
|
|
1711
|
+
if (isInformationalPrompt({ promptText, commandText, scores })) {
|
|
1712
|
+
return 'informational';
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1669
1715
|
if ((intentMode === 'review-specific' || explicitReviewLead) && !scores.implementSignal) {
|
|
1670
1716
|
return 'review-release';
|
|
1671
1717
|
}
|
|
@@ -1956,7 +2002,7 @@ function buildApproachSelectorResult({
|
|
|
1956
2002
|
}
|
|
1957
2003
|
|
|
1958
2004
|
function buildCompletionState({ executionMode = null, verificationRecommendation = null } = {}) {
|
|
1959
|
-
if (!executionMode) {
|
|
2005
|
+
if (!executionMode || executionMode === 'informational') {
|
|
1960
2006
|
return null;
|
|
1961
2007
|
}
|
|
1962
2008
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Every probe here is scoped to what changes the CALLING
|
|
23
23
|
* runtime's OWN outbound endpoint — same rule that already governs probes 1-3. Cross-tool
|
|
24
24
|
* configs are NOT probed. This module is only ever invoked from Claude Code / omp hooks
|
|
25
|
-
* (vision-router.sh,
|
|
25
|
+
* (vision-router.sh, route-task.mjs, and the omp bridge) to gate what THIS
|
|
26
26
|
* session does — a Codex `config.toml` `base_url`, a Kilo `secrets.json` endpoint, or an
|
|
27
27
|
* `OPENAI_BASE_URL` env var describe a completely different tool's outbound endpoint and say
|
|
28
28
|
* nothing about where Claude Code or omp itself is sending requests. Treating them as evidence
|
|
@@ -481,9 +481,80 @@ export function resetCompactPressureState(rawState = {}, config = {}) {
|
|
|
481
481
|
recentOutputs: [],
|
|
482
482
|
cooldownUntil: 0,
|
|
483
483
|
latestPlan: null,
|
|
484
|
+
// Stamp the moment a compaction was accounted for, so the transcript sync (which may
|
|
485
|
+
// run slightly later than the boundary was written) does not double-fire.
|
|
486
|
+
lastCompactBoundaryAt: Date.now(),
|
|
484
487
|
}, config);
|
|
485
488
|
}
|
|
486
489
|
|
|
490
|
+
const BOUNDARY_TAIL_BYTES = 4 * 1024 * 1024;
|
|
491
|
+
|
|
492
|
+
// Find the newest compaction recorded in the session transcript, as epoch ms (0 if none).
|
|
493
|
+
// Every real compaction — manual /compact AND harness auto-compact — appends a
|
|
494
|
+
// `compact_boundary` entry to the transcript, while PreCompact hooks do not fire on every
|
|
495
|
+
// compaction path (SDK/VSCode auto-compact being the known gap). That makes the transcript
|
|
496
|
+
// the one record that cannot miss a compaction. Only the tail is read: boundaries are
|
|
497
|
+
// appended chronologically and only the newest matters.
|
|
498
|
+
export async function detectLatestCompactBoundaryAt(transcriptPath, { tailBytes = BOUNDARY_TAIL_BYTES } = {}) {
|
|
499
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) {
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const stat = await fs.stat(transcriptPath);
|
|
504
|
+
if (!stat.isFile() || stat.size === 0) {
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
const start = Math.max(0, stat.size - tailBytes);
|
|
508
|
+
const length = stat.size - start;
|
|
509
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
510
|
+
const handle = await fs.open(transcriptPath, 'r');
|
|
511
|
+
let text;
|
|
512
|
+
try {
|
|
513
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
514
|
+
text = buffer.toString('utf8', 0, bytesRead);
|
|
515
|
+
} finally {
|
|
516
|
+
await handle.close();
|
|
517
|
+
}
|
|
518
|
+
const lines = text.split('\n');
|
|
519
|
+
if (start > 0) lines.shift(); // a tail read can split the first line
|
|
520
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
521
|
+
const line = lines[i];
|
|
522
|
+
if (!line.includes('compact_boundary')) continue;
|
|
523
|
+
try {
|
|
524
|
+
const entry = JSON.parse(line);
|
|
525
|
+
if (entry?.type === 'system' && entry?.subtype === 'compact_boundary') {
|
|
526
|
+
const at = Date.parse(entry.timestamp ?? '');
|
|
527
|
+
if (Number.isFinite(at)) return at;
|
|
528
|
+
}
|
|
529
|
+
} catch { /* not a usable boundary line */ }
|
|
530
|
+
}
|
|
531
|
+
return 0;
|
|
532
|
+
} catch {
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Rebase the tracked counter whenever the transcript proves a compaction this state has
|
|
538
|
+
// not accounted for yet (lastCompactBoundaryAt stamps what has been seen, so each
|
|
539
|
+
// boundary fires the reset exactly once). Without this, sessionTokens keeps counting
|
|
540
|
+
// history that was already summarised away and the hard-cap gate bricks a healthy
|
|
541
|
+
// session. Callers must persist when reset=true, or the reset would re-fire (and
|
|
542
|
+
// zero the counter) on every call.
|
|
543
|
+
export async function syncCompactPressureStateWithTranscript(rawState = null, config = {}, transcriptPath = '') {
|
|
544
|
+
const boundaryAt = await detectLatestCompactBoundaryAt(transcriptPath);
|
|
545
|
+
const accounted = finiteNumber(rawState?.lastCompactBoundaryAt, 0);
|
|
546
|
+
if (!boundaryAt || boundaryAt <= accounted) {
|
|
547
|
+
return { state: rawState, reset: false };
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
state: {
|
|
551
|
+
...resetCompactPressureState(rawState ?? {}, config),
|
|
552
|
+
lastCompactBoundaryAt: boundaryAt,
|
|
553
|
+
},
|
|
554
|
+
reset: true,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
487
558
|
function buildReleaseThresholds(thresholds = buildCompactThresholds()) {
|
|
488
559
|
const softReleaseThreshold = Math.max(
|
|
489
560
|
Math.min(thresholds.softThreshold, Math.round(thresholds.softThreshold * SOFT_RELEASE_RATIO)),
|
|
@@ -648,6 +719,7 @@ export function buildCompactPressureState(rawState = null, config = {}) {
|
|
|
648
719
|
phase: resolveCompactPhase(estimatedTotalTokens, thresholds, rawState),
|
|
649
720
|
taskMode,
|
|
650
721
|
cooldownUntil: finiteNumber(rawState?.cooldownUntil, 0),
|
|
722
|
+
lastCompactBoundaryAt: finiteNumber(rawState?.lastCompactBoundaryAt, 0),
|
|
651
723
|
routingContext: rawState?.routingContext && typeof rawState.routingContext === 'object'
|
|
652
724
|
? rawState.routingContext
|
|
653
725
|
: {},
|
|
@@ -1058,6 +1130,22 @@ async function runCli() {
|
|
|
1058
1130
|
return;
|
|
1059
1131
|
}
|
|
1060
1132
|
|
|
1133
|
+
// Heal a stale counter BEFORE the phase is computed. Auto-compact paths that never
|
|
1134
|
+
// fire the PreCompact hook leave sessionTokens counting history that was already
|
|
1135
|
+
// summarised away — that phantom total is how a healthy session got hard-cap blocked.
|
|
1136
|
+
// The transcript's compact_boundary entries record every real compaction, manual or
|
|
1137
|
+
// automatic, so they are the correction signal here.
|
|
1138
|
+
try {
|
|
1139
|
+
const synced = await syncCompactPressureStateWithTranscript(
|
|
1140
|
+
await readCompactPressureState(projectRoot, config),
|
|
1141
|
+
config,
|
|
1142
|
+
payload.transcript_path,
|
|
1143
|
+
);
|
|
1144
|
+
if (synced.reset) {
|
|
1145
|
+
await writeCompactPressureState(projectRoot, synced.state, config);
|
|
1146
|
+
}
|
|
1147
|
+
} catch { /* advisory healing only — never block the prompt over it */ }
|
|
1148
|
+
|
|
1061
1149
|
const nextState = await updateCompactPressureFromPrompt(projectRoot, {
|
|
1062
1150
|
promptText,
|
|
1063
1151
|
routingContext: routeState?.routingContext ?? {},
|
|
@@ -1065,9 +1153,16 @@ async function runCli() {
|
|
|
1065
1153
|
routeSummary: routeState?.routeSummary ?? null,
|
|
1066
1154
|
}, config);
|
|
1067
1155
|
|
|
1068
|
-
if (nextState.phase === '
|
|
1156
|
+
if (nextState.phase === 'hard') {
|
|
1157
|
+
process.stdout.write(
|
|
1158
|
+
`[ukit-skill-router] Context pressure: hard (est ${nextState.estimatedTotalTokens} / soft ${nextState.softThreshold} / hard ${nextState.hardThreshold}). `
|
|
1159
|
+
+ 'FIRST THING IN YOUR REPLY, tell the user plainly — do not stay silent: "Context sắp đầy — hãy gõ /compact ngay bây giờ (run /compact now)". '
|
|
1160
|
+
+ 'Then hold heavy work: no new investigations, subagents, or large edits until the user has compacted.\n',
|
|
1161
|
+
);
|
|
1162
|
+
} else if (nextState.phase === 'soft') {
|
|
1069
1163
|
process.stdout.write(
|
|
1070
|
-
`[ukit-skill-router] Context pressure:
|
|
1164
|
+
`[ukit-skill-router] Context pressure: soft (est ${nextState.estimatedTotalTokens} / soft ${nextState.softThreshold} / hard ${nextState.hardThreshold}). `
|
|
1165
|
+
+ 'Finish the current step, then tell the user to run /compact at the next natural pause instead of continuing indefinitely.\n',
|
|
1071
1166
|
);
|
|
1072
1167
|
}
|
|
1073
1168
|
}
|
package/templates/.omp/README.md
CHANGED
|
@@ -62,14 +62,14 @@ One source of truth, two runtimes — fix a hook once and both runtimes get the
|
|
|
62
62
|
Two things worth knowing:
|
|
63
63
|
|
|
64
64
|
**Tool names are mapped explicitly, never guessed.** omp's write surface is `edit`, `write` *and*
|
|
65
|
-
`ast_edit`; all three map to the `Edit` group, or `ast_edit` would slip past `protect-files.sh
|
|
66
|
-
`
|
|
65
|
+
`ast_edit`; all three map to the `Edit` group, or `ast_edit` would slip past `protect-files.sh`.
|
|
66
|
+
`eval` maps to `Bash` so it still hits `block-dangerous.sh`. Anything not in the
|
|
67
67
|
table maps to nothing and runs zero scripts — it never falls back to `Bash` or `Edit`.
|
|
68
68
|
|
|
69
69
|
**Failure direction is per-script, transcribed from each script's own header — not a blanket rule.**
|
|
70
70
|
|
|
71
71
|
- *Fail closed* (a crash or non-zero exit blocks the action): `protect-files.sh`,
|
|
72
|
-
`stale-spec-guard.sh`, `handoff-model-guard.sh`, `
|
|
72
|
+
`stale-spec-guard.sh`, `handoff-model-guard.sh`, `context-hardcap-gate.sh`,
|
|
73
73
|
`block-dangerous.sh`, `verification-guard.sh`. These are gates; a broken gate must not open.
|
|
74
74
|
- *Fail open* (a crash logs a warning and the action proceeds): the advisory scripts —
|
|
75
75
|
routing, backups, output compression, context reinjection, pressure reset, handoff resume.
|
|
@@ -27,7 +27,6 @@ export const HOOK_EVENT_MAP = {
|
|
|
27
27
|
'pre-edit-backup.sh',
|
|
28
28
|
'skill-router.sh',
|
|
29
29
|
'handoff-model-guard.sh',
|
|
30
|
-
'vision-gate.sh',
|
|
31
30
|
'context-hardcap-gate.sh',
|
|
32
31
|
],
|
|
33
32
|
Bash: [
|
|
@@ -83,7 +82,6 @@ export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
|
83
82
|
'protect-files.sh',
|
|
84
83
|
'stale-spec-guard.sh',
|
|
85
84
|
'handoff-model-guard.sh',
|
|
86
|
-
'vision-gate.sh',
|
|
87
85
|
'context-hardcap-gate.sh',
|
|
88
86
|
'block-dangerous.sh',
|
|
89
87
|
]);
|
package/templates/CLAUDE.md
CHANGED
|
@@ -209,7 +209,7 @@ This is internal orchestration — end users do not need to know about tiers, th
|
|
|
209
209
|
`unic-vision` is a **capability lane**, not a fourth cost tier — it is orthogonal to lite/code/smart above and never appears as a row in the tier table. `unic-code` and `unic-smart` cannot read images on the UNIC gateway; they must never guess at image contents.
|
|
210
210
|
|
|
211
211
|
- **Gateway detection**: UNIC routing for a Claude Code session is decided ONLY by what changes Claude Code's own outbound endpoint — `ANTHROPIC_BASE_URL` (env var, or the `env.ANTHROPIC_BASE_URL` key in project/home `.claude/settings.json`) containing `unicjsc.com`. Other tools' configs — Codex `config.toml`, Kilo `secrets.json`, or an `OPENAI_BASE_URL` env var — describe a different tool's endpoint entirely and never decide this session's routing.
|
|
212
|
-
- **
|
|
212
|
+
- **Advisory routing (no hard block)**: when an image reaches the prompt, the vision router reminds the session to have `ukit-vision-analyst` analyse it before relying on its contents. Edits are **never blocked** — correctness relies on the model routing images to the analyst instead of guessing.
|
|
213
213
|
- This is internal orchestration — end users never invoke a vision command directly; `ukit install` plus natural language remains the whole surface. No new commands.
|
|
214
214
|
|
|
215
215
|
## Skills
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"handoff": {
|
|
196
196
|
"enabled": true,
|
|
197
197
|
"crossTool": true,
|
|
198
|
-
"maxParallelAgents":
|
|
198
|
+
"maxParallelAgents": 2,
|
|
199
199
|
"autonomy": {
|
|
200
200
|
"askWindow": "plan-only",
|
|
201
201
|
"planReviewRounds": 2,
|
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
"handoff": {
|
|
512
512
|
"enabled": "Bật Quality Gate cho handoff: plan có Test Plan, executor test-first, reviewer model khác. Tắt = quay về flow cũ (dễ lọt lỗi vặt).",
|
|
513
513
|
"crossTool": "true nghĩa là handoff truyền qua file (PLAN/INDEX/tasks) chứ không qua in-process subagent — cho phép plan ở Claude Code, execute ở Kilo Code, review ở Claude Code khác model.",
|
|
514
|
-
"maxParallelAgents": "Số agent chạy song song TỐI ĐA trong một wave (mặc định
|
|
514
|
+
"maxParallelAgents": "Số agent chạy song song TỐI ĐA trong một wave (mặc định 2 để giữ session chính nhẹ và hạn chế rủi ro compact/worktree rác; nếu máy khỏe và task độc lập nhiều thì có thể nâng dần 3-5, tối đa ~10-15). Một wave có nhiều task hơn số này sẽ được chia thành nhiều batch chạy lần lượt — áp dụng cho cả Phase 3 Implement và Phase 4 Review. Lý do giới hạn vẫn còn: mỗi agent nền có context window riêng, và report của agent khi xong sẽ được inject ngược vào session chính — chạy quá nhiều cùng lúc (vd 20+) vẫn có thể làm session chính vượt context window và bỏ lại worktree rác. Hạ xuống 1-2 nếu task nặng (verification output dài) hoặc thấy compact bị trigger liên tục.",
|
|
515
515
|
"plan": {
|
|
516
516
|
"requireTestPlan": "Bắt buộc PLAN.md §4 phải có Test Plan trước khi task chuyển ready.",
|
|
517
517
|
"minTestsHappyPath": "Tối thiểu test cho happy path.",
|