@ngockhoale/ukit 2.1.0 → 2.1.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 CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.1.3 - 2026-08-16
6
+
7
+ The reason auto-compact never fired in the terminal was UKit itself. This makes it fire.
8
+
9
+ ### Fixed
10
+
11
+ - **Auto-compact now triggers below the hard cap instead of never.** `context-hardcap-gate.sh` refuses Edit/Write/Bash at `compact.hardCapTokens` (160,000 — 80% of a 200k window), while Claude Code's own auto-compact fires near the real window limit. The gate froze the session first, so the transcript stopped growing, so the auto-compact threshold was never reached: a deadlock whose only exit was typing `/compact` by hand. Default settings now ship `env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = 150000`, which lands 10,000 tokens *before* the gate — compaction happens on its own, the run keeps going, and the grace window added in 2.1.2 stays as the backstop rather than the normal path.
12
+ - **`autoCompactEnabled: true` is pinned in default settings**, so a stale toggle in `/config` cannot silently reinstate the deadlock.
13
+
14
+ ### Added
15
+
16
+ - **`tests/core/autoCompactWindow.test.js`** — locks the ordering invariant `env.CLAUDE_CODE_AUTO_COMPACT_WINDOW < compact.hardCapTokens`, the 100,000–1,000,000 range Claude Code accepts, `autoCompactEnabled`, template/mirror parity, and the `overwrite_with_backup` merge strategy that lets existing installs pick the fix up. Change either number in isolation and the suite fails instead of the deadlock returning silently.
17
+
18
+ ### Notes
19
+
20
+ - `env` in `settings.json` applies to **new sessions**. After `npm i -g @ngockhoale/ukit@latest && ukit install`, restart the session (or `/compact` once) before the change takes effect.
21
+ - Still true, and unchanged: no hook can invoke `/compact` — it is client-only. 2.1.3 does not call it; it moves the client's own threshold below UKit's gate so the client compacts on its own.
22
+
23
+ ## 2.1.2 - 2026-08-16
24
+
25
+ Follow-up to 2.1.0, which fixed the wrong hook. Runs were still freezing mid-task because the gate that actually blocks tools was never touched.
26
+
27
+ ### Fixed
28
+
29
+ - **`context-hardcap-gate.sh` gets a grace window for unfinished runs.** 2.1.0 made the *advisory* `context-window-guard.sh` (UserPromptSubmit) run-cursor aware, but the halt users hit came from `context-hardcap-gate.sh` — a separate PreToolUse hook that hard-refuses Edit/Write/Bash with `exit 2` once `estimatedTotalTokens >= compact.hardCapTokens`. It knew nothing about `docs/AI_HANDOFF/RUN.md`, so it froze runs mid-edit exactly as before 2.1.0. It now reads the run cursor and, while a run is unfinished, allows a bounded `compact.hardCapGraceCalls` (default 10) further calls with an explicit "spend this on landing, not new work" directive — finish the edit, commit, write the cursor, push — then blocks hard as it always did.
30
+
31
+ ### Added
32
+
33
+ - **`compact.hardCapGraceCalls`** (default `10`). Budget is per over-cap episode, not per wave: it resets only when the token estimate actually falls (a real compaction happened), so a long run cannot mint itself unlimited grace by advancing its cursor. With no unfinished run the gate's behavior is unchanged.
34
+ - **`tests/hooks/contextHardcapGate.test.js`** — 12 cases covering under-cap pass-through, blocking with no run in flight, `Phase: done` treated as no run, grace grant/exhaustion, no top-up on cursor advance, budget restored after a real compaction, `hardCapBlock=false` escape hatch, non-gated tools, fail-open on malformed input, template/mirror parity, and manifest+settings wiring.
35
+
36
+ ### Notes
37
+
38
+ - **No hook can call `/compact`** — it is a client-only command, so UKit cannot auto-compact on your behalf. The supported path is: land inside the grace window → user runs `/compact` (or Claude Code's own auto-compact fires) → the `handoff-resume.sh` SessionStart hook replays the cursor → the run continues. Enabling auto-compact in Claude Code's `/config` makes that middle step automatic.
39
+
5
40
  ## 2.1.0 - 2026-08-16
6
41
 
7
42
  Handoff autonomy wave: `/ukit:handoff-fullstack` runs a whole goal end-to-end without stopping to ask. Questions are collected once, up front, in the planning phase; everything that used to be a mid-run STOP now resolves automatically and keeps going.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,6 +16,14 @@
16
16
  # just brick the session with no way out, since only the user (not the agent) can invoke
17
17
  # real compaction.
18
18
  #
19
+ # Grace window (compact.hardCapGraceCalls, default 10): blocking the very first tool call
20
+ # past the cap strands a handoff run mid-edit — files half-written, nothing committed, no
21
+ # cursor — which is strictly worse than letting it land. When docs/AI_HANDOFF/RUN.md shows
22
+ # an unfinished run, the gate therefore allows a BOUNDED number of further calls so the run
23
+ # can commit, write its cursor and push; after that it blocks exactly as before. The budget
24
+ # is per over-cap episode, not per wave: it only resets when the estimate actually drops
25
+ # (i.e. a real compaction happened), so a run cannot mint itself fresh grace forever.
26
+ #
19
27
  # Config toggle: compact.hardCapBlock (default true). Set to false only to debug this
20
28
  # gate itself; it must not become a normal escape hatch.
21
29
 
@@ -59,6 +67,22 @@ function readJsonSafe(filePath, fallback = null) {
59
67
  }
60
68
  }
