@ngockhoale/ukit 3.4.15 → 3.4.17
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,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.17 - 2026-10-06
|
|
6
|
+
|
|
7
|
+
**Planners can revise an unimplemented task that still carries the skeleton Executor Report.**
|
|
8
|
+
`handoff-model-guard.sh` blocked every Edit/Write to an existing `TASK-xxx.md` whose
|
|
9
|
+
`## Executor Report` (or `## Reviewer Verdict`) was still the Phase-2 skeleton, demanding
|
|
10
|
+
`EXECUTOR_MODEL` — so `/ukit:handoff-create` could create a task but never amend it after
|
|
11
|
+
plan review. The model gate now fires only when the phase section itself changes to real
|
|
12
|
+
content; edits elsewhere, unchanged skeletons and inert placeholders (`Not started.`,
|
|
13
|
+
`KEY: -`) pass. Real executor/reviewer reports without a valid model, lite-tier models and
|
|
14
|
+
real-model demotion stay blocked. Regression: `tests/hooks/taskSkeletonAmendment.test.js`.
|
|
15
|
+
|
|
16
|
+
## 3.4.16 - 2026-10-05
|
|
17
|
+
|
|
18
|
+
**Create-scope guard no longer blocks planner subagents running in a git worktree.**
|
|
19
|
+
`handoff-model-guard.sh` classified write targets against the main project root, so a
|
|
20
|
+
`handoff-planner` isolated in a worktree (`.claude/worktrees/<n>/`, `.worktrees/<n>/`, or a
|
|
21
|
+
sibling checkout of the same repo) writing `docs/AI_HANDOFF/*` read as `deny-.claude` /
|
|
22
|
+
`outside-root` — an unfixable block the agent retried blind. Targets are now scoped against
|
|
23
|
+
that worktree's own root (same-repo verified via git common dir); source/config stay denied
|
|
24
|
+
inside the worktree and unrelated outside paths stay denied. Test: `tests/hooks/createScopeGuard.test.js` #13.
|
|
25
|
+
|
|
5
26
|
## 3.4.15 - 2026-10-05
|
|
6
27
|
|
|
7
28
|
**Hook chain no longer masks a real gate block (stops blind-retry token burn).**
|
package/package.json
CHANGED
|
@@ -197,6 +197,31 @@ function isPlaceholderValue(value) {
|
|
|
197
197
|
if (!raw || /^[<(]/.test(raw)) return true;
|
|
198
198
|
return ['-','--','tbd','todo','n/a','na','none','unknown','pending'].includes(raw.toLowerCase());
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
// Planning amendments (handoff-create revising an unimplemented task) re-submit a task whose
|
|
202
|
+
// `## Executor Report` / `## Reviewer Verdict` is still the Phase-2 skeleton. The model gate
|
|
203
|
+
// exists to stop a skeleton being promoted into a "real" report without a validated model, so it
|
|
204
|
+
// must only fire when the phase section itself is touched with real content — not when the edit
|
|
205
|
+
// leaves the skeleton unchanged or keeps it inert (headings/"Not started."/placeholder fields).
|
|
206
|
+
function phaseSection(text, heading) {
|
|
207
|
+
const m = new RegExp('^##[ \\t]+' + heading + '[ \\t]*$', 'm').exec(text);
|
|
208
|
+
if (!m) return null;
|
|
209
|
+
const rest = text.slice(m.index + m[0].length);
|
|
210
|
+
const next = rest.search(/^##[ \t]+\S/m);
|
|
211
|
+
return (next === -1 ? rest : rest.slice(0, next)).trim();
|
|
212
|
+
}
|
|
213
|
+
function phaseSectionInert(section) {
|
|
214
|
+
if (section === null) return true;
|
|
215
|
+
return section.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).every((line) => {
|
|
216
|
+
if (/^(?:[-*]\s*)?(?:not started|n\/a|tbd|todo|pending|none)\.?$/i.test(line)) return true;
|
|
217
|
+
const kv = line.match(/^(?:[-*]\s*)?\**[A-Za-z_ ]+\**\s*:\s*(.*)$/);
|
|
218
|
+
return Boolean(kv) && isPlaceholderValue(kv[1].replace(/\*+/g, '').trim());
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function phaseSectionUntouched(currentVisible, newVisible, heading) {
|
|
222
|
+
const next = phaseSection(newVisible, heading);
|
|
223
|
+
return phaseSection(currentVisible, heading) === next || phaseSectionInert(next);
|
|
224
|
+
}
|
|
200
225
|
// W2-D07 fix: `OVERRIDE_APPROVED_BY: human` was plain text inside gated files —
|
|
201
226
|
// self-authorizable by the gated model. The in-file `## Model-Tier Guard Override`
|
|
202
227
|
// declaration stays (it is the audit trail), but it only counts when a marker file
|
|
@@ -302,9 +327,33 @@ async function resolveReal(p, depth = 0) {
|
|
|
302
327
|
|
|
303
328
|
const SCOPE_DENY_CLASSES_OUTSIDE = 'outside-root';
|
|
304
329
|
// -> { block:boolean, cls:string, rel?:string }
|
|
305
|
-
|
|
330
|
+
// A subagent isolated in a git worktree writes under THAT worktree's tree: inside the project
|
|
331
|
+
// (`.claude/worktrees/<n>/`, `.worktrees/<n>/`) or a sibling checkout of the same repo. Scope
|
|
332
|
+
// must be judged against that worktree's root — against the main root the same `docs/…` file
|
|
333
|
+
// reads as deny-.claude / outside-root, a block the agent cannot fix and retries blind.
|
|
334
|
+
async function worktreeBaseFor(abs, projectDir) {
|
|
335
|
+
try {
|
|
336
|
+
const root = path.resolve(projectDir);
|
|
337
|
+
const rel = path.relative(root, abs).replace(/\\/g, '/');
|
|
338
|
+
const m = rel.match(/^(?:\.claude\/worktrees|\.worktrees)\/[^/]+(?=\/|$)/);
|
|
339
|
+
if (m) return path.join(root, m[0]);
|
|
340
|
+
if (!(rel === '..' || rel.startsWith('../') || path.isAbsolute(rel))) return null;
|
|
341
|
+
const { execFileSync } = require('child_process');
|
|
342
|
+
let dir = path.dirname(abs);
|
|
343
|
+
for (let i = 0; i < 64; i += 1) {
|
|
344
|
+
try { await fsp.access(dir); break; } catch { const up = path.dirname(dir); if (up === dir) return null; dir = up; }
|
|
345
|
+
}
|
|
346
|
+
const git = (cwd, ...a) => execFileSync('git', ['-C', cwd, ...a], { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
347
|
+
const common = (cwd) => fsp.realpath(path.resolve(cwd, git(cwd, 'rev-parse', '--git-common-dir')));
|
|
348
|
+
if ((await common(dir)) !== (await common(root))) return null;
|
|
349
|
+
return git(dir, 'rev-parse', '--show-toplevel') || null;
|
|
350
|
+
} catch { return null; }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function classifyTarget(filePath, projectDirArg) {
|
|
306
354
|
try {
|
|
307
|
-
const abs = path.resolve(
|
|
355
|
+
const abs = path.resolve(projectDirArg, filePath); // collapses `..`
|
|
356
|
+
const projectDir = (await worktreeBaseFor(abs, projectDirArg)) || projectDirArg;
|
|
308
357
|
const rootReal = await fsp.realpath(projectDir);
|
|
309
358
|
const relTo = (root, p) => path.relative(root, p).replace(/\\/g, '/');
|
|
310
359
|
const outside = (r) => r === '..' || r.startsWith('../') || path.isAbsolute(r);
|
|
@@ -553,7 +602,7 @@ if (toolName === 'Write' || toolName === 'Edit') {
|
|
|
553
602
|
if (hasExecutorReport && !currentExecutorReal) {
|
|
554
603
|
const executorModel = extractField(newVisible, 'EXECUTOR_MODEL');
|
|
555
604
|
if (isPlaceholderValue(executorModel) || /^unknown$/i.test(executorModel)) {
|
|
556
|
-
if (!isFreshTaskFile) {
|
|
605
|
+
if (!isFreshTaskFile && !phaseSectionUntouched(currentVisible, newVisible, 'Executor Report')) {
|
|
557
606
|
block(`${taskId}: EXECUTOR_MODEL missing/placeholder. Implementation must run via Agent tool subagent_type: "feature-implementer" (sonnet/unic-code) and self-report its model.`);
|
|
558
607
|
}
|
|
559
608
|
// Else: skeleton sections copied from a task template at creation time — the
|
|
@@ -595,7 +644,7 @@ if (toolName === 'Write' || toolName === 'Edit') {
|
|
|
595
644
|
const reviewOverride = await taskHasHumanOverride(newVisible, taskId)
|
|
596
645
|
|| await planHasHumanOverride(stripHtmlComments(planContentForReview));
|
|
597
646
|
if (isPlaceholderValue(reviewerModel) || /^unknown$/i.test(reviewerModel)) {
|
|
598
|
-
if (!isFreshTaskFile) {
|
|
647
|
+
if (!isFreshTaskFile && !phaseSectionUntouched(currentVisible, newVisible, 'Reviewer Verdict')) {
|
|
599
648
|
block(`${taskId}: REVIEWER_MODEL missing/placeholder. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart) and self-report its model.`);
|
|
600
649
|
}
|
|
601
650
|
// Else: skeleton Reviewer Verdict on a freshly created task file — Phase 4
|