@ngockhoale/ukit 2.3.17 → 2.3.19

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,21 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.3.19 - 2026-09-12
6
+
7
+ C14 bug-fix release record — one correctness fix is shipped; this release adds no new features.
8
+
9
+ - **Completion gate no longer silently releases a reentrant stop.** `templates/.claude/ukit/runtime/execution-ledger.mjs` (`--evaluate-stop`) had a `stop_hook_active` valve: when Claude Code re-fired `Stop` after the gate had already blocked once, the valve emitted only a `systemMessage` with no `decision` while completion evidence was still missing, so the turn ended and the session idled until the human typed "tiếp". The valve is removed — a reentrant stop is now evaluated like any other stop, blocking again with the actionable missing-evidence reason (produce the evidence or report a blocker), and the loop stays bounded by the existing visible continuation cap and the verification-loop blocker, so a finished task is never trapped.
10
+
11
+ ## 2.3.18 - 2026-09-12
12
+
13
+ C13 bug-sweep release record — two wave-1 correctness fixes are shipped; this release adds no new features.
14
+
15
+ - **Task-budget validator heading and target-count fix.** `src/core/taskBudgetValidator.js` and the shipped twin `templates/.claude/ukit/index/task-budget-validator.mjs` now accept the template's `## Test Cases (REQUIRED — TDD)` heading without a false `missing-field` verdict and count only top-level `## Target Files` bullets, avoiding indented sub-bullet miscounts.
16
+ - **Task-progress guard multi-section fix.** `src/core/taskProgressGuard.js` now evaluates milestone entries across every `## Progress` section, so a fresh appended entry is not masked by an older first-section entry while malformed and drift checks remain active.
17
+
18
+ Verification: version pin and targeted regression suites pass; `yarn release:verify` is the closing release gate.
19
+
5
20
  ## 2.3.17 - 2026-09-12
6
21
 
7
22
  Long-task resilience — a handoff run is now measured before planning, checkpointed during
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.3.17",
3
+ "version": "2.3.19",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,17 +2,22 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { writeFileAtomic } from './fileOps.js';
4
4
 
5
+ // Root-anchored (leading `/`) so the block only ignores project-root entries.
6
+ // Unanchored dir entries (`.claude/`) match same-named dirs at ANY depth and
7
+ // hid templates/.claude from git — the C12 trap `ukit install` kept re-adding.
8
+ // Entries with an inner slash (`.claude/ukit/...`) are already root-anchored
9
+ // by gitignore semantics.
5
10
  const UKIT_ENTRIES = [
6
- '.cache/',
11
+ '/.cache/',
7
12
  // legacy: Antigravity adapter removed in v2.2.0; leftovers must stay ignored
8
- '.antigravity/',
9
- '.claude/',
10
- '.codex/',
11
- '.omp/',
12
- '.ukit/',
13
- 'opencode.json',
14
- 'AGENTS.md',
15
- 'CLAUDE.md',
13
+ '/.antigravity/',
14
+ '/.claude/',
15
+ '/.codex/',
16
+ '/.omp/',
17
+ '/.ukit/',
18
+ '/opencode.json',
19
+ '/AGENTS.md',
20
+ '/CLAUDE.md',
16
21
  '.claude/ukit/.ukit/',
17
22
  '.claude/ukit/permission-usage.json',
18
23
  '.claude/ukit/permission-audit.log',
@@ -88,17 +88,23 @@ function splitSections(markdown) {
88
88
  }
89
89
 
90
90
  function getSectionBody(sections, name) {
91
- if (!sections.has(name)) return null;
92
- return sections.get(name).join('\n');
91
+ if (sections.has(name)) return sections.get(name).join('\n');
92
+ const normalizedName = name.replace(/\s*\([^)]*\)\s*$/, '');
93
+ for (const [heading, body] of sections) {
94
+ if (heading.replace(/\s*\([^)]*\)\s*$/, '') === normalizedName) {
95
+ return body.join('\n');
96
+ }
97
+ }
98
+ return null;
93
99
  }
94
100
 
95
- // Count bullets under `## Target Files` — lines whose first non-whitespace char is `- `
101
+ // Count bullets under `## Target Files` — only column-0 bullet lines.
96
102
  function countTargetFiles(body) {
97
103
  if (body == null) return 0;
98
104
  const lines = body.split('\n');
99
105
  let count = 0;
100
106
  for (const line of lines) {
101
- if (/^\s*-\s+\S/.test(line)) count += 1;
107
+ if (/^-\s+\S/.test(line)) count += 1;
102
108
  }
103
109
  return count;
104
110
  }
@@ -28,6 +28,7 @@
28
28
  // future watchdog integration) can inject a fixed timestamp.
