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 +2 -0
- package/agents-config/skills/audit/SKILL.md +126 -0
- package/agents-config/skills/audit/agents/openai.yaml +10 -0
- package/agents-config/skills/audit/assets/codex-icon.svg +20 -0
- package/agents-config/skills/audit-skills/SKILL.md +79 -0
- package/agents-config/skills/audit-skills/agents/openai.yaml +7 -0
- package/agents-config/skills/audit-skills/scripts/audit-skills.mjs +346 -0
- package/package.json +1 -1
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,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
|
+
}
|