@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,230 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# PreToolUse hook: hard-enforce the TASK-008 vision-analysis gate.
|
|
3
|
-
#
|
|
4
|
-
# Blocks Edit/Write whenever a pending image marker (written by vision-router.sh via
|
|
5
|
-
# extract-image.mjs --mark-pending) has no matching analyzed-<sha>.json receipt from a
|
|
6
|
-
# vision-capable model. Opposite fail direction from vision-router.sh: that hook fails
|
|
7
|
-
# OPEN (never wedge a prompt); this hook fails CLOSED (a broken gate must not silently
|
|
8
|
-
# permit blind edits). "Fails closed" applies only to this hook's own logic errors — if
|
|
9
|
-
# no pending marker exists at all, it exits 0 immediately, before any other logic. That
|
|
10
|
-
# is >99% of invocations and must stay free.
|
|
11
|
-
#
|
|
12
|
-
# Always hard-blocks (exit 2). No advisory/soft mode — matches handoff-model-guard.sh's
|
|
13
|
-
# posture: the user explicitly asked for strict, mechanical, non-bypassable enforcement.
|
|
14
|
-
# A wrong image analysis yields confidently wrong results, which is worse than no
|
|
15
|
-
# analysis at all.
|
|
16
|
-
#
|
|
17
|
-
# Matcher is Edit|Write ONLY. Never Read/Grep/Glob/Bash — ukit-vision-analyst itself
|
|
18
|
-
# needs Read to see the extracted image and Bash to write its receipt; gating those
|
|
19
|
-
# tools would deadlock the only lane that can clear the gate.
|
|
20
|
-
#
|
|
21
|
-
# Never recomputes a sha. extract-image.mjs (TASK-002) is the single owner of the hash;
|
|
22
|
-
# this hook only matches pending-<sha>.json to analyzed-<sha>.json by filename.
|
|
23
|
-
#
|
|
24
|
-
# Session-scoped and 24h-bounded: a stale marker from another session, or an abandoned
|
|
25
|
-
# session's marker older than 24h, must never permanently block writes.
|
|
26
|
-
|
|
27
|
-
INPUT=$(cat)
|
|
28
|
-
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
29
|
-
|
|
30
|
-
INPUT="$INPUT" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE'
|
|
31
|
-
const fs = require('fs');
|
|
32
|
-
const path = require('path');
|
|
33
|
-
const { pathToFileURL } = require('url');
|
|
34
|
-
|
|
35
|
-
const MARKER_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
|
|
36
|
-
|
|
37
|
-
const payload = (() => {
|
|
38
|
-
try {
|
|
39
|
-
const parsed = JSON.parse(process.env.INPUT || '');
|
|
40
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
41
|
-
} catch {
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
44
|
-
})();
|
|
45
|
-
|
|
46
|
-
const projectRoot = process.env.PROJECT_ROOT;
|
|
47
|
-
const toolName = payload?.tool_name || '';
|
|
48
|
-
|
|
49
|
-
// Gate only Edit|Write. Every other tool (Read, Grep, Glob, Bash, ...) is free — the
|
|
50
|
-
// vision analyst needs Read/Bash to clear this very gate. This check is independent of
|
|
51
|
-
// tool_input shape: the gate never inspects file_path/content, only tool_name.
|
|
52
|
-
if (toolName !== 'Edit' && toolName !== 'Write') {
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const sessionId = (typeof payload.session_id === 'string' && payload.session_id.trim())
|
|
57
|
-
? payload.session_id.trim()
|
|
58
|
-
: 'default';
|
|
59
|
-
|
|
60
|
-
const sessionDir = path.join(projectRoot, '.ukit', 'storage', 'cache', 'vision', sessionId);
|
|
61
|
-
|
|
62
|
-
let markerNames = [];
|
|
63
|
-
try {
|
|
64
|
-
markerNames = fs.readdirSync(sessionDir).filter((name) => /^pending-.+\.json$/.test(name));
|
|
65
|
-
} catch {
|
|
66
|
-
markerNames = [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// No pending marker at all: the overwhelmingly common path. Exit 0 immediately, before
|
|
70
|
-
// any config reads, gateway detection, or receipt checks.
|
|
71
|
-
if (markerNames.length === 0) {
|
|
72
|
-
process.exit(0);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function readJsonSafe(filePath) {
|
|
76
|
-
try {
|
|
77
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
78
|
-
const parsed = JSON.parse(raw);
|
|
79
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Tri-state on purpose: true | false | null(unknown). Returning false on a detection
|
|
86
|
-
// FAILURE would be read as "unicMode is off", which GRANTS the fallback-model exception
|
|
87
|
-
// below — i.e. deleting or breaking one gitignored file would make the gate accept a
|
|
88
|
-
// plain code-tier receipt. Unknown must deny the exception, not grant it.
|
|
89
|
-
async function detectUnicModeSafe() {
|
|
90
|
-
try {
|
|
91
|
-
const gatewayPath = path.join(projectRoot, '.claude', 'ukit', 'index', 'unic-gateway.mjs');
|
|
92
|
-
if (!fs.existsSync(gatewayPath)) return null;
|
|
93
|
-
const mod = await import(pathToFileURL(gatewayPath).href);
|
|
94
|
-
if (typeof mod.detectUnicGateway !== 'function') return null;
|
|
95
|
-
const result = mod.detectUnicGateway({ rootDir: projectRoot });
|
|
96
|
-
return !!result?.unicMode;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Anchored, not a substring match: "unic-vision-mini" and "not-unic-vision" are different
|
|
103
|
-
// models and must not inherit the real one's capability by sharing a substring.
|
|
104
|
-
// Only reached when unicMode is true or unknown — off-gateway sessions already exited
|
|
105
|
-
// above — so `unic-vision` is the only model that can legitimately clear the gate here.
|
|
106
|
-
function isVisionCapable(model) {
|
|
107
|
-
if (typeof model !== 'string' || !model.trim()) return false;
|
|
108
|
-
return /^unic-vision$/i.test(model.trim());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const VALID_STATUSES = new Set(['OK', 'NO_IMAGE', 'UNREADABLE']);
|
|
112
|
-
|
|
113
|
-
// A receipt whose self-reported model is not vision-capable (unic-code, unic-smart,
|
|
114
|
-
// sonnet, opus, ...) is treated as ABSENT and still blocks — accepting it would make the
|
|
115
|
-
// whole enforcement design theatre. A JSON.parse throw is caught per-receipt and turned
|
|
116
|
-
// into BLOCK, never an accidental skip-through.
|
|
117
|
-
// The gate keys its directory on the hook payload's session_id, but the analyst resolves
|
|
118
|
-
// its own sessionId by discovering the newest transcript. Those agree in the common case
|
|
119
|
-
// and diverge with concurrent sessions on one repo, which would leave the receipt in a
|
|
120
|
-
// sibling directory and hold the gate shut. The sha is content-addressed, so accepting a
|
|
121
|
-
// receipt from any session dir is safe: it still must exist and be vision-capable.
|
|
122
|
-
function findReceipt(sha) {
|
|
123
|
-
const own = path.join(sessionDir, `analyzed-${sha}.json`);
|
|
124
|
-
if (fs.existsSync(own)) return own;
|
|
125
|
-
const visionRoot = path.join(projectRoot, '.ukit', 'storage', 'cache', 'vision');
|
|
126
|
-
let entries = [];
|
|
127
|
-
try {
|
|
128
|
-
entries = fs.readdirSync(visionRoot, { withFileTypes: true });
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
if (!entry.isDirectory() || entry.name === sessionId) continue;
|
|
134
|
-
const candidate = path.join(visionRoot, entry.name, `analyzed-${sha}.json`);
|
|
135
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function checkReceipt(receiptPath) {
|
|
141
|
-
if (!receiptPath || !fs.existsSync(receiptPath)) {
|
|
142
|
-
return { ok: false, reason: 'no analysis receipt found' };
|
|
143
|
-
}
|
|
144
|
-
const receipt = readJsonSafe(receiptPath);
|
|
145
|
-
if (!receipt) {
|
|
146
|
-
return { ok: false, reason: 'receipt is not valid JSON' };
|
|
147
|
-
}
|
|
148
|
-
if (!isVisionCapable(receipt.model)) {
|
|
149
|
-
const modelLabel = typeof receipt.model === 'string' && receipt.model.trim() ? receipt.model.trim() : '(missing)';
|
|
150
|
-
return { ok: false, reason: `receipt model "${modelLabel}" is not vision-capable — treated as absent` };
|
|
151
|
-
}
|
|
152
|
-
const status = receipt.status;
|
|
153
|
-
if (!VALID_STATUSES.has(status)) {
|
|
154
|
-
return { ok: false, reason: `receipt status "${status}" is not a valid terminal outcome` };
|
|
155
|
-
}
|
|
156
|
-
return { ok: true };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
(async () => {
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
const unicMode = await detectUnicModeSafe();
|
|
162
|
-
|
|
163
|
-
// The gate exists because the UNIC gateway BLINDS the model: unic-code/unic-smart
|
|
164
|
-
// cannot see images there, so an unanalysed image means guessing. Off the gateway
|
|
165
|
-
// Claude reads images natively and there is nothing left to enforce.
|
|
166
|
-
//
|
|
167
|
-
// This early exit is what makes the gate satisfiable at all off-gateway. The previous
|
|
168
|
-
// design tried to cover this case by accepting a receipt whose model string equalled
|
|
169
|
-
// modelTiers.vision.fallbackModel — but the analyst's frontmatter pins `unic-vision`,
|
|
170
|
-
// so off-gateway it runs on whatever model is actually available and self-reports THAT.
|
|
171
|
-
// A hardcoded fallback ID could never match it, so no valid receipt was reachable and
|
|
172
|
-
// one pasted screenshot hard-blocked Edit/Write for the rest of the session.
|
|
173
|
-
//
|
|
174
|
-
// Only a POSITIVE false stands the gate down. null (detection failed) still enforces:
|
|
175
|
-
// breaking one gitignored file must not be a bypass.
|
|
176
|
-
if (unicMode === false) {
|
|
177
|
-
process.exit(0);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const unsatisfied = [];
|
|
181
|
-
for (const markerName of markerNames) {
|
|
182
|
-
const sha = markerName.replace(/^pending-/, '').replace(/\.json$/, '');
|
|
183
|
-
const markerPath = path.join(sessionDir, markerName);
|
|
184
|
-
|
|
185
|
-
let markerTs = null;
|
|
186
|
-
const marker = readJsonSafe(markerPath);
|
|
187
|
-
if (marker && typeof marker.ts === 'number' && Number.isFinite(marker.ts)) {
|
|
188
|
-
markerTs = marker.ts;
|
|
189
|
-
}
|
|
190
|
-
if (markerTs == null) {
|
|
191
|
-
try {
|
|
192
|
-
markerTs = fs.statSync(markerPath).mtimeMs;
|
|
193
|
-
} catch {
|
|
194
|
-
markerTs = now;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Abandoned-session bound: a marker older than 24h can never brick the repo.
|
|
198
|
-
if (now - markerTs > MARKER_MAX_AGE_MS) continue;
|
|
199
|
-
|
|
200
|
-
const receiptPath = findReceipt(sha);
|
|
201
|
-
const result = checkReceipt(receiptPath);
|
|
202
|
-
if (!result.ok) {
|
|
203
|
-
unsatisfied.push({ sha, reason: result.reason });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (unsatisfied.length === 0) {
|
|
208
|
-
process.exit(0);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const lines = [
|
|
213
|
-
'BLOCKED (vision gate): image(s) pending analysis — Edit/Write refused.',
|
|
214
|
-
...unsatisfied.map((u) => ` - sha ${u.sha}: ${u.reason}`),
|
|
215
|
-
'unic-code / unic-smart cannot read images on this gateway and must not guess at their',
|
|
216
|
-
'contents — a wrong image analysis yields confidently wrong results.',
|
|
217
|
-
'Remedy: delegate to Agent(subagent_type: "ukit-vision-analyst") to analyse the pending',
|
|
218
|
-
'image(s) first, then retry this edit.',
|
|
219
|
-
];
|
|
220
|
-
process.stderr.write(`${lines.join('\n')}\n`);
|
|
221
|
-
process.exit(2);
|
|
222
|
-
})().catch((err) => {
|
|
223
|
-
// A logic error inside this hook's own JS must still fail CLOSED — the opposite fail
|
|
224
|
-
// direction from vision-router.sh, which fails open unconditionally.
|
|
225
|
-
process.stderr.write(`BLOCKED (vision gate): internal error, failing closed: ${err?.message ?? err}\n`);
|
|
226
|
-
process.exit(2);
|
|
227
|
-
});
|
|
228
|
-
NODE
|
|
229
|
-
|
|
230
|
-
exit $?
|