@ngockhoale/ukit 2.6.4 → 2.6.6
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 +73 -0
- package/bin/ukit +30 -10
- package/manifests/documentation.yaml +605 -0
- package/package.json +2 -2
- package/src/core/memory/store.js +10 -3
- package/src/core/runtimeConfig.js +2 -0
- package/templates/.claude/hooks/auto-prune-bash.sh +33 -0
- package/templates/.claude/hooks/completion-gate.sh +20 -1
- package/templates/.claude/hooks/context-hardcap-gate.sh +7 -0
- package/templates/.claude/hooks/handoff-resume.sh +14 -1
- package/templates/.claude/hooks/skill-router.sh +180 -27
- package/templates/.claude/hooks/task-watchdog.sh +30 -5
- package/templates/.claude/hooks/vision-router.sh +14 -0
- package/templates/.claude/ukit/index/post-edit-verify.mjs +33 -1
- package/templates/.claude/ukit/index/stale-spec-check.mjs +32 -1
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +16 -8
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +19 -1
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +17 -1
- package/templates/.claude/ukit/runtime/output-compression.mjs +58 -8
- package/templates/.claude/ukit/runtime/reinject-context.mjs +37 -5
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +5 -1
- package/templates/.codex/README.md +1 -1
- package/templates/ukit/README.md +1 -1
|
@@ -200,6 +200,22 @@ export function maybeSweepTelemetryDir(dir, {
|
|
|
200
200
|
return { sampled: true, ...sweepTelemetryDir(dir, options) };
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// BUG-C23-10: a bare `catch {}` around the sweep swallowed every failure — a
|
|
204
|
+
// persistent EACCES/ENOSPC meant silent non-convergence of the dir bound.
|
|
205
|
+
// Announce the first failure and then every 25th, C22 rate-limit posture.
|
|
206
|
+
let sweepFailureCount = 0;
|
|
207
|
+
export function noteSweepFailure(error) {
|
|
208
|
+
sweepFailureCount += 1;
|
|
209
|
+
if (sweepFailureCount !== 1 && sweepFailureCount % 25 !== 0) return null;
|
|
210
|
+
const message =
|
|
211
|
+
`UKit hook-telemetry: latency dir sweep failed (${sweepFailureCount} failure(s) so far) — `
|
|
212
|
+
+ `the appended row is safe but the backlog bound may lag. Cause: ${error?.message || error}`;
|
|
213
|
+
try {
|
|
214
|
+
fs.writeSync(1, `${JSON.stringify({ systemMessage: message })}\n`);
|
|
215
|
+
} catch { /* stdout already gone */ }
|
|
216
|
+
return message;
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
export function appendTelemetryRow(projectRoot, sessionId, row, options = {}) {
|
|
204
220
|
try {
|
|
205
221
|
if (!row || typeof row !== 'object') return false;
|
|
@@ -211,7 +227,7 @@ export function appendTelemetryRow(projectRoot, sessionId, row, options = {}) {
|
|
|
211
227
|
fs.appendFileSync(filePath, line, 'utf8');
|
|
212
228
|
// Sampled bounded dir sweep (BUG-C21-10): advisory — a sweep failure must
|
|
213
229
|
// never alter the row this call just appended.
|
|
214
|
-
try { maybeSweepTelemetryDir(dir); } catch {
|
|
230
|
+
try { maybeSweepTelemetryDir(dir); } catch (error) { noteSweepFailure(error); }
|
|
215
231
|
return true;
|
|
216
232
|
} catch {
|
|
217
233
|
// Advisory: an unwritable or corrupt telemetry target must never alter a
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import fsSync from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
4
6
|
import {
|
|
5
7
|
buildCompactMachineKey,
|
|
6
8
|
buildRuntimePaths,
|
|
@@ -19,9 +21,42 @@ import { updateCompactPressureFromOutput } from './compact-threshold.mjs';
|
|
|
19
21
|
// Hook-context self-deadline (2.4.1 orphan-leak class): the compress-output hook passes
|
|
20
22
|
// UKIT_HOOK_DEADLINE_MS so a wedged read can never orphan this process past the hook
|
|
21
23
|
// budget. Non-hook usage never sets it and is never self-killed.
|
|
24
|
+
// SPEC §8 degrade channel: stdout is the only surface the hook consumer reliably
|
|
25
|
+
// shows the user; stderr is model-facing. writeSync(1) because a same-tick
|
|
26
|
+
// process.exit() can discard a buffered async write on a pipe.
|
|
27
|
+
function emitSystemMessage(text) {
|
|
28
|
+
try {
|
|
29
|
+
fsSync.writeSync(1, `${JSON.stringify({ systemMessage: text })}\n`);
|
|
30
|
+
} catch { /* stdout already gone */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// BUG-C23-10: bare `.catch(() => {})` swallowed every sweep failure — a
|
|
34
|
+
// persistent EACCES/ENOSPC on .ukit meant silent non-convergence of the tee/
|
|
35
|
+
// backlog bound (live dir observed at 2,417 files). Announce the first failure
|
|
36
|
+
// and then every 25th, C22 rate-limit posture.
|
|
37
|
+
let sweepFailureCount = 0;
|
|
38
|
+
export function noteSweepFailure(error) {
|
|
39
|
+
sweepFailureCount += 1;
|
|
40
|
+
if (sweepFailureCount !== 1 && sweepFailureCount % 25 !== 0) return null;
|
|
41
|
+
const message =
|
|
42
|
+
`UKit output-compression: tee cache sweep failed (${sweepFailureCount} failure(s) so far) — `
|
|
43
|
+
+ `the preserved output is safe but the backlog bound may lag. Cause: ${error?.message || error}`;
|
|
44
|
+
emitSystemMessage(message);
|
|
45
|
+
return message;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10);
|
|
23
49
|
if (Number.isFinite(HOOK_DEADLINE_MS) && HOOK_DEADLINE_MS > 0) {
|
|
24
|
-
setTimeout(() =>
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
// BUG-C23-04: deadline expiry abandons configured compression work — a
|
|
52
|
+
// degrade, and §8(ii) requires it be announced before the exit.
|
|
53
|
+
emitSystemMessage(
|
|
54
|
+
`UKit output-compression exceeded its ${HOOK_DEADLINE_MS}ms hook deadline — `
|
|
55
|
+
+ 'tool output passed through uncompressed this call (likely stalled filesystem). '
|
|
56
|
+
+ 'If this repeats, check the .ukit volume or raise UKIT_HOOK_DEADLINE_MS.',
|
|
57
|
+
);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}, HOOK_DEADLINE_MS).unref();
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
@@ -422,7 +457,9 @@ export async function sweepTeeCache(dir, {
|
|
|
422
457
|
// under-count and `overflow` was always 0. Expected drain: each sampled sweep
|
|
423
458
|
// removes up to maxRemovals, so a backlog of B files converges after
|
|
424
459
|
// ~ceil(B/maxRemovals) sweeps (~16x that many writes at the default 1/16
|
|
425
|
-
// sampling); the 7d age cutoff drains the
|
|
460
|
+
// sampling); the 7d age cutoff drains the remainder only when new writes keep
|
|
461
|
+
// arriving — a stopped writer converges at ~ceil(B/maxRemovals) sweeps and no
|
|
462
|
+
// faster, since sweeps are sampled off the write path (BUG-C23-10, FR-006).
|
|
426
463
|
const eligibleNames = [];
|
|
427
464
|
for (const dirent of dirents) {
|
|
428
465
|
if (dirent.isFile()) eligibleNames.push(dirent.name);
|
|
@@ -498,8 +535,9 @@ async function persistRawOutput(projectRoot, {
|
|
|
498
535
|
await fs.mkdir(teeCacheDir, { recursive: true });
|
|
499
536
|
await fs.writeFile(absolutePath, rawOutputText, 'utf8');
|
|
500
537
|
// Bounded, sampled tee/ prune (BUG-C21-07): advisory only — a sweep failure
|
|
501
|
-
// must never lose the preserved output this call just wrote.
|
|
502
|
-
|
|
538
|
+
// must never lose the preserved output this call just wrote. BUG-C23-10: a
|
|
539
|
+
// persistent failure (EACCES/ENOSPC) still surfaces as a bounded degrade.
|
|
540
|
+
await maybeSweepTeeCache(teeCacheDir).catch(noteSweepFailure);
|
|
503
541
|
|
|
504
542
|
return {
|
|
505
543
|
rawSaved: true,
|
|
@@ -1486,7 +1524,19 @@ async function main() {
|
|
|
1486
1524
|
process.stdout.write(result.summary);
|
|
1487
1525
|
}
|
|
1488
1526
|
|
|
1489
|
-
main()
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1527
|
+
// BUG-C23-13: only run main() on direct CLI invocation — an unguarded bottom
|
|
1528
|
+
// call executes on ANY import(), reading the importer's stdin and exiting its
|
|
1529
|
+
// process. Guard shape matches compact-threshold.mjs.
|
|
1530
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
1531
|
+
if (invokedPath && pathToFileURL(invokedPath).href === import.meta.url) {
|
|
1532
|
+
main().catch((error) => {
|
|
1533
|
+
console.error('[UKit] Failed to compress tool output:', error?.message || String(error));
|
|
1534
|
+
// BUG-C23-01: stderr alone is invisible to the hook consumer — the §8
|
|
1535
|
+
// announce contract requires a stdout systemMessage on the crash path too.
|
|
1536
|
+
emitSystemMessage(
|
|
1537
|
+
'UKit output-compression failed before it could compress this tool output; '
|
|
1538
|
+
+ 'the raw output was passed through uncompressed. Run `ukit doctor` if this repeats.',
|
|
1539
|
+
);
|
|
1540
|
+
process.exitCode = 1;
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import fsSync from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
4
6
|
import {
|
|
5
7
|
buildCompactMachineKey,
|
|
6
8
|
buildRuntimePaths,
|
|
@@ -22,9 +24,27 @@ import {
|
|
|
22
24
|
// Hook-context self-deadline (2.4.1 orphan-leak class): the reinject-context hook passes
|
|
23
25
|
// UKIT_HOOK_DEADLINE_MS so a wedged read can never orphan this process past the hook
|
|
24
26
|
// budget. Non-hook usage never sets it and is never self-killed.
|
|
27
|
+
// SPEC §8 degrade channel: stdout is the only surface the hook consumer reliably
|
|
28
|
+
// shows the user; stderr is model-facing. writeSync(1) because a same-tick
|
|
29
|
+
// process.exit() can discard a buffered async write on a pipe.
|
|
30
|
+
function emitSystemMessage(text) {
|
|
31
|
+
try {
|
|
32
|
+
fsSync.writeSync(1, `${JSON.stringify({ systemMessage: text })}\n`);
|
|
33
|
+
} catch { /* stdout already gone */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10);
|
|
26
37
|
if (Number.isFinite(HOOK_DEADLINE_MS) && HOOK_DEADLINE_MS > 0) {
|
|
27
|
-
setTimeout(() =>
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
// BUG-C23-04: deadline expiry abandons the configured context reinjection —
|
|
40
|
+
// a degrade, and §8(ii) requires it be announced before the exit.
|
|
41
|
+
emitSystemMessage(
|
|
42
|
+
`UKit reinject-context exceeded its ${HOOK_DEADLINE_MS}ms hook deadline — `
|
|
43
|
+
+ 'post-compaction context reinjection skipped this pass (likely stalled filesystem). '
|
|
44
|
+
+ 'If this repeats, check the .ukit volume or raise UKIT_HOOK_DEADLINE_MS.',
|
|
45
|
+
);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}, HOOK_DEADLINE_MS).unref();
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
const STATE_TTL_MS = 30 * 60 * 1000;
|
|
@@ -1047,7 +1067,19 @@ async function buildPreviousContextLines(projectRoot, state, config) {
|
|
|
1047
1067
|
return [`- Previous context: ${rankedItems.map((item) => buildPreviousContextSnippet(item)).join(' | ')}`];
|
|
1048
1068
|
}
|
|
1049
1069
|
|
|
1050
|
-
main()
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1070
|
+
// BUG-C23-13: only run main() on direct CLI invocation — an unguarded bottom
|
|
1071
|
+
// call executes on ANY import(), reading the importer's stdin and exiting its
|
|
1072
|
+
// process. Guard shape matches compact-threshold.mjs.
|
|
1073
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
1074
|
+
if (invokedPath && pathToFileURL(invokedPath).href === import.meta.url) {
|
|
1075
|
+
main().catch((error) => {
|
|
1076
|
+
console.error('[UKit] Failed to build compact reinject context:', error?.message || String(error));
|
|
1077
|
+
// BUG-C23-01: stderr alone is invisible to the hook consumer — the §8
|
|
1078
|
+
// announce contract requires a stdout systemMessage on the crash path too.
|
|
1079
|
+
emitSystemMessage(
|
|
1080
|
+
'UKit reinject-context failed before it could rebuild the compact context block; '
|
|
1081
|
+
+ 'the session continues without reinjected context this pass. Run `ukit doctor` if this repeats.',
|
|
1082
|
+
);
|
|
1083
|
+
process.exitCode = 1;
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
@@ -561,7 +561,11 @@ async function alreadyCoordinatedThisStop({ projectRoot, sessionKey, now, dedupe
|
|
|
561
561
|
return true;
|
|
562
562
|
}
|
|
563
563
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
564
|
-
|
|
564
|
+
// BUG-C23-08: spread `current` — a bare { lastStop } write erased the
|
|
565
|
+
// `handoff` stall-streak slot written by bumpHandoffStallCount, which ran
|
|
566
|
+
// AFTER this dedupe write each invocation → count always read 1 and the
|
|
567
|
+
// FR-004 liveness breaker could never fire.
|
|
568
|
+
await fs.writeFile(statePath, `${JSON.stringify({ ...current, lastStop: { key: sessionKey, ts: now } }, null, 1)}\n`, 'utf8');
|
|
565
569
|
return false;
|
|
566
570
|
});
|
|
567
571
|
return outcome.ok ? outcome.value === true : false;
|
|
@@ -80,7 +80,7 @@ For clearly non-code specialist lanes (docs-only, status, task queue), skip the
|
|
|
80
80
|
- If verification is needed, prefer `node .codex/ukit/index/verify-context.mjs ...`.
|
|
81
81
|
- **Treat helper commands as internal orchestration.** Run them yourself when needed; never ask end users to run them.
|
|
82
82
|
|
|
83
|
-
## UKit
|
|
83
|
+
## UKit Shared Runtime
|
|
84
84
|
|
|
85
85
|
- Shared runtime state lives in `.ukit/storage/`.
|
|
86
86
|
- Treat `.ukit/storage/config.json` as the source of compact, token-pipeline, router, memory, validation, and Safe Patch guardrail toggles.
|
package/templates/ukit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UKit Shared Runtime
|
|
2
2
|
|
|
3
|
-
This folder stores shared UKit runtime state
|
|
3
|
+
This folder stores shared UKit runtime state.
|
|
4
4
|
|
|
5
5
|
- `storage/config.json` — runtime feature flags and defaults
|
|
6
6
|
- `storage/cache/` — prompt-cache, compact history, compact pressure state, output summaries, and preserved raw tool outputs under `storage/cache/tee/`
|