@ngockhoale/ukit 2.6.2 → 2.6.4
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 +97 -0
- package/package.json +1 -1
- package/scripts/bench/parallel-agents.mjs +42 -4
- package/scripts/release/verify-release.mjs +43 -6
- package/src/core/applyPlan.js +65 -0
- package/src/core/compact/threshold.js +33 -30
- package/src/core/fileOps.js +9 -2
- package/src/core/runInstallPipeline.js +10 -1
- package/src/core/status.js +1 -1
- package/templates/.claude/hooks/auto-allow-bash.sh +29 -20
- package/templates/.claude/hooks/auto-prune-bash.sh +48 -30
- package/templates/.claude/hooks/completion-gate.sh +22 -1
- package/templates/.claude/hooks/context-hardcap-gate.sh +19 -4
- package/templates/.claude/hooks/handoff-model-guard.sh +10 -1
- package/templates/.claude/hooks/handoff-resume.sh +19 -17
- package/templates/.claude/hooks/post-edit-verify.sh +1 -0
- package/templates/.claude/hooks/pre-edit-backup.sh +1 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +39 -22
- package/templates/.claude/hooks/stale-spec-guard.sh +1 -0
- package/templates/.claude/hooks/verification-guard.sh +17 -1
- package/templates/.claude/settings.json +4 -4
- package/templates/.claude/ukit/index/extract-image.mjs +49 -0
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +62 -40
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +34 -5
- package/templates/.claude/ukit/runtime/hook-chain-budget.mjs +11 -4
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +7 -3
- package/templates/.claude/ukit/runtime/output-compression.mjs +16 -5
- package/templates/.claude/ukit/runtime/reinject-context.mjs +2 -2
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +3 -0
- package/templates/.claude/ukit/runtime/token-utils.mjs +9 -2
- package/templates/.omp/hooks/pre/ukit-bridge.js +72 -4
- package/templates/ukit/storage/config.json +2 -0
|
@@ -394,12 +394,16 @@ function sweepHookErrorsDir(dir, {
|
|
|
394
394
|
return { scanned: 0, removed: 0 };
|
|
395
395
|
}
|
|
396
396
|
const cutoff = now() - maxAgeMs;
|
|
397
|
+
// BUG-C22-13: eligible count comes from the full name listing (bounded by the
|
|
398
|
+
// single readdir, no per-name stat) so the count cap is enforceable even
|
|
399
|
+
// though maxFiles > maxEntries — capping `scanned` first made
|
|
400
|
+
// `entries.length` an under-count and `overflow` was always 0.
|
|
401
|
+
const eligibleNames = names.filter((name) => name.endsWith('.jsonl'));
|
|
397
402
|
const entries = [];
|
|
398
403
|
let scanned = 0;
|
|
399
|
-
for (const name of
|
|
404
|
+
for (const name of eligibleNames) {
|
|
400
405
|
if (scanned >= maxEntries) break;
|
|
401
406
|
scanned += 1;
|
|
402
|
-
if (!name.endsWith('.jsonl')) continue;
|
|
403
407
|
try {
|
|
404
408
|
const stats = fs.statSync(path.join(dir, name));
|
|
405
409
|
if (stats.isFile()) entries.push({ name, mtimeMs: stats.mtimeMs });
|
|
@@ -409,7 +413,7 @@ function sweepHookErrorsDir(dir, {
|
|
|
409
413
|
// Overflow counts eligible FILES only — foreign entries (non-matching names,
|
|
410
414
|
// dirs) inflate `names.length` and would evict real entries while the dir is
|
|
411
415
|
// actually under the cap.
|
|
412
|
-
const overflow = Math.max(0,
|
|
416
|
+
const overflow = Math.max(0, eligibleNames.length - maxFiles);
|
|
413
417
|
let removed = 0;
|
|
414
418
|
for (const entry of entries) {
|
|
415
419
|
if (removed >= maxRemovals) break;
|
|
@@ -820,6 +824,42 @@ export async function runSessionCompact(pi, event, { projectRoot, context: exten
|
|
|
820
824
|
return undefined;
|
|
821
825
|
}
|
|
822
826
|
|
|
827
|
+
// ── BUG-C22-05 crash-streak breaker (parity with .claude/hooks/completion-gate.sh) ──
|
|
828
|
+
// A persistently crashing completion evaluator must not fail every Stop forever.
|
|
829
|
+
// The byte-append file is shared with the shell hook so one streak tracks both
|
|
830
|
+
// harnesses; a clean evaluation deletes it, so the count is consecutive crashes.
|
|
831
|
+
const COMPLETION_CRASH_STREAK_FILE = path.join('.ukit', 'storage', 'cache', 'completion-gate-crash.streak');
|
|
832
|
+
|
|
833
|
+
async function bumpCompletionCrashStreak(projectRoot) {
|
|
834
|
+
const file = path.join(projectRoot, COMPLETION_CRASH_STREAK_FILE);
|
|
835
|
+
try {
|
|
836
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
837
|
+
await fs.promises.appendFile(file, '.');
|
|
838
|
+
const buf = await fs.promises.readFile(file);
|
|
839
|
+
return buf.length || 1;
|
|
840
|
+
} catch {
|
|
841
|
+
return 1;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function resetCompletionCrashStreak(projectRoot) {
|
|
846
|
+
try {
|
|
847
|
+
await fs.promises.rm(path.join(projectRoot, COMPLETION_CRASH_STREAK_FILE), { force: true });
|
|
848
|
+
} catch { /* best effort */ }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function readStopGateMaxCrashStreaks(projectRoot) {
|
|
852
|
+
try {
|
|
853
|
+
const parsed = JSON.parse(
|
|
854
|
+
await fs.promises.readFile(path.join(projectRoot, '.ukit', 'storage', 'config.json'), 'utf8'),
|
|
855
|
+
);
|
|
856
|
+
const value = parsed?.handoff?.fullstack?.stopGateMaxCrashStreaks;
|
|
857
|
+
return Number.isFinite(value) ? Math.max(1, value) : 3;
|
|
858
|
+
} catch {
|
|
859
|
+
return 3;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
823
863
|
export async function runSessionStop(
|
|
824
864
|
pi,
|
|
825
865
|
event,
|
|
@@ -834,7 +874,35 @@ export async function runSessionStop(
|
|
|
834
874
|
const payload = buildHookPayload('Stop', metadata);
|
|
835
875
|
const state = suppliedState ?? await readRouteState(projectRoot, payload);
|
|
836
876
|
const ledger = suppliedLedger ?? await readExecutionLedger(projectRoot, payload) ?? {};
|
|
837
|
-
|
|
877
|
+
let evaluation;
|
|
878
|
+
try {
|
|
879
|
+
evaluation = evaluateCompletion({ state, ledger });
|
|
880
|
+
} catch (error) {
|
|
881
|
+
// BUG-C22-05: fail-closed like the shell gate (continue the turn), but only up
|
|
882
|
+
// to the configured consecutive-crash cap — a persistently broken evaluator
|
|
883
|
+
// must release loudly instead of looping every Stop forever.
|
|
884
|
+
const streak = await bumpCompletionCrashStreak(projectRoot);
|
|
885
|
+
const maxStreak = await readStopGateMaxCrashStreaks(projectRoot);
|
|
886
|
+
if (streak > maxStreak) {
|
|
887
|
+
await resetCompletionCrashStreak(projectRoot);
|
|
888
|
+
sendContext(pi, [
|
|
889
|
+
`[UKit] stop coordinator released this stop after ${maxStreak} consecutive infrastructure failures `
|
|
890
|
+
+ '(the completion evaluator crashed every Stop — gate fail-closed cap reached). '
|
|
891
|
+
+ 'The completion gate is DOWN, not your work: run `ukit install` to repair, '
|
|
892
|
+
+ 'then re-send the task in a new message if the run must continue.',
|
|
893
|
+
], 'nextTurn', { display: true });
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
pi.logger?.warn?.(`[UKit] completion evaluator crashed (streak ${streak}/${maxStreak}): ${error?.message || error}`);
|
|
897
|
+
return {
|
|
898
|
+
continue: true,
|
|
899
|
+
additionalContext:
|
|
900
|
+
`UKit stop coordinator: infrastructure failure while evaluating this stop (crash streak ${streak}/${maxStreak}) — `
|
|
901
|
+
+ 'the stop is blocked fail-closed (details withheld). Run: ukit install, then re-send the task in a new message.',
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
// A clean evaluation resets the consecutive-crash streak (shared with the shell hook).
|
|
905
|
+
await resetCompletionCrashStreak(projectRoot);
|
|
838
906
|
if (!evaluation.continue) {
|
|
839
907
|
if (evaluation.capped) pi.logger?.warn?.(`[UKit] ${evaluation.reason}`);
|
|
840
908
|
const missingEvidence = Array.isArray(evaluation.missingEvidence) ? evaluation.missingEvidence : [];
|
|
@@ -273,6 +273,7 @@
|
|
|
273
273
|
"fullstack": {
|
|
274
274
|
"stopGateEnabled": true,
|
|
275
275
|
"stopGateMaxStalledBlocks": 12,
|
|
276
|
+
"stopGateMaxCrashStreaks": 3,
|
|
276
277
|
"quietScansRequired": 2,
|
|
277
278
|
"idleWatchdogMin": 5,
|
|
278
279
|
"autoArchive": true,
|
|
@@ -580,6 +581,7 @@
|
|
|
580
581
|
"fullstack": {
|
|
581
582
|
"stopGateEnabled": "Bật Stop gate handoff: RUN.md Phase != done/blocked → Stop hook từ chối stop kèm Next step, giữ handoff-fullstack chạy tiếp thay vì đứng sau recap. Tắt chỉ khi debug.",
|
|
582
583
|
"stopGateMaxStalledBlocks": "Liveness breaker: cursor không nhích qua N lần block liên tiếp thì gate thả stop (tránh vòng lặp vô tận). Mặc định 12.",
|
|
584
|
+
"stopGateMaxCrashStreaks": "Liveness breaker cho coordinator crash: sau N lần block liên tiếp do infrastructure failure, gate thả stop kèm cảnh báo lớn. Mặc định 3.",
|
|
583
585
|
"quietScansRequired": "Số scan liên tiếp không thấy việc mới trước khi handoff-fullstack được phép đóng run. Mặc định 2.",
|
|
584
586
|
"idleWatchdogMin": "Khoảng phút cho scheduled wakeup nếu harness hỗ trợ (cron/loop). Mặc định 5.",
|
|
585
587
|
"autoArchive": "true = Phase F tự archive cycle vào docs/AI_HANDOFF/archive/cycle-NN/ khi hoàn tất.",
|