@ngockhoale/ukit 2.2.14 → 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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.2.16 - 2026-09-06
6
+
7
+ The vision gate is gone — image work is advisory-routed, never blocked. Since TASK-008, a
8
+ pasted image on the UNIC gateway armed `vision-gate.sh`, which refused every Edit/Write until
9
+ `ukit-vision-analyst` had produced receipts for ALL pending images. In practice this
10
+ repeatedly stranded sessions (receipts that never landed, one stale marker blocking
11
+ unrelated writes), so the maintainer retired the hard block: `vision-gate.sh` and its
12
+ manifest entry `hook-vision-gate` are removed from both the live tree and templates, the omp
13
+ bridge and `hook-chain-runner.mjs` no longer list it, and `vision-router.sh` now says "never
14
+ guess at image contents" instead of claiming edits are GATED. Routing to
15
+ `ukit-vision-analyst` still happens — on this gateway the main tier genuinely cannot read
16
+ images — but nothing blocks while it runs. Supersedes the 2.2.15 escape hatch, which patched
17
+ a message on a gate that no longer exists. Verified: vision-hook, route-vision,
18
+ vision-agent, install-wiring, provision-worktree, and ompHookBridge (52/52) all green.
19
+ (An interim 2026-09-05 patch that added an escape hatch to the block message is included
20
+ here and immediately superseded — the gate itself no longer exists.)
21
+
5
22
  ## 2.2.14 - 2026-09-05
6
23
 
7
24
  The context hard-cap gate no longer bricks healthy sessions with a phantom token count. The
@@ -1131,19 +1131,6 @@ items:
1131
1131
  packs:
1132
1132
  - core
1133
1133
 
1134
- - id: hook-vision-gate
1135
- type: hook
1136
- sourceTemplate: .claude/hooks/vision-gate.sh
1137
- targetPath: .claude/hooks/vision-gate.sh
1138
- requires:
1139
- - ukit-index-extract-image-script
1140
- - ukit-index-unic-gateway-script
1141
- mergeStrategy: overwrite_with_backup
1142
- variables: []
1143
- enabledByDefault: true
1144
- packs:
1145
- - core
1146
-
1147
1134
  - id: hook-context-hardcap-gate
1148
1135
  type: hook
1149
1136
  sourceTemplate: .claude/hooks/context-hardcap-gate.sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.2.14",
3
+ "version": "2.2.16",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -177,8 +177,8 @@ function readRunCursor() {
177
177
  process.stderr.write(`${lines.join('\n')}\n`);
178
178
  process.exit(2);
179
179
  })().catch((err) => {
180
- // Unlike vision-gate.sh, a logic error here fails OPEN: this is a backstop on top of
181
- // advisory nudges, not a correctness gate — a broken gate must not brick every session.
180
+ // A logic error here fails OPEN: this is a backstop on top of advisory nudges,
181
+ // not a correctness gate — a broken gate must not brick every session.
182
182
  process.stderr.write(`context-hardcap-gate: internal error, failing open: ${err?.message ?? err}\n`);
183
183
  process.exit(0);
184
184
  });
@@ -1,11 +1,10 @@
1
1
  #!/bin/bash
2
- # UserPromptSubmit hook: detect image input (pasted / local path / URL) and arm the
3
- # TASK-008 write gate by writing pending-<sha>.json markers via extract-image.mjs.
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. This is the deliberate opposite of the TASK-008 gate,
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, same contract as vision-gate.sh: true | false | null(unknown).
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; `armed` reflects what actually got a
122
- // pending marker. They differ when a named path does not resolve from the project root
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 = armed > 0
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\"",
@@ -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/cycle4/vision-gate.test.mjs asserts the exec bit.
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
  *
@@ -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, vision-gate.sh, route-task.mjs, and the omp bridge) to gate what THIS
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
@@ -8,7 +8,6 @@ const FAIL_CLOSED_SCRIPTS = new Set([
8
8
  'protect-files.sh',
9
9
  'stale-spec-guard.sh',
10
10
  'handoff-model-guard.sh',
11
- 'vision-gate.sh',
12
11
  'context-hardcap-gate.sh',
13
12
  'block-dangerous.sh',
14
13
  ]);
@@ -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` and
66
- `vision-gate.sh`. `eval` maps to `Bash` so it still hits `block-dangerous.sh`. Anything not in the
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`, `vision-gate.sh`, `context-hardcap-gate.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
  ]);
@@ -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
- - **Enforcement**: every image (pasted, local file path, or URL) must be analysed by the `ukit-vision-analyst` agent running on the vision lane before any related edit happens. `Edit`/`Write` are **hard-blocked** until an analysis receipt exists for every pending image; `Read`/`Grep`/`Glob`/`Bash` stay unblocked so the analyst itself can see the image and write its receipt.
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
@@ -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 $?