@ngockhoale/ukit 1.4.4 → 1.5.0

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,29 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 1.5.0 - 2026-05-24
6
+
7
+ ### Fixed
8
+
9
+ - Switched safe-patch release regressions to `spawnSync`-based helpers so advisory-mode assertions capture `stderr` correctly even when the hook exits `0`.
10
+ - Raised the packed artifact size budget slightly to preserve the shipped canvas font lane and recent runtime/router surfaces without failing release smoke on a 625-byte overage.
11
+
12
+ ### Changed
13
+
14
+ - Bumped package and shared runtime version metadata to `1.5.0`.
15
+
16
+ ## 1.4.5 - 2026-05-24
17
+
18
+ ### Fixed
19
+
20
+ - Stopped safe-patch hooks (`stale-spec-check`, `post-edit-verify`) from hard-blocking tool calls when configured via the new `safePatch.advisoryOnly` flag or the `UKIT_SAFE_PATCH_ADVISORY=1` env. Removes the mid-task "stop and wait for continue" stalls that hit shared-risk files in `.claude/`, `templates/`, `scripts/`, etc., while preserving the diagnostic message so Claude/Codex can self-correct instead of stalling.
21
+ - Default behavior unchanged (`advisoryOnly: false`): pre-edit corruption guards (shebang-bom, non-text, whole-file Write to shared-risk) and the `protect-files` / `block-dangerous` hooks still hard-block.
22
+
23
+ ### Tests
24
+
25
+ - Added regression coverage for advisory-mode exit code and stderr in `tests/hooks/safePatchProtocol.test.js`.
26
+ - Added default + validation assertions for `safePatch.advisoryOnly` in `tests/core/runtimeConfig.test.js`.
27
+
5
28
  ## 1.4.4 - 2026-05-11
6
29
 
7
30
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
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",
@@ -49,7 +49,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
49
49
  const safeOverrides = isPlainObject(overrides) ? overrides : {};
50
50
 
