@ngockhoale/ukit 3.4.19 → 3.4.20

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,26 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.20 - 2026-10-06
6
+
7
+ **`ukit update` now displays the NEW version.** The running process is the old code, so the
8
+ banner used to print only the stale "Current version" and read like the update had not
9
+ worked. It now prints `Updated: <old> -> <new>` (version read from the freshly installed global
10
+ package.json) right after `npm install -g`, repeats the new version in the final line, and says
11
+ "already the latest" when nothing changed.
12
+
13
+ **Release/verify tooling no longer wastes minutes on trivial work.**
14
+ - `release:verify` now runs the cheap deterministic gates first (`docs:render:check`, `tests/consistency`) and
15
+ stops the ~4 min core suite after 5 failures (`--bail 5`); a stale instruction render or template/active
16
+ drift is reported in seconds, not after the full suite.
17
+ - Single-instance lock: a second concurrent `release:verify` for the same repo exits 3 naming the live pid
18
+ instead of racing the first (two concurrent suites made both slow and flaky).
19
+ - `rollout.test.js` no longer re-runs the entire `release:verify` from inside `release:verify` (that one test
20
+ took ~307 s, doubling the release gate).
21
+ - Instruction core: "Long commands run once" rule (never duplicate a backgrounded/timed-out long run).
22
+ - Fixed a comment in `handoff-model-guard.sh` that the watchdog-coverage scan mistook for an unarmed inline
23
+ `node -e` block (shipped red in 3.4.18/3.4.19 because only the hooks suite was run before release).
24
+
5
25
  ## 3.4.19 - 2026-10-06
6
26
 
