@ngockhoale/ukit 2.6.0 → 2.6.3

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 (36) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/manifests/platform.full.yaml +16 -0
  3. package/package.json +1 -1
  4. package/src/cli/commands/install.js +49 -2
  5. package/src/cli/commands/update.js +5 -0
  6. package/src/core/output/index.js +77 -0
  7. package/src/core/update.js +36 -5
  8. package/templates/.claude/commands/ukit/handoff-clear.md +2 -2
  9. package/templates/.claude/commands/ukit/handoff-fullstack.md +10 -7
  10. package/templates/.claude/hooks/auto-allow-bash.sh +17 -5
  11. package/templates/.claude/hooks/block-dangerous.sh +18 -5
  12. package/templates/.claude/hooks/completion-gate.sh +17 -5
  13. package/templates/.claude/hooks/compress-output.sh +15 -6
  14. package/templates/.claude/hooks/context-hardcap-gate.sh +77 -46
  15. package/templates/.claude/hooks/context-window-guard.sh +41 -27
  16. package/templates/.claude/hooks/handoff-model-guard.sh +41 -17
  17. package/templates/.claude/hooks/handoff-resume.sh +20 -7
  18. package/templates/.claude/hooks/post-edit-verify.sh +17 -5
  19. package/templates/.claude/hooks/pre-edit-backup.sh +17 -5
  20. package/templates/.claude/hooks/project-important.sh +18 -1
  21. package/templates/.claude/hooks/protect-files.sh +18 -5
  22. package/templates/.claude/hooks/record-execution.sh +17 -5
  23. package/templates/.claude/hooks/sensitive-data-guard.sh +48 -9
  24. package/templates/.claude/hooks/skill-router.sh +124 -86
  25. package/templates/.claude/hooks/stale-spec-guard.sh +22 -6
  26. package/templates/.claude/hooks/task-watchdog.sh +22 -7
  27. package/templates/.claude/hooks/verification-guard.sh +17 -5
  28. package/templates/.claude/hooks/vision-router.sh +17 -5
  29. package/templates/.claude/settings.json +15 -10
  30. package/templates/.claude/ukit/index/provision-worktree.mjs +30 -2
  31. package/templates/.claude/ukit/runtime/execution-ledger.mjs +115 -1
  32. package/templates/.claude/ukit/runtime/hook-input.sh +25 -0
  33. package/templates/.claude/ukit/runtime/hook-telemetry.mjs +86 -2
  34. package/templates/.claude/ukit/runtime/output-compression.mjs +87 -0
  35. package/templates/.omp/hooks/pre/ukit-bridge.js +110 -3
  36. package/templates/ukit/storage/config.json +1 -0
@@ -47,6 +47,7 @@ export const HOOK_EVENT_MAP = {
47
47
  'sensitive-data-guard.sh',
48
48
  'handoff-model-guard.sh',
49
49
  'context-hardcap-gate.sh',
50
+ 'verification-guard.sh',
50
51
  ],
51
52
  },
