aiblueprint-cli 1.4.102 → 1.4.104

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/README.md CHANGED
@@ -142,6 +142,8 @@ npx skills add Melvynx/aiblueprint --skill skill-manager
142
142
  | `apex` | Structured implementation workflow |
143
143
  | `app-icon` | Generate and prepare app icons |
144
144
  | `appstore-connect` | Manage App Store Connect workflows |
145
+ | `audit-memories` | Manually audit and clean project agent documentation |
146
+ | `audit-skills` | Manually audit skill usage, invocation controls, duplicates, and scope |
145
147
  | `commit` | Quick commit and push with clean messages |
146
148
  | `create-pr` | Auto-generated pull requests |
147
149
  | `fix-pr-comments` | Resolve PR review comments |
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: audit
3
+ description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations.
4
+ user-invocable: true
5
+ argument-hint: "[area]"
6
+ disable-model-invocation: true
7
+ ---
8
+
9
+ Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address.
10
+
11
+ **First**: Use the frontend-design skill for design principles and anti-patterns.
12
+
13
+ ## Diagnostic Scan
14
+
15
+ Run comprehensive checks across multiple dimensions:
16
+
17
+ 1. **Accessibility (A11y)** - Check for:
18
+ - **Contrast issues**: Text contrast ratios < 4.5:1 (or 7:1 for AAA)
19
+ - **Missing ARIA**: Interactive elements without proper roles, labels, or states
20
+ - **Keyboard navigation**: Missing focus indicators, illogical tab order, keyboard traps
21
+ - **Semantic HTML**: Improper heading hierarchy, missing landmarks, divs instead of buttons
22
+ - **Alt text**: Missing or poor image descriptions
23
+ - **Form issues**: Inputs without labels, poor error messaging, missing required indicators
24
+
25
+ 2. **Performance** - Check for:
26
+ - **Layout thrashing**: Reading/writing layout properties in loops
27
+ - **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
28
+ - **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
29
+ - **Bundle size**: Unnecessary imports, unused dependencies
30
+ - **Render performance**: Unnecessary re-renders, missing memoization
31
+
32
+ 3. **Theming** - Check for:
33
+ - **Hard-coded colors**: Colors not using design tokens
34
+ - **Broken dark mode**: Missing dark mode variants, poor contrast in dark theme
35
+ - **Inconsistent tokens**: Using wrong tokens, mixing token types
36
+ - **Theme switching issues**: Values that don't update on theme change
37
+
38
+ 4. **Responsive Design** - Check for:
39
+ - **Fixed widths**: Hard-coded widths that break on mobile
40
+ - **Touch targets**: Interactive elements < 44x44px
41
+ - **Horizontal scroll**: Content overflow on narrow viewports
42
+ - **Text scaling**: Layouts that break when text size increases
43
+ - **Missing breakpoints**: No mobile/tablet variants
44
+
45
+ 5. **Anti-Patterns (CRITICAL)** - Check against ALL the **DON'T** guidelines in the frontend-design skill. Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy).
46
+
47
+ **CRITICAL**: This is an audit, not a fix. Document issues thoroughly with clear explanations of impact. Use other commands (normalize, optimize, harden, etc.) to fix issues after audit.
48
+
49
+ ## Generate Comprehensive Report
50
+
51
+ Create a detailed audit report with the following structure:
52
+
53
+ ### Anti-Patterns Verdict
54
+ **Start here.** Pass/fail: Does this look AI-generated? List specific tells from the skill's Anti-Patterns section. Be brutally honest.
55
+
56
+ ### Executive Summary
57
+ - Total issues found (count by severity)
58
+ - Most critical issues (top 3-5)
59
+ - Overall quality score (if applicable)
60
+ - Recommended next steps
61
+
62
+ ### Detailed Findings by Severity
63
+
64
+ For each issue, document:
65
+ - **Location**: Where the issue occurs (component, file, line)
66
+ - **Severity**: Critical / High / Medium / Low
67
+ - **Category**: Accessibility / Performance / Theming / Responsive
68
+ - **Description**: What the issue is
69
+ - **Impact**: How it affects users
70
+ - **WCAG/Standard**: Which standard it violates (if applicable)
71
+ - **Recommendation**: How to fix it
72
+ - **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /critique, /colorize — or other installed skills you're sure exist)
73
+
74
+ #### Critical Issues
75
+ [Issues that block core functionality or violate WCAG A]
76
+
77
+ #### High-Severity Issues
78
+ [Significant usability/accessibility impact, WCAG AA violations]
79
+
80
+ #### Medium-Severity Issues
81
+ [Quality issues, WCAG AAA violations, performance concerns]
82
+
83
+ #### Low-Severity Issues
84
+ [Minor inconsistencies, optimization opportunities]
85
+
86
+ ### Patterns & Systemic Issues
87
+
88
+ Identify recurring problems:
89
+ - "Hard-coded colors appear in 15+ components, should use design tokens"
90
+ - "Touch targets consistently too small (<44px) throughout mobile experience"
91
+ - "Missing focus indicators on all custom interactive components"
92
+
93
+ ### Positive Findings
94
+
95
+ Note what's working well:
96
+ - Good practices to maintain
97
+ - Exemplary implementations to replicate elsewhere
98
+
99
+ ### Recommendations by Priority
100
+
101
+ Create actionable plan:
102
+ 1. **Immediate**: Critical blockers to fix first
103
+ 2. **Short-term**: High-severity issues (this sprint)
104
+ 3. **Medium-term**: Quality improvements (next sprint)
105
+ 4. **Long-term**: Nice-to-haves and optimizations
106
+
107
+ ### Suggested Commands for Fixes
108
+
109
+ Map issues to available commands. Prefer these: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /critique, /colorize. You may also suggest other installed skills you're sure exist, but never invent commands.
110
+
111
+ Examples:
112
+ - "Use `/normalize` to align with design system (addresses N theming issues)"
113
+ - "Use `/optimize` to improve performance (addresses N performance issues)"
114
+ - "Use `/harden` to improve resilience (addresses N edge cases)"
115
+
116
+ **IMPORTANT**: Be thorough but actionable. Too many low-priority issues creates noise. Focus on what actually matters.
117
+
118
+ **NEVER**:
119
+ - Report issues without explaining impact (why does this matter?)
120
+ - Mix severity levels inconsistently
121
+ - Skip positive findings (celebrate what works)
122
+ - Provide generic recommendations (be specific and actionable)
123
+ - Forget to prioritize (everything can't be critical)
124
+ - Report false positives without verification
125
+
126
+ Remember: You're a quality auditor with exceptional attention to detail. Document systematically, prioritize ruthlessly, and provide clear paths to improvement. A good audit makes fixing easy.
@@ -0,0 +1,10 @@
1
+ interface:
2
+ display_name: "Audit"
3
+ short_description: "Perform comprehensive audit of interface quality across..."
4
+ icon_small: "./assets/codex-icon.svg"
5
+ icon_large: "./assets/codex-icon.svg"
6
+ brand_color: "#615AA8"
7
+ default_prompt: "Use $audit to help with this task."
8
+
9
+ policy:
10
+ allow_implicit_invocation: false
@@ -0,0 +1,20 @@
1
+ <!-- @license lucide-static v1.24.0 - ISC -->
2
+ <svg role="img" aria-label="audit skill icon"
3
+ class="lucide lucide-scan-search"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="128"
6
+ height="128"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="#F5F5F5"
10
+ stroke-width="2"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M3 7V5a2 2 0 0 1 2-2h2" />
15
+ <path d="M17 3h2a2 2 0 0 1 2 2v2" />
16
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2" />
17
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2" />
18
+ <circle cx="12" cy="12" r="3" />
19
+ <path d="m16 16-1.9-1.9" />
20
+ </svg>
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: audit-skills
3
+ description: Audit installed skills for observed usage, explicit-only invocation controls, duplicate discovery, and global-versus-project scope. Use only when the user explicitly invokes `$audit-skills`.
4
+ argument-hint: "[audit|fix] [project-root]"
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Audit Skills
9
+
10
+ Audit skill discovery cost and placement from current files plus observable Codex and Claude session evidence. Treat missing telemetry as uncertainty, not proof that a skill has never been useful.
11
+
12
+ ## Invocation guard
13
+
14
+ Proceed only when the current user message explicitly invokes `$audit-skills`.
15
+
16
+ - `$audit-skills audit [project-root]`: produce a read-only audit. This is the default.
17
+ - `$audit-skills fix [project-root]`: audit first, then apply only high-confidence corrections.
18
+
19
+ Resolve `project-root` from the argument or current repository. Preserve unrelated dirty changes and record Git status for every modified repository.
20
+
21
+ ## Audit
22
+
23
+ Run the deterministic inventory as a bounded job:
24
+
25
+ ```bash
26
+ bun ~/.agents/skills/audit-skills/scripts/audit-skills.mjs \
27
+ --project "$PWD" \
28
+ --format markdown
29
+ ```
30
+
31
+ The script inventories global `~/.agents/skills` and `<project>/.agents/skills`, then scans observable Codex and Claude histories for:
32
+
33
+ - explicit user references such as `$skill-name` or `/skill-name`;
34
+ - Claude `Skill` tool calls;
35
+ - agent reads of a matching `SKILL.md`;
36
+ - working directories associated with observed use;
37
+ - Claude `disable-model-invocation` and Codex `policy.allow_implicit_invocation` controls;
38
+ - name collisions and hardcoded project-root references.
39
+
40
+ Report these evidence classes separately:
41
+
42
+ - `OBSERVED_USER`: explicitly named by a user in scanned history;
43
+ - `OBSERVED_MODEL`: loaded or invoked by an agent in scanned history;
44
+ - `UNOBSERVED`: no matching evidence in scanned sources;
45
+ - `EXPLICIT_ONLY`: protected for both Claude and Codex;
46
+ - `INVOCATION_MISMATCH`: protected on only one platform;
47
+ - `LOCALITY_CANDIDATE`: global skill with a project-specific runtime dependency or at least two observed uses confined to one project;
48
+ - `NAME_COLLISION`: same skill name discovered globally and locally.
49
+
50
+ Include scan coverage and limitations. Cursor ACP stores are opaque blobs and are not counted unless a reliable parser becomes available. Never label `UNOBSERVED` as “never used” without naming the scanned time range and sources.
51
+
52
+ ## Fix
53
+
54
+ Apply changes only after the audit ledger exists.
55
+
56
+ Safe automatic corrections:
57
+
58
+ 1. Mirror an existing explicit-only decision across platforms: add `disable-model-invocation: true` when Codex already blocks implicit invocation, or add `policy.allow_implicit_invocation: false` when Claude already blocks it.
59
+ 2. Mark a skill explicit-only when its own instructions already require direct user invocation but one or both platform controls are missing.
60
+ 3. Repair metadata made stale by a move or rename.
61
+
62
+ Require explicit user direction or a project-specific runtime dependency before moving a skill between scopes. Multiple uses confined to one project justify review, not automatic relocation; a single observed use is insufficient evidence.
63
+
64
+ When moving a skill:
65
+
66
+ 1. copy the complete directory, including scripts, references, assets, and `agents/openai.yaml`;
67
+ 2. rewrite absolute self-paths and dependencies to their new canonical locations;
68
+ 3. validate the destination before removing the source;
69
+ 4. use `trash` for source removal;
70
+ 5. search all skill roots and plugin/profile metadata for stale references;
71
+ 6. confirm the name is discovered exactly once in the intended scope.
72
+
73
+ For unobserved skills, recommend rather than mutate unless their own text clearly establishes user-only intent. Rarely used incident, security, recovery, and migration skills remain protected from usage-only pruning.
74
+
75
+ ## Completion criteria
76
+
77
+ Complete an audit only when every inventoried skill has a row, scan coverage is reported, and every recommendation cites static or usage evidence.
78
+
79
+ Complete a fix only when touched skills validate, moved dependencies resolve, unintended duplicate discovery is absent, and final Git status distinguishes pre-existing changes from this run.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Audit Skills"
3
+ short_description: "Audit skill usage, invocation, and scope"
4
+ default_prompt: "Use $audit-skills to audit global and project skill placement."
5
+
6
+ policy:
7
+ allow_implicit_invocation: false
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Database } from "bun:sqlite";
4
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join, resolve } from "node:path";
7
+
8
+ const args = process.argv.slice(2);
9
+ const valueOf = (flag, fallback) => {
10
+ const index = args.indexOf(flag);
11
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
12
+ };
13
+
14
+ const projectRoot = resolve(valueOf("--project", process.cwd()));
15
+ const format = valueOf("--format", "markdown");
16
+ const home = homedir();
17
+ const globalRoot = join(home, ".agents", "skills");
18
+ const localRoot = join(projectRoot, ".agents", "skills");
19
+
20
+ const walk = (root, accept) => {
21
+ if (!existsSync(root)) return [];
22
+ const found = [];
23
+ const visit = (path) => {
24
+ let entries = [];
25
+ try {
26
+ entries = readdirSync(path, { withFileTypes: true });
27
+ } catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ const child = join(path, entry.name);
32
+ if (entry.isDirectory()) visit(child);
33
+ else if (entry.isFile() && accept(child)) found.push(child);
34
+ }
35
+ };
36
+ visit(root);
37
+ return found;
38
+ };
39
+
40
+ const readText = (path) => {
41
+ try {
42
+ return readFileSync(path, "utf8");
43
+ } catch {
44
+ return "";
45
+ }
46
+ };
47
+
48
+ const parseFrontmatter = (text) => {
49
+ if (!text.startsWith("---\n")) return {};
50
+ const end = text.indexOf("\n---", 4);
51
+ if (end < 0) return {};
52
+ const result = {};
53
+ for (const line of text.slice(4, end).split("\n")) {
54
+ const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
55
+ if (!match) continue;
56
+ result[match[1]] = match[2].trim().replace(/^(["'])(.*)\1$/, "$2");
57
+ }
58
+ return result;
59
+ };
60
+
61
+ const inventoryRoot = (root, scope) => {
62
+ if (!existsSync(root)) return [];
63
+ return readdirSync(root, { withFileTypes: true })
64
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
65
+ .map((entry) => {
66
+ const directory = join(root, entry.name);
67
+ const skillPath = join(directory, "SKILL.md");
68
+ if (!existsSync(skillPath)) return null;
69
+ const body = readText(skillPath);
70
+ const metadata = parseFrontmatter(body);
71
+ const openai = readText(join(directory, "agents", "openai.yaml"));
72
+ const selfFiles = walk(directory, (path) => !/\.(png|jpe?g|gif|webp|mp4|mov|pdf|db|sqlite)$/i.test(path));
73
+ const escapedName = (metadata.name || entry.name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
74
+ const invocationGuard = new RegExp(`(?:manual-only[^\\n]*|proceed only[^\\n]*)\\$${escapedName}(?![a-zA-Z0-9_-])`, "i");
75
+ const projectReferences = selfFiles.filter((path) => readText(path).includes(projectRoot));
76
+ return {
77
+ name: metadata.name || entry.name,
78
+ scope,
79
+ directory,
80
+ skill_path: skillPath,
81
+ description: metadata.description || "",
82
+ claude_explicit_only: metadata["disable-model-invocation"] === "true",
83
+ codex_explicit_only: /allow_implicit_invocation:\s*false\b/.test(openai),
84
+ direct_user_intent: invocationGuard.test(body.slice(0, 2500)),
85
+ project_references: projectReferences,
86
+ project_runtime_references: projectReferences.filter((path) => path.includes("/scripts/") || path.includes("/agents/")),
87
+ };
88
+ })
89
+ .filter(Boolean);
90
+ };
91
+
92
+ const skills = [...inventoryRoot(globalRoot, "global"), ...inventoryRoot(localRoot, "project")];
93
+ const names = [...new Set(skills.map((skill) => skill.name))].sort((a, b) => b.length - a.length);
94
+ const usage = new Map(names.map((name) => [name, {
95
+ user: 0,
96
+ nativeModel: 0,
97
+ manifestRead: 0,
98
+ cwd: new Map(),
99
+ sources: new Set(),
100
+ }]));
101
+
102
+ const record = (name, kind, cwd, source) => {
103
+ const target = usage.get(name);
104
+ if (!target) return;
105
+ target[kind] += 1;
106
+ if (cwd) target.cwd.set(cwd, (target.cwd.get(cwd) || 0) + 1);
107
+ target.sources.add(source);
108
+ };
109
+
110
+ const strictUserNames = (text) => {
111
+ if (typeof text !== "string") return [];
112
+ return names.filter((name) => {
113
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
+ return new RegExp(`\\$${escaped}(?![a-zA-Z0-9_-])`, "i").test(text);
115
+ });
116
+ };
117
+
118
+ const manifestNames = (text) => {
119
+ if (typeof text !== "string") return [];
120
+ return names.filter((name) => {
121
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ return new RegExp(`skills\\/${escaped}\\/SKILL\\.md`, "i").test(text);
123
+ });
124
+ };
125
+
126
+ const codexCwd = new Map();
127
+ const stateDbPath = join(home, ".codex", "sqlite", "state_5.sqlite");
128
+ if (existsSync(stateDbPath)) {
129
+ try {
130
+ const db = new Database(stateDbPath, { readonly: true });
131
+ for (const row of db.query("SELECT rollout_path, cwd FROM threads").all()) {
132
+ codexCwd.set(resolve(String(row.rollout_path)), String(row.cwd || ""));
133
+ }
134
+ db.close();
135
+ } catch {
136
+ // The transcript remains usable; only cwd attribution is reduced.
137
+ }
138
+ }
139
+
140
+ const transcriptRoots = [
141
+ join(home, ".codex", "sessions"),
142
+ join(home, ".codex", "archived_sessions"),
143
+ join(home, ".claude", "projects"),
144
+ join(home, ".cursor", "projects"),
145
+ ].filter(existsSync);
146
+
147
+ const transcriptFiles = transcriptRoots.flatMap((root) => walk(root, (path) => path.endsWith(".jsonl")));
148
+ const escapedNames = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
149
+ const patterns = [
150
+ `\\$(?:${escapedNames})(?:[^a-zA-Z0-9_-]|$)`,
151
+ `\\"name\\":\\"Skill\\"`,
152
+ `skills/(?:${escapedNames})/SKILL\\.md`,
153
+ ];
154
+
155
+ let matchedLines = 0;
156
+ let earliest = null;
157
+ let latest = null;
158
+ const observeTimestamp = (value) => {
159
+ if (typeof value !== "string") return;
160
+ if (!earliest || value < earliest) earliest = value;
161
+ if (!latest || value > latest) latest = value;
162
+ };
163
+
164
+ if (transcriptRoots.length > 0 && names.length > 0) {
165
+ const rgArgs = ["rg", "--json", "--no-messages", ...patterns.flatMap((pattern) => ["-e", pattern]), ...transcriptRoots];
166
+ const process = Bun.spawn(rgArgs, { stdout: "pipe", stderr: "pipe" });
167
+ const [output, errorOutput, exitCode] = await Promise.all([
168
+ new Response(process.stdout).text(),
169
+ new Response(process.stderr).text(),
170
+ process.exited,
171
+ ]);
172
+ if (exitCode > 1) {
173
+ throw new Error(`rg transcript scan failed (${exitCode}): ${errorOutput.trim()}`);
174
+ }
175
+ for (const rgLine of output.split("\n")) {
176
+ if (!rgLine) continue;
177
+ let event;
178
+ try {
179
+ event = JSON.parse(rgLine);
180
+ } catch {
181
+ continue;
182
+ }
183
+ if (event.type !== "match") continue;
184
+ const path = resolve(event.data?.path?.text || "");
185
+ const line = event.data?.lines?.text || "";
186
+ let item;
187
+ try {
188
+ item = JSON.parse(line);
189
+ } catch {
190
+ continue;
191
+ }
192
+ matchedLines += 1;
193
+ observeTimestamp(item.timestamp);
194
+ const isClaude = path.startsWith(join(home, ".claude"));
195
+ const isCodex = path.startsWith(join(home, ".codex"));
196
+ const isCursor = path.startsWith(join(home, ".cursor"));
197
+ const source = isClaude ? "claude" : isCodex ? "codex" : isCursor ? "cursor" : "unknown";
198
+ const cwd = item.cwd || codexCwd.get(path) || "";
199
+
200
+ if (isCodex && item.type === "event_msg" && item.payload?.type === "user_message") {
201
+ for (const name of strictUserNames(item.payload.message || "")) record(name, "user", cwd, source);
202
+ }
203
+
204
+ if (isClaude && item.type === "user" && !item.sourceToolAssistantUUID) {
205
+ const content = item.message?.content;
206
+ const text = typeof content === "string"
207
+ ? content
208
+ : Array.isArray(content)
209
+ ? content.filter((part) => part.type === "text").map((part) => part.text || "").join("\n")
210
+ : "";
211
+ for (const name of strictUserNames(text)) record(name, "user", cwd, source);
212
+ }
213
+
214
+ if (isCursor && item.role === "user") {
215
+ const content = item.message?.content;
216
+ const text = Array.isArray(content)
217
+ ? content.filter((part) => part.type === "text").map((part) => part.text || "").join("\n")
218
+ : "";
219
+ for (const name of strictUserNames(text)) record(name, "user", cwd, source);
220
+ }
221
+
222
+ if (isClaude && item.type === "assistant" && Array.isArray(item.message?.content)) {
223
+ for (const part of item.message.content) {
224
+ if (part.type === "tool_use" && part.name === "Skill" && usage.has(part.input?.skill)) {
225
+ record(part.input.skill, "nativeModel", cwd, source);
226
+ }
227
+ }
228
+ }
229
+
230
+ const isGenericTool = (isCodex && item.type === "response_item" && ["custom_tool_call", "function_call"].includes(item.payload?.type))
231
+ || (isClaude && item.type === "assistant")
232
+ || (isCursor && item.role === "assistant");
233
+ if (isGenericTool) {
234
+ for (const name of manifestNames(line)) record(name, "manifestRead", cwd, source);
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!earliest || !latest) {
240
+ for (const path of transcriptFiles) {
241
+ try {
242
+ const timestamp = statSync(path).mtime.toISOString();
243
+ if (!earliest || timestamp < earliest) earliest = timestamp;
244
+ if (!latest || timestamp > latest) latest = timestamp;
245
+ } catch {
246
+ // Coverage count remains valid even when a file disappears during the audit.
247
+ }
248
+ }
249
+ }
250
+
251
+ const duplicateNames = new Set(
252
+ names.filter((name) => skills.filter((skill) => skill.name === name).length > 1),
253
+ );
254
+
255
+ const rows = skills.map((skill) => {
256
+ const observed = usage.get(skill.name);
257
+ const cwd = Object.fromEntries([...observed.cwd.entries()].sort((a, b) => b[1] - a[1]));
258
+ const classifications = [];
259
+ const recommendations = [];
260
+ if (observed.user > 0) classifications.push("OBSERVED_USER");
261
+ if (observed.nativeModel > 0) classifications.push("OBSERVED_MODEL");
262
+ if (observed.manifestRead > 0) classifications.push("MANIFEST_READ_EVIDENCE");
263
+ if (observed.user + observed.nativeModel + observed.manifestRead === 0) classifications.push("UNOBSERVED");
264
+ if (skill.claude_explicit_only && skill.codex_explicit_only) classifications.push("EXPLICIT_ONLY");
265
+ if (skill.claude_explicit_only !== skill.codex_explicit_only) {
266
+ classifications.push("INVOCATION_MISMATCH");
267
+ recommendations.push("mirror-explicit-only-control");
268
+ }
269
+ if (skill.direct_user_intent && !(skill.claude_explicit_only && skill.codex_explicit_only)) {
270
+ recommendations.push("make-explicit-only");
271
+ }
272
+ if (duplicateNames.has(skill.name)) classifications.push("NAME_COLLISION");
273
+ const observedCwds = Object.keys(cwd);
274
+ const observedCount = observed.user + observed.nativeModel + observed.manifestRead;
275
+ const onlyProjectUsage = observedCount >= 2
276
+ && observedCwds.length > 0
277
+ && observedCwds.every((path) => path === projectRoot || path.startsWith(`${projectRoot}/`));
278
+ const hasProjectRuntimeDependency = skill.project_runtime_references.length > 0;
279
+ if (skill.scope === "global" && (hasProjectRuntimeDependency || onlyProjectUsage)) {
280
+ classifications.push("LOCALITY_CANDIDATE");
281
+ recommendations.push(hasProjectRuntimeDependency ? "review-move-to-project-runtime-evidence" : "review-move-to-project-usage-only");
282
+ }
283
+ return {
284
+ ...skill,
285
+ usage: {
286
+ user_explicit: observed.user,
287
+ native_model: observed.nativeModel,
288
+ manifest_read_evidence: observed.manifestRead,
289
+ cwd,
290
+ sources: [...observed.sources].sort(),
291
+ },
292
+ classifications,
293
+ recommendations: [...new Set(recommendations)],
294
+ };
295
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
296
+
297
+ const result = {
298
+ schema_version: 1,
299
+ generated_at: new Date().toISOString(),
300
+ project_root: projectRoot,
301
+ coverage: {
302
+ transcript_files: transcriptFiles.length,
303
+ matched_jsonl_records: matchedLines,
304
+ codex_model_invocation: "NOT_INSTRUMENTED; manifest reads are heuristic evidence only",
305
+ claude_model_invocation: "NATIVE Skill tool calls",
306
+ cursor_model_invocation: "NOT_INSTRUMENTED; manifest reads are heuristic evidence only",
307
+ earliest_timestamp: earliest,
308
+ latest_timestamp: latest,
309
+ },
310
+ summary: {
311
+ skill_rows: rows.length,
312
+ unique_names: new Set(rows.map((row) => row.name)).size,
313
+ global: rows.filter((row) => row.scope === "global").length,
314
+ project: rows.filter((row) => row.scope === "project").length,
315
+ unobserved: rows.filter((row) => row.classifications.includes("UNOBSERVED")).length,
316
+ explicit_only: rows.filter((row) => row.classifications.includes("EXPLICIT_ONLY")).length,
317
+ invocation_mismatches: rows.filter((row) => row.classifications.includes("INVOCATION_MISMATCH")).length,
318
+ locality_candidates: rows.filter((row) => row.classifications.includes("LOCALITY_CANDIDATE")).length,
319
+ name_collisions: duplicateNames.size,
320
+ },
321
+ skills: rows,
322
+ };
323
+
324
+ if (format === "json") {
325
+ console.log(JSON.stringify(result, null, 2));
326
+ process.exit(0);
327
+ }
328
+
329
+ console.log("# Skill audit");
330
+ console.log(`\n- Project: \`${projectRoot}\``);
331
+ console.log(`- Coverage: ${result.coverage.transcript_files} transcript files; ${result.coverage.matched_jsonl_records} relevant records parsed`);
332
+ console.log(`- Time range observed: ${earliest || "unknown"} → ${latest || "unknown"}`);
333
+ console.log(`- Skills: ${result.summary.skill_rows} rows / ${result.summary.unique_names} unique names`);
334
+ console.log(`- Unobserved: ${result.summary.unobserved}; explicit-only: ${result.summary.explicit_only}; invocation mismatches: ${result.summary.invocation_mismatches}`);
335
+ console.log(`- Locality candidates: ${result.summary.locality_candidates}; name collisions: ${result.summary.name_collisions}`);
336
+ console.log(`- Codex/Cursor model invocation: NOT INSTRUMENTED; manifest reads are heuristic only`);
337
+ console.log("\n| Skill | Scope | User | Claude native | Manifest read | Invocation | Classification | Recommendation |");
338
+ console.log("|---|---:|---:|---:|---:|---|---|---|");
339
+ for (const row of rows) {
340
+ const invocation = row.claude_explicit_only && row.codex_explicit_only
341
+ ? "explicit-only"
342
+ : row.claude_explicit_only || row.codex_explicit_only
343
+ ? "mismatch"
344
+ : "implicit";
345
+ console.log(`| ${row.name} | ${row.scope} | ${row.usage.user_explicit} | ${row.usage.native_model} | ${row.usage.manifest_read_evidence} | ${invocation} | ${row.classifications.join(", ") || "-"} | ${row.recommendations.join(", ") || "-"} |`);
346
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiblueprint-cli",
3
- "version": "1.4.102",
3
+ "version": "1.4.104",
4
4
  "description": "AIBlueprint CLI for setting up AI coding configurations",
5
5
  "author": "AIBlueprint",
6
6
  "license": "MIT",