@ngockhoale/ukit 1.6.6 → 1.6.7
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,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.6.7 - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`unic-gateway.mjs` `detectUnicGateway()`** is now scoped to Claude Code's own outbound endpoint only (`process.env.ANTHROPIC_BASE_URL`, and the `env.ANTHROPIC_BASE_URL` key in project/home `.claude/settings.json`). It no longer probes Codex `config.toml`, Kilo `secrets.json`, or `OPENAI_BASE_URL` — those describe a different tool's endpoint and were producing permanent false positives (`unicMode: true` for a plain Claude Code session whenever some other tool on the machine happened to be pointed at the UNIC gateway), which would deadlock Edit/Write on any image-mentioning turn under the vision gate.
|
|
10
|
+
- **`vision-router.sh`** no longer always tells the caller to invoke `Agent(subagent_type: "ukit-vision-analyst") [model: unic-vision]`. `unic-vision` is only an invokable model when the UNIC gateway is actually active for this session; the remedy message now branches on `detectUnicGateway()`'s `unicMode`: gateway active keeps today's `unic-vision` text, gateway inactive with a readable `orchestration.modelTiers.vision.fallbackModel` (read from `.ukit/storage/config.json`, the same path `vision-gate.sh` uses) tells the caller to override the model to that fallback and to self-report it in the analyst's receipt, and any missing/broken detection falls back to the strict `unic-vision` text as before. The hook's fail-open contract (never blocks the prompt, always exit 0) is unchanged.
|
|
11
|
+
- **`handoff-model-guard.sh`** no longer re-blocks `git push` forever over a closed, human-approved model-tier exception (a documented `## Model-Tier Guard Override` section with `OVERRIDE_APPROVED_BY: human` now exempts that task from the push-time reviewer/executor tier scan).
|
|
12
|
+
|
|
5
13
|
## 1.6.6 - 2026-08-03
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/package.json
CHANGED
|
@@ -151,6 +151,12 @@ if (toolName === 'Bash') {
|
|
|
151
151
|
const text = fs.readFileSync(taskPath, 'utf8');
|
|
152
152
|
if (!text.includes('## Reviewer Verdict')) continue; // not yet reviewed, not this push's concern
|
|
153
153
|
|
|
154
|
+
// A documented, human-approved exception (e.g. an opus-tier executor escalation left no
|
|
155
|
+
// second opus identity free to review it) is a closed, one-time decision — it must not
|
|
156
|
+
// re-block every future unrelated push forever just because the task file still contains
|
|
157
|
+
// the exempted model string.
|
|
158
|
+
if (/## Model-Tier Guard Override/.test(text) && /OVERRIDE_APPROVED_BY:\s*human/i.test(text)) continue;
|
|
159
|
+
|
|
154
160
|
const executorModel = extractField(text, 'EXECUTOR_MODEL');
|
|
155
161
|
const reviewerModel = extractField(text, 'REVIEWER_MODEL');
|
|
156
162
|
|
|
@@ -105,8 +105,17 @@ const { pathToFileURL } = require('url');
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
108
|
+
// TASK-008: branch the remedy on whether unic-vision is actually invokable for THIS
|
|
109
|
+
// session. `remedyMode` is tri-state and defaults to the strict/fail-safe variant:
|
|
110
|
+
// 'gateway' — unicMode true: today's `unic-vision` text (unchanged).
|
|
111
|
+
// 'fallback' — unicMode false + a readable orchestration.modelTiers.vision.fallbackModel
|
|
112
|
+
// (same config path vision-gate.sh reads, so the advertised model and the
|
|
113
|
+
// accepted model never drift): tell the caller to override the model.
|
|
114
|
+
// 'strict' — gateway module missing/broken, or unicMode false but fallbackModel is
|
|
115
|
+
// unreadable: today's `unic-vision` text (fail-safe default).
|
|
116
|
+
// Never fatal — any error in this block falls back to 'strict' and no note.
|
|
117
|
+
let remedyMode = 'strict';
|
|
118
|
+
let fallbackModel = null;
|
|
110
119
|
let unicNote = '';
|
|
111
120
|
try {
|
|
112
121
|
const gatewayPath = path.join(projectRoot, '.claude', 'ukit', 'index', 'unic-gateway.mjs');
|
|
@@ -115,25 +124,51 @@ const { pathToFileURL } = require('url');
|
|
|
115
124
|
if (typeof mod.detectUnicGateway === 'function') {
|
|
116
125
|
const result = mod.detectUnicGateway({ rootDir: projectRoot });
|
|
117
126
|
if (result?.unicMode) {
|
|
127
|
+
remedyMode = 'gateway';
|
|
118
128
|
unicNote = ` (UNIC gateway active — ${result.visionModel} routes through it.)`;
|
|
129
|
+
} else {
|
|
130
|
+
const configPath = path.join(projectRoot, '.ukit', 'storage', 'config.json');
|
|
131
|
+
const configRaw = fs.readFileSync(configPath, 'utf8');
|
|
132
|
+
const config = JSON.parse(configRaw);
|
|
133
|
+
const candidate = config?.orchestration?.modelTiers?.vision?.fallbackModel;
|
|
134
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
135
|
+
fallbackModel = candidate.trim();
|
|
136
|
+
remedyMode = 'fallback';
|
|
137
|
+
}
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
} catch {
|
|
142
|
+
remedyMode = 'strict';
|
|
143
|
+
fallbackModel = null;
|
|
123
144
|
unicNote = '';
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
const isFallback = remedyMode === 'fallback';
|
|
148
|
+
const reasonLine = isFallback
|
|
149
|
+
? (armed > 0
|
|
150
|
+
? 'Edit/Write is now GATED until a vision analysis exists. Do this before anything else:'
|
|
151
|
+
: 'Do this before anything else:')
|
|
152
|
+
: (armed > 0
|
|
153
|
+
? '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:'
|
|
154
|
+
: 'unic-code / unic-smart cannot read images on this gateway. Do this before anything else:');
|
|
155
|
+
const agentLine = isFallback
|
|
156
|
+
? ` 2. Agent(subagent_type: "ukit-vision-analyst", model: "${fallbackModel}")`
|
|
157
|
+
: ' 2. Agent(subagent_type: "ukit-vision-analyst") [model: unic-vision]';
|
|
158
|
+
|
|
126
159
|
const lines = [
|
|
127
160
|
`UKIT VISION ROUTE — image input detected (${cases.join(', ')}).`,
|
|
128
|
-
|
|
129
|
-
? '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:'
|
|
130
|
-
: 'unic-code / unic-smart cannot read images on this gateway. Do this before anything else:',
|
|
161
|
+
reasonLine,
|
|
131
162
|
' 1. node .claude/ukit/index/extract-image.mjs --json',
|
|
132
|
-
|
|
163
|
+
agentLine,
|
|
133
164
|
' Pass the ABSOLUTE paths from images[].path as text — a subagent does NOT',
|
|
134
165
|
' inherit image blocks; it can only Read files.',
|
|
135
|
-
' 3. Continue the real task using the returned description.',
|
|
136
166
|
];
|
|
167
|
+
if (isFallback) {
|
|
168
|
+
lines.push(` The analyst must self-report "model": "${fallbackModel}" in its receipt —`);
|
|
169
|
+
lines.push(' otherwise the gate will reject it.');
|
|
170
|
+
}
|
|
171
|
+
lines.push(' 3. Continue the real task using the returned description.');
|
|
137
172
|
if (markedUrl > 0) {
|
|
138
173
|
lines.push(' Image URL detected: ukit-vision-analyst downloads it with Bash into');
|
|
139
174
|
lines.push(' .ukit/storage/cache/vision/, then Reads the downloaded file.');
|
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* unic-gateway.mjs (TASK-003)
|
|
3
|
+
* unic-gateway.mjs (TASK-003, narrowed by TASK-007)
|
|
4
4
|
*
|
|
5
|
-
* Detects whether
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* is available in the Codex model catalog, and fall back safely when it is not.
|
|
5
|
+
* Detects whether THIS Claude Code session's own outbound endpoint is pointed at the UNIC gateway
|
|
6
|
+
* — i.e. `ANTHROPIC_BASE_URL` contains "unicjsc.com" — so the vision lane can pin `unic-vision`
|
|
7
|
+
* when it is available in the Codex model catalog, and fall back safely when it is not.
|
|
9
8
|
*
|
|
10
9
|
* Self-contained: reads only env vars and local files. Does NOT read `.ukit/storage/config.json`,
|
|
11
10
|
* so it has no dependency on TASK-001 and stays usable in Wave 1.
|
|
12
11
|
*
|
|
13
12
|
* Probe order (first hit sets `source`; every hit is recorded in `sources`):
|
|
14
|
-
* 1. process.env.ANTHROPIC_BASE_URL
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
17
|
-
* 4. ~/.claude/settings.json -> env -> 'claude-settings'
|
|
18
|
-
* 5. project .codex/config.toml -> 'codex'
|
|
19
|
-
* 6. ~/.codex/config.toml -> 'codex'
|
|
20
|
-
* 7. ~/.kilocode/secrets.json (presence only) -> 'kilo'
|
|
13
|
+
* 1. process.env.ANTHROPIC_BASE_URL -> 'env'
|
|
14
|
+
* 2. project .claude/settings.json -> env.ANTHROPIC_BASE_URL -> 'claude-settings'
|
|
15
|
+
* 3. ~/.claude/settings.json -> env.ANTHROPIC_BASE_URL -> 'claude-settings'
|
|
21
16
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `{"yaml":"^2.8.2"}` only, and adding a parser for one `base_url` line fails Simplicity First.
|
|
30
|
-
* Codex config.toml is scanned with a narrow anchored regex that looks for a `base_url = "..."`
|
|
31
|
-
* assignment containing "unicjsc.com" on a single line. It does not attempt real TOML parsing, so
|
|
32
|
-
* exotic quoting/escaping (multi-line strings, unusual escapes) will not match. That degrades to
|
|
33
|
-
* "no hit" (`unicMode: false`), which fails safe rather than throwing or false-negatively passing.
|
|
17
|
+
* Cross-tool configs are NOT probed. This module is only ever invoked from Claude Code hooks
|
|
18
|
+
* (vision-router.sh, vision-gate.sh, route-task.mjs) to gate what THIS session does — a Codex
|
|
19
|
+
* `config.toml` `base_url`, a Kilo `secrets.json` endpoint, or an `OPENAI_BASE_URL` env var
|
|
20
|
+
* describe a completely different tool's outbound endpoint and say nothing about where Claude
|
|
21
|
+
* Code itself is sending requests. Treating them as evidence produced permanent false positives
|
|
22
|
+
* (any machine with Codex/Kilo pointed at the UNIC gateway would report `unicMode: true` for
|
|
23
|
+
* every plain Claude Code session, deadlocking Edit/Write on any image-mentioning turn).
|
|
34
24
|
*
|
|
35
25
|
* Contract: this module must NEVER throw and must NEVER exit non-zero, in any case (missing files,
|
|
36
|
-
* malformed JSON
|
|
26
|
+
* malformed JSON, unreadable paths, etc. all degrade to a clean "no hit").
|
|
37
27
|
*
|
|
38
28
|
* Test overrides: pass { rootDir, homeDir } directly to detectUnicGateway(), or set the env vars
|
|
39
29
|
* UKIT_TEST_ROOT / UKIT_TEST_HOME (used only when explicit options are not supplied) so tests never
|
|
@@ -48,10 +38,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
48
38
|
const UNIC_TOKEN = 'unicjsc.com';
|
|
49
39
|
const VISION_MODEL_ALIAS = 'unic-vision';
|
|
50
40
|
|
|
51
|
-
// Narrow, anchored: a `base_url` key assignment whose quoted value contains the UNIC token.
|
|
52
|
-
// See file header "TOML" note for why this is a regex and not a real parser.
|
|
53
|
-
const CODEX_BASE_URL_UNIC_RE = /\bbase_url\s*=\s*(["'])(?:(?!\1)[^\r\n])*unicjsc\.com(?:(?!\1)[^\r\n])*\1/i;
|
|
54
|
-
|
|
55
41
|
function safeReadFile(filePath) {
|
|
56
42
|
try {
|
|
57
43
|
return fs.readFileSync(filePath, 'utf8');
|
|
@@ -87,35 +73,15 @@ function probeEnvVar(name) {
|
|
|
87
73
|
return typeof value === 'string' && value.includes(UNIC_TOKEN);
|
|
88
74
|
}
|
|
89
75
|
|
|
76
|
+
// Key-scoped: only the ANTHROPIC_BASE_URL entry of the env block decides Claude Code's own
|
|
77
|
+
// gateway state. A different key (e.g. OPENAI_BASE_URL) parked in the same env block describes
|
|
78
|
+
// some other tool's endpoint and must NOT trip this probe.
|
|
90
79
|
function probeClaudeSettings(filePath) {
|
|
91
80
|
const json = safeReadJson(filePath);
|
|
92
81
|
const envBlock = json && typeof json === 'object' ? json.env : null;
|
|
93
82
|
if (!envBlock || typeof envBlock !== 'object') return false;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function probeCodexConfig(filePath) {
|
|
100
|
-
const raw = safeReadFile(filePath);
|
|
101
|
-
if (raw == null) return false;
|
|
102
|
-
return CODEX_BASE_URL_UNIC_RE.test(raw);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Presence-only check against a secrets file. Reads the file once, tests for the substring, and
|
|
107
|
-
* returns a boolean. The raw content is never retained, logged, or returned — it falls out of
|
|
108
|
-
* scope as soon as this function returns. A read failure (missing file, permission error, etc.)
|
|
109
|
-
* is swallowed and treated as "no hit"; the error itself is never surfaced (it could, in theory,
|
|
110
|
-
* embed a path — this function does not let it propagate at all).
|
|
111
|
-
*/
|
|
112
|
-
function probeKiloSecretsPresence(filePath) {
|
|
113
|
-
try {
|
|
114
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
115
|
-
return raw.includes(UNIC_TOKEN);
|
|
116
|
-
} catch {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
83
|
+
const value = envBlock.ANTHROPIC_BASE_URL;
|
|
84
|
+
return typeof value === 'string' && value.includes(UNIC_TOKEN);
|
|
119
85
|
}
|
|
120
86
|
|
|
121
87
|
/**
|
|
@@ -141,7 +107,7 @@ function checkCodexVisionAlias(catalogPath) {
|
|
|
141
107
|
}
|
|
142
108
|
|
|
143
109
|
/**
|
|
144
|
-
* Runs all
|
|
110
|
+
* Runs all three probes in the documented order and returns the output contract object. Never
|
|
145
111
|
* throws — any unexpected error in an individual probe is contained so the overall result still
|
|
146
112
|
* reflects whatever was determined so far.
|
|
147
113
|
*
|
|
@@ -158,12 +124,8 @@ export function detectUnicGateway(options = {}) {
|
|
|
158
124
|
|
|
159
125
|
const probes = [
|
|
160
126
|
() => recordHit(probeEnvVar('ANTHROPIC_BASE_URL'), 'env'),
|
|
161
|
-
() => recordHit(probeEnvVar('OPENAI_BASE_URL'), 'env'),
|
|
162
127
|
() => recordHit(probeClaudeSettings(path.join(rootDir, '.claude', 'settings.json')), 'claude-settings'),
|
|
163
128
|
() => recordHit(probeClaudeSettings(path.join(homeDir, '.claude', 'settings.json')), 'claude-settings'),
|
|
164
|
-
() => recordHit(probeCodexConfig(path.join(rootDir, '.codex', 'config.toml')), 'codex'),
|
|
165
|
-
() => recordHit(probeCodexConfig(path.join(homeDir, '.codex', 'config.toml')), 'codex'),
|
|
166
|
-
() => recordHit(probeKiloSecretsPresence(path.join(homeDir, '.kilocode', 'secrets.json')), 'kilo'),
|
|
167
129
|
];
|
|
168
130
|
for (const probe of probes) {
|
|
169
131
|
try {
|
package/templates/CLAUDE.md
CHANGED
|
@@ -166,7 +166,7 @@ This is internal orchestration — end users do not need to know about tiers, th
|
|
|
166
166
|
|
|
167
167
|
`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.
|
|
168
168
|
|
|
169
|
-
- **Gateway detection**:
|
|
169
|
+
- **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.
|
|
170
170
|
- **Enforcement**: every image (pasted, local file path, or URL) must be analysed by the `ukit-vision-analyst` agent running on `unic-vision` 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.
|
|
171
171
|
- This is internal orchestration — end users never invoke a vision command directly; `ukit install` plus natural language remains the whole surface. No new commands.
|
|
172
172
|
|