@ngockhoale/ukit 3.0.4 → 3.0.6
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 +14 -0
- package/package.json +1 -1
- package/src/cli/commands/memory.js +3 -6
- package/src/core/memory/userMemory.js +15 -0
- package/src/core/runtimeConfig.js +6 -3
- package/src/decision/client.js +11 -3
- package/src/index/resolveContext.js +7 -4
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +18 -9
- package/template_project/.claude/ukit/index/lib/index-core.mjs +8 -5
- package/template_project/.claude/ukit/index/unic-decision.mjs +6 -2
- package/template_project/docs/UKIT_INTERNALS.md +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.0.6 - 2026-09-24
|
|
6
|
+
|
|
7
|
+
- Clarified the project owner's `unic-decision` contract in owner instructions, project memory, the technical spec, and shipped internals: a local Lava/JEV model in UNIC Provider, not an LLM; the OpenAI-compatible API is transport for convenient integration, not evidence of remote hosting or generative-model pricing.
|
|
8
|
+
- Documented the preferred bounded typed-decision usage and the existing stage-off rollout/fallback in package-side client/config comments and the installed adapter, without changing runtime behavior or enabling decision calls.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 3.0.5 - 2026-09-24
|
|
12
|
+
|
|
13
|
+
- **Review-fix hardening (cycle C53 fix round 1)** — defects found by the release reviewer after 3.0.4 shipped:
|
|
14
|
+
- **Memory**: `appendCompactHistory` read-modify-write now runs under the store lock — concurrent appends can no longer lose entries (unlocked RMW race).
|
|
15
|
+
- **Installed mirror**: restored the missing `index-core` declaration in the installed `.claude/ukit/` mirror so dev-mirror parity checks pass on hydrated workspaces.
|
|
16
|
+
- **Shipped hooks**: `sensitive-data-guard` no longer bypassed by wrapper commands (`sudo`/`env`/`bash -c`/`xargs`) or basename-obfuscated invocations — the guard now unwraps wrappers and matches on the real binary name.
|
|
17
|
+
- **Test scoping**: `payloadDocs` stale-prose sweep restricted to shipped surfaces (root docs, `template_project`, `src`, `bin`, `scripts`) — it no longer walks `docs/AI_HANDOFF/**` reports or gitignored runtime dirs; `mirrorPayload` parity now tolerates stale/runtime files under the gitignored installed mirror.
|
|
18
|
+
- **Installed-mirror resync**: stale `index/detectProjectContext.js`, query-index cache, lock dirs, and resolve-context cache regenerated so the installed mirror matches the template payload.
|
|
5
19
|
|
|
6
20
|
## 3.0.4 - 2026-09-24
|
|
7
21
|
|
package/package.json
CHANGED
|
@@ -18,9 +18,8 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
addUserRecord,
|
|
20
20
|
getUserRecord,
|
|
21
|
-
loadUserRecords,
|
|
22
21
|
queryUserRecords,
|
|
23
|
-
|
|
22
|
+
removeUserRecord,
|
|
24
23
|
updateUserRecord,
|
|
25
24
|
userMemoryStats,
|
|
26
25
|
} from '../../core/memory/userMemory.js';
|
|
@@ -297,12 +296,10 @@ async function runUserMemory(homeDir, args) {
|
|
|
297
296
|
if (!id) {
|
|
298
297
|
throw new Error('Missing record id. Usage: ukit memory --user forget <id>');
|
|
299
298
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (remaining.length === records.length) {
|
|
299
|
+
const removed = await removeUserRecord(id, { homeDir });
|
|
300
|
+
if (!removed) {
|
|
303
301
|
throw new Error(`User memory record not found: ${id}`);
|
|
304
302
|
}
|
|
305
|
-
await saveUserRecords(remaining, { homeDir });
|
|
306
303
|
console.log(`[UKit] Forgot user record ${id}.`);
|
|
307
304
|
return;
|
|
308
305
|
}
|
|
@@ -63,6 +63,21 @@ export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* removeUserRecord(id, {homeDir}={}) → record | null
|
|
68
|
+
* Deletes the record with `id` under the shared store lock — a concurrent
|
|
69
|
+
* add/update cannot be clobbered by the removal. Returns the removed record,
|
|
70
|
+
* or null when the id is absent.
|
|
71
|
+
*/
|
|
72
|
+
export async function removeUserRecord(id, { homeDir } = {}) {
|
|
73
|
+
return mutateRecordStore(userRecordsPath(homeDir), (records) => {
|
|
74
|
+
const index = records.findIndex((r) => r.id === id);
|
|
75
|
+
if (index === -1) return null;
|
|
76
|
+
const removed = records[index];
|
|
77
|
+
return { result: removed, records: records.filter((r) => r.id !== id) };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
/**
|
|
67
82
|
* getUserRecord(id, {homeDir}={}) → record | null
|
|
68
83
|
*/
|
|
@@ -293,9 +293,12 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
293
293
|
fastPath: { stage: 'off' },
|
|
294
294
|
escalation: { stage: 'off' },
|
|
295
295
|
},
|
|
296
|
-
// C52 M07
|
|
297
|
-
//
|
|
298
|
-
//
|
|
296
|
+
// C52 M07: `unic-decision` is the owner's local non-LLM Lava/JEV model in
|
|
297
|
+
// UNIC Provider. Its OpenAI-compatible API is transport only, not proof of
|
|
298
|
+
// remote hosting or generative-LLM billing. Prefer bounded typed decisions
|
|
299
|
+
// when staged on; all stages currently ship 'off' (no decision request),
|
|
300
|
+
// deterministic safety/completion policy stays authoritative.
|
|
301
|
+
// `enabled:false` is the global emergency disable.
|
|
299
302
|
decisionPlane: {
|
|
300
303
|
enabled: true,
|
|
301
304
|
stage: 'off',
|
package/src/decision/client.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* client.js (TASK-002 / M07)
|
|
3
3
|
*
|
|
4
|
-
* Typed client for the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Typed client for the owner's LOCAL `unic-decision` model in UNIC Provider.
|
|
5
|
+
* It uses Lava/JEV technology and is NOT an LLM. OpenAI-compatible
|
|
6
|
+
* `POST <baseUrl>/v1/chat/completions` is only its interoperability transport,
|
|
7
|
+
* not evidence of remote hosting, generative semantics, or LLM pricing.
|
|
8
|
+
* See docs/pstack/UNIC_DECISION_SPEC.md (identity, §3/§15/§16).
|
|
9
|
+
*
|
|
10
|
+
* Prefer bounded classify/select/rank/score questions when decisionPlane.stage
|
|
11
|
+
* enables the caller; derive candidates in code, pass compact state and typed
|
|
12
|
+
* questions to createDecisionClient(...).requestBatch({ statePacket, questions }),
|
|
13
|
+
* then validate the outcome and retain deterministic safety/permission gates.
|
|
14
|
+
* The client does not itself switch a disabled rollout stage on.
|
|
7
15
|
*
|
|
8
16
|
* - Endpoint + credentials resolve via gatewayProbe env indirection only —
|
|
9
17
|
* no literal keys, no new credential store.
|
|
@@ -10,9 +10,9 @@ const TASK_TYPE_BUDGETS = {
|
|
|
10
10
|
|
|
11
11
|
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
12
12
|
const RISKY_SIGNALS = [
|
|
13
|
-
'auth', '
|
|
14
|
-
'
|
|
15
|
-
'core', 'shared', 'runtime',
|
|
13
|
+
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
14
|
+
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
15
|
+
'flakiness', 'intermittent', 'timeout', 'deadlock', 'core', 'shared', 'runtime',
|
|
16
16
|
];
|
|
17
17
|
const EXPANSION_SIGNALS = [
|
|
18
18
|
'similar',
|
|
@@ -39,12 +39,15 @@ const EXPANSION_SIGNALS = [
|
|
|
39
39
|
|
|
40
40
|
// Signals must match whole words: bare `includes` let 'score' fire 'core',
|
|
41
41
|
// 'author' fire 'auth', 'context' fire 'text', and 'install ' fire 'all '.
|
|
42
|
+
// Common inflections (plural/past/-ing) still count — 'timeouts', 'migrations',
|
|
43
|
+
// 'deadlocked' — but 'authentication'/'authorization'/'flakiness' need explicit
|
|
44
|
+
// entries since their stems diverge from the base signal.
|
|
42
45
|
function escapeSignalRegExp(signal) {
|
|
43
46
|
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
function containsSignalWord(lower, signals) {
|
|
47
|
-
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}
|
|
50
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}(?:s|es|ed|d|ing)?\\b`).test(lower));
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export async function resolveContext({
|
|
@@ -271,9 +271,25 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
271
271
|
if (t === undefined) break;
|
|
272
272
|
if (t === '--') { headIdx += 1; break; }
|
|
273
273
|
if (isAssignToken(t)) { headIdx += 1; continue; }
|
|
274
|
+
if (wrapperPositionalPending > 0 && !isVerbToken(t) && !isFlagToken(t)) {
|
|
275
|
+
// The wrapper's own operand (user/host/duration/image/service), then
|
|
276
|
+
// the next token is the real command. Consumed BEFORE the wrapper
|
|
277
|
+
// branch so a 0-positional wrapper token sitting in an operand slot
|
|
278
|
+
// (`exec` after `docker`/`kubectl`) cannot clobber the pending count.
|
|
279
|
+
// Flags are never operands — they fall through to the flag branch.
|
|
280
|
+
wrapperPositionalPending -= 1;
|
|
281
|
+
headIdx += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
274
284
|
const positionalCount = wrapperPositionals(t);
|
|
275
285
|
if (positionalCount !== undefined) {
|
|
276
286
|
wrapperPositionalPending = positionalCount;
|
|
287
|
+
// `docker compose <sub> <svc> <cmd>` carries one extra operand (the
|
|
288
|
+
// service) beyond the flat two — `compose` is a sub-CLI, not a
|
|
289
|
+
// subcommand like `exec`.
|
|
290
|
+
if (positionalCount === 2 && unquote(tokens[headIdx + 1]) === 'compose') {
|
|
291
|
+
wrapperPositionalPending += 1;
|
|
292
|
+
}
|
|
277
293
|
headIdx += 1;
|
|
278
294
|
continue;
|
|
279
295
|
}
|
|
@@ -291,18 +307,12 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
291
307
|
}
|
|
292
308
|
continue;
|
|
293
309
|
}
|
|
294
|
-
if (wrapperPositionalPending > 0 && !isVerbToken(t)) {
|
|
295
|
-
// The wrapper's own operand (user/host/duration/image), then the
|
|
296
|
-
// next token is the real command.
|
|
297
|
-
wrapperPositionalPending -= 1;
|
|
298
|
-
headIdx += 1;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
310
|
break;
|
|
302
311
|
}
|
|
303
312
|
const effectiveHead = unquote(tokens[headIdx] ?? '');
|
|
313
|
+
const headBase = effectiveHead.replace(/^.*\//, '');
|
|
304
314
|
|
|
305
|
-
if (DUMP_VERBS.has(
|
|
315
|
+
if (DUMP_VERBS.has(headBase)) {
|
|
306
316
|
for (const token of tokens.slice(headIdx + 1)) {
|
|
307
317
|
const cleaned = token.replace(/^["']+|["',:]+$/g, '');
|
|
308
318
|
const kind = classifySecretFile(cleaned);
|
|
@@ -312,7 +322,6 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
const headBase = effectiveHead.replace(/^.*\//, '');
|
|
316
325
|
const CRED_USER_TOOLS = new Set(['curl', 'wget', 'ftp', 'lftp', 'aria2c', 'http', 'https']);
|
|
317
326
|
// `-uuser:pass`, `-u user:pass`, `--user=user:pass`, `--user user:pass` —
|
|
318
327
|
// the old `\s+` separator missed the joined and `=` forms entirely.
|
|
@@ -967,6 +967,7 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
967
967
|
const previousCodePaths = new Set(
|
|
968
968
|
filesArtifact.items.filter((item) => CODE_EXTENSIONS.has(item?.ext)).map((item) => item.filePath),
|
|
969
969
|
);
|
|
970
|
+
const currentCodePaths = new Set(codeFiles.map((file) => file.filePath));
|
|
970
971
|
const canReuseArchetypes = parsedFiles.length === 0
|
|
971
972
|
&& !recordsChanged
|
|
972
973
|
&& archetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
@@ -1358,11 +1359,10 @@ const TASK_TYPE_BUDGETS = {
|
|
|
1358
1359
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
1359
1360
|
};
|
|
1360
1361
|
|
|
1361
|
-
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
1362
1362
|
const RISKY_SIGNALS = [
|
|
1363
|
-
'auth', '
|
|
1364
|
-
'
|
|
1365
|
-
'core', 'shared', 'runtime',
|
|
1363
|
+
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
1364
|
+
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
1365
|
+
'flakiness', 'intermittent', 'timeout', 'deadlock', 'core', 'shared', 'runtime',
|
|
1366
1366
|
];
|
|
1367
1367
|
const EXPANSION_SIGNALS = [
|
|
1368
1368
|
'similar',
|
|
@@ -1389,12 +1389,15 @@ const EXPANSION_SIGNALS = [
|
|
|
1389
1389
|
|
|
1390
1390
|
// Signals must match whole words: bare `includes` let 'score' fire 'core',
|
|
1391
1391
|
// 'author' fire 'auth', 'context' fire 'text', and 'install ' fire 'all '.
|
|
1392
|
+
// Common inflections (plural/past/-ing) still count — 'timeouts', 'migrations',
|
|
1393
|
+
// 'deadlocked' — but 'authentication'/'authorization'/'flakiness' need explicit
|
|
1394
|
+
// entries since their stems diverge from the base signal.
|
|
1392
1395
|
function escapeSignalRegExp(signal) {
|
|
1393
1396
|
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1394
1397
|
}
|
|
1395
1398
|
|
|
1396
1399
|
function containsSignalWord(lower, signals) {
|
|
1397
|
-
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}
|
|
1400
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}(?:s|es|ed|d|ing)?\\b`).test(lower));
|
|
1398
1401
|
}
|
|
1399
1402
|
|
|
1400
1403
|
export async function resolveContext({
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* unic-decision.mjs (TASK-005 / M07, SPEC §5 FR-011)
|
|
4
4
|
*
|
|
5
|
-
* Standalone installed-side adapter for the
|
|
6
|
-
*
|
|
5
|
+
* Standalone installed-side adapter for the owner's local, non-LLM
|
|
6
|
+
* Lava/JEV `unic-decision` model in UNIC Provider. OpenAI-compatible
|
|
7
|
+
* /v1/chat/completions is a transport for easy integration, not evidence of
|
|
8
|
+
* an external generative LLM or LLM pricing. Prefer bounded typed semantic
|
|
9
|
+
* decisions when the caller's decisionPlane stage is enabled; this adapter
|
|
10
|
+
* does not change the rollout stage or override deterministic safety gates.
|
|
7
11
|
*
|
|
8
12
|
* Modes:
|
|
9
13
|
* node unic-decision.mjs --health-probe one no-tools request; the ONLY
|
|
@@ -140,6 +140,15 @@ the root contract is the only carrier — keep both in sync with
|
|
|
140
140
|
|
|
141
141
|
## 3-Tier Model Routing — full detail
|
|
142
142
|
|
|
143
|
+
`unic-decision` is **not** another LLM tier. The owner built this local,
|
|
144
|
+
near-zero-cost Lava/JEV model in UNIC Provider for fast typed semantic
|
|
145
|
+
decisions; the OpenAI-compatible API is a convenience transport, not a
|
|
146
|
+
generative-LLM or external-hosting claim. When `decisionPlane.stage` enables
|
|
147
|
+
it, use bounded candidate choices/rankings/scores through
|
|
148
|
+
`.claude/ukit/index/unic-decision.mjs`; keep safety, authorization and
|
|
149
|
+
completion gates deterministic. The shipped stage is `off` until explicitly
|
|
150
|
+
promoted. See `docs/pstack/UNIC_DECISION_SPEC.md` in the UKit development repo.
|
|
151
|
+
|
|
143
152
|
UKit routes tasks to one of three model tiers based on task complexity. The main session
|
|
144
153
|
model never changes mid-turn: a tier only takes effect when work is handed to an agent
|
|
145
154
|
whose own definition binds that model.
|