Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions skills/skill-cleaner/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 <path>`.
- `--root-only` requires at least one `--root <path>`, 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.
Expand Down
23 changes: 23 additions & 0 deletions skills/skill-cleaner/scripts/skill-cleaner.test.ts
Original file line number Diff line number Diff line change
@@ -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([
{
Expand Down
39 changes: 28 additions & 11 deletions skills/skill-cleaner/scripts/skill-cleaner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "");
Expand All @@ -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);
Expand Down Expand Up @@ -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<string, string>();
[
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");
Expand Down Expand Up @@ -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 <path>");
process.exitCode = 2;
return;
}
const skills = discoverSkills();
const live = livePrompt();
const liveSkills = live ? parseLiveSkills(live) : [];
Expand Down