@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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.1.2 - 2026-09-26
6
+
7
+ - **Decision plane goes live on the UNIC provider** — the decision lane now resolves its own endpoint independent of the engine's LLM gateway, so UKit uses UNIC even when Claude Code/Codex/omp point elsewhere:
8
+ - **Endpoint chain**: `~/.ukit/storage/gateway.json` `{decision:{baseUrl,apiKey,scheme}}` → env `UKIT_DECISION_BASE_URL`/`UKIT_DECISION_API_KEY` → default `https://openai.unicjsc.com`; credential falls back to the shared UNIC token (ANTHROPIC_*). New resolvers `resolveDecisionBaseUrl`/`resolveDecisionApiKey` in `src/core/gatewayProbe.js`, mirrored in `unic-decision.mjs`.
9
+ - **Live transport fixes** (verified against the real gateway): SSE `data: [DONE]` trailer — sometimes glued to the JSON body — is stripped via balanced-object extraction; model answers use native arg names (`choice`/`noul`/`score`) now accepted alongside the schema's `value`; a `model_not_found` variant checkpoint retries once on the bare `unic-decision` alias.
10
+ - **Checkpoints**: shipped defaults collapse to the bare `unic-decision` model (the credential-safe alias); `unic-decision-multilingual`/variants remain configurable via `decisionPlane.checkpoints`.
11
+ - **Optional backlog drained**: injection-point contracts documented (deliberation `judge`, vmEngine `classifyFn`, completionGate `modelJudgment`), `memoryV2.decision` plane marked reserved, `resume.next-action.v1` + `verify.depth.v1` shadow questions wired, gatewayProbe marked migration-exempt.
12
+
13
+ ## 3.1.1 - 2026-09-26
14
+
15
+ - **Decision plane — review + workflow families (UNIC_DECISION_MIGRATION S1–S4)** — the reserved `review` and `workflow` registry families are now populated and wired; all keys ship `rolloutStage: 'off'` so deterministic fallbacks stay authoritative (stage promotion is a separate owner decision):
16
+ - **Registry** (`src/decision/registry.js`): `review.verdict.v1`, `review.finding-bucket.v1`, `review.panel-verdict.v1`, `workflow.sidecar-lane.v1`, `workflow.sidecar-risk.v1`, `workflow.routing-needed.v1`, `workflow.step-budget.v1`, `workflow.compact-now.v1`, `workflow.summarize-vs-keep.v1` — all `choice` kind, `off`, deterministic fallback owners recorded.
17
+ - **Review adapter**: `src/decision/reviewVerdict.js` + installed twin `template_project/.claude/ukit/index/review-verdict.mjs` — findings (whitelisted `{id, severity, file, line, claim}`) in, stage-gated verdict + per-finding buckets out; reviewers still generate findings on the general model, only the bounded verdict moves to `unic-decision` when `decisionPlane.families.review.stage` is enabled.
18
+ - **Sidecar adapter**: `template_project/.claude/ukit/index/sidecar-decision.mjs` answers the six `.codex/settings.json` `smallTaskModel.decisionPolicy.decisions` via a bounded `unic-decision` batch (stage-gated `decisionPlane.families.workflow.stage`); adapter failure → deterministic rule answer, never another LLM.
19
+ - **Engine parity**: `.codex/ukit` is a symlink to `.claude/ukit` (installed layout), `decisionAdapter` hints in `.codex/settings.json`; all three index CLIs' `isMainModule` gates now use `realpathSync` + basename equality — symlinked invocation previously compared unresolved paths and silently exited 0 without running `main()`.
20
+ - **Docs/tests**: `docs/pstack/UNIC_DECISION_GUIDE.md` (family keys + adapters), `docs/GATEWAY.md` §8, `docs/MEMORY.md`; parity + symlink-invocation coverage in `tests/index/{unicDecision,reviewVerdictCli,sidecarDecision}.test.js`.
21
+
5
22
  ## 3.1.0 - 2026-09-26
6
23
 
7
24
  - **Flight recorder — Data Foundation wave (cycle C81)** — the observability pipeline grows from record/segment plumbing into a full local diagnostics surface; still opt-in behind `observability.stage` (default `off`, `off→shadow→canary→default` per seam, absolute kill switch):
