@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,9 +18,8 @@ import {
18
18
  import {
19
19
  addUserRecord,
20
20
  getUserRecord,
21
- loadUserRecords,
22
21
  queryUserRecords,
23
- saveUserRecords,
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 records = await loadUserRecords({ homeDir });
301
- const remaining = records.filter((r) => r.id !== id);
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 UNIC Decision Agent (docs/pstack/UNIC_DECISION_SPEC §17). All
297
- // stages ship 'off' — zero outbound calls, deterministic policy stays
298
- // authoritative. `enabled:false` is the global emergency disable.
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',
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * client.js (TASK-002 / M07)
3
3
  *
4
- * Typed client for the UNIC Decision Agent: OpenAI-compatible
5
- * `POST <baseUrl>/v1/chat/completions` against the configured UNIC gateway
6
- * (docs/pstack/UNIC_DECISION_SPEC.md §3/§15/§16).
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', 'security', 'migration', 'uninstall', 'password', 'token', 'permission',
14
- 'delete all', 'drop table', 'race', 'flaky', 'intermittent', 'timeout', 'deadlock',
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)}\\b`).test(lower));
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(effectiveHead)) {
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', 'security', 'migration', 'uninstall', 'password', 'token', 'permission',
1364
- 'delete all', 'drop table', 'race', 'flaky', 'intermittent', 'timeout', 'deadlock',
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)}\\b`).test(lower));
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 UNIC Decision Agent
6
- * (docs/pstack/UNIC_DECISION_SPEC.md §8/§9, CONTRACTS.md C13/C14).
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.