51
51
  return mergeObjects({
52
- version: '1.4.4',
52
+ version: '1.5.0',
53
53
  agent: 'claude-code',
54
54
  autonomy: {
55
55
  level: 'balanced',
@@ -183,6 +183,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
183
183
  safePatch: {
184
184
  enabled: true,
185
185
  strictSharedRisk: true,
186
+ advisoryOnly: false,
186
187
  largeFileLineThreshold: 800,
187
188
  largeFileByteThreshold: 200_000,
188
189
  backupEnabled: true,
@@ -368,6 +369,7 @@ export function validateRuntimeConfig(config) {
368
369
  } else {
369
370
  pushBooleanError(errors, config.safePatch.enabled, 'safePatch.enabled');
370
371
  pushBooleanError(errors, config.safePatch.strictSharedRisk, 'safePatch.strictSharedRisk');
372
+ pushBooleanError(errors, config.safePatch.advisoryOnly, 'safePatch.advisoryOnly');
371
373
  pushPositiveNumberError(errors, config.safePatch.largeFileLineThreshold, 'safePatch.largeFileLineThreshold');
372
374
  pushPositiveNumberError(errors, config.safePatch.largeFileByteThreshold, 'safePatch.largeFileByteThreshold');
373
375
  pushBooleanError(errors, config.safePatch.backupEnabled, 'safePatch.backupEnabled');
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { analyzeTextBuffer, analyzeTextFile, publicTextProfile } from '../runtime/text-profile.mjs';
5
- import { classifySafePatchRisk, parseJsonInput, pathExists, readRuntimeSafePatchConfig, resolveProjectFile } from '../runtime/safe-patch-core.mjs';
5
+ import { classifySafePatchRisk, isSafePatchAdvisoryOnly, parseJsonInput, pathExists, readRuntimeSafePatchConfig, resolveProjectFile } from '../runtime/safe-patch-core.mjs';
6
6
 
7
7
  async function readStdin() {
8
8
  return await new Promise((resolve) => {
@@ -183,8 +183,10 @@ async function main() {
183
183
  process.stdout.write(json ? `${JSON.stringify({ help: text })}\n` : `${text}\n`);
184
184
  return;
185
185
  }
186
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
187
+ const runtimeConfig = await readRuntimeSafePatchConfig(projectRoot);
186
188
  const result = await verifyPostEdit({
187
- projectRoot: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
189
+ projectRoot,
188
190
  payload: parseJsonInput(await readStdin(), {}),
189
191
  });
190
192
  if (json) {
@@ -193,7 +195,14 @@ async function main() {
193
195
  process.stdout.write(`[ukit-safe-patch] ${result.message}\n`);
194
196
  }
195
197
  if (result.status === 'blocked') {
196
- process.stderr.write(`[ukit-safe-patch] ${result.message}\n`);
198
+ const advisory = isSafePatchAdvisoryOnly(runtimeConfig);
199
+ const prefix = advisory ? 'ADVISORY' : 'BLOCKED';
200
+ const message = String(result.message || '').replace(/^BLOCKED:/, `${prefix}:`);
201
+ process.stderr.write(`[ukit-safe-patch] ${message}\n`);
202
+ if (advisory) {
203
+ process.stderr.write('[ukit-safe-patch] advisoryOnly=true — change is already written; continue planned follow-up without stopping.\n');
204
+ return;
205
+ }
197
206
  process.exit(2);
198
207
  }
199
208
  }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import {
5
5
  classifySafePatchRisk,
6
6
  countOccurrences,
7
+ isSafePatchAdvisoryOnly,
7
8
  lineNumberForIndex,
8
9
  parseJsonInput,
9
10
  pathExists,
@@ -169,7 +170,9 @@ async function main() {
169
170
  return;
170
171
  }
171
172
  const payload = parseJsonInput(await readStdin(), {});
172
- const result = await checkStaleSpec({ projectRoot: process.env.CLAUDE_PROJECT_DIR || process.cwd(), payload });
173
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
174
+ const runtimeConfig = await readRuntimeSafePatchConfig(projectRoot);
175
+ const result = await checkStaleSpec({ projectRoot, payload });
173
176
  if (args.json) {
174
177
  process.stdout.write(`${JSON.stringify(result)}\n`);
175
178
  } else if (result.status === 'ok') {
@@ -179,7 +182,14 @@ async function main() {
179
182
  }
180
183
 
181
184
  if (result.status === 'blocked') {
182
- process.stderr.write(`[ukit-safe-patch] ${result.message}\n`);
185
+ const advisory = isSafePatchAdvisoryOnly(runtimeConfig);
186
+ const prefix = advisory ? 'ADVISORY' : 'BLOCKED';
187
+ const message = String(result.message || '').replace(/^BLOCKED:/, `${prefix}:`);
188
+ process.stderr.write(`[ukit-safe-patch] ${message}\n`);
189
+ if (advisory) {
190
+ process.stderr.write('[ukit-safe-patch] advisoryOnly=true — continue with corrected old_string; do not stop and ask the user.\n');
191
+ return;
192
+ }
183
193
  process.exit(2);
184
194
  }
185
195
  }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  export const DEFAULT_SAFE_PATCH_CONFIG = {
5
5
  enabled: true,
6
6
  strictSharedRisk: true,
7
+ advisoryOnly: false,
7
8
  largeFileLineThreshold: 800,
8
9
  largeFileByteThreshold: 200_000,
9
10
  backupEnabled: true,
@@ -13,6 +14,15 @@ export const DEFAULT_SAFE_PATCH_CONFIG = {
13
14
  deltaMaxDiffCells: 2_000_000,
14
15
  };
15
16
 
17
+ // When advisoryOnly is true (via config or env UKIT_SAFE_PATCH_ADVISORY=1),
18
+ // safe-patch hooks downgrade exit 2 (block) to exit 0 + WARNING. This keeps
19
+ // Claude/Codex from stalling mid-task on stale/ambiguous spec or delta-budget
20
+ // trips without losing the diagnostic message.
21
+ export function isSafePatchAdvisoryOnly(config = DEFAULT_SAFE_PATCH_CONFIG) {
22
+ if (process.env.UKIT_SAFE_PATCH_ADVISORY === '1') return true;
23
+ return Boolean(config?.advisoryOnly);
24
+ }
25
+
16
26
  const SHARED_RISK_PATTERNS = [
17
27
  /^\.claude\/hooks\//,
18
28
  /^\.claude\/ukit\//,
@@ -1,6 +1,6 @@
1
1
  # UKit Shared Runtime
2
2
 
3
- This folder stores shared UKit runtime state for v1.4.4 features.
3
+ This folder stores shared UKit runtime state for v1.5.0 features.
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/`
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.4.4",
2
+ "version": "1.5.0",
3
3
  "agent": "claude-code",
4
4
  "autonomy": {
5
5
  "level": "balanced",
@@ -147,6 +147,7 @@
147
147
  "safePatch": {
148
148
  "enabled": true,
149
149
  "strictSharedRisk": true,
150
+ "advisoryOnly": false,
150
151
  "largeFileLineThreshold": 800,
151
152
  "largeFileByteThreshold": 200000,
152
153
  "backupEnabled": true,
@@ -365,6 +366,7 @@
365
366
  "safePatch": {
366
367
  "enabled": "Bật Safe Patch Protocol: UKit âm thầm chặn patch stale/ambiguous trên file rủi ro và giữ an toàn encoding khi helper nội bộ sửa file.",
367
368
  "strictSharedRisk": "Nếu true, file runtime/shared-risk như .claude/hooks hoặc .claude/ukit sẽ bị guard chặt hơn; người dùng vẫn không cần nhớ command mới.",
369
+ "advisoryOnly": "Nếu true (hoặc env UKIT_SAFE_PATCH_ADVISORY=1), guard chỉ in WARNING thay vì exit 2 — tránh Claude/Codex dừng giữa chừng chờ user 'continue'. Vẫn giữ message để model tự retry.",
368
370
  "largeFileLineThreshold": "Số dòng từ đó file được xem là lớn và cần anchor/spec guard kỹ hơn.",
369
371
  "largeFileByteThreshold": "Kích thước byte từ đó file được xem là lớn/rủi ro khi agent edit.",
370
372
  "backupEnabled": "Nếu true, UKit tạo rollback bytes dưới .ukit/storage/backups cho edit rủi ro.",