@@ -1750,6 +1750,30 @@ items:
1750
1750
  packs:
1751
1751
  - core
1752
1752
 
1753
+ - id: ukit-index-review-verdict-script
1754
+ type: config
1755
+ sourceTemplate: .claude/ukit/index/review-verdict.mjs
1756
+ targetPath: .claude/ukit/index/review-verdict.mjs
1757
+ requires:
1758
+ - ukit-index-unic-decision-script
1759
+ mergeStrategy: overwrite_with_backup
1760
+ variables: []
1761
+ enabledByDefault: true
1762
+ packs:
1763
+ - core
1764
+
1765
+ - id: ukit-index-sidecar-decision-script
1766
+ type: config
1767
+ sourceTemplate: .claude/ukit/index/sidecar-decision.mjs
1768
+ targetPath: .claude/ukit/index/sidecar-decision.mjs
1769
+ requires:
1770
+ - ukit-index-unic-decision-script
1771
+ mergeStrategy: overwrite_with_backup
1772
+ variables: []
1773
+ enabledByDefault: true
1774
+ packs:
1775
+ - core
1776
+
1753
1777
  - id: ukit-index-extract-image-script
1754
1778
  type: config
1755
1779
  sourceTemplate: .claude/ukit/index/extract-image.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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",
@@ -152,7 +152,10 @@ export function evaluateCompletionEvidence({
152
152
  verdict = 'FAILED';
153
153
  }
154
154
 
155
- // modelJudgment may only request more evidence, never promote.
155
+ // modelJudgment may only request more evidence, never promote (enforced
156
+ // below: a judgment can downgrade PASS → MORE_EVIDENCE, nothing else). When
157
+ // model-backed, the judgment MUST originate from the decision plane
158
+ // (unic-decision, a bounded verdict question) — never a general LLM call.
156
159
  const judgment = modelJudgment != null
157
160
  ? (typeof modelJudgment === 'string' ? modelJudgment : String(modelJudgment))
158
161
  : null;
@@ -85,6 +85,11 @@ async function writeFileAtomic(target, data) {
85
85
  }
86
86
 
87
87
  /**
88
+ * Migration contract (decision plane): `classifyFn` is invoked only when the
89
+ * IR itself cannot route an event (see deliver()). It may only consult the
90
+ * decision plane (unic-decision / createDecisionClient registry keys) for a
91
+ * bounded route/escalate answer — never a general LLM call.
92
+ *
88
93
  * @param {{dir:string, now?:()=>Date, classifyFn?:Function, startFn?:Function, hooks?:object}} opts
89
94
  */
90
95
  export function createVmEngine({ dir, now, classifyFn, startFn, hooks } = {}) {
@@ -512,7 +517,9 @@ export function createVmEngine({ dir, now, classifyFn, startFn, hooks } = {}) {
512
517
  return { consumed: false, code: 'out_of_order' };
513
518
  }
514
519
 
515
- // classify only when the IR itself cannot route the event (G4-FR05)
520
+ // classify only when the IR itself cannot route the event (G4-FR05);
521
+ // classifyFn may consult the decision plane for bounded route/escalate
522
+ // answers only — never a general LLM call (see createVmEngine opts).
516
523
  let outcome = event.safePayload?.outcome;
517
524
  let targetMatched = matched;
518
525
  if (!targetMatched) {
@@ -244,6 +244,12 @@ function deepFreezeCopy(value) {
244
244
  * verdict is post-checked: hard-constraint waivers/violations and
245
245
  * majority-only (citation-free) verdicts are rejected.
246
246
  *
247
+ * Migration contract (decision plane): an injected `judge` implementation
248
+ * that performs bounded verdict work MUST route it through the decision
249
+ * plane (`createDecisionClient` + `src/decision/registry.js` keys) — never a
250
+ * general LLM call. The default `smartJudge` is deterministic: pure scoring,
251
+ * hard-constraint filtering, and a stable sort — no I/O, no model calls.
252
+ *
247
253
  * @param {{candidates: object[], rubric: object, judge: Function,
248
254
  * config?: object}} input
249
255
  * @returns {{status: 'ok'|'disabled'|'rejected', selected?: object|null,
@@ -23,6 +23,52 @@ import path from 'node:path';
23
23
 
24
24
  const GATEWAY_DOC = 'docs/GATEWAY.md';
25
25
  const SETTINGS_RELATIVE_PROBE_PATH = path.join('.claude', 'settings.json');
26
+ // --- Decision-plane gateway resolution (UNIC_DECISION_MIGRATION backlog) -----
27
+ // The decision plane is a SEPARATE lane from the host's LLM endpoint: it must
28
+ // reach the UNIC OpenAI-compatible provider no matter what Claude Code/omp is
29
+ // configured to. Chain: ~/.ukit/storage/gateway.json {decision:{baseUrl,
30
+ // apiKey, scheme}} → env UKIT_DECISION_BASE_URL / UKIT_DECISION_API_KEY →
31
+ // DEFAULT_DECISION_BASE_URL + the shared UNIC token (ANTHROPIC_*) so a host
32
+ // already pointed at UNIC works with zero extra config. Missing key → the
33
+ // caller surfaces 'unavailable' and deterministic policy stays authoritative.
34
+ export const DEFAULT_DECISION_BASE_URL = 'https://openai.unicjsc.com';
35
+ const USER_GATEWAY_REL = path.join('.ukit', 'storage', 'gateway.json');
36
+
37
+ function readUserGatewayDecision(homeDir) {
38
+ if (!homeDir) return null;
39
+ const json = safeReadJson(path.join(homeDir, USER_GATEWAY_REL));
40
+ const decision = json?.decision;
41
+ return decision && typeof decision === 'object' ? decision : null;
42
+ }
43
+
44
+ // Endpoint for the decision lane. Never throws; file and env malformed values
45
+ // are ignored, the default is the final fallback (not ANTHROPIC_BASE_URL —
46
+ // that env may point at a non-decision gateway).
47
+ export function resolveDecisionBaseUrl({ homeDir = os.homedir(), env = process.env } = {}) {
48
+ const file = readUserGatewayDecision(homeDir);
49
+ const fromFile = pickString(file?.baseUrl);
50
+ if (fromFile) return { baseUrl: fromFile, source: 'ukit-gateway-json' };
51
+ const fromEnv = pickString(env?.UKIT_DECISION_BASE_URL);
52
+ if (fromEnv) return { baseUrl: fromEnv, source: 'env' };
53
+ return { baseUrl: DEFAULT_DECISION_BASE_URL, source: 'default' };
54
+ }
55
+
56
+ // API key for the decision lane. `scheme`: 'auth-token' → Authorization: Bearer
57
+ // (UNIC OpenAI-compatible default) | 'api-key' → x-api-key. Falls back to the
58
+ // host's UNIC token so existing installs need no duplicate credential.
59
+ export function resolveDecisionApiKey({ projectRoot, homeDir = os.homedir(), env = process.env } = {}) {
60
+ const file = readUserGatewayDecision(homeDir);
61
+ const fileKey = pickString(file?.apiKey);
62
+ if (fileKey) {
63
+ const scheme = file?.scheme === 'api-key' ? 'api-key' : 'auth-token';
64
+ return { value: fileKey, scheme, source: 'ukit-gateway-json' };
65
+ }
66
+ const envKey = pickString(env?.UKIT_DECISION_API_KEY);
67
+ if (envKey) return { value: envKey, scheme: 'auth-token', source: 'env' };
68
+ const shared = resolveGatewayApiKey({ projectRoot, homeDir, env });
69
+ if (shared?.value) return { ...shared, source: 'shared-unic-token' };
70
+ return { value: null, scheme: null, source: null };
71
+ }
26
72
 
27
73
  function safeReadFile(filePath) {
28
74
  try {
@@ -491,6 +537,13 @@ async function runNonStreamingProbe({ url, headers, body: requestBody, fetchImpl
491
537
  }
492
538
 
493
539
  /**
540
+ * Migration-contract exemption (decision plane): the two `fetchImpl` calls below
541
+ * are the ONLY generic-LLM HTTP calls under src/. They are diagnostic-only —
542
+ * connectivity probes that send a trivial "pong?" message and measure the
543
+ * transport (SSE events, buffering, Anthropic Message shape) — and are
544
+ * intentionally NOT routed through the decision plane: no bounded verdict
545
+ * vocabulary exists for a connectivity probe. Keep-as-probe; do not migrate.
546
+ *
494
547
  * Runs both probes against `baseUrl`. Returns a structured result; never propagates exceptions
495
548
  * — every failure is reflected in the returned object so the CLI can print it.
496
549
  *
@@ -4,6 +4,12 @@
4
4
  // resolveConfigStage machinery: off → shadow → canary → default. Flags only
5
5
  // ever REDUCE capability — malformed/missing resolves 'off', killSwitch is
6
6
  // absolute, and 'canary' without an opted-in projectId resolves 'off'.
7
+ // The `decision` plane is reserved for the memoryV2 candidate-classification
8
+ // lane (registry key `learn.candidate-class.v1`, family 'learn'). Note the
9
+ // decision-plane stage namespace is separate: promotion gates that key via
10
+ // `decisionPlane.families.learn.stage`, NOT `memoryV2.decision.stage` — this
11
+ // flag only guards the memoryV2 lane itself. Zero consumers today — do not
12
+ // remove; user configs may pin its stage key.
7
13
  //
8
14
  // Shadow receipts are the only telemetry: bounded JSONL of counts/codes/
9
15
  // latency bands — never record text, prompt bodies, or secrets.
@@ -304,9 +304,9 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
304
304
  stage: 'off',
305
305
  logicalModel: 'unic-decision',
306
306
  checkpoints: {
307
- default: 'unic-decision/laya-multilingual',
308
- english: 'unic-decision/laya',
309
- unknownLanguage: 'unic-decision/auto',
307
+ default: 'unic-decision',
308
+ english: 'unic-decision',
309
+ unknownLanguage: 'unic-decision',
310
310
  },
311
311
  timeoutMs: 5000,
312
312
  maxStateTokens: { multilingual: 1024, english: 512 },
@@ -27,8 +27,8 @@
27
27
 
28
28
  import { createHash } from 'node:crypto';
29
29
  import {
30
- resolveGatewayBaseUrl,
31
- resolveGatewayApiKey,
30
+ resolveDecisionBaseUrl,
31
+ resolveDecisionApiKey,
32
32
  } from '../core/gatewayProbe.js';
33
33
  import {
34
34
  encodeBatch,
@@ -42,11 +42,20 @@ import {
42
42
  } from './statePacket.js';
43
43
 
44
44
  const DEFAULT_CHECKPOINTS = {
45
- default: 'unic-decision/laya-multilingual',
46
- english: 'unic-decision/laya',
47
- unknownLanguage: 'unic-decision/auto',
45
+ // Bare 'unic-decision' is the credential-safe alias — the only checkpoint a
46
+ // freshly-bound UNIC decision key serves. Variants (e.g.
47
+ // unic-decision-multilingual once the provider binds it) override via
48
+ // decisionPlane.checkpoints.
49
+ default: 'unic-decision',
50
+ english: 'unic-decision',
51
+ unknownLanguage: 'unic-decision',
48
52
  };
49
53
 
54
+ // Credential-safe alias: every UNIC decision credential binds the bare name
55
+ // first — variants (unic-decision-multilingual, …) may lag. On model_not_found
56
+ // the client falls back here once per batch.
57
+ const FALLBACK_CHECKPOINT = 'unic-decision';
58
+
50
59
  const DEFAULT_TIMEOUT_MS = 5000;
51
60
  const DEFAULT_MAX_RETRIES = 1;
52
61
  const DEFAULT_BREAKER = { failureThreshold: 3, cooldownMs: 30_000 };
@@ -102,9 +111,44 @@ function isTransientStatus(status) {
102
111
  return status === 429 || (status >= 500 && status <= 599);
103
112
  }
104
113
 
114
+ // The UNIC decision endpoint streams the completion even without stream:true —
115
+ // the body is one JSON object followed by an SSE "data: [DONE]" trailer.
116
+ // Parse the first JSON object, ignore the trailer.
117
+ function parseMaybeSse(text) {
118
+ const trimmed = String(text ?? '').trim();
119
+ if (!trimmed.startsWith('{')) {
120
+ // Pure SSE frames: join data payloads.
121
+ const payload = trimmed
122
+ .split('\n')
123
+ .filter((line) => line.startsWith('data:') && !line.includes('[DONE]'))
124
+ .map((line) => line.slice(5).trim())
125
+ .join('');
126
+ return JSON.parse(payload || trimmed);
127
+ }
128
+ // JSON object possibly followed by a glued SSE trailer (`}data: [DONE]`,
129
+ // observed live) — extract the balanced object, strings/comments aware.
130
+ let depth = 0;
131
+ for (let i = 0; i < trimmed.length; i += 1) {
132
+ const ch = trimmed[i];
133
+ if (ch === '"' || ch === "'") {
134
+ for (let j = i + 1; j < trimmed.length; j += 1) {
135
+ if (trimmed[j] === '\\') j += 1;
136
+ else if (trimmed[j] === ch) { i = j; break; }
137
+ }
138
+ continue;
139
+ }
140
+ if (ch === '{') depth += 1;
141
+ else if (ch === '}') {
142
+ depth -= 1;
143
+ if (depth === 0) return JSON.parse(trimmed.slice(0, i + 1));
144
+ }
145
+ }
146
+ return JSON.parse(trimmed);
147
+ }
148
+
105
149
  async function readResponseBody(res) {
150
+ if (res && typeof res.text === 'function') return parseMaybeSse(await res.text());
106
151
  if (res && typeof res.json === 'function') return res.json();
107
- if (res && typeof res.text === 'function') return JSON.parse(await res.text());
108
152
  return res;
109
153
  }
110
154
 
@@ -167,9 +211,13 @@ export function createDecisionClient({
167
211
  }
168
212
 
169
213
  async function resolveEndpoint() {
170
- const base = await resolveGatewayBaseUrl({ projectRoot, homeDir, env });
214
+ // Decision lane resolves its own endpoint (user-level gateway.json →
215
+ // env → openai.unicjsc.com default), independent of the host's LLM
216
+ // endpoint — the UNIC provider is used even when the engine points
217
+ // elsewhere.
218
+ const base = await resolveDecisionBaseUrl({ projectRoot, homeDir, env });
171
219
  if (!base?.baseUrl) return null;
172
- const key = await resolveGatewayApiKey({ projectRoot, homeDir, env });
220
+ const key = await resolveDecisionApiKey({ projectRoot, homeDir, env });
173
221
  return {
174
222
  url: `${String(base.baseUrl).replace(/\/+$/, '')}/v1/chat/completions`,
175
223
  headers: buildAuthHeaders(key),
@@ -305,6 +353,27 @@ export function createDecisionClient({
305
353
 
306
354
  const status = res?.status ?? (res?.ok === false ? 500 : 200);
307
355
  if (res?.ok === false || status < 200 || status >= 300) {
356
+ // A variant checkpoint without bound credentials (e.g.
357
+ // unic-decision-multilingual before provider binding) returns a 4xx
358
+ // model_not_found body — retry once on the bare `unic-decision`
359
+ // model, the credential-safe alias the owner provisions first.
360
+ if (checkpoint !== FALLBACK_CHECKPOINT) {
361
+ let notFound = status === 404;
362
+ try {
363
+ const clone = typeof res?.clone === 'function' ? res.clone() : null;
364
+ const errBody = clone ? parseMaybeSse(await clone.text()) : null;
365
+ if (errBody?.error?.code === 'model_not_found') notFound = true;
366
+ } catch { /* keep status-based guess */ }
367
+ if (notFound) {
368
+ checkpoint = FALLBACK_CHECKPOINT;
369
+ try {
370
+ body = JSON.stringify(
371
+ encodeBatch({ model: checkpoint, statePacket: serialized, questions }),
372
+ );
373
+ } catch { /* keep prior body */ }
374
+ continue;
375
+ }
376
+ }
308
377
  if (isTransientStatus(status) && attempt + 1 < maxAttempts) continue;
309
378
  if (status === 401 || status === 403) {
310
379
  lastError = 'unavailable';
@@ -169,7 +169,10 @@ function normalizeProbabilities(raw) {
169
169
  }
170
170
 
171
171
  function validateValue(question, args) {
172
- const value = args?.value;
172
+ // The model answers with its native field names — `choice`/`noul`/`score` —
173
+ // not the schema's `value`; accept both so a live gateway isn't parsed as
174
+ // malformed.
175
+ const value = args?.value ?? args?.choice ?? args?.noul ?? args?.score;
173
176
  if (question.kind === 'choice') {
174
177
  if (typeof value !== 'string') return 'missing-value';
175
178
  if (!Array.isArray(question.candidates) || !question.candidates.includes(value)) return 'out-of-enum';
@@ -259,7 +259,7 @@ export const DECISION_REGISTRY = Object.freeze([
259
259
  family: 'learn',
260
260
  owner: 'learningCandidates',
261
261
  kind: 'choice',
262
- description: 'Learning candidate classification for a captured signal.',
262
+ description: 'Learning candidate classification for a captured signal. Reserved for the memoryV2 decision lane; promotion gates via decisionPlane.families.learn.stage.',
263
263
  candidatePolicy: 'owner-shortlist',
264
264
  hardConstraints: ['promotion-approval'],
265
265
  probabilityPolicy: 'raw-label',
@@ -269,6 +269,150 @@ export const DECISION_REGISTRY = Object.freeze([
269
269
  cacheSensitivity: 'none',
270
270
  leasePolicy: 'none',
271
271
  },
272
+ {
273
+ decisionKey: 'review.verdict.v1',
274
+ schemaVersion: 1,
275
+ family: 'review',
276
+ owner: 'handoffReview',
277
+ kind: 'choice',
278
+ description: 'Bounded review verdict among APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL.',
279
+ candidatePolicy: 'fixed-verdict-vocabulary',
280
+ hardConstraints: ['verdict-vocabulary'],
281
+ probabilityPolicy: 'raw-label',
282
+ fallbackPolicy: 'deterministic-human-review',
283
+ telemetryClass: 'decision',
284
+ rolloutStage: 'off',
285
+ cacheSensitivity: 'none',
286
+ leasePolicy: 'none',
287
+ },
288
+ {
289
+ decisionKey: 'review.finding-bucket.v1',
290
+ schemaVersion: 1,
291
+ family: 'review',
292
+ owner: 'handoffReview',
293
+ kind: 'choice',
294
+ description: 'Triage bucket for one review finding among Act on | Consider | Noted | Dismissed.',
295
+ candidatePolicy: 'fixed-bucket-vocabulary',
296
+ hardConstraints: ['bucket-vocabulary'],
297
+ probabilityPolicy: 'raw-label',
298
+ fallbackPolicy: 'deterministic-human-review',
299
+ telemetryClass: 'decision',
300
+ rolloutStage: 'off',
301
+ cacheSensitivity: 'none',
302
+ leasePolicy: 'none',
303
+ },
304
+ {
305
+ decisionKey: 'review.panel-verdict.v1',
306
+ schemaVersion: 1,
307
+ family: 'review',
308
+ owner: 'handoffReview',
309
+ kind: 'choice',
310
+ description: 'Aggregated panel verdict for a review artifact.',
311
+ candidatePolicy: 'fixed-verdict-vocabulary',
312
+ hardConstraints: ['verdict-vocabulary'],
313
+ probabilityPolicy: 'raw-label',
314
+ fallbackPolicy: 'deterministic-human-review',
315
+ telemetryClass: 'decision',
316
+ rolloutStage: 'off',
317
+ cacheSensitivity: 'none',
318
+ leasePolicy: 'none',
319
+ },
320
+ {
321
+ decisionKey: 'workflow.sidecar-lane.v1',
322
+ schemaVersion: 1,
323
+ family: 'workflow',
324
+ owner: 'smallTaskMaintainer',
325
+ kind: 'choice',
326
+ description: 'Bounded decision whether a chore belongs to the sidecar lane.',
327
+ candidatePolicy: 'owner-shortlist',
328
+ hardConstraints: ['lane-eligibility'],
329
+ probabilityPolicy: 'raw-label',
330
+ fallbackPolicy: 'deterministic-lane-rules',
331
+ telemetryClass: 'decision',
332
+ rolloutStage: 'off',
333
+ cacheSensitivity: 'none',
334
+ leasePolicy: 'none',
335
+ },
336
+ {
337
+ decisionKey: 'workflow.sidecar-risk.v1',
338
+ schemaVersion: 1,
339
+ family: 'workflow',
340
+ owner: 'smallTaskMaintainer',
341
+ kind: 'choice',
342
+ description: 'Bounded risk class for a sidecar candidate (safe/reversible vs escalate).',
343
+ candidatePolicy: 'owner-shortlist',
344
+ hardConstraints: ['risk-floor'],
345
+ probabilityPolicy: 'raw-label',
346
+ fallbackPolicy: 'deterministic-lane-rules',
347
+ telemetryClass: 'decision',
348
+ rolloutStage: 'off',
349
+ cacheSensitivity: 'none',
350
+ leasePolicy: 'none',
351
+ },
352
+ {
353
+ decisionKey: 'workflow.routing-needed.v1',
354
+ schemaVersion: 1,
355
+ family: 'workflow',
356
+ owner: 'smallTaskMaintainer',
357
+ kind: 'choice',
358
+ description: 'Whether routing/intent classification is needed for a prompt.',
359
+ candidatePolicy: 'owner-shortlist',
360
+ hardConstraints: ['lane-eligibility'],
361
+ probabilityPolicy: 'raw-label',
362
+ fallbackPolicy: 'deterministic-route',
363
+ telemetryClass: 'decision',
364
+ rolloutStage: 'off',
365
+ cacheSensitivity: 'none',
366
+ leasePolicy: 'none',
367
+ },
368
+ {
369
+ decisionKey: 'workflow.step-budget.v1',
370
+ schemaVersion: 1,
371
+ family: 'workflow',
372
+ owner: 'smallTaskMaintainer',
373
+ kind: 'choice',
374
+ description: 'Bounded step-budget class for an in-flight task.',
375
+ candidatePolicy: 'owner-shortlist',
376
+ hardConstraints: ['task-boundary'],
377
+ probabilityPolicy: 'raw-label',
378
+ fallbackPolicy: 'deterministic-budget',
379
+ telemetryClass: 'decision',
380
+ rolloutStage: 'off',
381
+ cacheSensitivity: 'none',
382
+ leasePolicy: 'none',
383
+ },
384
+ {
385
+ decisionKey: 'workflow.compact-now.v1',
386
+ schemaVersion: 1,
387
+ family: 'workflow',
388
+ owner: 'smallTaskMaintainer',
389
+ kind: 'choice',
390
+ description: 'Whether to compact context at the current boundary.',
391
+ candidatePolicy: 'owner-shortlist',
392
+ hardConstraints: ['task-boundary'],
393
+ probabilityPolicy: 'raw-label',
394
+ fallbackPolicy: 'deterministic-threshold',
395
+ telemetryClass: 'decision',
396
+ rolloutStage: 'off',
397
+ cacheSensitivity: 'none',
398
+ leasePolicy: 'none',
399
+ },
400
+ {
401
+ decisionKey: 'workflow.summarize-vs-keep.v1',
402
+ schemaVersion: 1,
403
+ family: 'workflow',
404
+ owner: 'smallTaskMaintainer',
405
+ kind: 'choice',
406
+ description: 'Summarize vs keep for a stored artifact/message.',
407
+ candidatePolicy: 'owner-shortlist',
408
+ hardConstraints: ['task-boundary', 'source-freshness'],
409
+ probabilityPolicy: 'raw-label',
410
+ fallbackPolicy: 'deterministic-threshold',
411
+ telemetryClass: 'decision',
412
+ rolloutStage: 'off',
413
+ cacheSensitivity: 'none',
414
+ leasePolicy: 'none',
415
+ },
272
416
  ]);
273
417
 
274
418
  // Deep-freeze the shipped catalog — entries are shared metadata consumed by