agent-skill-doctor 0.1.0 → 0.3.0
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 +11 -0
- package/README.en.md +4 -1
- package/README.md +3 -0
- package/bin/agent-skill-doctor.js +100 -26
- package/package.json +2 -1
- package/src/doctor/governance.js +136 -0
- package/src/doctor/i18n.js +30 -0
- package/src/doctor/index.js +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-07-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Added `governance` registry readiness diagnostics for owner, version, lifecycle status, release label, and trusted source metadata.
|
|
12
|
+
- Added `agent-skill-doctor governance --json` for focused governance findings.
|
|
13
|
+
- Added governance findings to diagnose summaries, Markdown reports, JSON reports, and HTML dashboard cards.
|
|
14
|
+
- Added targeted `fix --type governance` guide text in English and Chinese.
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
- Documented governance checks and CLI examples in English and Chinese READMEs.
|
|
18
|
+
|
|
8
19
|
## [0.1.0] - 2026-06-08
|
|
9
20
|
|
|
10
21
|
### Added
|
package/README.en.md
CHANGED
|
@@ -23,6 +23,7 @@ Agent Skill Doctor diagnoses local AI Agent Skills: duplicate installs, version
|
|
|
23
23
|
- `conflict`: contradictory instructions, such as `npm install` vs `pnpm install`.
|
|
24
24
|
- `duplicate`: exact, same-source, or same-name duplicate skills.
|
|
25
25
|
- `version_drift`: the same skill installed with different refs or content.
|
|
26
|
+
- `governance`: registry / team-sharing readiness, such as missing owner, version, lifecycle status, stable/dev label, or trusted source.
|
|
26
27
|
- `zombie`: low-activity or possibly abandoned skills.
|
|
27
28
|
- `description_quality`: missing trigger, input/output, risk notes, or too-short descriptions.
|
|
28
29
|
- `scan_warning`: missing `SKILL.md` or malformed frontmatter.
|
|
@@ -67,7 +68,7 @@ Use agent-skill-doctor to diagnose my local Agent Skills:
|
|
|
67
68
|
2. Generate an HTML report: npx agent-skill-doctor report --format html --lang en
|
|
68
69
|
3. Review conflicts, duplicates, version drift, zombie skills, and risks.
|
|
69
70
|
4. Do not delete files yet. First produce a repair plan and explain which skills would change.
|
|
70
|
-
5. Give recommendations for risk, duplicate, version_drift, zombie, and description_quality findings.
|
|
71
|
+
5. Give recommendations for risk, duplicate, version_drift, governance, zombie, and description_quality findings.
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
The agent can use `fix` to generate targeted repair prompts:
|
|
@@ -174,6 +175,7 @@ agent-skill-doctor diagnose --json
|
|
|
174
175
|
agent-skill-doctor risks --json
|
|
175
176
|
agent-skill-doctor conflicts --json
|
|
176
177
|
agent-skill-doctor duplicates --json
|
|
178
|
+
agent-skill-doctor governance --json
|
|
177
179
|
agent-skill-doctor zombies --json
|
|
178
180
|
|
|
179
181
|
# Generate reports
|
|
@@ -185,6 +187,7 @@ agent-skill-doctor report --format html --lang en
|
|
|
185
187
|
agent-skill-doctor fix --lang en
|
|
186
188
|
agent-skill-doctor fix --type duplicate --lang en
|
|
187
189
|
agent-skill-doctor fix --type version_drift --lang en
|
|
190
|
+
agent-skill-doctor fix --type governance --lang en
|
|
188
191
|
|
|
189
192
|
# Fail CI by severity
|
|
190
193
|
agent-skill-doctor diagnose --ci --fail-on high
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Agent Skill Doctor 用来帮你检查本地 AI Agent Skills 是否健康:有
|
|
|
23
23
|
- `conflict`:Skill 之间的指令冲突,例如一个要求 `npm install`,另一个要求 `pnpm install`。
|
|
24
24
|
- `duplicate`:重复 Skill,包含完全重复、同源重复、同名不同内容。
|
|
25
25
|
- `version_drift`:同一个 Skill 在多个位置存在不同版本、不同 ref 或不同内容。
|
|
26
|
+
- `governance`:Registry / 团队共享就绪度,例如缺少 owner、version、生命周期状态、stable/dev 标签或可信来源。
|
|
26
27
|
- `zombie`:疑似长期不用或无人维护的 Skill。
|
|
27
28
|
- `description_quality`:缺少触发条件、输入输出、风险说明,或者描述过短。
|
|
28
29
|
- `scan_warning`:目录结构问题,例如缺少 `SKILL.md` 或 frontmatter 格式异常。
|
|
@@ -174,6 +175,7 @@ agent-skill-doctor diagnose --json
|
|
|
174
175
|
agent-skill-doctor risks --json
|
|
175
176
|
agent-skill-doctor conflicts --json
|
|
176
177
|
agent-skill-doctor duplicates --json
|
|
178
|
+
agent-skill-doctor governance --json
|
|
177
179
|
agent-skill-doctor zombies --json
|
|
178
180
|
|
|
179
181
|
# 生成报告
|
|
@@ -185,6 +187,7 @@ agent-skill-doctor report --format html --lang zh
|
|
|
185
187
|
agent-skill-doctor fix --lang zh
|
|
186
188
|
agent-skill-doctor fix --type duplicate --lang zh
|
|
187
189
|
agent-skill-doctor fix --type version_drift --lang zh
|
|
190
|
+
agent-skill-doctor fix --type governance --lang zh
|
|
188
191
|
|
|
189
192
|
# CI 中按严重程度失败
|
|
190
193
|
agent-skill-doctor diagnose --ci --fail-on high
|
|
@@ -11,9 +11,21 @@ const { detectConflicts } = require('../src/doctor/conflict');
|
|
|
11
11
|
const { detectZombies } = require('../src/doctor/zombie');
|
|
12
12
|
const { DEFAULT_CONFLICT_RULES } = require('../src/doctor/rules');
|
|
13
13
|
const phase2 = require('../src/doctor/phase2');
|
|
14
|
+
const { detectGovernanceFindings } = require('../src/doctor/governance');
|
|
14
15
|
const { t, dictionaries } = require('../src/doctor/i18n');
|
|
15
16
|
|
|
16
|
-
const SKIP_DIRS = new Set([
|
|
17
|
+
const SKIP_DIRS = new Set([
|
|
18
|
+
'.git', 'node_modules', 'target', 'dist', 'build', '.tmp', '.DS_Store',
|
|
19
|
+
// Agent-specific non-skill directories
|
|
20
|
+
'sessions', 'backups', 'shell-snapshots', 'session-env',
|
|
21
|
+
'debug', 'file-history', 'paste-cache', 'plans',
|
|
22
|
+
'daemon', 'ide', 'hooks', 'logs', 'log', 'errors',
|
|
23
|
+
'archived_sessions', 'worktrees', 'sqlite',
|
|
24
|
+
'accounts', 'memories', 'rules', 'docs', 'vendor_imports',
|
|
25
|
+
'process_manager', 'node_repl', 'ambient-suggestions',
|
|
26
|
+
'automations', 'codexmate', 'computer-use', 'computer-use-turn-ended',
|
|
27
|
+
'browser', 'pets',
|
|
28
|
+
]);
|
|
17
29
|
|
|
18
30
|
function sha256(input) {
|
|
19
31
|
return crypto.createHash('sha256').update(input).digest('hex');
|
|
@@ -34,6 +46,10 @@ function normalizePath(p) {
|
|
|
34
46
|
return path.resolve(expandHome(p)).replace(/\\/g, '/');
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
function isDir(p) {
|
|
50
|
+
try { return fs.existsSync(p) && fs.statSync(p).isDirectory(); } catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
function ensureDir(dir) {
|
|
38
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
39
55
|
}
|
|
@@ -106,34 +122,76 @@ function loadConfig() {
|
|
|
106
122
|
const home = doctorHome();
|
|
107
123
|
ensureDir(home);
|
|
108
124
|
ensureDir(path.join(home, 'reports'));
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
|
|
126
|
+
// Agent root directories (global)
|
|
127
|
+
const agentRoots = [
|
|
112
128
|
'~/.skills-manager',
|
|
113
|
-
|
|
114
|
-
'~/.
|
|
115
|
-
'~/.
|
|
116
|
-
'~/.
|
|
117
|
-
'~/.
|
|
118
|
-
'~/.
|
|
119
|
-
'~/.
|
|
120
|
-
'~/.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
'~/.agent',
|
|
130
|
+
'~/.agents',
|
|
131
|
+
'~/.codex',
|
|
132
|
+
'~/.claude',
|
|
133
|
+
'~/.cursor',
|
|
134
|
+
'~/.opencode',
|
|
135
|
+
'~/.windsurf',
|
|
136
|
+
'~/.aider',
|
|
137
|
+
'~/.continue',
|
|
138
|
+
'~/.cody',
|
|
139
|
+
'~/.copilot',
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// Project-local agent directories
|
|
143
|
+
const projectAgents = [
|
|
144
|
+
'.agent',
|
|
145
|
+
'.agents',
|
|
146
|
+
'.codex',
|
|
147
|
+
'.claude',
|
|
148
|
+
'.cursor',
|
|
149
|
+
'.opencode',
|
|
150
|
+
'.windsurf',
|
|
151
|
+
'.aider',
|
|
152
|
+
'.continue',
|
|
153
|
+
'.cody',
|
|
154
|
+
'.copilot',
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const candidates = [];
|
|
158
|
+
|
|
159
|
+
// For each agent root, discover skill directories automatically
|
|
160
|
+
for (const root of agentRoots) {
|
|
161
|
+
const expanded = expandHome(root);
|
|
162
|
+
if (!isDir(expanded)) continue;
|
|
163
|
+
|
|
164
|
+
// Add the root itself (for ~/.skills-manager pattern)
|
|
165
|
+
candidates.push(expanded);
|
|
166
|
+
|
|
167
|
+
// Add skills/ subdirectory if exists
|
|
168
|
+
const skillsDir = path.join(expanded, 'skills');
|
|
169
|
+
if (isDir(skillsDir)) candidates.push(skillsDir);
|
|
170
|
+
|
|
171
|
+
// Add skills-core/ subdirectory if exists (for ~/.agents/skills-core)
|
|
172
|
+
const skillsCoreDir = path.join(expanded, 'skills-core');
|
|
173
|
+
if (isDir(skillsCoreDir)) candidates.push(skillsCoreDir);
|
|
174
|
+
|
|
175
|
+
// Add plugins/ subdirectory if exists (for plugin cache/marketplace)
|
|
176
|
+
const pluginsDir = path.join(expanded, 'plugins');
|
|
177
|
+
if (isDir(pluginsDir)) candidates.push(pluginsDir);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// For project-local directories
|
|
181
|
+
for (const agent of projectAgents) {
|
|
182
|
+
const expanded = path.join(process.cwd(), agent);
|
|
183
|
+
if (!isDir(expanded)) continue;
|
|
184
|
+
|
|
185
|
+
const skillsDir = path.join(expanded, 'skills');
|
|
186
|
+
if (isDir(skillsDir)) candidates.push(skillsDir);
|
|
187
|
+
}
|
|
188
|
+
|
|
131
189
|
return {
|
|
132
190
|
home,
|
|
133
191
|
dbPath: path.join(home, 'doctor.db'),
|
|
134
192
|
reportsDir: path.join(home, 'reports'),
|
|
135
193
|
scan: { maxDepth: 6 },
|
|
136
|
-
roots: candidates,
|
|
194
|
+
roots: [...new Set(candidates)], // deduplicate
|
|
137
195
|
};
|
|
138
196
|
}
|
|
139
197
|
|
|
@@ -691,6 +749,12 @@ function runPhase2Analysis(db, skills) {
|
|
|
691
749
|
return { groups, drifts };
|
|
692
750
|
}
|
|
693
751
|
|
|
752
|
+
function runGovernanceAnalysis(db, skills) {
|
|
753
|
+
const findings = detectGovernanceFindings(skills);
|
|
754
|
+
for (const finding of findings) upsertFinding(db, finding, finding.links || []);
|
|
755
|
+
return findings;
|
|
756
|
+
}
|
|
757
|
+
|
|
694
758
|
function recordRun(db, run) {
|
|
695
759
|
db.prepare('INSERT INTO doctor_runs (id, started_at, finished_at, status, skill_count, finding_count, duplicate_group_count, high_count, critical_count, config_json, summary_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(run.id, run.startedAt, run.finishedAt, run.status, run.skillCount, run.findingCount, 0, run.highCount, run.criticalCount, JSON.stringify(run.config || {}), JSON.stringify(run.summary || {}));
|
|
696
760
|
}
|
|
@@ -727,6 +791,7 @@ function buildReportData(db, includeIgnored) {
|
|
|
727
791
|
riskFindings: findings.filter(f => f.type === 'risk').length,
|
|
728
792
|
conflictFindings: findings.filter(f => f.type === 'conflict').length,
|
|
729
793
|
zombieCandidates: findings.filter(f => f.type === 'zombie').length,
|
|
794
|
+
governanceFindings: findings.filter(f => f.type === 'governance').length,
|
|
730
795
|
descriptionQualityFindings: findings.filter(f => f.type === 'description_quality').length,
|
|
731
796
|
duplicateFindings: findings.filter(f => f.type === 'duplicate').length,
|
|
732
797
|
ignoredFindings: rows.filter(row => row.ignored).length,
|
|
@@ -749,6 +814,7 @@ function renderMarkdown(data, lang) {
|
|
|
749
814
|
`- ${L('report.versionDriftFindings')}: ${data.summary.versionDriftFindings}`,
|
|
750
815
|
`- ${L('report.conflictFindings')}: ${data.summary.conflictFindings}`,
|
|
751
816
|
`- ${L('report.riskFindings')}: ${data.summary.riskFindings}`,
|
|
817
|
+
`- ${L('report.governanceFindings')}: ${data.summary.governanceFindings}`,
|
|
752
818
|
`- ${L('report.zombieCandidates')}: ${data.summary.zombieCandidates}`,
|
|
753
819
|
`- ${L('report.ignoredFindings')}: ${data.summary.ignoredFindings}`,
|
|
754
820
|
`- ${L('report.bySeverity')}: ${JSON.stringify(data.summary.bySeverity)}`,
|
|
@@ -908,6 +974,7 @@ function renderHtml(data, lang, reportPath) {
|
|
|
908
974
|
{ key: 'report.totalFindings', descKey: 'dashboard.totalFindings.desc', value: data.summary.totalFindings, color: '#8b5cf6', tooltipTitle: null, tooltipText: null, filterFn: null },
|
|
909
975
|
// Actionable — high priority (red)
|
|
910
976
|
{ key: 'report.conflictFindings', descKey: 'dashboard.conflictFindings.desc', value: data.summary.conflictFindings, color: '#ef4444', tooltipTitle: 'tooltip.conflict.title', tooltipText: 'tooltip.conflict.text', filterFn: f => f.type === 'conflict' },
|
|
977
|
+
{ key: 'report.governanceFindings', descKey: 'dashboard.governanceFindings.desc', value: data.summary.governanceFindings, color: '#0f766e', tooltipTitle: 'tooltip.governance.title', tooltipText: 'tooltip.governance.text', filterFn: f => f.type === 'governance' },
|
|
911
978
|
// Actionable — medium priority (orange)
|
|
912
979
|
{ key: 'report.zombieCandidates', descKey: 'dashboard.zombieCandidates.desc', value: data.summary.zombieCandidates, color: '#f59e0b', tooltipTitle: 'tooltip.zombie.title', tooltipText: 'tooltip.zombie.text', filterFn: f => f.type === 'zombie' },
|
|
913
980
|
// Actionable — lower priority (yellow)
|
|
@@ -1367,6 +1434,7 @@ Commands:
|
|
|
1367
1434
|
risks [--json] [--ci] [--fail-on high|critical|medium]
|
|
1368
1435
|
conflicts [--json] [--ci] [--fail-on high|critical|medium]
|
|
1369
1436
|
zombies [--json] [--ci] [--fail-on high|critical|medium]
|
|
1437
|
+
governance [--json] [--ci] [--fail-on high|critical|medium]
|
|
1370
1438
|
plan [--safe|--normal|--aggressive] [--json] [--output <path>]
|
|
1371
1439
|
apply <plan.json> --dry-run [--json]
|
|
1372
1440
|
ignore <finding-id> [--reason <text>]
|
|
@@ -1374,7 +1442,7 @@ Commands:
|
|
|
1374
1442
|
ignored list
|
|
1375
1443
|
help
|
|
1376
1444
|
|
|
1377
|
-
Fix types: risk, zombie, duplicate, conflict, version_drift, description_quality, scan_warning
|
|
1445
|
+
Fix types: risk, zombie, duplicate, conflict, version_drift, governance, description_quality, scan_warning
|
|
1378
1446
|
Fix severity: critical, high, medium, low, info
|
|
1379
1447
|
`);
|
|
1380
1448
|
}
|
|
@@ -1473,6 +1541,9 @@ function runDiagnose(args) {
|
|
|
1473
1541
|
const zombieFindings = detectZombies(skills);
|
|
1474
1542
|
for (const f of zombieFindings) upsertFinding(db, f, f.links || []);
|
|
1475
1543
|
|
|
1544
|
+
// Governance readiness detection
|
|
1545
|
+
const governanceFindings = runGovernanceAnalysis(db, skills);
|
|
1546
|
+
|
|
1476
1547
|
// Duplicate and version drift detection
|
|
1477
1548
|
const phase2Result = runPhase2Analysis(db, skills);
|
|
1478
1549
|
|
|
@@ -1482,6 +1553,7 @@ function runDiagnose(args) {
|
|
|
1482
1553
|
riskFindings: riskFindings.length,
|
|
1483
1554
|
conflictFindings: conflictFindings.length,
|
|
1484
1555
|
zombieCandidates: zombieFindings.length,
|
|
1556
|
+
governanceFindings: governanceFindings.length,
|
|
1485
1557
|
duplicateGroups: phase2Result.groups.length,
|
|
1486
1558
|
versionDriftFindings: phase2Result.drifts.length,
|
|
1487
1559
|
};
|
|
@@ -1494,6 +1566,7 @@ function runDiagnose(args) {
|
|
|
1494
1566
|
console.log(t('cli.riskFindings', lang, summary.riskFindings));
|
|
1495
1567
|
console.log(t('cli.conflictFindings', lang, summary.conflictFindings));
|
|
1496
1568
|
console.log(t('cli.zombieCandidates', lang, summary.zombieCandidates));
|
|
1569
|
+
console.log(t('cli.governanceFindings', lang, summary.governanceFindings));
|
|
1497
1570
|
}
|
|
1498
1571
|
|
|
1499
1572
|
if (args.ci) {
|
|
@@ -1684,7 +1757,7 @@ function runIgnored(args) {
|
|
|
1684
1757
|
|
|
1685
1758
|
function runGuide(args) {
|
|
1686
1759
|
const lang = args.lang || 'en';
|
|
1687
|
-
const types = ['risk', 'zombie', 'duplicate', 'conflict', 'version_drift', 'description_quality', 'scan_warning'];
|
|
1760
|
+
const types = ['risk', 'zombie', 'duplicate', 'conflict', 'version_drift', 'governance', 'description_quality', 'scan_warning'];
|
|
1688
1761
|
const lines = [];
|
|
1689
1762
|
for (const type of types) {
|
|
1690
1763
|
lines.push(`=== ${t(`guide.${type}.title`, lang)} ===`);
|
|
@@ -1721,7 +1794,7 @@ function runFix(args) {
|
|
|
1721
1794
|
}
|
|
1722
1795
|
|
|
1723
1796
|
// Validate type
|
|
1724
|
-
const validTypes = ['risk', 'zombie', 'duplicate', 'conflict', 'version_drift', 'description_quality', 'scan_warning'];
|
|
1797
|
+
const validTypes = ['risk', 'zombie', 'duplicate', 'conflict', 'version_drift', 'governance', 'description_quality', 'scan_warning'];
|
|
1725
1798
|
if (typeFilter && !validTypes.includes(typeFilter)) {
|
|
1726
1799
|
console.error(`${lang === 'zh' ? '无效的问题类型' : 'Invalid type'}: ${typeFilter}. ${lang === 'zh' ? '可选值' : 'Valid values'}: ${validTypes.join(', ')}`);
|
|
1727
1800
|
process.exit(3);
|
|
@@ -1819,6 +1892,7 @@ function main() {
|
|
|
1819
1892
|
if (command === 'risks') return runFindingsByType(args, 'risk');
|
|
1820
1893
|
if (command === 'conflicts') return runFindingsByType(args, 'conflict');
|
|
1821
1894
|
if (command === 'zombies') return runFindingsByType(args, 'zombie');
|
|
1895
|
+
if (command === 'governance') return runFindingsByType(args, 'governance');
|
|
1822
1896
|
if (command === 'plan') return runPlan(args);
|
|
1823
1897
|
if (command === 'apply') return runApply(args);
|
|
1824
1898
|
if (command === 'ignore') return runIgnore(args, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-skill-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Diagnostic and governance CLI for AI agent skills",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/doctor/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"./conflict": "./src/doctor/conflict.js",
|
|
11
11
|
"./zombie": "./src/doctor/zombie.js",
|
|
12
12
|
"./risk": "./src/doctor/risk-lite.js",
|
|
13
|
+
"./governance": "./src/doctor/governance.js",
|
|
13
14
|
"./rules": "./src/doctor/rules.js",
|
|
14
15
|
"./i18n": "./src/doctor/i18n.js"
|
|
15
16
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { buildParticipantIdentityKey } = require('./phase2');
|
|
5
|
+
|
|
6
|
+
function sha256(input) {
|
|
7
|
+
return crypto.createHash('sha256').update(String(input)).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeList(value) {
|
|
11
|
+
if (Array.isArray(value)) return value.map(String).map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
12
|
+
return String(value || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function frontmatterValue(skill, keys) {
|
|
16
|
+
const frontmatter = skill.frontmatter || {};
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = frontmatter[key];
|
|
19
|
+
if (value != null && String(value).trim()) return String(value).trim();
|
|
20
|
+
}
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasTrustedSource(skill) {
|
|
25
|
+
const source = skill.source || {};
|
|
26
|
+
if (['builtin', 'marketplace'].includes(source.type)) return true;
|
|
27
|
+
if (['git', 'plugin'].includes(source.type) && source.url) return true;
|
|
28
|
+
return Boolean(skill.upstreamSkillId || skill.upstream_skill_id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function lifecycleStatus(skill) {
|
|
32
|
+
return frontmatterValue(skill, ['lifecycle', 'status', 'release_status', 'registry_status', 'governance_status']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasGovernanceLabel(skill) {
|
|
36
|
+
const tags = normalizeList(skill.tags || []);
|
|
37
|
+
const labels = normalizeList(frontmatterValue(skill, ['label', 'labels']));
|
|
38
|
+
return [...tags, ...labels].some(label => ['stable', 'dev', 'latest'].includes(label));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeFinding(skill, ruleId, severity, title, description, recommendation) {
|
|
42
|
+
const participantKey = buildParticipantIdentityKey([skill]);
|
|
43
|
+
const evidenceText = `${skill.slug || skill.name}: ${ruleId}`;
|
|
44
|
+
const signature = sha256(`${ruleId}:${skill.id || skill.slug || skill.name}`);
|
|
45
|
+
const id = sha256(`${participantKey}:governance:governance-detector:${ruleId}:${signature}`);
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
type: 'governance',
|
|
49
|
+
severity,
|
|
50
|
+
detectorId: 'governance-detector',
|
|
51
|
+
ruleId,
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
signature,
|
|
55
|
+
evidence: [{
|
|
56
|
+
file: skill.location?.path || skill.local_path || '',
|
|
57
|
+
text: evidenceText,
|
|
58
|
+
anchor: evidenceText.toLowerCase(),
|
|
59
|
+
}],
|
|
60
|
+
recommendation,
|
|
61
|
+
skills: [skill],
|
|
62
|
+
links: [{ skillId: skill.id, role: 'primary' }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectGovernanceFindings(skills) {
|
|
67
|
+
const findings = [];
|
|
68
|
+
|
|
69
|
+
for (const skill of skills) {
|
|
70
|
+
const owner = frontmatterValue(skill, ['owner', 'owners', 'maintainer', 'maintainers']);
|
|
71
|
+
if (!owner) {
|
|
72
|
+
findings.push(makeFinding(
|
|
73
|
+
skill,
|
|
74
|
+
'missing-owner',
|
|
75
|
+
'medium',
|
|
76
|
+
'Missing owner',
|
|
77
|
+
'The skill has no owner or maintainer metadata, so registry review and rollback ownership are unclear.',
|
|
78
|
+
'Add owner or maintainer metadata before sharing this skill through a registry.'
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const version = skill.version || frontmatterValue(skill, ['version']);
|
|
83
|
+
if (!version) {
|
|
84
|
+
findings.push(makeFinding(
|
|
85
|
+
skill,
|
|
86
|
+
'missing-version',
|
|
87
|
+
'medium',
|
|
88
|
+
'Missing version',
|
|
89
|
+
'The skill has no version metadata, which makes stable release and rollback decisions harder.',
|
|
90
|
+
'Add a version field or lock the skill to an upstream ref before publishing.'
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!lifecycleStatus(skill)) {
|
|
95
|
+
findings.push(makeFinding(
|
|
96
|
+
skill,
|
|
97
|
+
'missing-lifecycle-status',
|
|
98
|
+
'medium',
|
|
99
|
+
'Missing lifecycle status',
|
|
100
|
+
'The skill has no governance lifecycle status such as draft, review, or online.',
|
|
101
|
+
'Add lifecycle metadata such as draft, review, online, deprecated, or archived.'
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!hasGovernanceLabel(skill)) {
|
|
106
|
+
findings.push(makeFinding(
|
|
107
|
+
skill,
|
|
108
|
+
'missing-governance-label',
|
|
109
|
+
'low',
|
|
110
|
+
'Missing governance label',
|
|
111
|
+
'The skill has no stable, dev, or latest label to control adoption scope.',
|
|
112
|
+
'Add a stable, dev, or latest label to make release targeting explicit.'
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!hasTrustedSource(skill)) {
|
|
117
|
+
findings.push(makeFinding(
|
|
118
|
+
skill,
|
|
119
|
+
'missing-source',
|
|
120
|
+
'high',
|
|
121
|
+
'Missing trusted source',
|
|
122
|
+
'The skill has no upstream or registry source, so users cannot tell which copy is authoritative.',
|
|
123
|
+
'Add source metadata or import this skill into a central library or registry before team-wide use.'
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return findings;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
detectGovernanceFindings,
|
|
133
|
+
hasGovernanceLabel,
|
|
134
|
+
hasTrustedSource,
|
|
135
|
+
lifecycleStatus,
|
|
136
|
+
};
|
package/src/doctor/i18n.js
CHANGED
|
@@ -8,6 +8,7 @@ const en = {
|
|
|
8
8
|
'cli.riskFindings': 'Risk findings: %s',
|
|
9
9
|
'cli.conflictFindings': 'Conflict findings: %s',
|
|
10
10
|
'cli.zombieCandidates': 'Zombie candidates: %s',
|
|
11
|
+
'cli.governanceFindings': 'Governance findings: %s',
|
|
11
12
|
'cli.reportWritten': 'Report written: %s',
|
|
12
13
|
'cli.noFindings': 'No %s findings.',
|
|
13
14
|
'cli.noDuplicateGroups': 'No duplicate groups. Run: agent-skill-doctor diagnose',
|
|
@@ -32,6 +33,7 @@ const en = {
|
|
|
32
33
|
'report.riskFindings': 'Risk findings',
|
|
33
34
|
'report.conflictFindings': 'Conflict findings',
|
|
34
35
|
'report.zombieCandidates': 'Zombie candidates',
|
|
36
|
+
'report.governanceFindings': 'Governance readiness',
|
|
35
37
|
'report.descriptionQualityFindings': 'Description issues',
|
|
36
38
|
'report.duplicateFindings': 'Duplicate findings',
|
|
37
39
|
'report.ignoredFindings': 'Ignored',
|
|
@@ -118,6 +120,8 @@ const en = {
|
|
|
118
120
|
'tooltip.zombie.formulaText': 'Score = presetCount==0 (0.25) + noAgents (0.20) + noProjects (0.20) + noModification (0.15) + noActivityLog (0.15) + lowDescriptionQuality (0.05). Then: official (*0), plugin (*0.5), thirdParty (*0.75). Max = 1.0.',
|
|
119
121
|
'tooltip.conflict.title': 'Conflict Detection Criteria',
|
|
120
122
|
'tooltip.conflict.text': 'Checks if 2+ skills match different alternatives within the same conflict rule. Built-in rules: package manager conflict (npm vs pnpm vs yarn), output format conflict (JSON vs Markdown). Binary detection: conflict exists or not.',
|
|
123
|
+
'tooltip.governance.title': 'Governance Readiness Criteria',
|
|
124
|
+
'tooltip.governance.text': 'Checks whether a skill is ready for team or registry use: owner, version, lifecycle status, release label, and trusted source metadata.',
|
|
121
125
|
'tooltip.descriptionQuality.title': 'Description Quality (Informational)',
|
|
122
126
|
'tooltip.descriptionQuality.text': 'Evaluates skill descriptions on 4 dimensions: length, risk mention, usage guidance, and structure. Low scores indicate poor documentation, but this does not affect skill functionality. Can be improved by Agent at low priority.',
|
|
123
127
|
'tooltip.versionDrift.title': 'Version Drift Detection',
|
|
@@ -144,6 +148,7 @@ const en = {
|
|
|
144
148
|
'type.zombie': 'Zombie',
|
|
145
149
|
'type.duplicate': 'Duplicate',
|
|
146
150
|
'type.version_drift': 'Version Drift',
|
|
151
|
+
'type.governance': 'Governance',
|
|
147
152
|
'type.description_quality': 'Description Quality',
|
|
148
153
|
'type.scan_warning': 'Scan Warning',
|
|
149
154
|
|
|
@@ -193,6 +198,15 @@ const en = {
|
|
|
193
198
|
'guide.version_drift.prompt': 'Please update all installations of skill "%s" to the latest version and consolidate to a single install location if possible.',
|
|
194
199
|
'guide.version_drift.agentExample': 'Agent interaction example:\nUser: Please help me unify skill versions\nAgent: I\'ll check all installation locations and find the latest version...',
|
|
195
200
|
|
|
201
|
+
'guide.governance.title': 'Governance Readiness',
|
|
202
|
+
'guide.governance.definition': 'Governance findings indicate that a skill is missing metadata needed for team sharing, registry review, release labels, or rollback.',
|
|
203
|
+
'guide.governance.cause': 'Common causes:\n• Skill was created locally without owner metadata\n• Skill has no version or lifecycle status\n• Skill has no stable/dev/latest label\n• Skill has no trusted source or upstream registry record',
|
|
204
|
+
'guide.governance.severity': 'Severity explanation:\n• High: Missing trusted source\n• Medium: Missing owner, version, or lifecycle status\n• Low: Missing release label',
|
|
205
|
+
'guide.governance.meaning': 'The skill may work locally, but it is not yet ready to be treated as a governed team asset.',
|
|
206
|
+
'guide.governance.steps': 'Fix steps:\n1. Add owner or maintainer metadata\n2. Add version metadata\n3. Add lifecycle status such as draft, review, or online\n4. Add stable/dev/latest label\n5. Add source metadata or import into a central registry',
|
|
207
|
+
'guide.governance.prompt': 'Please improve governance metadata for the skill at "%s". Add owner, version, lifecycle, label, and trusted source fields where missing.',
|
|
208
|
+
'guide.governance.agentExample': 'Agent interaction example:\nUser: Please prepare this skill for registry sharing\nAgent: I\'ll inspect the skill metadata, add missing owner/version/lifecycle fields, and keep content changes minimal...',
|
|
209
|
+
|
|
196
210
|
'guide.description_quality.title': 'Description Quality',
|
|
197
211
|
'guide.description_quality.definition': 'Description quality issues indicate that the skill\'s SKILL.md file is missing necessary information or has unclear descriptions.',
|
|
198
212
|
'guide.description_quality.cause': 'Common causes:\n• Skill author didn\'t provide complete description\n• Description is too brief, missing key information\n• No trigger conditions specified\n• No input/output documented\n• Known risks not recorded',
|
|
@@ -235,6 +249,7 @@ const en = {
|
|
|
235
249
|
'dashboard.riskFindings.desc': 'Inherent permissions, not actionable',
|
|
236
250
|
'dashboard.zombieCandidates.desc': 'Unused or abandoned skills (score >= 0.4)',
|
|
237
251
|
'dashboard.conflictFindings.desc': 'Skills with contradictory instructions',
|
|
252
|
+
'dashboard.governanceFindings.desc': 'Skills missing registry readiness metadata',
|
|
238
253
|
'dashboard.descriptionQualityFindings.desc': 'Documentation quality, low priority',
|
|
239
254
|
'dashboard.duplicateFindings.desc': 'Findings related to duplicate skill content',
|
|
240
255
|
'dashboard.versionDriftFindings.desc': 'Skills with outdated versions compared to source',
|
|
@@ -281,6 +296,7 @@ const zh = {
|
|
|
281
296
|
'cli.riskFindings': '风险发现: %s',
|
|
282
297
|
'cli.conflictFindings': '冲突发现: %s',
|
|
283
298
|
'cli.zombieCandidates': '僵尸技能: %s',
|
|
299
|
+
'cli.governanceFindings': '治理发现: %s',
|
|
284
300
|
'cli.reportWritten': '报告已写入: %s',
|
|
285
301
|
'cli.noFindings': '没有 %s 类型的发现。',
|
|
286
302
|
'cli.noDuplicateGroups': '没有重复组。运行: agent-skill-doctor diagnose',
|
|
@@ -304,6 +320,7 @@ const zh = {
|
|
|
304
320
|
'report.riskFindings': '风险发现',
|
|
305
321
|
'report.conflictFindings': '冲突发现',
|
|
306
322
|
'report.zombieCandidates': '僵尸技能',
|
|
323
|
+
'report.governanceFindings': '治理就绪',
|
|
307
324
|
'report.descriptionQualityFindings': '描述问题',
|
|
308
325
|
'report.duplicateFindings': '重复发现',
|
|
309
326
|
'report.ignoredFindings': '已忽略',
|
|
@@ -389,6 +406,8 @@ const zh = {
|
|
|
389
406
|
'tooltip.zombie.formulaText': '分数 = 未被预设引用(0.25) + 未安装到Agent(0.20) + 未安装到项目(0.20) + 无近期修改(0.15) + 无活动日志(0.15) + 描述质量低(0.05)。然后:官方(*0)、插件(*0.5)、第三方(*0.75)。最大值 = 1.0。',
|
|
390
407
|
'tooltip.conflict.title': '冲突检测标准',
|
|
391
408
|
'tooltip.conflict.text': '检查 2+ 个技能是否在同一冲突规则中匹配了不同的备选项。内置规则:包管理器冲突(npm vs pnpm vs yarn)、输出格式冲突(JSON vs Markdown)。二元检测:存在或不存在。',
|
|
409
|
+
'tooltip.governance.title': '治理就绪检测标准',
|
|
410
|
+
'tooltip.governance.text': '检查技能是否具备团队共享或 Registry 使用所需的元数据:owner、version、生命周期状态、发布标签和可信来源。',
|
|
392
411
|
'tooltip.descriptionQuality.title': '描述质量(仅供参考)',
|
|
393
412
|
'tooltip.descriptionQuality.text': '从 4 个维度评估技能描述:长度、风险提示、使用指导、结构化。低分说明文档质量差,但不影响技能功能。可由 Agent 低优先级补充完善。',
|
|
394
413
|
'tooltip.versionDrift.title': '版本漂移检测标准',
|
|
@@ -413,6 +432,7 @@ const zh = {
|
|
|
413
432
|
'type.zombie': '僵尸',
|
|
414
433
|
'type.duplicate': '重复',
|
|
415
434
|
'type.version_drift': '版本漂移',
|
|
435
|
+
'type.governance': '治理',
|
|
416
436
|
'type.description_quality': '描述质量',
|
|
417
437
|
'type.scan_warning': '扫描警告',
|
|
418
438
|
|
|
@@ -461,6 +481,15 @@ const zh = {
|
|
|
461
481
|
'guide.version_drift.prompt': '请将技能 "%s" 的所有安装更新到最新版本,如可能请合并到单一安装位置。',
|
|
462
482
|
'guide.version_drift.agentExample': 'Agent 交互示例:\n用户:请帮我统一技能版本\nAgent:我来检查所有安装位置,找出最新版本...',
|
|
463
483
|
|
|
484
|
+
'guide.governance.title': '治理就绪',
|
|
485
|
+
'guide.governance.definition': '治理发现表示技能缺少团队共享、Registry 审核、发布标签或回滚所需的元数据。',
|
|
486
|
+
'guide.governance.cause': '常见成因:\n• 技能只在本地创建,未写 owner\n• 技能没有 version 或生命周期状态\n• 技能没有 stable/dev/latest 标签\n• 技能没有可信来源或上游 Registry 记录',
|
|
487
|
+
'guide.governance.severity': '严重程度说明:\n• High(高):缺少可信来源\n• Medium(中):缺少 owner、version 或生命周期状态\n• Low(低):缺少发布标签',
|
|
488
|
+
'guide.governance.meaning': '该技能可能本地可用,但还不能被当作可治理的团队资产。',
|
|
489
|
+
'guide.governance.steps': '修复步骤:\n1. 添加 owner 或 maintainer 元数据\n2. 添加 version 元数据\n3. 添加 draft、review 或 online 等生命周期状态\n4. 添加 stable/dev/latest 标签\n5. 添加 source 元数据,或导入中心库/Registry',
|
|
490
|
+
'guide.governance.prompt': '请完善技能 "%s" 的治理元数据。补齐缺失的 owner、version、lifecycle、label 和 trusted source 字段。',
|
|
491
|
+
'guide.governance.agentExample': 'Agent 交互示例:\n用户:请把这个技能整理到可以团队共享\nAgent:我会检查技能元数据,补齐缺失的 owner/version/lifecycle 字段,并尽量保持内容改动最小...',
|
|
492
|
+
|
|
464
493
|
'guide.description_quality.title': '描述质量',
|
|
465
494
|
'guide.description_quality.definition': '描述质量问题是指标技能的 SKILL.md 文件缺少必要信息或描述不清晰。',
|
|
466
495
|
'guide.description_quality.cause': '常见成因:\n• 技能作者未提供完整描述\n• 描述过于简短,缺少关键信息\n• 未说明触发条件\n• 未说明输入输出\n• 未记录已知风险',
|
|
@@ -503,6 +532,7 @@ const zh = {
|
|
|
503
532
|
'dashboard.riskFindings.desc': '固有权限,无需修复',
|
|
504
533
|
'dashboard.zombieCandidates.desc': '疑似废弃的技能数(分数 >= 0.4)',
|
|
505
534
|
'dashboard.conflictFindings.desc': '指令相互矛盾的技能数',
|
|
535
|
+
'dashboard.governanceFindings.desc': '缺少治理元数据的技能数',
|
|
506
536
|
'dashboard.descriptionQualityFindings.desc': '文档质量问题,低优先级',
|
|
507
537
|
'dashboard.duplicateFindings.desc': '与重复内容相关的发现数',
|
|
508
538
|
'dashboard.versionDriftFindings.desc': '与源版本不一致的技能数',
|
package/src/doctor/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const zombie = require('./zombie');
|
|
|
6
6
|
const riskLite = require('./risk-lite');
|
|
7
7
|
const rules = require('./rules');
|
|
8
8
|
const i18n = require('./i18n');
|
|
9
|
+
const governance = require('./governance');
|
|
9
10
|
|
|
10
11
|
module.exports = {
|
|
11
12
|
// Phase 2 - Duplicate and drift detection
|
|
@@ -35,6 +36,12 @@ module.exports = {
|
|
|
35
36
|
loadJsonRules: riskLite.loadJsonRules,
|
|
36
37
|
scanSkillForRisks: riskLite.scanSkillForRisks,
|
|
37
38
|
|
|
39
|
+
// Governance readiness
|
|
40
|
+
detectGovernanceFindings: governance.detectGovernanceFindings,
|
|
41
|
+
hasGovernanceLabel: governance.hasGovernanceLabel,
|
|
42
|
+
hasTrustedSource: governance.hasTrustedSource,
|
|
43
|
+
lifecycleStatus: governance.lifecycleStatus,
|
|
44
|
+
|
|
38
45
|
// Rules and utilities
|
|
39
46
|
DEFAULT_RISK_RULES: rules.DEFAULT_RISK_RULES,
|
|
40
47
|
DEFAULT_CONFLICT_RULES: rules.DEFAULT_CONFLICT_RULES,
|