29
29
 
30
30
  const PROGRESS_HEADING = /^##\s+Progress\s*$/m;
31
+ const PROGRESS_HEADING_GLOBAL = /^##\s+Progress\s*$/gm;
31
32
  const REPORT_HEADING = /^##\s+Executor Report\s*$/m;
32
33
  const TARGET_HEADING = /^##\s+Target Files\s*$/m;
33
34
  const ENTRY_RE = /^-\s+(\S+)\s+·\s+milestone:\s*([^·]*)·\s+last-green:\s*([^·]*)·\s+files:\s*([^·]*)·\s+drift:\s*(.*)$/;
@@ -87,6 +88,16 @@ function collectProgressEntries(progressSection) {
87
88
  return entries;
88
89
  }
89
90
 
91
+ function collectAllProgressEntries(markdown) {
92
+ const headings = [...markdown.matchAll(PROGRESS_HEADING_GLOBAL)];
93
+ return headings.flatMap((heading, index) => {
94
+ const start = heading.index + heading[0].length;
95
+ const nextHeading = /\n##\s+/.exec(markdown.slice(start));
96
+ const end = nextHeading ? start + nextHeading.index : markdown.length;
97
+ return collectProgressEntries(markdown.slice(start, end));
98
+ });
99
+ }
100
+
90
101
  export function checkMilestoneProgress({
91
102
  markdown,
92
103
  now = Date.now(),
@@ -110,8 +121,7 @@ export function checkMilestoneProgress({
110
121
  };
111
122
  }
112
123
 
113
- const progressSection = sliceSection(markdown, PROGRESS_HEADING);
114
- const entries = collectProgressEntries(progressSection || '');
124
+ const entries = collectAllProgressEntries(markdown);
115
125
 
116
126
  if (entries.length === 0) {
117
127
  violations.push({ type: 'no-progress', detail: '## Progress section has no entries' });
@@ -70,15 +70,21 @@ function splitSections(markdown) {
70
70
  }
71
71
 
72
72
  function getSectionBody(sections, name) {
73
- if (!sections.has(name)) return null;
74
- return sections.get(name).join('\n');
73
+ if (sections.has(name)) return sections.get(name).join('\n');
74
+ const normalizedName = name.replace(/\s*\([^)]*\)\s*$/, '');
75
+ for (const [heading, body] of sections) {
76
+ if (heading.replace(/\s*\([^)]*\)\s*$/, '') === normalizedName) {
77
+ return body.join('\n');
78
+ }
79
+ }
80
+ return null;
75
81
  }
76
82
 
77
83
  function countTargetFiles(body) {
78
84
  if (body == null) return 0;
79
85
  let count = 0;
80
86
  for (const line of body.split('\n')) {
81
- if (/^\s*-\s+\S/.test(line)) count += 1;
87
+ if (/^-\s+\S/.test(line)) count += 1;
82
88
  }
83
89
  return count;
84
90
  }
@@ -938,23 +938,12 @@ async function main() {
938
938
  return;
939
939
  }
940
940
 
941
- // Claude Code invokes Stop again after a Stop hook blocks the first stop. Re-blocking
942
- // that recovery turn creates a self-sustaining loop, so let it end normally instead.
943
- // If work still lacks evidence, surface the recovery reason to the user rather than
944
- // silently ending after the automatic continuation.
945
- // Vibecode autonomy keeps pushing through the reentrant Stop as well: the recovery turn
946
- // after a block is another chance to finish the work, not a release valve. Block again
947
- // with the reason; the loop still ends the moment evidence lands or a blocker appears.
948
- if (payload.stop_hook_active === true && state?.routeSummary?.autonomyLevel !== 'vibecode') {
949
- if (result.continue || result.capped || result.notify) {
950
- const recoveryReason = result.reason
951
- || 'UKit completion gate reached its continuation limit; unfinished work was not retried again.';
952
- process.stdout.write(`${JSON.stringify({
953
- systemMessage: `UKit stopped automatic recovery after one continuation: ${recoveryReason}`,
954
- })}\n`);
955
- }
956
- return;
957
- }
941
+ // A reentrant Stop (Claude Code re-fires Stop after this hook already blocked once) is
942
+ // gated exactly like any other Stop: while evidence is still missing it blocks again
943
+ // with the actionable reason instead of silently releasing the recovery turn. Loop
944
+ // termination is guaranteed elsewhere — non-vibecode routes end at the continuation cap
945
+ // (final-notice block -> capped visible release), and vibecode ends on the visible
946
+ // verification-loop blocker.
958
947
 
959
948
  if (result.continue) {
960
949
  if (result.finalNotice) await markNotified(projectRoot, payload, ledger);