52
53
  tool_result: {
@@ -102,6 +103,7 @@ export const FAIL_CLOSED_SCRIPTS = new Set([
102
103
 
103
104
  export const ADVISORY_SCRIPTS = new Set([
104
105
  'skill-router.sh',
106
+ 'verification-guard.sh',
105
107
  'record-execution.sh',
106
108
  'auto-allow-bash.sh',
107
109
  'pre-edit-backup.sh',
@@ -331,17 +333,122 @@ export { translateExecResult };
331
333
  // 30s cap that silently disagreed with the runner whenever the env knobs were set.
332
334
  const chainExecTimeoutMs = resolveChainExecTimeoutMs;
333
335
 
334
- function recordHookErrorDiagnostic(projectRoot, sessionId, diagnostic) {
336
+ // BUG-C21-09 (FR-006): hook-errors/ used to append one JSONL row per chain
337
+ // transport error with no byte cap, no rotation, and no sweep — files grew per
338
+ // session forever. Per-file cap mirrors hook-telemetry's <=512KB rotate; the
339
+ // directory sweep is bounded (<= maxEntries stats, <= maxRemovals unlinks) and
340
+ // sampled (~1/16 writes, UKIT_HOOK_ERRORS_SWEEP_PROBABILITY to force/disable).
341
+ const HOOK_ERROR_MAX_BYTES = 512 * 1024;
342
+ const HOOK_ERROR_SWEEP_PROBABILITY_DEFAULT = 1 / 16;
343
+ const HOOK_ERROR_SWEEP_MAX_ENTRIES = 128;
344
+ const HOOK_ERROR_SWEEP_MAX_REMOVALS = 64;
345
+ const HOOK_ERROR_MAX_FILES = 200;
346
+ const HOOK_ERROR_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
347
+
348
+ function hookErrorsDirFor(projectRoot) {
349
+ return path.join(projectRoot, '.ukit', 'storage', 'cache', 'hook-errors');
350
+ }
351
+
352
+ // Bounded work on THIS session's file only — identical keep-newest-half shape
353
+ // as hook-telemetry's rotateIfNeeded.
354
+ function rotateHookErrorFileIfNeeded(filePath, incomingBytes, maxBytes) {
355
+ let size = 0;
356
+ try {
357
+ size = fs.statSync(filePath).size;
358
+ } catch {
359
+ return; // first row for this session
360
+ }
361
+ if (size + incomingBytes <= maxBytes) return;
362
+ try {
363
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n');
364
+ if (lines.length && lines[lines.length - 1] === '') lines.pop();
365
+ const keepBudget = Math.floor(maxBytes / 2);
366
+ const keep = [];
367
+ let kept = 0;
368
+ for (let i = lines.length - 1; i >= 0; i--) {
369
+ const lineBytes = Buffer.byteLength(lines[i], 'utf8') + 1;
370
+ if (kept + lineBytes > keepBudget) break;
371
+ keep.unshift(lines[i]);
372
+ kept += lineBytes;
373
+ }
374
+ fs.writeFileSync(filePath, keep.length ? `${keep.join('\n')}\n` : '', 'utf8');
375
+ } catch {
376
+ // Rotation failed; drop this row rather than grow past the cap.
377
+ }
378
+ }
379
+
380
+ // sweepHookErrorsDir(dir, {now, maxAgeMs, maxFiles, maxEntries, maxRemovals}) ->
381
+ // { scanned, removed } — bounded: never scans or removes more than the caps,
382
+ // so a pre-existing oversized dir is amortized down across sampled sweeps.
383
+ function sweepHookErrorsDir(dir, {
384
+ now = Date.now,
385
+ maxAgeMs = HOOK_ERROR_MAX_AGE_MS,
386
+ maxFiles = HOOK_ERROR_MAX_FILES,
387
+ maxEntries = HOOK_ERROR_SWEEP_MAX_ENTRIES,
388
+ maxRemovals = HOOK_ERROR_SWEEP_MAX_REMOVALS,
389
+ } = {}) {
390
+ let names;
391
+ try {
392
+ names = fs.readdirSync(dir);
393
+ } catch {
394
+ return { scanned: 0, removed: 0 };
395
+ }
396
+ const cutoff = now() - maxAgeMs;
397
+ const entries = [];
398
+ let scanned = 0;
399
+ for (const name of names) {
400
+ if (scanned >= maxEntries) break;
401
+ scanned += 1;
402
+ if (!name.endsWith('.jsonl')) continue;
403
+ try {
404
+ const stats = fs.statSync(path.join(dir, name));
405
+ if (stats.isFile()) entries.push({ name, mtimeMs: stats.mtimeMs });
406
+ } catch { /* raced away — fine */ }
407
+ }
408
+ entries.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
409
+ // Overflow counts eligible FILES only — foreign entries (non-matching names,
410
+ // dirs) inflate `names.length` and would evict real entries while the dir is
411
+ // actually under the cap.
412
+ const overflow = Math.max(0, entries.length - maxFiles);
413
+ let removed = 0;
414
+ for (const entry of entries) {
415
+ if (removed >= maxRemovals) break;
416
+ if (removed < overflow || entry.mtimeMs < cutoff) {
417
+ try {
418
+ fs.rmSync(path.join(dir, entry.name), { force: true });
419
+ removed += 1;
420
+ } catch { /* raced away — fine */ }
421
+ }
422
+ }
423
+ return { scanned, removed };
424
+ }
425
+
426
+ function hookErrorsSweepProbabilityFromEnv() {
427
+ const raw = Number(process.env.UKIT_HOOK_ERRORS_SWEEP_PROBABILITY);
428
+ if (!Number.isFinite(raw)) return HOOK_ERROR_SWEEP_PROBABILITY_DEFAULT;
429
+ return Math.min(1, Math.max(0, raw));
430
+ }
431
+
432
+ function recordHookErrorDiagnostic(projectRoot, sessionId, diagnostic, { maxBytes = HOOK_ERROR_MAX_BYTES } = {}) {
335
433
  try {
336
- const dir = path.join(projectRoot, '.ukit', 'storage', 'cache', 'hook-errors');
434
+ const dir = hookErrorsDirFor(projectRoot);
337
435
  fs.mkdirSync(dir, { recursive: true });
338
436
  const safeSession = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'unknown';
339
- fs.appendFileSync(path.join(dir, `${safeSession}.jsonl`), `${JSON.stringify(diagnostic)}\n`, 'utf8');
437
+ const filePath = path.join(dir, `${safeSession}.jsonl`);
438
+ const line = `${JSON.stringify(diagnostic)}\n`;
439
+ rotateHookErrorFileIfNeeded(filePath, Buffer.byteLength(line, 'utf8'), maxBytes);
440
+ fs.appendFileSync(filePath, line, 'utf8');
441
+ // Sampled bounded dir sweep — amortizes down any pre-existing oversized dir.
442
+ if (Math.random() < hookErrorsSweepProbabilityFromEnv()) {
443
+ sweepHookErrorsDir(dir);
444
+ }
340
445
  } catch {
341
446
  // Diagnostics are advisory and must never block or throw.
342
447
  }
343
448
  }
344
449
 
450
+ export { recordHookErrorDiagnostic, sweepHookErrorsDir };
451
+
345
452
  // Cached across the omp process lifetime: resolution below spawns a probe process, and every
346
453
  // tool call would otherwise pay that cost again.
347
454
  let cachedNodeExecutable = null;
@@ -329,6 +329,7 @@
329
329
  "skill-routing-needed",
330
330
  "step-budget-enough",
331
331
  "compact-now-or-later",
332
+ "codex-context-budget",
332
333
  "summarize-docs-or-keep-detail"
333
334
  ],
334
335
  "stepBudgets": {