From e9506809cde27a13c22d39d0bd07fa361caafb62 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 07:00:37 +0100 Subject: [PATCH] feat(skill-cleaner): add exclusive root scans --- CHANGELOG.md | 3 ++ skills/skill-cleaner/SKILL.md | 2 + .../scripts/skill-cleaner.test.ts | 23 +++++++++++ skills/skill-cleaner/scripts/skill-cleaner.ts | 39 +++++++++++++------ 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 716b17b6..1584b598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ summary: Timeline of guardrail helper changes mirrored from Sweetistics and rela # Changelog +## 2026-07-01 — Isolated Skill Audits +- Added `skill-cleaner --root-only` for auditing only explicitly supplied skill roots without Codex inventory noise. Thanks @its-How. + ## 2026-07-01 — OSS Maintainer Orchestration - Expanded `maintainer-orchestrator` into a long-running control plane with one worker thread per repository, a 10-thread concurrency target with immediate smallest-queue refill, concrete status-based thread titles, safe repository synchronization, forgotten-work preservation, PR rewrite/deduplication, decision-ready risk and diff summaries, durable `VISION.md` policy capture, dependency audits, release proposals with strongest-first highlights, and a persistent daily log plus heartbeat. diff --git a/skills/skill-cleaner/SKILL.md b/skills/skill-cleaner/SKILL.md index 678766f7..0bed02d7 100644 --- a/skills/skill-cleaner/SKILL.md +++ b/skills/skill-cleaner/SKILL.md @@ -23,6 +23,7 @@ node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts -- node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts --months 6 --max-log-mb 800 --deep-logs node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts --context-tokens 272000 --budget-percent 2 --no-logs node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts --root ~/Dropbox/boxd/skills --no-logs +node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts --root ~/.agents/skills --root-only --no-logs ``` 2. Read the report in this order: @@ -47,6 +48,7 @@ node --experimental-strip-types skills/skill-cleaner/scripts/skill-cleaner.ts -- - It follows Codex `core-skills/src/render.rs`: 2% of raw `context_window`, token cost `ceil(utf8_bytes / 4)`, then full descriptions -> equal description truncation -> omitted minimum lines. Alias-table line cost is included. - It reads `~/.codex/models_cache.json` for GPT-5.5 `context_window`; fallback is 272,000 tokens and 2%. - It scans only normal Codex/plugin/repo skill roots by default. Extra folders such as Dropbox archives are included only with `--root `. +- `--root-only` requires at least one `--root `, skips the live Codex inventory, and scans only those supplied roots. - It realpath-dedupes roots, so symlinked roots such as `~/.codex/skills/agent-scripts -> ~/Projects/agent-scripts/skills` do not create false duplicates. - For duplicate names, it reports description/body similarity and suggests deletion candidates only when bodies are near copies. Keep priority defaults to direct Codex system skills, then direct Codex skills, then plugin skills, then personal/repo copies. - It scans `~/.codex/history.jsonl` and recent `~/.codex/sessions/**/*.jsonl` by default. Add `--deep-logs` for archived sessions and common OpenClaw/Clawd log folders. diff --git a/skills/skill-cleaner/scripts/skill-cleaner.test.ts b/skills/skill-cleaner/scripts/skill-cleaner.test.ts index 66fb4325..d556b58a 100644 --- a/skills/skill-cleaner/scripts/skill-cleaner.test.ts +++ b/skills/skill-cleaner/scripts/skill-cleaner.test.ts @@ -1,14 +1,37 @@ import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import test from "node:test"; import { compactDescription, + discoverRoots, parseLiveSkillsPrompt, plainLogSkillReads, referencedSkillPaths, usageEvidence, } from "./skill-cleaner.ts"; +test("limits root discovery to explicitly supplied roots", (context) => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-cleaner-roots-")); + context.after(() => fs.rmSync(temp, { recursive: true, force: true })); + const defaultRoots = [ + path.join(temp, ".codex/skills"), + path.join(temp, ".codex/plugins/cache"), + path.join(temp, "Projects/agent-scripts/skills"), + path.join(temp, "Projects/demo/.agents/skills"), + ]; + const isolatedRoot = path.join(temp, "isolated/skills"); + for (const root of [...defaultRoots, isolatedRoot]) fs.mkdirSync(root, { recursive: true }); + + assert.deepEqual(discoverRoots(temp, [isolatedRoot], true), [isolatedRoot]); + assert.deepEqual( + discoverRoots(temp, [isolatedRoot], false), + [...defaultRoots, isolatedRoot].sort(), + ); +}); + test("parses Codex skill roots and model-visible lines", () => { const raw = JSON.stringify([ { diff --git a/skills/skill-cleaner/scripts/skill-cleaner.ts b/skills/skill-cleaner/scripts/skill-cleaner.ts index b4d2edc0..4ff736a7 100755 --- a/skills/skill-cleaner/scripts/skill-cleaner.ts +++ b/skills/skill-cleaner/scripts/skill-cleaner.ts @@ -75,7 +75,8 @@ const noLogs = args.has("--no-logs"); const deepLogs = args.has("--deep-logs"); const json = args.has("--json"); const includeAll = args.has("--all"); -const noLive = args.has("--no-live"); +const rootOnly = args.has("--root-only"); +const noLive = args.has("--no-live") || rootOnly; const model = argValue("--model", "gpt-5.5"); const budgetPercent = Number(argValue("--budget-percent", "2")); const contextTokensOverride = argValue("--context-tokens", ""); @@ -84,7 +85,10 @@ const maxLogBytes = Number(argValue("--max-log-mb", "300")) * 1024 * 1024; const cutoffMs = Date.now() - Math.max(0, months) * 31 * 24 * 60 * 60 * 1000; const extraRoots = process.argv .slice(2) - .flatMap((arg, index, all) => (arg === "--root" && all[index + 1] ? [all[index + 1]] : [])); + .flatMap((arg, index, all) => { + const value = all[index + 1]; + return arg === "--root" && value && !value.startsWith("--") ? [value] : []; + }); function expandHome(input: string): string { return input.replace(/^~(?=$|\/)/, home); @@ -504,21 +508,29 @@ function configState(): { return { disabledPaths, disabledNames, disabledPlugins }; } -function discoverRoots(): string[] { +export function discoverRoots( + baseHome = home, + providedRoots = extraRoots, + exclusive = rootOnly, +): string[] { const rootsByRealPath = new Map(); - [ - path.join(home, ".codex/skills"), - path.join(home, ".codex/plugins/cache"), - path.join(home, "Projects/agent-scripts/skills"), - ...extraRoots.map(expandHome), - ].forEach((root) => { + const roots = providedRoots.map((root) => root.replace(/^~(?=$|\/)/, baseHome)); + const candidates = exclusive + ? roots + : [ + path.join(baseHome, ".codex/skills"), + path.join(baseHome, ".codex/plugins/cache"), + path.join(baseHome, "Projects/agent-scripts/skills"), + ...roots, + ]; + candidates.forEach((root) => { if (!exists(root)) return; const real = fs.realpathSync(root); const current = rootsByRealPath.get(real); if (!current || root.length < current.length) rootsByRealPath.set(real, root); }); - const projects = path.join(home, "Projects"); - if (exists(projects)) { + const projects = path.join(baseHome, "Projects"); + if (!exclusive && exists(projects)) { for (const entry of fs.readdirSync(projects, { withFileTypes: true })) { if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; const skillRoot = path.join(projects, entry.name, ".agents/skills"); @@ -1209,6 +1221,11 @@ function render( } function main(): void { + if (rootOnly && extraRoots.length === 0) { + console.error("skill-cleaner: --root-only requires at least one --root "); + process.exitCode = 2; + return; + } const skills = discoverSkills(); const live = livePrompt(); const liveSkills = live ? parseLiveSkills(live) : [];