7
27
  **Create-scope guard: more read-only shell false positives removed (found by replaying 4vn's real blocked commands).**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.19",
3
+ "version": "3.4.20",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
3
+ import crypto from 'node:crypto';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
 
@@ -38,7 +39,48 @@ if (postPublish) {
38
39
  process.exit(0);
39
40
  }
40
41
 
42
+
43
+ // Single-instance guard (full verification takes ~4 min). Starting a second run while one is
44
+ // alive (e.g. after the first was moved to the background by a tool timeout) only makes both
45
+ // slower and flakier — wait for / read the first run's result instead.
46
+ let lockPath = null;
47
+ function acquireVerifyLock() {
48
+ // Nested runs (the release suite's own tests spawn this script) and test workers must not contend.
49
+ if (process.env.UKIT_RELEASE_VERIFY_ACTIVE === '1' || process.env.VITEST || process.env.UKIT_VERIFY_NO_LOCK === '1') return;
50
+ lockPath = path.join(os.tmpdir(), `ukit-release-verify-${crypto.createHash('sha1').update(rootDir).digest('hex').slice(0, 10)}.lock`);
51
+ if (existsSync(lockPath)) {
52
+ const pid = Number(readFileSync(lockPath, 'utf8').trim());
53
+ let alive = false;
54
+ try { process.kill(pid, 0); alive = Number.isInteger(pid) && pid > 0 && pid !== process.pid; } catch { /* stale */ }
55
+ if (alive) {
56
+ console.error(`[release:verify] another release:verify is already running for this repo (pid ${pid}). Wait for it (or read its output) — do NOT start a second full run; two concurrent suites slow each other and cause flaky failures.`);
57
+ process.exit(3);
58
+ }
59
+ }
60
+ writeFileSync(lockPath, String(process.pid));
61
+ process.env.UKIT_RELEASE_VERIFY_ACTIVE = '1';
62
+ const release = () => { try { if (readFileSync(lockPath, 'utf8').trim() === String(process.pid)) unlinkSync(lockPath); } catch { /* gone */ } };
63
+ process.on('exit', release);
64
+ for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { release(); process.exit(130); });
65
+ }
66
+
67
+ acquireVerifyLock();
68
+
69
+ // Cheap, deterministic gates FIRST (seconds): a stale instruction render or a consistency drift
70
+ // used to surface only after the ~4 min full suite.
41
71
  const steps = [
72
+ {
73
+ label: 'Rendered instructions up to date (fix: yarn docs:render)',
74
+ command: 'yarn',
75
+ args: ['docs:render:check'],
76
+ env: process.env,
77
+ },
78
+ {
79
+ label: 'Consistency suite (template<->active sync, watchdog coverage, render parity)',
80
+ command: 'yarn',
81
+ args: ['vitest', 'run', 'tests/consistency'],
82
+ env: process.env,
83
+ },
42
84
  {
43
85
  label: 'Artifact smoke',
44
86
  command: 'yarn',
@@ -54,7 +96,7 @@ const steps = [
54
96
  {
55
97
  label: 'Core test suite',
56
98
  command: 'yarn',
57
- args: ['test:release-core'],
99
+ args: ['test:release-core', '--bail', '5'], // stop after 5 failures instead of finishing a doomed 4 min run
58
100
  env: process.env,
59
101
  },
60
102
  {
@@ -1,4 +1,4 @@
1
- import { updateUkit, runInstallAfterUpdate, UKIT_PACKAGE_NAME } from '../../core/update.js';
1
+ import { updateUkit, runInstallAfterUpdate, readInstalledGlobalVersion, UKIT_PACKAGE_NAME } from '../../core/update.js';
2
2
 
3
3
  const KNOWN_UPDATE_FLAGS = new Set(['--help', '-h', '--no-install']);
4
4
 
@@ -27,13 +27,23 @@ export async function runUpdate({ packageVersion, argv = [], cwd = process.cwd()
27
27
  );
28
28
  }
29
29
 
30
- console.log(`[UKit] Current version: ${packageVersion}`);
30
+ console.log(`[UKit] Version before update: ${packageVersion}`);
31
31
  console.log(`[UKit] Running: npm install -g ${UKIT_PACKAGE_NAME}`);
32
32
 
33
33
  updateUkit();
34
34
 
35
+ // This process still runs the OLD code, so report the version the update actually
36
+ // installed (read from disk), never the stale `packageVersion`.
37
+ const installedVersion = readInstalledGlobalVersion();
38
+ const versionLine = installedVersion
39
+ ? installedVersion === packageVersion
40
+ ? `[UKit] Installed version: ${installedVersion} (already the latest)`
41
+ : `[UKit] Updated: ${packageVersion} -> ${installedVersion}`
42
+ : '[UKit] Updated (could not read the new version — run `ukit --version`)';
43
+ console.log(versionLine);
44
+
35
45
  if (argv.includes('--no-install')) {
36
- console.log('[UKit] Update complete. Skipped project refresh (--no-install).');
46
+ console.log(`[UKit] Update complete${installedVersion ? ` — UKit ${installedVersion}` : ''}. Skipped project refresh (--no-install).`);
37
47
  return;
38
48
  }
39
49
 
@@ -41,7 +51,7 @@ export async function runUpdate({ packageVersion, argv = [], cwd = process.cwd()
41
51
  const { ran, reason } = runInstallAfterUpdate({ projectRoot: cwd });
42
52
 
43
53
  if (ran) {
44
- console.log('[UKit] Update complete — CLI upgraded and project refreshed.');
54
+ console.log(`[UKit] Update complete — UKit ${installedVersion ?? 'latest'} installed and project refreshed.`);
45
55
  return;
46
56
  }
47
57
 
@@ -62,6 +62,22 @@ function resolveGlobalUkitBinDetailed({ spawnSync = defaultSpawnSync } = {}) {
62
62
  return { binPath: fs.existsSync(binPath) ? binPath : null, timedOut: false };
63
63
  }
64
64
 
65
+ /**
66
+ * Version of the globally installed UKit package as it is on disk NOW, or null if unreadable.
67
+ * `packageVersion` in the running process is the OLD code's version (the process predates
68
+ * `npm install -g`), so it must never be printed as the post-update version.
69
+ */
70
+ export function readInstalledGlobalVersion({ spawnSync = defaultSpawnSync } = {}) {
71
+ try {
72
+ const { binPath } = resolveGlobalUkitBinDetailed({ spawnSync });
73
+ if (!binPath) return null;
74
+ const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(binPath), '..', 'package.json'), 'utf8'));
75
+ return typeof pkg.version === 'string' ? pkg.version : null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
65
81
  /**
66
82
  * Run `ukit install` in `projectRoot` using the freshly installed global package.
67
83
  * Returns { ran, reason } so the caller can report accurately rather than claim an
@@ -388,7 +388,7 @@ const SHELL_WRITE_PATTERNS = [
388
388
  new RegExp(String.raw`${CMD_POS}curl\s(?:[^;&|\n]*\s)?(?:-[sSfLvkiI#]*[oO]\b|--(?:output|remote-name|output-dir)\b)`),
389
389
  new RegExp(String.raw`${CMD_POS}[gm]?awk\s(?:[^;&|\n]*\s)?-i\s*inplace\b`),
390
390
  ];
391
- // Read-only inline interpreters (`node -e '…'`, `python3 -c '…'`, `python3 - <<'PY'`, `jq '…'`):
391
+ // Read-only inline interpreters (inline node scripts (-e flag), `python3 -c '…'`, `python3 - <<'PY'`, `jq '…'`):
392
392
  // their quoted script/program body is code, not shell. Scanning it with the shell write
393
393
  // patterns misfires on arrow functions (`l=>{…}`), comparisons (`n>1750`), and words such as
394
394
  // `ln`/`cp`/`install` used as identifiers — which blocked read-only transcript analysis in a
@@ -86,6 +86,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
86
86
  - **Trivial**: no docs, no index query unless the target is unclear. **Simple**: `docs/MEMORY.md` + resolver-selected files/tests. **Non-trivial**: + `docs/PROJECT.md` + `docs/CODE_MAP.md`.
87
87
  <!-- RULES: BUDGET-02 BUDGET-03 -->
88
88
  - `docs/STATUS.md` for open-ended/continue; `docs/TASKS.md` only for queued-task prompts; `docs/WORKLOG.md` recent entries only. Verification: targeted first, widen on risk/shared scope, ask before broad runs.
89
+ - **Long commands run once:** start a full suite/release gate (>1 min) a single time; if it times out or goes to background, read its output — never launch a duplicate in parallel. Cheap deterministic gates first; trivial/simple tasks never need the full suite.
89
90
 
90
91
  ## Living Status Workflow
91
92
  <!-- RULE: STATUS-01 -->
@@ -86,6 +86,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
86
86
  - **Trivial**: no docs, no index query unless the target is unclear. **Simple**: `docs/MEMORY.md` + resolver-selected files/tests. **Non-trivial**: + `docs/PROJECT.md` + `docs/CODE_MAP.md`.
87
87
  <!-- RULES: BUDGET-02 BUDGET-03 -->
88
88
  - `docs/STATUS.md` for open-ended/continue; `docs/TASKS.md` only for queued-task prompts; `docs/WORKLOG.md` recent entries only. Verification: targeted first, widen on risk/shared scope, ask before broad runs.
89
+ - **Long commands run once:** start a full suite/release gate (>1 min) a single time; if it times out or goes to background, read its output — never launch a duplicate in parallel. Cheap deterministic gates first; trivial/simple tasks never need the full suite.
89
90
 
90
91
  ## Living Status Workflow
91
92
  <!-- RULE: STATUS-01 -->
@@ -83,6 +83,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
83
83
  - **Trivial**: no docs, no index query unless the target is unclear. **Simple**: `docs/MEMORY.md` + resolver-selected files/tests. **Non-trivial**: + `docs/PROJECT.md` + `docs/CODE_MAP.md`.
84
84
  <!-- RULES: BUDGET-02 BUDGET-03 -->
85
85
  - `docs/STATUS.md` for open-ended/continue; `docs/TASKS.md` only for queued-task prompts; `docs/WORKLOG.md` recent entries only. Verification: targeted first, widen on risk/shared scope, ask before broad runs.
86
+ - **Long commands run once:** start a full suite/release gate (>1 min) a single time; if it times out or goes to background, read its output — never launch a duplicate in parallel. Cheap deterministic gates first; trivial/simple tasks never need the full suite.
86
87
 
87
88
  ## Living Status Workflow
88
89
  <!-- RULE: STATUS-01 -->