@ngockhoale/ukit 3.4.15 → 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,16 @@
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
+
5
15
  ## 3.4.15 - 2026-10-05
6
16
 
7
17
  **Hook chain no longer masks a real gate block (stops blind-retry token burn).**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.15",
3
+ "version": "3.4.16",
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",
@@ -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
- async function classifyTarget(filePath, projectDir) {
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 abs = path.resolve(projectDir, filePath); // collapses `..`
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);