@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.
- package/CHANGELOG.md +59 -0
- package/manifests/platform.full.yaml +16 -0
- package/package.json +1 -1
- package/src/cli/commands/install.js +49 -2
- package/src/cli/commands/update.js +5 -0
- package/src/core/output/index.js +77 -0
- package/src/core/update.js +36 -5
- package/templates/.claude/commands/ukit/handoff-clear.md +2 -2
- package/templates/.claude/commands/ukit/handoff-fullstack.md +10 -7
- package/templates/.claude/hooks/auto-allow-bash.sh +17 -5
- package/templates/.claude/hooks/block-dangerous.sh +18 -5
- package/templates/.claude/hooks/completion-gate.sh +17 -5
- package/templates/.claude/hooks/compress-output.sh +15 -6
- package/templates/.claude/hooks/context-hardcap-gate.sh +77 -46
- package/templates/.claude/hooks/context-window-guard.sh +41 -27
- package/templates/.claude/hooks/handoff-model-guard.sh +41 -17
- package/templates/.claude/hooks/handoff-resume.sh +20 -7
- package/templates/.claude/hooks/post-edit-verify.sh +17 -5
- package/templates/.claude/hooks/pre-edit-backup.sh +17 -5
- package/templates/.claude/hooks/project-important.sh +18 -1
- package/templates/.claude/hooks/protect-files.sh +18 -5
- package/templates/.claude/hooks/record-execution.sh +17 -5
- package/templates/.claude/hooks/sensitive-data-guard.sh +48 -9
- package/templates/.claude/hooks/skill-router.sh +124 -86
- package/templates/.claude/hooks/stale-spec-guard.sh +22 -6
- package/templates/.claude/hooks/task-watchdog.sh +22 -7
- package/templates/.claude/hooks/verification-guard.sh +17 -5
- package/templates/.claude/hooks/vision-router.sh +17 -5
- package/templates/.claude/settings.json +15 -10
- package/templates/.claude/ukit/index/provision-worktree.mjs +30 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +115 -1
- package/templates/.claude/ukit/runtime/hook-input.sh +25 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +86 -2
- package/templates/.claude/ukit/runtime/output-compression.mjs +87 -0
- package/templates/.omp/hooks/pre/ukit-bridge.js +110 -3
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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;
|