@ngockhoale/ukit 2.6.3 → 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.
Files changed (32) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/package.json +1 -1
  3. package/scripts/bench/parallel-agents.mjs +42 -4
  4. package/scripts/release/verify-release.mjs +43 -6
  5. package/src/core/applyPlan.js +65 -0
  6. package/src/core/compact/threshold.js +33 -30
  7. package/src/core/fileOps.js +9 -2
  8. package/src/core/runInstallPipeline.js +10 -1
  9. package/src/core/status.js +1 -1
  10. package/templates/.claude/hooks/auto-allow-bash.sh +29 -20
  11. package/templates/.claude/hooks/auto-prune-bash.sh +48 -30
  12. package/templates/.claude/hooks/completion-gate.sh +22 -1
  13. package/templates/.claude/hooks/context-hardcap-gate.sh +19 -4
  14. package/templates/.claude/hooks/handoff-model-guard.sh +10 -1
  15. package/templates/.claude/hooks/handoff-resume.sh +19 -17
  16. package/templates/.claude/hooks/post-edit-verify.sh +1 -0
  17. package/templates/.claude/hooks/pre-edit-backup.sh +1 -0
  18. package/templates/.claude/hooks/reset-compact-pressure.sh +39 -22
  19. package/templates/.claude/hooks/stale-spec-guard.sh +1 -0
  20. package/templates/.claude/hooks/verification-guard.sh +17 -1
  21. package/templates/.claude/settings.json +4 -4
  22. package/templates/.claude/ukit/index/extract-image.mjs +49 -0
  23. package/templates/.claude/ukit/runtime/compact-threshold.mjs +62 -40
  24. package/templates/.claude/ukit/runtime/execution-ledger.mjs +18 -5
  25. package/templates/.claude/ukit/runtime/hook-chain-budget.mjs +11 -4
  26. package/templates/.claude/ukit/runtime/hook-telemetry.mjs +7 -3
  27. package/templates/.claude/ukit/runtime/output-compression.mjs +16 -5
  28. package/templates/.claude/ukit/runtime/reinject-context.mjs +2 -2
  29. package/templates/.claude/ukit/runtime/stop-coordinator.mjs +3 -0
  30. package/templates/.claude/ukit/runtime/token-utils.mjs +9 -2
  31. package/templates/.omp/hooks/pre/ukit-bridge.js +72 -4
  32. 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 names) {
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, entries.length - maxFiles);
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
- const evaluation = evaluateCompletion({ state, ledger });
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.",