@ngockhoale/ukit 2.7.10 → 2.7.12
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 +12 -1
- package/manifests/instructionRules.yaml +2 -2
- package/package.json +1 -1
- package/src/core/executionContracts.js +0 -2
- package/src/core/output/index.js +10 -6
- package/src/core/runtimeConfig.js +11 -16
- package/src/core/status.js +1 -2
- package/src/index/taskRouting.js +0 -5
- package/templates/.claude/agents/code-reviewer.md +1 -42
- package/templates/.claude/ukit/index/route-task.mjs +0 -2
- package/templates/.claude/ukit/runtime/output-compression.mjs +33 -5
- package/templates/.claude/ukit/runtime/reinject-context.mjs +1 -1
- package/templates/.codex/settings.json +0 -1
- package/templates/.omp/agents/code-reviewer.md +1 -42
- package/templates/.omp/hooks/pre/ukit-bridge.js +21 -0
- package/templates/AGENTS.md +54 -76
- package/templates/CLAUDE.md +54 -76
- package/templates/docs/UKIT_INTERNALS.md +78 -16
- package/templates/instructions/core.md +54 -76
- package/templates/ukit/storage/config.json +12 -23
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
## 2.7.
|
|
6
|
+
## 2.7.12 - 2026-09-22
|
|
7
|
+
|
|
8
|
+
- **Token & latency diet (cycle C45)**:
|
|
9
|
+
- 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.
|
|
10
|
+
- omp sessions now write memory v2 episodes on session stop (`session-episode.sh` wired into the bridge), so memory recall accumulates under omp.
|
|
11
|
+
- System prompt ~23% smaller (AGENTS.md 17.1KB → 13.1KB); all rule IDs preserved.
|
|
12
|
+
- Unattended retry budget reduced to 2 attempts / 2 strategies (UNATTENDED-02).
|
|
13
|
+
- Removed dead config: `memory.autoCapture`, all `advisor*` keys, `subagents.diffReview*`, `postEditReviewPolicy`, and the code-reviewer `REVIEW_TARGET_TYPE=diff` sidecar mode.
|
|
14
|
+
- `memory.maxInjectionTokens` now honored within [160, 640] (was silently clamped to 320).
|
|
15
|
+
- Hook deadline audit: all shipped hooks verified deadline-armed or bounded; watchdog coverage test extended.
|
|
16
|
+
|
|
17
|
+
## 2.7.11 - 2026-09-22
|
|
7
18
|
|
|
8
19
|
- **User permission mode is never overwritten**: `ukit install`/`ukit update`
|
|
9
20
|
used to rewrite `.claude/settings.json` wholesale, so the template's
|
|
@@ -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:
|
|
131
|
-
maxRecoveryStrategies:
|
|
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
|
@@ -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,
|
package/src/core/output/index.js
CHANGED
|
@@ -500,7 +500,7 @@ function isTailSummaryLine(line, profile = null) {
|
|
|
500
500
|
]);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
function buildCompactedSummaryLines(lines, { maxTokens =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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');
|
package/src/core/status.js
CHANGED
|
@@ -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}
|
|
253
|
+
`${padLabel('Model')} ${report.defaultModel}`,
|
|
255
254
|
];
|
|
256
255
|
}
|
package/src/index/taskRouting.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|