61
69
 
70
+ // An unfinished handoff run — same cursor file the resume hook and the advisory
71
+ // context guard read. `Phase: done` (or no file) means there is nothing in flight.
72
+ function readRunCursor() {
73
+ try {
74
+ const text = fs.readFileSync(path.join(projectRoot, 'docs', 'AI_HANDOFF', 'RUN.md'), 'utf8');
75
+ const runPhase = (text.match(/^Phase:\s*(.+)$/m)?.[1] || '').trim();
76
+ if (!runPhase || /^done$/i.test(runPhase)) return null;
77
+ return {
78
+ phase: runPhase,
79
+ cursor: (text.match(/^Cursor:\s*(.+)$/m)?.[1] || '').trim(),
80
+ };
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
62
86
  (async () => {
63
87
  const config = readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'config.json'), {}) || {};
64
88
  if (config?.compact?.hardCapBlock === false) {
@@ -78,15 +102,58 @@ function readJsonSafe(filePath, fallback = null) {
78
102
  const state = mod.buildCompactPressureState(rawState, config);
79
103
  const thresholds = mod.buildCompactThresholds(config);
80
104
 
105
+ const gracePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'hardcap-grace.json');
106
+
81
107
  if (state.estimatedTotalTokens < thresholds.hardCapTokens) {
108
+ // Back under the cap — the episode is over, so the next one starts with a full budget.
109
+ fs.rmSync(gracePath, { force: true });
82
110
  process.exit(0);
83
111
  return;
84
112
  }
85
113
 
114
+ const run = readRunCursor();
115
+ if (run) {
116
+ const graceCalls = Number.isFinite(config?.compact?.hardCapGraceCalls)
117
+ ? config.compact.hardCapGraceCalls
118
+ : 10;
119
+ const prior = readJsonSafe(gracePath, null);
120
+ // Reset only when the estimate actually fell since grace started: that is the signal a
121
+ // real compaction/shed happened. Advancing the cursor alone must NOT top the budget up,
122
+ // otherwise a long run gets unlimited grace and the ceiling stops meaning anything.
123
+ const carried =
124
+ prior && Number.isFinite(prior.startedAtTokens) && state.estimatedTotalTokens >= prior.startedAtTokens
125
+ ? prior
126
+ : { startedAtTokens: state.estimatedTotalTokens, used: 0 };
127
+
128
+ if (carried.used < graceCalls) {
129
+ const used = carried.used + 1;
130
+ try {
131
+ fs.mkdirSync(path.dirname(gracePath), { recursive: true });
132
+ fs.writeFileSync(gracePath, JSON.stringify({ ...carried, used }));
133
+ } catch {
134
+ // Losing the counter must not block the run; worst case grace restarts.
135
+ }
136
+ process.stderr.write(
137
+ [
138
+ `CONTEXT OVER CAP — grace ${used}/${graceCalls} (~${state.estimatedTotalTokens} tokens >= ${thresholds.hardCapTokens}).`,
139
+ `An unfinished run is in flight (Phase: ${run.phase}${run.cursor ? `, Cursor: ${run.cursor}` : ''}), so this call is allowed instead of stranding it mid-edit.`,
140
+ 'Spend the remaining grace on LANDING, not on new work: finish the current edit, commit, update docs/AI_HANDOFF/RUN.md, push.',
141
+ 'Do NOT start a new task, open new files, or spawn agents. When grace runs out the gate blocks hard.',
142
+ 'After the user runs /compact, the SessionStart resume hook replays the cursor and the run continues automatically.',
143
+ ].join('\n') + '\n',
144
+ );
145
+ process.exit(0);
146
+ return;
147
+ }
148
+ }
149
+
86
150
  const lines = [
87
151
  `BLOCKED (context hard cap): estimated context ~${state.estimatedTotalTokens} tokens >= hard cap ${thresholds.hardCapTokens}.`,
88
152
  'This is an absolute ceiling (compact.hardCapTokens), separate from the soft/hard advisory phases — those were apparently not followed.',
89
153
  `Edit/Write/Bash refused (tool_name=${toolName}) until real compaction happens.`,
154
+ run
155
+ ? 'The unfinished-run grace window (compact.hardCapGraceCalls) is already exhausted — progress should be committed and the cursor written by now.'
156
+ : 'No unfinished run in docs/AI_HANDOFF/RUN.md, so there is no grace window to spend.',
90
157
  'Remedy (any one): run /compact, or start a new session (SessionStart resets the counter), or delete .ukit/storage/cache/compact-pressure.json.',
91
158
  'Do not work around this by summarizing inline and continuing, and do not reach for a non-gated write tool.',
92
159
  ];
@@ -9,6 +9,10 @@
9
9
  "showThinkingSummaries": false,
10
10
  "spinnerTipsEnabled": false,
11
11
  "defaultView": "chat",
12
+ "autoCompactEnabled": true,
13
+ "env": {
14
+ "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "150000"
15
+ },
12
16
  "permissions": {
13
17
  "defaultMode": "bypassPermissions",
14
18
  "allow": [
@@ -79,6 +79,9 @@ Next: <bước kế tiếp chính xác>
79
79
  - Subagent ghi **full log vào task file trên đĩa**, chỉ trả về orchestrator ≤10 dòng (executor) / ≤6 dòng (reviewer). Paste log ngược lại orchestrator là nguyên nhân số 1 làm run chết vì hết context.
80
80
  - Hết mỗi wave: commit, ghi cursor, **collapse** wave đó còn 1 dòng/task trong bộ nhớ làm việc, rồi chạy tiếp.
81
81
  - Yêu cầu `/compact` **chỉ** được đặt ở cuối command, giữa 2 cycle. Giữa cycle thì tuyệt đối không — state đã nằm hết ở git + `INDEX.md` + `RUN.md` nên compact ở ranh giới cycle không mất gì.
82
+ - Vượt `compact.hardCapTokens` (mặc định 160k) mà `RUN.md` còn run dở: `context-hardcap-gate` cho thêm `compact.hardCapGraceCalls` (mặc định 10) tool call rồi mới chặn cứng. **Grace đó chỉ để hạ cánh** — hoàn tất edit đang dở, commit, ghi cursor, push. Không mở task mới, không đọc thêm file, không spawn agent. Hết grace là chặn thật; budget chỉ reset khi ước lượng token thực sự giảm (có compact thật), không reset theo wave.
83
+ - Không hook nào gọi được `/compact` — đó là lệnh client-only. Nhưng từ 2.1.3, settings mặc định đặt `env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = 150000` < `hardCapTokens` (160k), nên **client tự auto-compact trước khi gate chặn**. Đường thường: auto-compact chạy → `handoff-resume.sh` replay cursor → chạy tiếp, không cần người gõ gì. Grace window ở trên chỉ còn là lưới an toàn.
84
+ - Sửa một trong hai số đó thì phải giữ `autoCompactWindow < hardCapTokens`. Đảo thứ tự là deadlock: gate chặn tool trước → transcript ngừng lớn → ngưỡng auto-compact không bao giờ tới. `tests/core/autoCompactWindow.test.js` khóa bất biến này.
82
85
 
83
86
  ### Git
84
87
 
@@ -11,6 +11,7 @@
11
11
  "tokenThreshold": 50000,
12
12
  "hardCapTokens": 160000,
13
13
  "hardCapBlock": true,
14
+ "hardCapGraceCalls": 10,
14
15
  "contextRotDetection": true,
15
16
  "askBeforeDrop": true,
16
17
  "codexContext": {
@@ -384,7 +385,8 @@
384
385
  "enabled": "Bật/tắt toàn bộ helper compact của UKit.",
385
386
  "tokenThreshold": "Ngưỡng token chung cho runtime compact dùng chung.",
386
387
  "hardCapTokens": "Ngưỡng cứng tuyệt đối (mặc định 160000 token ước lượng). Chạm/vượt ngưỡng này thì context coi như quá dài — không phải gợi ý nữa, là bắt buộc. PHẢI thấp hơn context window thật của model (200k), nếu không API sẽ báo lỗi vượt context trước khi gate kịp chặn.",
387
- "hardCapBlock": "Nếu true, hook context-hardcap-gate chặn cứng Edit/Write/Bash (exit 2) khi vượt hardCapTokens, cho tới khi có compact thật (PreCompact) reset lại bộ đếm. Không có ngoại lệ.",
388
+ "hardCapBlock": "Nếu true, hook context-hardcap-gate chặn cứng Edit/Write/Bash (exit 2) khi vượt hardCapTokens, cho tới khi có compact thật (PreCompact) reset lại bộ đếm.",
389
+ "hardCapGraceCalls": "Số tool call được phép chạy tiếp sau khi vượt hardCapTokens KHI docs/AI_HANDOFF/RUN.md còn run dở (mặc định 10). Dùng để run kịp commit + ghi cursor + push rồi mới bị chặn, thay vì chết giữa lúc đang Edit. Hết grace là chặn cứng như cũ. Budget tính theo mỗi đợt vượt cap, chỉ reset khi ước lượng token thật sự giảm (có compact thật) — không reset theo wave.",
388
390
  "contextRotDetection": "Phát hiện context quá dài/dễ mục để giữ lại state quan trọng trước khi AI nhớ sai.",
389
391
  "askBeforeDrop": "Giữ thái độ thận trọng trước khi bỏ context quan trọng. Nếu rủi ro thì hand back cho main model.",
390
392
  "agentContext": {