@ngockhoale/ukit 3.1.0 → 3.1.2
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 +17 -0
- package/manifests/platform.full.yaml +24 -0
- package/package.json +1 -1
- package/src/core/agentRuntime/completionGate.js +4 -1
- package/src/core/agentRuntime/vmEngine.js +8 -1
- package/src/core/experiments/deliberation.js +6 -0
- package/src/core/gatewayProbe.js +53 -0
- package/src/core/memory/memoryFlags.js +6 -0
- package/src/core/runtimeConfig.js +3 -3
- package/src/decision/client.js +77 -8
- package/src/decision/protocol.js +4 -1
- package/src/decision/registry.js +145 -1
- package/src/decision/reviewVerdict.js +309 -0
- package/template_project/.claude/agents/code-reviewer.md +25 -1
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +16 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +2 -0
- package/template_project/.claude/commands/ukit/handoff-review.md +12 -0
- package/template_project/.claude/ukit/index/review-verdict.mjs +592 -0
- package/template_project/.claude/ukit/index/route-task.mjs +41 -0
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +595 -0
- package/template_project/.claude/ukit/index/unic-decision.mjs +119 -13
- package/template_project/.codex/settings.json +3 -0
- package/template_project/.omp/agents/code-reviewer.md +25 -1
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +16 -0
- package/template_project/ukit/storage/config.json +276 -178
|
@@ -181,7 +181,10 @@ function normalizeProbabilities(raw) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
function validateValue(question, args) {
|
|
184
|
-
|
|
184
|
+
// The model answers with its native field names — `choice`/`noul`/`score` —
|
|
185
|
+
// not the schema's `value`; accept both so a live gateway isn't parsed as
|
|
186
|
+
// malformed.
|
|
187
|
+
const value = args?.value ?? args?.choice ?? args?.noul ?? args?.score;
|
|
185
188
|
if (question.kind === 'choice') {
|
|
186
189
|
if (typeof value !== 'string') return 'missing-value';
|
|
187
190
|
if (!Array.isArray(question.candidates) || !question.candidates.includes(value)) return 'out-of-enum';
|
|
@@ -327,6 +330,44 @@ function pickString(...candidates) {
|
|
|
327
330
|
return null;
|
|
328
331
|
}
|
|
329
332
|
|
|
333
|
+
// --- Decision-plane gateway resolution -------------------------------------
|
|
334
|
+
// The decision lane is independent of the host's LLM endpoint: default is the
|
|
335
|
+
// UNIC OpenAI-compatible provider; ~/.ukit/storage/gateway.json {decision:
|
|
336
|
+
// {baseUrl, apiKey, scheme}} overrides; env UKIT_DECISION_BASE_URL /
|
|
337
|
+
// UKIT_DECISION_API_KEY next; the shared UNIC token (ANTHROPIC_*) is the
|
|
338
|
+
// credential fallback so existing installs need nothing extra.
|
|
339
|
+
const DEFAULT_DECISION_BASE_URL = 'https://openai.unicjsc.com';
|
|
340
|
+
const USER_GATEWAY_REL = () => path.join('.ukit', 'storage', 'gateway.json');
|
|
341
|
+
|
|
342
|
+
function readUserGatewayDecision(homeDir) {
|
|
343
|
+
if (!homeDir) return null;
|
|
344
|
+
const json = safeReadJson(path.join(homeDir, USER_GATEWAY_REL()));
|
|
345
|
+
const decision = json?.decision;
|
|
346
|
+
return decision && typeof decision === 'object' ? decision : null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function resolveDecisionBaseUrl({ homeDir, env }) {
|
|
350
|
+
const fileUrl = pickString(readUserGatewayDecision(homeDir)?.baseUrl);
|
|
351
|
+
if (fileUrl) return { baseUrl: fileUrl, source: 'ukit-gateway-json' };
|
|
352
|
+
const envUrl = pickString(env?.UKIT_DECISION_BASE_URL);
|
|
353
|
+
if (envUrl) return { baseUrl: envUrl, source: 'env' };
|
|
354
|
+
return { baseUrl: DEFAULT_DECISION_BASE_URL, source: 'default' };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function resolveDecisionApiKey({ projectRoot, homeDir, env }) {
|
|
358
|
+
const file = readUserGatewayDecision(homeDir);
|
|
359
|
+
const fileKey = pickString(file?.apiKey);
|
|
360
|
+
if (fileKey) {
|
|
361
|
+
const scheme = file?.scheme === 'api-key' ? 'api-key' : 'auth-token';
|
|
362
|
+
return { value: fileKey, scheme, source: 'ukit-gateway-json' };
|
|
363
|
+
}
|
|
364
|
+
const envKey = pickString(env?.UKIT_DECISION_API_KEY);
|
|
365
|
+
if (envKey) return { value: envKey, scheme: 'auth-token', source: 'env' };
|
|
366
|
+
const shared = resolveGatewayApiKey({ projectRoot, homeDir, env });
|
|
367
|
+
if (shared?.value) return { ...shared, source: 'shared-unic-token' };
|
|
368
|
+
return { value: null, scheme: null, source: null };
|
|
369
|
+
}
|
|
370
|
+
|
|
330
371
|
function readSettingsEnvBlock(filePath) {
|
|
331
372
|
const json = safeReadJson(filePath);
|
|
332
373
|
if (!json || typeof json !== 'object') return null;
|
|
@@ -422,13 +463,14 @@ function readMergedConfig(rootDir, homeDir) {
|
|
|
422
463
|
|
|
423
464
|
// ---------------------------------------------------------------------------
|
|
424
465
|
// Client core — mirror of src/decision/client.js semantics minus the circuit
|
|
425
|
-
// breaker (a one-shot CLI has no cross-request state to protect).
|
|
426
|
-
// ---------------------------------------------------------------------------
|
|
427
|
-
|
|
428
466
|
const DEFAULT_CHECKPOINTS = {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
467
|
+
// Bare 'unic-decision' is the credential-safe alias — the only checkpoint a
|
|
468
|
+
// freshly-bound UNIC decision key serves. Variants (e.g.
|
|
469
|
+
// unic-decision-multilingual once the provider binds it) override via
|
|
470
|
+
// decisionPlane.checkpoints.
|
|
471
|
+
default: 'unic-decision',
|
|
472
|
+
english: 'unic-decision',
|
|
473
|
+
unknownLanguage: 'unic-decision',
|
|
432
474
|
};
|
|
433
475
|
|
|
434
476
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
@@ -466,9 +508,44 @@ function isTransientStatus(status) {
|
|
|
466
508
|
return status === 429 || (status >= 500 && status <= 599);
|
|
467
509
|
}
|
|
468
510
|
|
|
511
|
+
// The UNIC decision endpoint streams the completion even without stream:true —
|
|
512
|
+
// the body is one JSON object followed by an SSE "data: [DONE]" trailer.
|
|
513
|
+
// Parse the first JSON object, ignore the trailer.
|
|
514
|
+
function parseMaybeSse(text) {
|
|
515
|
+
const trimmed = String(text ?? '').trim();
|
|
516
|
+
if (!trimmed.startsWith('{')) {
|
|
517
|
+
// Pure SSE frames: join data payloads.
|
|
518
|
+
const payload = trimmed
|
|
519
|
+
.split('\n')
|
|
520
|
+
.filter((line) => line.startsWith('data:') && !line.includes('[DONE]'))
|
|
521
|
+
.map((line) => line.slice(5).trim())
|
|
522
|
+
.join('');
|
|
523
|
+
return JSON.parse(payload || trimmed);
|
|
524
|
+
}
|
|
525
|
+
// JSON object possibly followed by a glued SSE trailer (`}data: [DONE]`,
|
|
526
|
+
// observed live) — extract the balanced object, strings/comments aware.
|
|
527
|
+
let depth = 0;
|
|
528
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
529
|
+
const ch = trimmed[i];
|
|
530
|
+
if (ch === '"' || ch === "'") {
|
|
531
|
+
for (let j = i + 1; j < trimmed.length; j += 1) {
|
|
532
|
+
if (trimmed[j] === '\\') j += 1;
|
|
533
|
+
else if (trimmed[j] === ch) { i = j; break; }
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (ch === '{') depth += 1;
|
|
538
|
+
else if (ch === '}') {
|
|
539
|
+
depth -= 1;
|
|
540
|
+
if (depth === 0) return JSON.parse(trimmed.slice(0, i + 1));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return JSON.parse(trimmed);
|
|
544
|
+
}
|
|
545
|
+
|
|
469
546
|
async function readResponseBody(res) {
|
|
547
|
+
if (res && typeof res.text === 'function') return parseMaybeSse(await res.text());
|
|
470
548
|
if (res && typeof res.json === 'function') return res.json();
|
|
471
|
-
if (res && typeof res.text === 'function') return JSON.parse(await res.text());
|
|
472
549
|
return res;
|
|
473
550
|
}
|
|
474
551
|
|
|
@@ -574,14 +651,14 @@ export async function requestBatch(batch, {
|
|
|
574
651
|
return outcome('blocked-sensitive', { checkpoint });
|
|
575
652
|
}
|
|
576
653
|
|
|
577
|
-
const base =
|
|
654
|
+
const base = resolveDecisionBaseUrl({ homeDir, env });
|
|
578
655
|
if (!base?.baseUrl) {
|
|
579
656
|
return outcome('unsupported', {
|
|
580
657
|
fallbackCode: 'endpoint-unconfigured',
|
|
581
658
|
checkpoint,
|
|
582
659
|
});
|
|
583
660
|
}
|
|
584
|
-
const key =
|
|
661
|
+
const key = resolveDecisionApiKey({ projectRoot, homeDir, env });
|
|
585
662
|
const endpoint = {
|
|
586
663
|
url: `${String(base.baseUrl).replace(/\/+$/, '')}/v1/chat/completions`,
|
|
587
664
|
headers: buildAuthHeaders(key),
|
|
@@ -621,6 +698,26 @@ export async function requestBatch(batch, {
|
|
|
621
698
|
|
|
622
699
|
const status = res?.status ?? (res?.ok === false ? 500 : 200);
|
|
623
700
|
if (res?.ok === false || status < 200 || status >= 300) {
|
|
701
|
+
// A variant checkpoint without bound credentials (e.g.
|
|
702
|
+
// unic-decision-multilingual before provider binding) returns a 4xx
|
|
703
|
+
// model_not_found body — retry once on the bare `unic-decision` model.
|
|
704
|
+
if (checkpoint !== 'unic-decision') {
|
|
705
|
+
let notFound = status === 404;
|
|
706
|
+
try {
|
|
707
|
+
const clone = typeof res?.clone === 'function' ? res.clone() : null;
|
|
708
|
+
const errBody = clone ? parseMaybeSse(await clone.text()) : null;
|
|
709
|
+
if (errBody?.error?.code === 'model_not_found') notFound = true;
|
|
710
|
+
} catch { /* keep status-based guess */ }
|
|
711
|
+
if (notFound) {
|
|
712
|
+
checkpoint = 'unic-decision';
|
|
713
|
+
try {
|
|
714
|
+
body = JSON.stringify(
|
|
715
|
+
encodeBatch({ model: checkpoint, statePacket: serialized, questions }),
|
|
716
|
+
);
|
|
717
|
+
} catch { /* keep prior body */ }
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
624
721
|
if (isTransientStatus(status) && attempt + 1 < maxAttempts) continue;
|
|
625
722
|
if (status === 401 || status === 403) {
|
|
626
723
|
lastError = 'unavailable';
|
|
@@ -680,9 +777,9 @@ export async function runHealthProbe({
|
|
|
680
777
|
const dp = decisionPlaneConfig(config);
|
|
681
778
|
if (dp.enabled === false) return { status: 'unavailable' };
|
|
682
779
|
if (!transport) return { status: 'unsupported' };
|
|
683
|
-
const base =
|
|
780
|
+
const base = resolveDecisionBaseUrl({ homeDir, env });
|
|
684
781
|
if (!base?.baseUrl) return { status: 'unsupported', fallbackCode: 'endpoint-unconfigured' };
|
|
685
|
-
const key =
|
|
782
|
+
const key = resolveDecisionApiKey({ projectRoot, homeDir, env });
|
|
686
783
|
const checkpoint = resolveCheckpoint('multilingual', config);
|
|
687
784
|
const timeoutMs = Number.isFinite(dp.timeoutMs) ? dp.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
688
785
|
const body = JSON.stringify({
|
|
@@ -789,7 +886,16 @@ async function main() {
|
|
|
789
886
|
|
|
790
887
|
const isMainModule = (() => {
|
|
791
888
|
try {
|
|
792
|
-
|
|
889
|
+
const invoked = process.argv[1] ?? '';
|
|
890
|
+
if (!invoked) return false;
|
|
891
|
+
const self = path.resolve(fileURLToPath(import.meta.url));
|
|
892
|
+
const target = path.resolve(invoked);
|
|
893
|
+
if (self === target) return true;
|
|
894
|
+
// Installed mirrors may be reached through a symlinked directory (e.g.
|
|
895
|
+
// .codex/ukit -> ../.claude/ukit): realpath resolves the link, basename
|
|
896
|
+
// equality keeps same-named unrelated scripts from matching.
|
|
897
|
+
return path.basename(invoked) === 'unic-decision.mjs'
|
|
898
|
+
&& fs.realpathSync(target) === self;
|
|
793
899
|
} catch {
|
|
794
900
|
return false;
|
|
795
901
|
}
|
|
@@ -188,6 +188,9 @@
|
|
|
188
188
|
"compact-now-or-later",
|
|
189
189
|
"summarize-docs-or-keep-detail"
|
|
190
190
|
],
|
|
191
|
+
"decisionAdapter": "sidecar-decision.mjs",
|
|
192
|
+
"decisionStageGate": "decisionPlane.families.workflow.stage",
|
|
193
|
+
"decisionKeysSource": "cli",
|
|
191
194
|
"stepBudgets": {
|
|
192
195
|
"trivial": {
|
|
193
196
|
"maxSteps": 1,
|
|
@@ -48,6 +48,26 @@ If any input is missing, return `CHANGES-REQUESTED` with reason "incomplete hand
|
|
|
48
48
|
- **APPROVED-WITH-MINOR** — Minor naming / doc / style issues. Logged on task file but handoff allowed.
|
|
49
49
|
- **APPROVED** — Clean.
|
|
50
50
|
|
|
51
|
+
### Stage gate — verdict emission (unic-decision)
|
|
52
|
+
|
|
53
|
+
You generate the findings; the FINAL verdict/bucket classification is a bounded
|
|
54
|
+
decision owned by the local decision model when staged. Read
|
|
55
|
+
`decisionPlane.families.review.stage` from `.ukit/storage/config.json`:
|
|
56
|
+
|
|
57
|
+
- `off` (default) or any adapter failure (`outcomeClass` ≠ `accepted`/`partial`) →
|
|
58
|
+
you emit the verdict directly, exactly as today; the deterministic severity
|
|
59
|
+
aggregate (any critical → CHANGES-REQUESTED; ≥1 important or any
|
|
60
|
+
unclassified/unknown severity → APPROVED-WITH-MINOR; else APPROVED) stays
|
|
61
|
+
authoritative.
|
|
62
|
+
- not `off` → emit the verdict via `node .claude/ukit/index/review-verdict.mjs`
|
|
63
|
+
(registered key `review.verdict.v1`): pipe your extracted findings as JSON on
|
|
64
|
+
stdin — whitelisted fields only (`id`, `severity`, `file`, `line`, short
|
|
65
|
+
`claim`; never diff hunks or secrets). Use the adapter's `verdict`/`buckets`
|
|
66
|
+
fields in the `## Reviewer Verdict` block.
|
|
67
|
+
|
|
68
|
+
Model isolation is preserved: the verdict pass runs on the local unic-decision
|
|
69
|
+
model, so reviewer ≠ executor still stands.
|
|
70
|
+
|
|
51
71
|
### Output (append to task file as `## Reviewer Verdict`)
|
|
52
72
|
|
|
53
73
|
```
|
|
@@ -139,7 +159,11 @@ additions:
|
|
|
139
159
|
`node .claude/ukit/index/review-panel-aggregate.mjs <TASK-xxx.md...>` and hands you
|
|
140
160
|
the output, the lead fills `AGREEMENT_MAP` with the emitted finding → members map
|
|
141
161
|
and applies the lead-judgment buckets to every finding: **Act on** / **Consider** /
|
|
142
|
-
**Noted** / **Dismissed**.
|
|
162
|
+
**Noted** / **Dismissed**. When `decisionPlane.families.review.stage` is not `off`,
|
|
163
|
+
the lead's bucketing and panel verdict go through
|
|
164
|
+
`node .claude/ukit/index/review-verdict.mjs --panel` (`review.finding-bucket.v1` /
|
|
165
|
+
`review.panel-verdict.v1`) per the stage gate above — the deterministic aggregate
|
|
166
|
+
stays authoritative at `off` or on adapter failure.
|
|
143
167
|
- `consensus≥2 identical findings = high signal`: a finding reported by two or more
|
|
144
168
|
panel members is high-signal and must not be bucketed below **Consider** without a
|
|
145
169
|
stated reason.
|
|
@@ -27,6 +27,22 @@ You are UKit's internal small-task maintainer. You run as a sidecar/parallel/non
|
|
|
27
27
|
- Keeping agent context compact without removing existing lanes: Claude PreCompact/reinject stays active and Codex Desktop soft handoffs use `compact.codexContext.compactTarget` (default 150 lines; preferred 120-150; hard max 170) while preserving critical state.
|
|
28
28
|
- Small, reversible UKit runtime maintenance decisions.
|
|
29
29
|
|
|
30
|
+
## Bounded Decisions (stage-gated unic-decision)
|
|
31
|
+
|
|
32
|
+
The six bounded decisions enumerated in `.codex/settings.json` `smallTaskModel.decisionPolicy.decisions` — `fast-vs-slow-lane`, `safe-vs-risky-lane`, `skill-routing-needed`, `step-budget-enough`, `compact-now-or-later`, `summarize-docs-or-keep-detail` — consult `unic-decision` first via the installed CLI when the decision-plane stage allows:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
node .claude/ukit/index/sidecar-decision.mjs --decision <name> [--root <dir>] # context JSON on stdin
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The CLI resolves `decisionPlane.families.workflow.stage` and owns the name → `workflow.*` decision-key mapping (`--list` prints it):
|
|
39
|
+
|
|
40
|
+
- `off` (default): the CLI's deterministic rules answer directly — zero transport, authoritative.
|
|
41
|
+
- `shadow`: run the batch for comparison only; the deterministic rule answer stays authoritative.
|
|
42
|
+
- `canary`/`default`: a valid unic-decision answer wins; an invalid or unavailable adapter falls back to the deterministic rule (`outcomeClass: 'unavailable'`).
|
|
43
|
+
|
|
44
|
+
`unic-decision` is the only model allowed in these decision steps — on any failure the deterministic rule is the fallback, never another LLM for the verdict. This lane's own model (`unic-lite`) keeps generative work only: summarization, doc maintenance, and cleanup — it never emits a verdict for these decisions.
|
|
45
|
+
|
|
30
46
|
## Never Use For
|
|
31
47
|
|
|
32
48
|
- Security/auth/permission/secrets work.
|