@ngockhoale/ukit 2.7.11 → 2.7.13

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
@@ -3,6 +3,21 @@
3
3
  All notable changes to UKit are documented here.
4
4
 
5
5
 
6
+ ## 2.7.13 - 2026-09-22
7
+
8
+ - Same content as 2.7.12 (npm staged-publish never completed; re-published as 2.7.13).
9
+
10
+ ## 2.7.12 - 2026-09-22
11
+
12
+ - **Token & latency diet (cycle C45)**:
13
+ - Output compression now configurable: `tokenPipeline.outputMaxTokens` (default 600) and `outputMaxLines` (default 40); output already under budget passes through uncompressed — no tee file, no forced re-read.
14
+ - omp sessions now write memory v2 episodes on session stop (`session-episode.sh` wired into the bridge), so memory recall accumulates under omp.
15
+ - System prompt ~23% smaller (AGENTS.md 17.1KB → 13.1KB); all rule IDs preserved.
16
+ - Unattended retry budget reduced to 2 attempts / 2 strategies (UNATTENDED-02).
17
+ - Removed dead config: `memory.autoCapture`, all `advisor*` keys, `subagents.diffReview*`, `postEditReviewPolicy`, and the code-reviewer `REVIEW_TARGET_TYPE=diff` sidecar mode.
18
+ - `memory.maxInjectionTokens` now honored within [160, 640] (was silently clamped to 320).
19
+ - Hook deadline audit: all shipped hooks verified deadline-armed or bounded; watchdog coverage test extended.
20
+
6
21
  ## 2.7.11 - 2026-09-22
7
22
 
8
23
  - **User permission mode is never overwritten**: `ukit install`/`ukit update`
@@ -127,8 +127,8 @@ rules:
127
127
  title: Bounded attempts and recovery strategies in unattended mode
128
128
  kind: supporting
129
129
  statement: >-
130
- maxAttemptsPerFailure: 5 per failing verification and
131
- maxRecoveryStrategies: 3 distinct fix strategies before reassessing —
130
+ maxAttemptsPerFailure: 2 per failing verification and
131
+ maxRecoveryStrategies: 2 distinct fix strategies before reassessing —
132
132
  do not loop one strategy.
133
133
  marker_in: [templates/CLAUDE.md, templates/AGENTS.md, CLAUDE.md, AGENTS.md]
134
134
  - id: UNATTENDED-03
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.7.11",
3
+ "version": "2.7.13",
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",
@@ -46,7 +46,6 @@ export const EXECUTION_CONTRACTS = {
46
46
  completionRule: 'require-write-and-verification',
47
47
  delegationPolicy: 'disallow-by-default',
48
48
  completionEvidence: ['write-evidence', 'verification-evidence'],
49
- postEditReviewPolicy: 'sidecar-non-blocking',
50
49
  },
51
50
  'find-cause': {
52
51
  maxReadPassesBeforeReassess: 3,
@@ -63,7 +62,6 @@ export const EXECUTION_CONTRACTS = {
63
62
  delegationPolicy: 'allow-qualified-sidecar',
64
63
  completionEvidence: ['write-evidence', 'verification-evidence'],
65
64
  mirrorConsistencyRequired: true,
66
- postEditReviewPolicy: 'sidecar-non-blocking',
67
65
  },
68
66
  'map-impact': {
69
67
  maxReadPasses: 3,
@@ -500,7 +500,7 @@ function isTailSummaryLine(line, profile = null) {
500
500
  ]);
501
501
  }
502
502
 
