@ngockhoale/ukit 2.3.4 → 2.3.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 +50 -0
- package/package.json +1 -1
- package/src/core/memory/store.js +14 -5
- package/src/core/status.js +17 -5
- package/src/index/buildIndex.js +14 -1
- package/src/index/paths.js +1 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +17 -1
- package/templates/AGENTS.md +8 -0
- package/templates/CLAUDE.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.3.6 - 2026-09-10
|
|
6
|
+
|
|
7
|
+
Anti-stall coverage parity release: the compact-recovery and stop-reason directives that
|
|
8
|
+
UKit hooks inject at runtime on Claude Code and omp now also reach harnesses that have no
|
|
9
|
+
hook system at all.
|
|
10
|
+
|
|
11
|
+
A new `## Long-Run Continuity` section ships in both `templates/CLAUDE.md` and
|
|
12
|
+
`templates/AGENTS.md` (byte-identical, enforced by the per-section context-docs parity
|
|
13
|
+
test). Codex and OpenCode load no hooks, so root `AGENTS.md` is their only instruction
|
|
14
|
+
channel — before this release those sessions never saw the near-cap recovery order (LAND
|
|
15
|
+
one thing → DEFER the rest to `docs/STATUS.md` → DELEGATE broad work to subagents → only
|
|
16
|
+
then compact) or the post-compact discipline (no rereads of pre-compact context; if the
|
|
17
|
+
first turn after a compact still sits at ≥60% of the cap, recover by delegating or starting
|
|
18
|
+
fresh from disk state instead of burning the window again).
|
|
19
|
+
|
|
20
|
+
The same session closed the long-running stall investigation with debug-log evidence: the
|
|
21
|
+
observed "runs a while, then goes silent" behaviour is UNIC-gateway/network streaming
|
|
22
|
+
latency (repeated 30s slow-first-byte, one 300s byte-idle abort), not UKit hooks —
|
|
23
|
+
hook-error cache empty, exec ledgers healthy. When a dead stream ends a turn early, the
|
|
24
|
+
existing ≥2.3.3 Stop-hook completion gate blocks the premature stop, so staying current
|
|
25
|
+
(`ukit update`) plus restarting stale sessions (hook config loads at session start) is the
|
|
26
|
+
full remediation available in-repo; gateway-side idle/failover fixes belong upstream.
|
|
27
|
+
|
|
28
|
+
## 2.3.5 - 2026-09-10
|
|
29
|
+
|
|
30
|
+
Source-audit hardening release: the code index cannot corrupt itself, and a corrupt runtime
|
|
31
|
+
cache can no longer take down `ukit status` or `ukit memory`.
|
|
32
|
+
|
|
33
|
+
Index artifacts (`files`, `symbols`, `imports`, `calls`, `tests-map`, `hotspots`,
|
|
34
|
+
`archetypes`, `relations`, `analogs`, `meta`) are now written through same-directory
|
|
35
|
+
`.tmp-<timestamp>-<random>` files followed by an atomic `rename`, in both the source builder
|
|
36
|
+
(`src/index/buildIndex.js`) and the shipped `templates/.claude/ukit/index/lib/index-core.mjs`
|
|
37
|
+
runtime mirror. A build that crashes mid-write — or two builds racing in one repo — can no
|
|
38
|
+
longer leave a half-written artifact that poisons every subsequent index read; failed writes
|
|
39
|
+
clean their temp file up before rethrowing.
|
|
40
|
+
|
|
41
|
+
`ukit status` and `ukit memory` now read their cache/memory JSON through tolerant readers:
|
|
42
|
+
a corrupt `compact-history.json`, `compact-pressure.json`, `prompt-cache.json`,
|
|
43
|
+
`output-history.json`, or memory file prints a warning naming the exact file path and falls
|
|
44
|
+
back to defaults instead of crashing the whole command mid-report. `readJsonIfExists` itself
|
|
45
|
+
stays strict everywhere else, so genuinely bad callers still fail loudly.
|
|
46
|
+
|
|
47
|
+
Nested test layouts (`packages/widget/tests/unit/...`) are now recognized as tests by the
|
|
48
|
+
source path classifier and both runtime-mirror predicates — those files previously slipped
|
|
49
|
+
past test detection, broke archetype classification, and never appeared in the test map.
|
|
50
|
+
|
|
51
|
+
Each fix ships with regression coverage: a rename-spy test asserting all ten artifacts move
|
|
52
|
+
through temp files, corrupt-fixture tests asserting the warned paths and surviving output,
|
|
53
|
+
and a nested-`tests/` mapping/archetype test. Full suite: 77 files, 1,348 tests green.
|
|
54
|
+
|
|
5
55
|
## 2.3.4 - 2026-09-10
|
|
6
56
|
|
|
7
57
|
Silent-stall audit and compact-recovery release. Two themes: every block reports a
|
package/package.json
CHANGED
package/src/core/memory/store.js
CHANGED
|
@@ -14,6 +14,15 @@ function defaultUserMemory() {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
async function readMemoryJson(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return await readJsonIfExists(filePath);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn(`[UKit] Ignoring corrupt memory JSON file ${filePath}: ${error?.message ?? String(error)}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
async function readDirectoryJsonItems(dirPath) {
|
|
18
27
|
let entries = [];
|
|
19
28
|
try {
|
|
@@ -29,7 +38,7 @@ async function readDirectoryJsonItems(dirPath) {
|
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
const fullPath = path.join(dirPath, entry.name);
|
|
32
|
-
const content = await
|
|
41
|
+
const content = await readMemoryJson(fullPath);
|
|
33
42
|
if (content) {
|
|
34
43
|
items.push({
|
|
35
44
|
fileName: entry.name,
|
|
@@ -91,7 +100,7 @@ function createSearchableText(type, content) {
|
|
|
91
100
|
|
|
92
101
|
export async function exportMemory(projectRoot) {
|
|
93
102
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
94
|
-
const user = (await
|
|
103
|
+
const user = (await readMemoryJson(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
|
|
95
104
|
const projects = (await readDirectoryJsonItems(runtimePaths.projectsDir)).map((item) => item.content);
|
|
96
105
|
const sessions = (await readDirectoryJsonItems(runtimePaths.sessionsDir)).map((item) => item.content);
|
|
97
106
|
|
|
@@ -100,7 +109,7 @@ export async function exportMemory(projectRoot) {
|
|
|
100
109
|
|
|
101
110
|
export async function listMemoryItems(projectRoot) {
|
|
102
111
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
103
|
-
const userMemory = (await
|
|
112
|
+
const userMemory = (await readMemoryJson(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
|
|
104
113
|
const projectMemories = await readDirectoryJsonItems(runtimePaths.projectsDir);
|
|
105
114
|
const sessionMemories = await readDirectoryJsonItems(runtimePaths.sessionsDir);
|
|
106
115
|
|
|
@@ -188,7 +197,7 @@ function projectMemoryPath(runtimePaths, projectId) {
|
|
|
188
197
|
|
|
189
198
|
async function readProjectMemoryForId(runtimePaths, projectId) {
|
|
190
199
|
const filePath = projectMemoryPath(runtimePaths, projectId);
|
|
191
|
-
const existing = await
|
|
200
|
+
const existing = await readMemoryJson(filePath);
|
|
192
201
|
const memory = {
|
|
193
202
|
id: projectId,
|
|
194
203
|
conventions: [],
|
|
@@ -204,7 +213,7 @@ async function appendSessionArchive(runtimePaths, projectId, archivedSessions) {
|
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
const archivePath = path.join(runtimePaths.projectsDir, `${sanitizeProjectId(projectId)}.archive.json`);
|
|
207
|
-
const existing = (await
|
|
216
|
+
const existing = (await readMemoryJson(archivePath)) ?? { sessions: [] };
|
|
208
217
|
const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
|
|
209
218
|
await writeJson(archivePath, { sessions: [...sessions, ...archivedSessions] });
|
|
210
219
|
}
|
package/src/core/status.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadRuntimeConfig } from './runtimeConfig.js';
|
|
|
5
5
|
import { summarizeOutputHistory } from './output/index.js';
|
|
6
6
|
import { buildPromptCacheStats } from './token/index.js';
|
|
7
7
|
import { countMemoryItems } from './memory/store.js';
|
|
8
|
-
import {
|
|
8
|
+
import { buildCompactPressureState } from './compact/threshold.js';
|
|
9
9
|
import { detectProjectContext } from '../context/detectProjectContext.js';
|
|
10
10
|
|
|
11
11
|
function formatPrimaryAgent(agentKey) {
|
|
@@ -122,6 +122,15 @@ function formatRecoveryLabel(count) {
|
|
|
122
122
|
return ` / ${count} recover${count === 1 ? 'y' : 'ies'}`;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function readStatusJson(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
return await readJsonIfExists(filePath);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn(`[UKit] Ignoring corrupt status JSON file ${filePath}: ${error?.message ?? String(error)}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
export async function buildStatusReport(projectRoot) {
|
|
126
135
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
127
136
|
const runtimeExists = await pathExists(runtimePaths.runtimeRoot);
|
|
@@ -131,11 +140,14 @@ export async function buildStatusReport(projectRoot) {
|
|
|
131
140
|
|
|
132
141
|
const projectContext = await detectProjectContext(projectRoot);
|
|
133
142
|
const config = await loadRuntimeConfig(projectRoot);
|
|
134
|
-
const compactHistory = normalizeCompactEntries(await
|
|
143
|
+
const compactHistory = normalizeCompactEntries(await readStatusJson(runtimePaths.compactHistoryPath));
|
|
135
144
|
const compactSummary = buildCompactSummary(compactHistory);
|
|
136
|
-
const compactPressure =
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
const compactPressure = buildCompactPressureState(
|
|
146
|
+
await readStatusJson(runtimePaths.compactPressurePath),
|
|
147
|
+
config,
|
|
148
|
+
);
|
|
149
|
+
const promptCacheStats = buildPromptCacheStats(await readStatusJson(runtimePaths.promptCachePath));
|
|
150
|
+
const outputCompressionStats = summarizeOutputHistory(await readStatusJson(runtimePaths.outputHistoryPath));
|
|
139
151
|
const memoryCounts = await countMemoryItems(projectRoot);
|
|
140
152
|
const adapters = await detectAdapterLabels(projectRoot, config.agent);
|
|
141
153
|
|
package/src/index/buildIndex.js
CHANGED
|
@@ -1197,7 +1197,20 @@ function areSourceFingerprintsEqual(previousFingerprint, nextFingerprint) {
|
|
|
1197
1197
|
|
|
1198
1198
|
async function writeArtifact(rootDir, artifactName, payload) {
|
|
1199
1199
|
const artifactPath = getArtifactPath(rootDir, artifactName);
|
|
1200
|
-
|
|
1200
|
+
const temporaryPath = `${artifactPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
await fs.writeFile(temporaryPath, JSON.stringify(payload, null, 2));
|
|
1204
|
+
await fs.rename(temporaryPath, artifactPath);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
try {
|
|
1207
|
+
await fs.rm(temporaryPath, { force: true });
|
|
1208
|
+
} catch {
|
|
1209
|
+
// ignore cleanup errors
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1201
1214
|
}
|
|
1202
1215
|
|
|
1203
1216
|
async function readArtifactIfExists(rootDir, artifactName) {
|
package/src/index/paths.js
CHANGED
|
@@ -37,6 +37,7 @@ export function isLikelyTestFilePath(filePath) {
|
|
|
37
37
|
|| lower.startsWith('specs/')
|
|
38
38
|
|| lower.includes('/__tests__/')
|
|
39
39
|
|| lower.includes('/test/')
|
|
40
|
+
|| lower.includes('/tests/')
|
|
40
41
|
|| lower.includes('/spec/')
|
|
41
42
|
|| lower.includes('/specs/')
|
|
42
43
|
|| lower.endsWith('.test.js')
|
|
@@ -2028,7 +2028,21 @@ function areBugIndexSnapshotsEqual(previousSnapshot, nextSnapshot) {
|
|
|
2028
2028
|
}
|
|
2029
2029
|
|
|
2030
2030
|
async function writeArtifact(rootDir, artifactName, payload) {
|
|
2031
|
-
|
|
2031
|
+
const artifactPath = getArtifactPath(rootDir, artifactName);
|
|
2032
|
+
const temporaryPath = `${artifactPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
2033
|
+
|
|
2034
|
+
try {
|
|
2035
|
+
await fs.writeFile(temporaryPath, JSON.stringify(payload, null, 2));
|
|
2036
|
+
await fs.rename(temporaryPath, artifactPath);
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
try {
|
|
2039
|
+
await fs.rm(temporaryPath, { force: true });
|
|
2040
|
+
} catch {
|
|
2041
|
+
// ignore cleanup errors
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
throw error;
|
|
2045
|
+
}
|
|
2032
2046
|
}
|
|
2033
2047
|
|
|
2034
2048
|
async function readArtifact(rootDir, artifactName) {
|
|
@@ -2557,6 +2571,7 @@ function isLikelyTestFilePath(filePath) {
|
|
|
2557
2571
|
|| lower.startsWith('specs/')
|
|
2558
2572
|
|| lower.includes('/__tests__/')
|
|
2559
2573
|
|| lower.includes('/test/')
|
|
2574
|
+
|| lower.includes('/tests/')
|
|
2560
2575
|
|| lower.includes('/spec/')
|
|
2561
2576
|
|| lower.includes('/specs/')
|
|
2562
2577
|
|| lower.endsWith('.test.js')
|
|
@@ -2988,6 +3003,7 @@ function isLikelyTestFile(filePath) {
|
|
|
2988
3003
|
|| lower.startsWith('specs/')
|
|
2989
3004
|
|| lower.includes('/__tests__/')
|
|
2990
3005
|
|| lower.includes('/test/')
|
|
3006
|
+
|| lower.includes('/tests/')
|
|
2991
3007
|
|| lower.includes('/spec/')
|
|
2992
3008
|
|| lower.includes('/specs/')
|
|
2993
3009
|
|| lower.endsWith('.test.js')
|
package/templates/AGENTS.md
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
- **Do NOT say "done", "applied", or "fixed" after Read/Grep/analysis alone.** Completion wording requires concrete Edit/Write evidence in the current turn, and verification when the scope is risky.
|
|
27
27
|
- **Every stop says why — no silent idle.** When a turn ends because only the user can act (login, approval, protected-file edit), open the reply with one line naming the exact action: `WAITING ON YOU: <command/action>`, and schedule a one-shot wakeup (~20-30 min) when the harness provides one so the session re-checks and auto-continues once the user has acted. An ended turn cannot observe external/auth changes by itself, so without that line (and the wakeup) the idle session looks identical to a stall. Any error — failed command, hook, test, publish — is reported verbatim in the same turn, never silently retried past the user.
|
|
28
28
|
|
|
29
|
+
## Long-Run Continuity
|
|
30
|
+
|
|
31
|
+
Mirrors what UKit hooks inject at runtime on Claude Code and omp; on harnesses without hooks (Codex, OpenCode) this section is the only carrier — keep it in sync with `.claude/hooks/context-window-guard.sh`.
|
|
32
|
+
|
|
33
|
+
- Near token-cap: **LAND one thing** — finish the smallest in-flight item end-to-end (edit + verify, ≤3 tool calls) and report it done. **DEFER the rest** — one line per remaining step into `docs/STATUS.md`, or split into bounded `docs/AI_HANDOFF/` tasks. **DELEGATE** broad work (searches, big reads, multi-file edits) to subagents whose tool output lives in their own windows. Only then compact.
|
|
34
|
+
- After any compact or handoff: do not reread pre-compact context — continue from the persisted disk state, delegate broad work, keep replies short. If the first turn after a compact still sits at ≥60% of the cap, stop rereading immediately and recover by delegating or starting a fresh session from the disk state; do not burn the window again.
|
|
35
|
+
- A run ends only on completion evidence, a genuine blocker, or a user-only action — and per the Execution Contract above, every such stop names its reason in the final reply.
|
|
36
|
+
|
|
29
37
|
## Index-First Loop
|
|
30
38
|
|
|
31
39
|
For any task that needs code context:
|
package/templates/CLAUDE.md
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
- **Do NOT say "done", "applied", or "fixed" after Read/Grep/analysis alone.** Completion wording requires concrete Edit/Write evidence in the current turn, and verification when the scope is risky.
|
|
27
27
|
- **Every stop says why — no silent idle.** When a turn ends because only the user can act (login, approval, protected-file edit), open the reply with one line naming the exact action: `WAITING ON YOU: <command/action>`, and schedule a one-shot wakeup (~20-30 min) when the harness provides one so the session re-checks and auto-continues once the user has acted. An ended turn cannot observe external/auth changes by itself, so without that line (and the wakeup) the idle session looks identical to a stall. Any error — failed command, hook, test, publish — is reported verbatim in the same turn, never silently retried past the user.
|
|
28
28
|
|
|
29
|
+
## Long-Run Continuity
|
|
30
|
+
|
|
31
|
+
Mirrors what UKit hooks inject at runtime on Claude Code and omp; on harnesses without hooks (Codex, OpenCode) this section is the only carrier — keep it in sync with `.claude/hooks/context-window-guard.sh`.
|
|
32
|
+
|
|
33
|
+
- Near token-cap: **LAND one thing** — finish the smallest in-flight item end-to-end (edit + verify, ≤3 tool calls) and report it done. **DEFER the rest** — one line per remaining step into `docs/STATUS.md`, or split into bounded `docs/AI_HANDOFF/` tasks. **DELEGATE** broad work (searches, big reads, multi-file edits) to subagents whose tool output lives in their own windows. Only then compact.
|
|
34
|
+
- After any compact or handoff: do not reread pre-compact context — continue from the persisted disk state, delegate broad work, keep replies short. If the first turn after a compact still sits at ≥60% of the cap, stop rereading immediately and recover by delegating or starting a fresh session from the disk state; do not burn the window again.
|
|
35
|
+
- A run ends only on completion evidence, a genuine blocker, or a user-only action — and per the Execution Contract above, every such stop names its reason in the final reply.
|
|
36
|
+
|
|
29
37
|
## Index-First Loop
|
|
30
38
|
|
|
31
39
|
For any task that needs code context:
|