@ngockhoale/ukit 3.4.14 → 3.4.16
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.16 - 2026-10-05
|
|
6
|
+
|
|
7
|
+
**Create-scope guard no longer blocks planner subagents running in a git worktree.**
|
|
8
|
+
`handoff-model-guard.sh` classified write targets against the main project root, so a
|
|
9
|
+
`handoff-planner` isolated in a worktree (`.claude/worktrees/<n>/`, `.worktrees/<n>/`, or a
|
|
10
|
+
sibling checkout of the same repo) writing `docs/AI_HANDOFF/*` read as `deny-.claude` /
|
|
11
|
+
`outside-root` — an unfixable block the agent retried blind. Targets are now scoped against
|
|
12
|
+
that worktree's own root (same-repo verified via git common dir); source/config stay denied
|
|
13
|
+
inside the worktree and unrelated outside paths stay denied. Test: `tests/hooks/createScopeGuard.test.js` #13.
|
|
14
|
+
|
|
15
|
+
## 3.4.15 - 2026-10-05
|
|
16
|
+
|
|
17
|
+
**Hook chain no longer masks a real gate block (stops blind-retry token burn).**
|
|
18
|
+
When a mid-chain gate exited 2 with its own reason (e.g. `handoff-model-guard.sh`:
|
|
19
|
+
`PLAN.md has no valid PLANNER_MODEL`), `hook-chain-runner.mjs` treated the unrun later gate
|
|
20
|
+
as "chain broke before fail-closed gate(s) ran: context-hardcap-gate.sh" and discarded the
|
|
21
|
+
real reason. Agents (notably `handoff-planner`) could not tell what to fix and were respawned
|
|
22
|
+
repeatedly. A completed exit-2 gate is now the verdict: its stderr/systemMessage is surfaced
|
|
23
|
+
and later gates stay unrun. Infra failures (timeout/signal/budget) still fail closed with the
|
|
24
|
+
"chain broke" message. Regression test in `tests/hooks/hookChainFailureSurface.test.js`.
|
|
25
|
+
|
|
5
26
|
## 3.4.14 - 2026-10-04
|
|
6
27
|
|
|
7
28
|
**New `advisor-plan` skill, a create-session write-scope guard, and a deliberate-future queue contract.**
|
package/package.json
CHANGED
|
@@ -302,9 +302,33 @@ async function resolveReal(p, depth = 0) {
|
|
|
302
302
|
|
|
303
303
|
const SCOPE_DENY_CLASSES_OUTSIDE = 'outside-root';
|
|
304
304
|
// -> { block:boolean, cls:string, rel?:string }
|
|
305
|
-
|
|
305
|
+
// A subagent isolated in a git worktree writes under THAT worktree's tree: inside the project
|
|
306
|
+
// (`.claude/worktrees/<n>/`, `.worktrees/<n>/`) or a sibling checkout of the same repo. Scope
|
|
307
|
+
// must be judged against that worktree's root — against the main root the same `docs/…` file
|
|
308
|
+
// reads as deny-.claude / outside-root, a block the agent cannot fix and retries blind.
|
|
309
|
+
async function worktreeBaseFor(abs, projectDir) {
|
|
306
310
|
try {
|
|
307
|
-
const
|
|
311
|
+
const root = path.resolve(projectDir);
|
|
312
|
+
const rel = path.relative(root, abs).replace(/\\/g, '/');
|
|
313
|
+
const m = rel.match(/^(?:\.claude\/worktrees|\.worktrees)\/[^/]+(?=\/|$)/);
|
|
314
|
+
if (m) return path.join(root, m[0]);
|
|
315
|
+
if (!(rel === '..' || rel.startsWith('../') || path.isAbsolute(rel))) return null;
|
|
316
|
+
const { execFileSync } = require('child_process');
|
|
317
|
+
let dir = path.dirname(abs);
|
|
318
|
+
for (let i = 0; i < 64; i += 1) {
|
|
319
|
+
try { await fsp.access(dir); break; } catch { const up = path.dirname(dir); if (up === dir) return null; dir = up; }
|
|
320
|
+
}
|
|
321
|
+
const git = (cwd, ...a) => execFileSync('git', ['-C', cwd, ...a], { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
322
|
+
const common = (cwd) => fsp.realpath(path.resolve(cwd, git(cwd, 'rev-parse', '--git-common-dir')));
|
|
323
|
+
if ((await common(dir)) !== (await common(root))) return null;
|
|
324
|
+
return git(dir, 'rev-parse', '--show-toplevel') || null;
|
|
325
|
+
} catch { return null; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function classifyTarget(filePath, projectDirArg) {
|
|
329
|
+
try {
|
|
330
|
+
const abs = path.resolve(projectDirArg, filePath); // collapses `..`
|
|
331
|
+
const projectDir = (await worktreeBaseFor(abs, projectDirArg)) || projectDirArg;
|
|
308
332
|
const rootReal = await fsp.realpath(projectDir);
|
|
309
333
|
const relTo = (root, p) => path.relative(root, p).replace(/\\/g, '/');
|
|
310
334
|
const outside = (r) => r === '..' || r.startsWith('../') || path.isAbsolute(r);
|
|
@@ -386,7 +386,16 @@ async function run(payloadText, scriptArgs, { chainMarker = true, decisionShortC
|
|
|
386
386
|
// ... unless this break WAS the decision: a decision owns the verdict, so the
|
|
387
387
|
// unrun gates are not a fail-closed gap (SPEC §FR-001 — the verdict must stay
|
|
388
388
|
// exit 0 with the decision JSON).
|
|
389
|
-
|
|
389
|
+
// ... and unless the break was a REAL block: a script that ran to completion and
|
|
390
|
+
// exited 2 (e.g. handoff-model-guard: exit 2 + systemMessage, no decision JSON) is
|
|
391
|
+
// itself the verdict. Reporting "chain broke before <later gate>" instead hides the
|
|
392
|
+
// actual reason, so the agent cannot fix it and retries blind (token burn).
|
|
393
|
+
const brokeAt = results[results.length - 1];
|
|
394
|
+
const realBlock = Boolean(brokeAt)
|
|
395
|
+
&& brokeAt.code === 2
|
|
396
|
+
&& brokeAt.failureKind === 'exit-code'
|
|
397
|
+
&& !brokeAt.killed;
|
|
398
|
+
const skippedFailClosed = !decisionEmitted && !realBlock && scriptPaths
|
|
390
399
|
.slice(results.length)
|
|
391
400
|
.some((p) => FAIL_CLOSED_SCRIPTS.has(failClosedName(p)));
|
|
392
401
|
|