503
- function buildCompactedSummaryLines(lines, { maxTokens = 180, maxLines = 10, forceFirstCount = 1 } = {}) {
503
+ function buildCompactedSummaryLines(lines, { maxTokens = 600, maxLines = 40, forceFirstCount = 1 } = {}) {
504
504
  const hardTokenCap = Math.max(maxTokens, Math.ceil(maxTokens * 2));
505
505
  const sourceLines = Array.isArray(lines) ? lines : [];
506
506
  const selected = [];
@@ -1007,7 +1007,8 @@ export function compressToolOutput({
1007
1007
  stdout = '',
1008
1008
  stderr = '',
1009
1009
  exitCode = null,
1010
- maxTokens = 180,
1010
+ maxTokens = 600,
1011
+ maxLines = 40,
1011
1012
  projectRoot = null,
1012
1013
  } = {}) {
1013
1014
  const commandText = String(command ?? '').trim();
@@ -1024,7 +1025,7 @@ export function compressToolOutput({
1024
1025
  const headerLine = `- Recent command: ${displayCommand || 'unknown command'}${exitCode === null || exitCode === undefined ? '' : ` (exit ${exitCode})`}`;
1025
1026
  const compactedLines = buildCompactedSummaryLines([headerLine, ...candidateLines.map((line) => `- ${line}`)], {
1026
1027
  maxTokens,
1027
- maxLines: 10,
1028
+ maxLines,
1028
1029
  });
1029
1030
  let summary = compactedLines.join('\n').trim();
1030
1031
  let validationMode = 'default';
@@ -1037,7 +1038,7 @@ export function compressToolOutput({
1037
1038
  ...candidateLines.map((line) => `- ${line}`),
1038
1039
  ], {
1039
1040
  maxTokens,
1040
- maxLines: 10,
1041
+ maxLines,
1041
1042
  forceFirstCount: Math.min(1 + anchorLines.length, 5),
1042
1043
  });
1043
1044
  const anchorFirstSummary = anchorFirstLines.join('\n').trim();
@@ -1052,7 +1053,7 @@ export function compressToolOutput({
1052
1053
  ...anchorLines.map((line) => `- ${line}`),
1053
1054
  ], {
1054
1055
  maxTokens,
1055
- maxLines: 10,
1056
+ maxLines,
1056
1057
  forceFirstCount: Math.min(1 + anchorLines.length, 5),
1057
1058
  }).join('\n').trim();
1058
1059
  validationMode = 'forced-anchors';
@@ -1236,7 +1237,8 @@ export async function captureCompressedToolOutput(
1236
1237
  stderr = '',
1237
1238
  exitCode = null,
1238
1239
  promptCache = true,
1239
- maxTokens = 180,
1240
+ maxTokens = 600,
1241
+ maxLines = 40,
1240
1242
  } = {},
1241
1243
  ) {
1242
1244
  const requestKey = buildCompactMachineKey('tool-output-v1', {
@@ -1245,6 +1247,7 @@ export async function captureCompressedToolOutput(
1245
1247
  stderrHash: buildCompactMachineKey('stderr', stripAnsi(stderr)),
1246
1248
  exitCode,
1247
1249
  maxTokens,
1250
+ maxLines,
1248
1251
  });
1249
1252
 
1250
1253
  if (promptCache) {
@@ -1293,6 +1296,7 @@ export async function captureCompressedToolOutput(
1293
1296
  stderr,
1294
1297
  exitCode,
1295
1298
  maxTokens,
1299
+ maxLines,
1296
1300
  projectRoot,
1297
1301
  });
1298
1302
  const recoveryReason = buildRawOutputRecoveryReason({
@@ -130,18 +130,16 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
130
130
  inputCompression: true,
131
131
  outputCompression: true,
132
132
  promptCache: true,
133
+ outputMaxTokens: 600,
134
+ outputMaxLines: 40,
133
135
  },
134
136
  router: {
135
137
  enabled: true,
136
138
  defaultModel: 'claude-sonnet-5',
137
- advisorModel: 'claude-opus-5',
138
- advisorEnabled: true,
139
- maxAdvisorCalls: 3,
140
139
  },
141
140
  orchestration: {
142
141
  enabled: true,
143
142
  orchestratorModel: 'claude-sonnet-5',
144
- advisorEnabled: true,
145
143
  permissionMode: 'unattended',
146
144
  // FR-003: eval-subprocess gate for rendered omp approval (tools.approval.eval).
147
145
  // Fail-closed: only an explicit user `true` renders `eval: allow`; absent,
@@ -207,9 +205,8 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
207
205
  },
208
206
  memory: {
209
207
  enabled: true,
210
- autoCapture: true,
211
208
  progressiveRetrieval: true,
212
- maxInjectionTokens: 1000,
209
+ maxInjectionTokens: 320,
213
210
  archiveAfterDays: 30,
214
211
  maxSessions: 20,
215
212
  maxArchivedSessions: 50,
@@ -245,9 +242,6 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
245
242
  enabled: true,
246
243
  smallTaskModel: 'unic-lite',
247
244
  smallTaskAgent: 'ukit-small-task-maintainer',
248
- diffReviewEnabled: true,
249
- diffReviewAgent: 'code-reviewer',
250
- diffReviewModel: 'unic-smart',
251
245
  visionEnabled: true,
252
246
  visionModel: 'unic-vision',
253
247
  visionAgent: 'ukit-vision-analyst',
@@ -383,6 +377,14 @@ export function validateRuntimeConfig(config) {
383
377
  pushBooleanError(errors, config.tokenPipeline.inputCompression, 'tokenPipeline.inputCompression');
384
378
  pushBooleanError(errors, config.tokenPipeline.outputCompression, 'tokenPipeline.outputCompression');
385
379
  pushBooleanError(errors, config.tokenPipeline.promptCache, 'tokenPipeline.promptCache');
380
+ // FR-001: optional-present — absent keys fall back to code defaults on merge;
381
+ // when present they must be positive numbers.
382
+ if (config.tokenPipeline.outputMaxTokens !== undefined) {
383
+ pushPositiveNumberError(errors, config.tokenPipeline.outputMaxTokens, 'tokenPipeline.outputMaxTokens');
384
+ }
385
+ if (config.tokenPipeline.outputMaxLines !== undefined) {
386
+ pushPositiveNumberError(errors, config.tokenPipeline.outputMaxLines, 'tokenPipeline.outputMaxLines');
387
+ }
386
388
  }
387
389
 
388
390
  if (!isPlainObject(config.router)) {
@@ -392,11 +394,6 @@ export function validateRuntimeConfig(config) {
392
394
  if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim() === '') {
393
395
  errors.push('router.defaultModel must be a non-empty string.');
394
396
  }
395
- if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim() === '') {
396
- errors.push('router.advisorModel must be a non-empty string.');
397
- }
398
- pushBooleanError(errors, config.router.advisorEnabled, 'router.advisorEnabled');
399
- pushPositiveNumberError(errors, config.router.maxAdvisorCalls, 'router.maxAdvisorCalls');
400
397
  }
401
398
 
402
399
  if (!isPlainObject(config.orchestration)) {
@@ -404,7 +401,6 @@ export function validateRuntimeConfig(config) {
404
401
  } else {
405
402
  pushBooleanError(errors, config.orchestration.enabled, 'orchestration.enabled');
406
403
  pushNonEmptyStringError(errors, config.orchestration.orchestratorModel, 'orchestration.orchestratorModel');
407
- pushBooleanError(errors, config.orchestration.advisorEnabled, 'orchestration.advisorEnabled');
408
404
  // permissionMode is optional-present: absent → tolerated (pre-C35 configs);
409
405
  // present → must be a reserved mode.
410
406
  if (config.orchestration.permissionMode !== undefined
@@ -560,7 +556,6 @@ export function validateRuntimeConfig(config) {
560
556
  errors.push('memory must be an object.');
561
557
  } else {
562
558
  pushBooleanError(errors, config.memory.enabled, 'memory.enabled');
563
- pushBooleanError(errors, config.memory.autoCapture, 'memory.autoCapture');
564
559
  pushBooleanError(errors, config.memory.progressiveRetrieval, 'memory.progressiveRetrieval');
565
560
  pushPositiveNumberError(errors, config.memory.maxInjectionTokens, 'memory.maxInjectionTokens');
566
561
  pushPositiveNumberError(errors, config.memory.archiveAfterDays, 'memory.archiveAfterDays');
@@ -201,7 +201,6 @@ export async function buildStatusReport(projectRoot) {
201
201
  (entry) => `${entry.profile} ${entry.entryCount} / ${entry.savingsLabel}`,
202
202
  ),
203
203
  defaultModel: config.router.defaultModel,
204
- advisorEnabled: Boolean(config.router.advisorEnabled),
205
204
  projectImportant,
206
205
  };
207
206
  }
@@ -251,6 +250,6 @@ export function formatStatusReport(report) {
251
250
  `${padLabel('Cache lanes')} ${report.cacheLanes}`,
252
251
  `${padLabel('Output comp.')} ${report.outputCompression}`,
253
252
  `${padLabel('Output lanes')} ${report.outputLanes}`,
254
- `${padLabel('Model')} ${report.defaultModel} (advisor: ${report.advisorEnabled ? 'on' : 'off'})`,
253
+ `${padLabel('Model')} ${report.defaultModel}`,
255
254
  ];
256
255
  }
@@ -271,9 +271,6 @@ export function buildRouteSummary({
271
271
  });
272
272
  const executionContract = buildExecutionContract(executionMode);
273
273
  const continuousExecution = buildContinuousExecutionPolicy(autonomyLevel);
274
- const postEditReview = executionContract?.postEditReviewPolicy
275
- ? { policy: executionContract.postEditReviewPolicy, agent: 'code-reviewer', reviewTargetType: 'diff' }
276
- : null;
277
274
  const completionState = buildCompletionState({
278
275
  executionMode,
279
276
  verificationRecommendation,
@@ -311,7 +308,6 @@ export function buildRouteSummary({
311
308
  editGuardHint ? `editGuard=${editGuardHint}` : null,
312
309
  delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
313
310
  policyMode ? `policy=${policyMode}` : null,
314
- postEditReview ? `review=${postEditReview.agent}(${postEditReview.reviewTargetType})` : null,
315
311
  handoffBudget?.warning ? `budget=${handoffBudget.warning}` : null,
316
312
  worklogBudget?.warning ? `budget=${worklogBudget.warning}` : null,
317
313
  ].filter(Boolean).join(' | ');
@@ -328,7 +324,6 @@ export function buildRouteSummary({
328
324
  approachSelector,
329
325
  executionContract,
330
326
  completionState,
331
- postEditReview,
332
327
  continuationState,
333
328
  autonomyLevel,
334
329
  continuousExecution,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-reviewer
3
- description: "Independent reviewer for handoff Phase 3, for spec/plan documents, and for non-blocking sidecar diff review of daily-flow edits. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found. For sidecar diff review (set REVIEW_TARGET_TYPE=diff): reviews the current uncommitted git diff after a local-build/shared-edit task, runs in the background and never blocks the main task, produces STATUS: clean | issues-found."
3
+ description: "Independent reviewer for handoff Phase 3 and for spec/plan documents. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found."
4
4
  model: opus # unic-smart
5
5
  color: yellow
6
6
  tools: ["Read", "Grep", "Glob", "Bash"]
@@ -14,7 +14,6 @@ You are the independent reviewer for UKit's handoff Quality Gate. Your model is
14
14
 
15
15
  - `code` (default, if not specified) — reviewing a handoff task diff. Follow **Code Review** below, unchanged.
16
16
  - `spec` | `plan` — reviewing a document (e.g. `docs/plans/*.md`), no diff/task file/executor report involved. Skip straight to **Spec/Plan Review** at the end of this file instead.
17
- - `diff` — non-blocking sidecar review of the current uncommitted diff in the daily (non-handoff) flow. No task file/executor report/model-isolation check involved. Skip straight to **Sidecar Diff Review** at the end of this file instead.
18
17
 
19
18
  ## Code Review (REVIEW_TARGET_TYPE=code)
20
19
 
@@ -175,43 +174,3 @@ NOTES: [1-2 sentences if needed]
175
174
 
176
175
  `<N>` = 1 + however many `### Round` entries already exist in the log (1 if this is the first review).
177
176
 
178
- ## Sidecar Diff Review (REVIEW_TARGET_TYPE=diff)
179
-
180
- This mode exists so a weaker daily-flow executor model still gets a second pair of eyes,
181
- without adding wait time to the main task. You are launched in the background right after
182
- the main task already has write + verification evidence; the caller is not waiting on you.
183
-
184
- ### Inputs you expect
185
-
186
- - No task file, no executor report, no model-isolation check. Just read the current uncommitted
187
- diff yourself: `git diff` (and `git diff --stat` for an overview). If there is no diff, report
188
- `STATUS: clean` with `FINDINGS: none` and stop.
189
-
190
- ### Review order
191
-
192
- Apply the same lenses as Code Review's steps 2-7, scoped to what the diff actually touches:
193
-
194
- 1. **Correctness** — Does the diff do what it looks like it's trying to do? Wrong assumptions, stale refs, missing cases?
195
- 2. **Regression risk** — Any existing behavior/tests/contracts this plausibly breaks?
196
- 3. **Safety / security / data loss** — Destructive actions, auth/permission, path handling, unsafe shell/DB/file ops.
197
- 4. **Performance / scale** — Accidental N+1, repeated I/O, large scans in hot paths.
198
- 5. **Maintainability** — Duplicated logic, dead branches, misleading naming, drift between docs/tests/source.
199
- 6. **Solution fit** — Apply the same 5 evidence-first questions as Code Review step 7 (duplicate semantics / standard library or native suffices / simplification dropping validation, security, accessibility or tests / shared root cause / speculative abstraction). **Never grade brevity or line count as a quality win.** Findings must cite the construct + reason, and here they remain advisory hypotheses only — never a delete-list, never an edit.
200
-
201
- Do not re-run the project's full verification suite here — this is an advisory pass, not a gate.
202
- You may read files for context but this mode never edits anything.
203
-
204
- ### Output
205
-
206
- Keep it short — this is a quick advisory pass, not a full verdict:
207
-
208
- ```
209
- STATUS: clean | issues-found
210
- FINDINGS:
211
- - file:line — what's wrong, why it matters
212
- NOTES: [advisory only, non-blocking — 1 sentence if needed]
213
- ```
214
-
215
- There is no task file or INDEX.md to update in this mode. Findings are advisory only: the main
216
- task is not blocked on this review and may already be reported done by the time you finish.
217
- Report back to the caller in a few lines; do not paste the full diff.
@@ -2683,7 +2683,6 @@ function buildExecutionContract(executionMode = null) {
2683
2683
  completionRule: 'require-write-and-verification',
2684
2684
  delegationPolicy: 'disallow-by-default',
2685
2685
  completionEvidence: ['write-evidence', 'verification-evidence'],
2686
- postEditReviewPolicy: 'sidecar-non-blocking',
2687
2686
  },
2688
2687
  'find-cause': {
2689
2688
  modelTier: 'code',
@@ -2702,7 +2701,6 @@ function buildExecutionContract(executionMode = null) {
2702
2701
  delegationPolicy: 'allow-qualified-sidecar',
2703
2702
  completionEvidence: ['write-evidence', 'verification-evidence'],
2704
2703
  mirrorConsistencyRequired: true,
2705
- postEditReviewPolicy: 'sidecar-non-blocking',
2706
2704
  },
2707
2705
  'map-impact': {
2708
2706
  modelTier: 'code',
@@ -60,7 +60,8 @@ if (Number.isFinite(HOOK_DEADLINE_MS) && HOOK_DEADLINE_MS > 0) {
60
60
  }
61
61
 
62
62
  const ANSI_RE = /\u001b\[[0-9;]*m/g;
63
- const DEFAULT_MAX_OUTPUT_TOKENS = 180;
63
+ const DEFAULT_MAX_OUTPUT_TOKENS = 600;
64
+ const DEFAULT_MAX_OUTPUT_LINES = 40;
64
65
  const DEFAULT_OUTPUT_HISTORY_MAX_ENTRIES = 25;
65
66
  const RAW_OUTPUT_SAVE_MIN_TOKENS = 600;
66
67
  const RAW_OUTPUT_SAVE_MIN_SAVED_TOKENS = 250;
@@ -582,7 +583,7 @@ function isTailSummaryLine(line, profile = null) {
582
583
  ]);
583
584
  }
584
585
 
585
- function buildCompactedSummaryLines(lines, { maxTokens = DEFAULT_MAX_OUTPUT_TOKENS, maxLines = 10, forceFirstCount = 1 } = {}) {
586
+ function buildCompactedSummaryLines(lines, { maxTokens = DEFAULT_MAX_OUTPUT_TOKENS, maxLines = DEFAULT_MAX_OUTPUT_LINES, forceFirstCount = 1 } = {}) {
586
587
  const sourceLines = Array.isArray(lines) ? lines : [];
587
588
  const selected = [];
588
589
  const seen = new Set();
@@ -1089,6 +1090,7 @@ function compressToolOutput({
1089
1090
  stderr = '',
1090
1091
  exitCode = null,
1091
1092
  maxTokens = DEFAULT_MAX_OUTPUT_TOKENS,
1093
+ maxLines = DEFAULT_MAX_OUTPUT_LINES,
1092
1094
  projectRoot = null,
1093
1095
  } = {}) {
1094
1096
  const commandText = String(command ?? '').trim();
@@ -1105,7 +1107,7 @@ function compressToolOutput({
1105
1107
  const headerLine = `- Recent command: ${displayCommand || 'unknown command'}${exitCode === null || exitCode === undefined ? '' : ` (exit ${exitCode})`}`;
1106
1108
  const compactedLines = buildCompactedSummaryLines([headerLine, ...candidateLines.map((line) => `- ${line}`)], {
1107
1109
  maxTokens,
1108
- maxLines: 10,
1110
+ maxLines,
1109
1111
  });
1110
1112
  let summary = compactedLines.join('\n').trim();
1111
1113
  const missingAnchors = findMissingAnchors(summary, anchorLines);
@@ -1117,7 +1119,7 @@ function compressToolOutput({
1117
1119
  ...candidateLines.map((line) => `- ${line}`),
1118
1120
  ], {
1119
1121
  maxTokens,
1120
- maxLines: 10,
1122
+ maxLines,
1121
1123
  forceFirstCount: Math.min(1 + anchorLines.length, 5),
1122
1124
  }).join('\n').trim();
1123
1125
 
@@ -1130,7 +1132,7 @@ function compressToolOutput({
1130
1132
  ...anchorLines.map((line) => `- ${line}`),
1131
1133
  ], {
1132
1134
  maxTokens,
1133
- maxLines: 10,
1135
+ maxLines,
1134
1136
  forceFirstCount: Math.min(1 + anchorLines.length, 5),
1135
1137
  }).join('\n').trim();
1136
1138
  }
@@ -1317,10 +1319,19 @@ async function loadRuntimeConfig(projectRoot) {
1317
1319
  tokenPipeline: {
1318
1320
  outputCompression: true,
1319
1321
  promptCache: true,
1322
+ outputMaxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
1323
+ outputMaxLines: DEFAULT_MAX_OUTPUT_LINES,
1320
1324
  },
1321
1325
  }, await readJson(runtimePaths.configPath, {}));
1322
1326
  }
1323
1327
 
1328
+ // FR-001: the hook's local merge does not run schema validation — a malformed
1329
+ // value ("abc", -5) must fall back to the default, never throw or disable the
1330
+ // skip-small fast path.
1331
+ function positiveNumberOrDefault(value, fallback) {
1332
+ return (typeof value === 'number' && Number.isFinite(value) && value > 0) ? value : fallback;
1333
+ }
1334
+
1324
1335
  async function main() {
1325
1336
  const rawInput = await readStdin();
1326
1337
  const payload = (() => {
@@ -1360,11 +1371,26 @@ async function main() {
1360
1371
  return;
1361
1372
  }
1362
1373
 
1374
+ // FR-003 skip-small fast path: when the raw output already fits the configured
1375
+ // budget, exit before the prompt-cache lookup, tee write, and history append —
1376
+ // compressing it would cost a re-read that outweighs the savings.
1377
+ const outputMaxTokens = positiveNumberOrDefault(
1378
+ config?.tokenPipeline?.outputMaxTokens, DEFAULT_MAX_OUTPUT_TOKENS);
1379
+ const outputMaxLines = positiveNumberOrDefault(
1380
+ config?.tokenPipeline?.outputMaxLines, DEFAULT_MAX_OUTPUT_LINES);
1381
+ const rawText = [command, stdout, stderr].filter(Boolean).join('\n');
1382
+ if (estimateTokenCount(rawText) <= outputMaxTokens) {
1383
+ process.exit(0);
1384
+ return;
1385
+ }
1386
+
1363
1387
  const requestKey = buildCompactMachineKey('tool-output-v1', {
1364
1388
  command,
1365
1389
  stdoutHash: buildCompactMachineKey('stdout', stripAnsi(stdout)),
1366
1390
  stderrHash: buildCompactMachineKey('stderr', stripAnsi(stderr)),
1367
1391
  exitCode,
1392
+ maxTokens: outputMaxTokens,
1393
+ maxLines: outputMaxLines,
1368
1394
  });
1369
1395
 
1370
1396
  if (config?.tokenPipeline?.promptCache) {
@@ -1410,6 +1436,8 @@ async function main() {
1410
1436
  stdout,
1411
1437
  stderr,
1412
1438
  exitCode,
1439
+ maxTokens: outputMaxTokens,
1440
+ maxLines: outputMaxLines,
1413
1441
  projectRoot,
1414
1442
  });
1415
1443
 
@@ -143,7 +143,7 @@ async function main() {
143
143
  || line.startsWith('- Recent command output:')
144
144
  || line.startsWith('- Recent delegation hint:')
145
145
  ));
146
- const maxTokens = Math.max(160, Math.min(320, Number(config.memory?.maxInjectionTokens) || 320));
146
+ const maxTokens = Math.max(160, Math.min(640, Number(config.memory?.maxInjectionTokens) || 320));
147
147
  const thresholdPlan = await buildThresholdCompactPlan({
148
148
  state: pressureState,
149
149
  config,
@@ -262,7 +262,6 @@
262
262
  "enabled": true,
263
263
  "configPath": ".ukit/storage/config.json",
264
264
  "modelField": "orchestration.orchestratorModel",
265
- "advisorField": "orchestration.advisorEnabled",
266
265
  "defaultModel": "claude-sonnet-5",
267
266
  "internalOnly": true,
268
267
  "qualityFirst": true,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-reviewer
3
- description: "Independent reviewer for handoff Phase 3, for spec/plan documents, and for non-blocking sidecar diff review of daily-flow edits. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found. For sidecar diff review (set REVIEW_TARGET_TYPE=diff): reviews the current uncommitted git diff after a local-build/shared-edit task, runs in the background and never blocks the main task, produces STATUS: clean | issues-found."
3
+ description: "Independent reviewer for handoff Phase 3 and for spec/plan documents. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found."
4
4
  model: "@slow"
5
5
  tools: ["read","grep","glob","bash"]
6
6
  ---
@@ -13,7 +13,6 @@ You are the independent reviewer for UKit's handoff Quality Gate. Your model is
13
13
 
14
14
  - `code` (default, if not specified) — reviewing a handoff task diff. Follow **Code Review** below, unchanged.
15
15
  - `spec` | `plan` — reviewing a document (e.g. `docs/plans/*.md`), no diff/task file/executor report involved. Skip straight to **Spec/Plan Review** at the end of this file instead.
16
- - `diff` — non-blocking sidecar review of the current uncommitted diff in the daily (non-handoff) flow. No task file/executor report/model-isolation check involved. Skip straight to **Sidecar Diff Review** at the end of this file instead.
17
16
 
18
17
  ## Code Review (REVIEW_TARGET_TYPE=code)
19
18
 
@@ -174,43 +173,3 @@ NOTES: [1-2 sentences if needed]
174
173
 
175
174
  `<N>` = 1 + however many `### Round` entries already exist in the log (1 if this is the first review).
176
175
 
177
- ## Sidecar Diff Review (REVIEW_TARGET_TYPE=diff)
178
-
179
- This mode exists so a weaker daily-flow executor model still gets a second pair of eyes,
180
- without adding wait time to the main task. You are launched in the background right after
181
- the main task already has write + verification evidence; the caller is not waiting on you.
182
-
183
- ### Inputs you expect
184
-
185
- - No task file, no executor report, no model-isolation check. Just read the current uncommitted
186
- diff yourself: `git diff` (and `git diff --stat` for an overview). If there is no diff, report
187
- `STATUS: clean` with `FINDINGS: none` and stop.
188
-
189
- ### Review order
190
-
191
- Apply the same lenses as Code Review's steps 2-7, scoped to what the diff actually touches:
192
-
193
- 1. **Correctness** — Does the diff do what it looks like it's trying to do? Wrong assumptions, stale refs, missing cases?
194
- 2. **Regression risk** — Any existing behavior/tests/contracts this plausibly breaks?
195
- 3. **Safety / security / data loss** — Destructive actions, auth/permission, path handling, unsafe shell/DB/file ops.
196
- 4. **Performance / scale** — Accidental N+1, repeated I/O, large scans in hot paths.
197
- 5. **Maintainability** — Duplicated logic, dead branches, misleading naming, drift between docs/tests/source.
198
- 6. **Solution fit** — Apply the same 5 evidence-first questions as Code Review step 7 (duplicate semantics / standard library or native suffices / simplification dropping validation, security, accessibility or tests / shared root cause / speculative abstraction). **Never grade brevity or line count as a quality win.** Findings must cite the construct + reason, and here they remain advisory hypotheses only — never a delete-list, never an edit.
199
-
200
- Do not re-run the project's full verification suite here — this is an advisory pass, not a gate.
201
- You may read files for context but this mode never edits anything.
202
-
203
- ### Output
204
-
205
- Keep it short — this is a quick advisory pass, not a full verdict:
206
-
207
- ```
208
- STATUS: clean | issues-found
209
- FINDINGS:
210
- - file:line — what's wrong, why it matters
211
- NOTES: [advisory only, non-blocking — 1 sentence if needed]
212
- ```
213
-
214
- There is no task file or INDEX.md to update in this mode. Findings are advisory only: the main
215
- task is not blocked on this review and may already be reported done by the time you finish.
216
- Report back to the caller in a few lines; do not paste the full diff.
@@ -64,6 +64,11 @@ export const HOOK_EVENT_MAP = {
64
64
  before_agent_start: ['sensitive-data-guard.mjs', 'skill-router.sh', 'vision-router.sh', 'context-window-guard.sh'],
65
65
  'session.compacting': ['reinject-context.sh'],
66
66
  session_start: ['project-important.sh', 'auto-prune-bash.sh', 'reset-compact-pressure.sh', 'handoff-resume.sh'],
67
+ // TASK-002 (cycle 45): omp emits `session_stop` (no `session_end`); the chain
68
+ // runs only on RELEASED stops inside runSessionStop — never on blocked or
69
+ // dedupe-skipped stops, since `ukit memory episode` dedupes on meta.ledgerKey
70
+ // and a partial episode would suppress the real one.
71
+ session_stop: ['session-episode.sh'],
67
72
  };
68
73
 
69
74
  const TOOL_NAME_MAP = {
@@ -129,6 +134,7 @@ export const ADVISORY_SCRIPTS = new Set([
129
134
  'auto-prune-bash.sh',
130
135
  'reset-compact-pressure.sh',
131
136
  'handoff-resume.sh',
137
+ 'session-episode.sh',
132
138
  ]);
133
139
 
134
140
  function classifyFailure(scriptName) {
@@ -971,6 +977,19 @@ function loadStopCoordinatorModule() {
971
977
  }
972
978
  return stopCoordinatorModulePromise;
973
979
  }
980
+ // TASK-002 (cycle 45): the session-episode chain is advisory end-to-end — a
981
+ // failing script (or a transport failure) warns and never changes the stop
982
+ // decision. Called ONLY on released stops: a blocked stop means the turn
983
+ // continues, and `ukit memory episode` dedupes on meta.ledgerKey, so firing
984
+ // early would freeze a partial episode and suppress the real one.
985
+ async function runSessionEpisodeChain(pi, payload, projectRoot) {
986
+ try {
987
+ await runScriptChain(pi, HOOK_EVENT_MAP.session_stop, payload, { projectRoot });
988
+ } catch (error) {
989
+ pi.logger?.warn?.(`[UKit] session-episode chain failed (advisory): ${error?.message || error}`);
990
+ }
991
+ }
992
+
974
993
 
975
994
  export async function runSessionStop(
976
995
  pi,
@@ -1061,6 +1080,7 @@ export async function runSessionStop(
1061
1080
  + 'The completion gate is DOWN, not your work: run `ukit install` to repair, '
1062
1081
  + 'then re-send the task in a new message if the run must continue.',
1063
1082
  ], 'nextTurn', { display: true });
1083
+ await runSessionEpisodeChain(pi, payload, projectRoot);
1064
1084
  return undefined;
1065
1085
  }
1066
1086
  pi.logger?.warn?.(`[UKit] completion evaluator crashed (streak ${streak}/${maxStreak}): ${error?.message || error}`);
@@ -1099,6 +1119,7 @@ export async function runSessionStop(
1099
1119
  if (handoffBlock) {
1100
1120
  return { continue: true, additionalContext: handoffBlock };
1101
1121
  }
1122
+ await runSessionEpisodeChain(pi, payload, projectRoot);
1102
1123
  return undefined;
1103
1124
  }
1104
1125