@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 +15 -0
- package/package.json +1 -1
- package/src/core/ensureGitignore.js +14 -9
- package/src/core/taskBudgetValidator.js +10 -4
- package/src/core/taskProgressGuard.js +12 -2
- package/templates/.claude/ukit/index/task-budget-validator.mjs +9 -3
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +6 -17
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
|
@@ -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
|
-
'
|
|
11
|
+
'/.cache/',
|
|
7
12
|
// legacy: Antigravity adapter removed in v2.2.0; leftovers must stay ignored
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
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 (
|
|
92
|
-
|
|
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` —
|
|
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 (
|
|
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
|
|
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 (
|
|
74
|
-
|
|
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 (
|
|
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
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
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);
|