continuous-improvement 3.1.0 → 3.9.1
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/.claude-plugin/marketplace.json +66 -0
- package/CHANGELOG.md +216 -0
- package/QUICKSTART.md +79 -12
- package/README.md +229 -349
- package/SKILL.md +87 -9
- package/action.yml +1 -1
- package/bin/analyze.sh +9 -1
- package/bin/backfill.mjs +172 -0
- package/bin/check-doc-runtime-claims.mjs +120 -0
- package/bin/check-docs-substrings.mjs +333 -0
- package/bin/check-everything-mirror.mjs +145 -0
- package/bin/check-routing-targets.mjs +151 -0
- package/bin/check-skill-law-tag.mjs +128 -0
- package/bin/check-skill-mirror.mjs +119 -0
- package/bin/check-skill-tiers.mjs +116 -0
- package/bin/check-third-party-shape.mjs +202 -0
- package/bin/generate-plugin-manifests.mjs +171 -0
- package/bin/harvest-friction.mjs +279 -0
- package/bin/hook-stats.mjs +258 -0
- package/bin/install.mjs +407 -499
- package/bin/lint-transcript.mjs +182 -210
- package/bin/mcp-server.mjs +834 -608
- package/bin/observe.mjs +148 -0
- package/bin/pre-commit-block-strays.sh +49 -0
- package/bin/refresh-third-party.mjs +416 -0
- package/bin/unified-cli.mjs +533 -0
- package/commands/continuous-improvement.md +43 -2
- package/commands/discipline.md +14 -0
- package/commands/harvest.md +76 -0
- package/commands/learn-eval.md +117 -0
- package/commands/planning-with-files.md +66 -0
- package/commands/proceed-with-the-recommendation.md +62 -0
- package/commands/ralph.md +103 -0
- package/commands/release-train.md +81 -0
- package/commands/seven-laws.md +16 -0
- package/commands/superpowers.md +180 -0
- package/commands/swarm.md +101 -0
- package/commands/workspace-surface-audit.md +77 -0
- package/hooks/gateguard.mjs +172 -0
- package/hooks/observe.sh +42 -4
- package/hooks/session.sh +3 -3
- package/hooks/three-section-close.mjs +181 -0
- package/instinct-packs/meta.json +16 -0
- package/lib/cli-anything.mjs +401 -0
- package/lib/compound-engineering.mjs +831 -0
- package/lib/gateguard-state.mjs +85 -0
- package/lib/observe-event.mjs +128 -0
- package/lib/plugin-metadata.mjs +435 -0
- package/lib/pm-marketplace.mjs +61 -0
- package/lib/pm-skills.mjs +1274 -0
- package/lib/resolve-home-dir.mjs +43 -0
- package/lib/skill-tiers.mjs +137 -0
- package/lib/unified-plugin.mjs +924 -0
- package/llms.txt +32 -7
- package/package.json +29 -19
- package/plugins/beginner.json +17 -6
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +20 -0
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +26 -0
- package/plugins/continuous-improvement/LICENSE +21 -0
- package/plugins/continuous-improvement/README.md +57 -0
- package/plugins/continuous-improvement/agents/README.md +120 -0
- package/plugins/continuous-improvement/agents/code-reviewer.md +97 -0
- package/plugins/continuous-improvement/agents/security-auditor.md +101 -0
- package/plugins/continuous-improvement/agents/test-engineer.md +95 -0
- package/plugins/continuous-improvement/bin/backfill.mjs +172 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +889 -0
- package/plugins/continuous-improvement/bin/observe.mjs +148 -0
- package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -0
- package/plugins/continuous-improvement/commands/dashboard.md +56 -0
- package/plugins/continuous-improvement/commands/discipline.md +51 -0
- package/plugins/continuous-improvement/commands/harvest.md +76 -0
- package/plugins/continuous-improvement/commands/learn-eval.md +117 -0
- package/plugins/continuous-improvement/commands/planning-with-files.md +66 -0
- package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +62 -0
- package/plugins/continuous-improvement/commands/ralph.md +103 -0
- package/plugins/continuous-improvement/commands/release-train.md +81 -0
- package/plugins/continuous-improvement/commands/seven-laws.md +16 -0
- package/plugins/continuous-improvement/commands/superpowers.md +180 -0
- package/plugins/continuous-improvement/commands/swarm.md +101 -0
- package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +172 -0
- package/plugins/continuous-improvement/hooks/hooks.json +65 -0
- package/plugins/continuous-improvement/hooks/observe.sh +172 -0
- package/plugins/continuous-improvement/hooks/session.sh +106 -0
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +181 -0
- package/plugins/continuous-improvement/instinct-packs/go.json +58 -0
- package/plugins/continuous-improvement/instinct-packs/meta.json +16 -0
- package/plugins/continuous-improvement/instinct-packs/python.json +58 -0
- package/plugins/continuous-improvement/instinct-packs/react.json +58 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +128 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +435 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +43 -0
- package/plugins/continuous-improvement/skills/README.md +37 -0
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +249 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +178 -0
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +163 -0
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +108 -0
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +447 -0
- package/plugins/continuous-improvement/skills/ralph/SKILL.md +221 -0
- package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +73 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +76 -0
- package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +63 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +104 -0
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +219 -0
- package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -0
- package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +136 -0
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +224 -0
- package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +191 -0
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +147 -0
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +66 -0
- package/plugins/continuous-improvement/templates/planning-with-files/findings.md +8 -0
- package/plugins/continuous-improvement/templates/planning-with-files/progress.md +7 -0
- package/plugins/continuous-improvement/templates/planning-with-files/task_plan.md +23 -0
- package/plugins/expert.json +26 -5
- package/skills/README.md +79 -0
- package/skills/deploy-receipt.md +178 -0
- package/skills/gateguard.md +163 -0
- package/skills/para-memory-files.md +108 -0
- package/skills/proceed-with-the-recommendation.md +447 -0
- package/skills/ralph.md +221 -0
- package/skills/recovery-classification.md +73 -0
- package/skills/safety-guard.md +76 -0
- package/skills/state-reconciliation.md +63 -0
- package/skills/strategic-compact.md +104 -0
- package/skills/superpowers.md +219 -0
- package/skills/tdd-workflow.md +411 -0
- package/skills/token-budget-advisor.md +136 -0
- package/skills/verification-loop.md +224 -0
- package/skills/wild-risa-balance.md +191 -0
- package/skills/workspace-surface-audit.md +147 -0
- package/skills/worktree-safety.md +66 -0
- package/templates/insights-claude-md.md +91 -0
- package/templates/planning-with-files/findings.md +8 -0
- package/templates/planning-with-files/progress.md +7 -0
- package/templates/planning-with-files/task_plan.md +23 -0
- package/templates/verify-ladder.example.json +60 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Law-Tag Check
|
|
4
|
+
*
|
|
5
|
+
* Verifies that every source skill in skills/<name>.md announces — in its
|
|
6
|
+
* YAML frontmatter `description:` — which of the 7 Laws of AI Agent
|
|
7
|
+
* Discipline it enforces. The brand-alignment audit (PR #43) made the Law
|
|
8
|
+
* tag the first clause of every skill description so the discipline shows
|
|
9
|
+
* up every time the skill is loaded; this lint guards against silent
|
|
10
|
+
* regression of that work.
|
|
11
|
+
*
|
|
12
|
+
* Recognized Law tags (case-insensitive):
|
|
13
|
+
* "Law 1" through "Law 7"
|
|
14
|
+
* "all 7 Laws" (used by the orchestrator skill)
|
|
15
|
+
* "Law activator" (used by skills that route to other Law-aligned skills)
|
|
16
|
+
*
|
|
17
|
+
* The core skill (root SKILL.md, `tier: core`) is exempt — it IS the 7 Laws
|
|
18
|
+
* spec, not a Law-aligned skill that enforces one. Every other skill must
|
|
19
|
+
* carry a tag.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node bin/check-skill-law-tag.mjs # Check the current repo
|
|
23
|
+
* node bin/check-skill-law-tag.mjs <repo-root> # Check a specific repo root
|
|
24
|
+
*
|
|
25
|
+
* Exit codes:
|
|
26
|
+
* 0 — every non-core source skill carries a recognized Law tag
|
|
27
|
+
* 1 — at least one skill has a description with no recognized Law tag
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { argv, cwd, exit } from "node:process";
|
|
32
|
+
import { normalizeTier, parseSkillFrontmatter } from "../lib/skill-tiers.mjs";
|
|
33
|
+
const SKILLS_DIR = "skills";
|
|
34
|
+
const CORE_SKILL_FILE = "SKILL.md";
|
|
35
|
+
const CORE_SKILL_NAME = "continuous-improvement";
|
|
36
|
+
const LAW_TAG_PATTERNS = [
|
|
37
|
+
/\bLaw\s*[1-7]\b/i,
|
|
38
|
+
/\ball\s+7\s+Laws\b/i,
|
|
39
|
+
/\bLaw\s+activator\b/i,
|
|
40
|
+
];
|
|
41
|
+
export function descriptionHasLawTag(description) {
|
|
42
|
+
if (!description)
|
|
43
|
+
return false;
|
|
44
|
+
return LAW_TAG_PATTERNS.some((p) => p.test(description));
|
|
45
|
+
}
|
|
46
|
+
export function discoverSkillSources(repoRoot) {
|
|
47
|
+
// Mirrors check-skill-tiers' discovery — the core SKILL.md is included so
|
|
48
|
+
// the caller can decide whether to exempt it. This lint exempts it.
|
|
49
|
+
const out = [];
|
|
50
|
+
const corePath = join(repoRoot, CORE_SKILL_FILE);
|
|
51
|
+
try {
|
|
52
|
+
statSync(corePath);
|
|
53
|
+
out.push({ name: CORE_SKILL_NAME, path: corePath });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// No root SKILL.md — misconfigured repo, but skill-mirror lint covers it.
|
|
57
|
+
}
|
|
58
|
+
const skillsDir = join(repoRoot, SKILLS_DIR);
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(skillsDir);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!entry.endsWith(".md"))
|
|
68
|
+
continue;
|
|
69
|
+
if (entry === "README.md")
|
|
70
|
+
continue;
|
|
71
|
+
const fullPath = join(skillsDir, entry);
|
|
72
|
+
try {
|
|
73
|
+
const stat = statSync(fullPath);
|
|
74
|
+
if (!stat.isFile())
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
out.push({ name: entry.replace(/\.md$/, ""), path: fullPath });
|
|
81
|
+
}
|
|
82
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
}
|
|
84
|
+
export function checkSkillLawTags(repoRoot) {
|
|
85
|
+
const sources = discoverSkillSources(repoRoot);
|
|
86
|
+
const problems = [];
|
|
87
|
+
for (const src of sources) {
|
|
88
|
+
const content = readFileSync(src.path, "utf8");
|
|
89
|
+
const front = parseSkillFrontmatter(content);
|
|
90
|
+
// Exempt the core skill — it IS the 7 Laws spec itself.
|
|
91
|
+
if (normalizeTier(front.tier) === "core")
|
|
92
|
+
continue;
|
|
93
|
+
const description = (front.description ?? "").toString();
|
|
94
|
+
if (!descriptionHasLawTag(description)) {
|
|
95
|
+
problems.push({
|
|
96
|
+
name: src.name,
|
|
97
|
+
path: src.path,
|
|
98
|
+
description,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return problems;
|
|
103
|
+
}
|
|
104
|
+
function main() {
|
|
105
|
+
const repoRoot = argv[2] ?? cwd();
|
|
106
|
+
const sources = discoverSkillSources(repoRoot);
|
|
107
|
+
const problems = checkSkillLawTags(repoRoot);
|
|
108
|
+
const checked = sources.length; // includes core, even though we exempt it from problems
|
|
109
|
+
if (problems.length === 0) {
|
|
110
|
+
console.log(`OK skill-law-tag: every non-core source skill carries a recognized Law tag (out of ${checked} source(s) discovered).`);
|
|
111
|
+
exit(0);
|
|
112
|
+
}
|
|
113
|
+
console.error(`FAIL skill-law-tag: ${problems.length} skill(s) with no recognized Law tag in their description.\n`);
|
|
114
|
+
for (const p of problems) {
|
|
115
|
+
console.error(` - ${p.name}`);
|
|
116
|
+
console.error(` file: ${p.path}`);
|
|
117
|
+
console.error(` description: ${p.description.slice(0, 160)}${p.description.length > 160 ? "..." : ""}`);
|
|
118
|
+
}
|
|
119
|
+
console.error("\nFix: edit the skill's YAML frontmatter `description:` so it announces which of the 7 Laws it enforces.\n" +
|
|
120
|
+
"Recognized tags (case-insensitive): \"Law 1\"–\"Law 7\", \"all 7 Laws\" (orchestrator), \"Law activator\" (dispatcher).\n" +
|
|
121
|
+
"Lead with the tag in the first clause — the brand should fire every time the skill is loaded.\n" +
|
|
122
|
+
"Example: \"Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. <existing description>\"");
|
|
123
|
+
exit(1);
|
|
124
|
+
}
|
|
125
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
126
|
+
if (invokedDirectly || argv[1]?.endsWith("check-skill-law-tag.mjs")) {
|
|
127
|
+
main();
|
|
128
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Mirror Check
|
|
4
|
+
*
|
|
5
|
+
* Verifies that every skill exists as both a plugin copy
|
|
6
|
+
* (plugins/continuous-improvement/skills/<name>/SKILL.md) and a standalone
|
|
7
|
+
* copy (skills/<name>.md, or root SKILL.md for the core continuous-improvement
|
|
8
|
+
* skill), and that the two copies are byte-identical.
|
|
9
|
+
*
|
|
10
|
+
* Enforces the "Skill mirror rule" from CONTRIBUTING.md so the curl-install
|
|
11
|
+
* path and the plugin-bundle path produce the same skill content.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/check-skill-mirror.mjs # Check the current repo
|
|
15
|
+
* node bin/check-skill-mirror.mjs <repo-root> # Check a specific repo root
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — every pair matches
|
|
19
|
+
* 1 — at least one drift detected (missing standalone or content mismatch)
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { argv, cwd, exit } from "node:process";
|
|
24
|
+
const PLUGIN_SKILLS_DIR = "plugins/continuous-improvement/skills";
|
|
25
|
+
const CORE_SKILL_NAME = "continuous-improvement";
|
|
26
|
+
export function discoverPairs(repoRoot) {
|
|
27
|
+
const pluginDir = join(repoRoot, PLUGIN_SKILLS_DIR);
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = readdirSync(pluginDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const pairs = [];
|
|
36
|
+
for (const name of entries) {
|
|
37
|
+
const skillDir = join(pluginDir, name);
|
|
38
|
+
let stat;
|
|
39
|
+
try {
|
|
40
|
+
stat = statSync(skillDir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!stat.isDirectory())
|
|
46
|
+
continue;
|
|
47
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
48
|
+
try {
|
|
49
|
+
statSync(skillFile);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const standalonePath = name === CORE_SKILL_NAME
|
|
55
|
+
? join(repoRoot, "SKILL.md")
|
|
56
|
+
: join(repoRoot, "skills", `${name}.md`);
|
|
57
|
+
pairs.push({ name, pluginPath: skillFile, standalonePath });
|
|
58
|
+
}
|
|
59
|
+
return pairs.sort((a, b) => a.name.localeCompare(b.name));
|
|
60
|
+
}
|
|
61
|
+
export function checkPairs(repoRoot) {
|
|
62
|
+
const pairs = discoverPairs(repoRoot);
|
|
63
|
+
const drifts = [];
|
|
64
|
+
for (const pair of pairs) {
|
|
65
|
+
let standaloneContent;
|
|
66
|
+
try {
|
|
67
|
+
standaloneContent = readFileSync(pair.standalonePath, "utf8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
drifts.push({
|
|
71
|
+
name: pair.name,
|
|
72
|
+
reason: "missing-standalone",
|
|
73
|
+
pluginPath: pair.pluginPath,
|
|
74
|
+
standalonePath: pair.standalonePath,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const pluginContent = readFileSync(pair.pluginPath, "utf8");
|
|
79
|
+
if (pluginContent !== standaloneContent) {
|
|
80
|
+
drifts.push({
|
|
81
|
+
name: pair.name,
|
|
82
|
+
reason: "content-drift",
|
|
83
|
+
pluginPath: pair.pluginPath,
|
|
84
|
+
standalonePath: pair.standalonePath,
|
|
85
|
+
pluginLines: pluginContent.split("\n").length,
|
|
86
|
+
standaloneLines: standaloneContent.split("\n").length,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return drifts;
|
|
91
|
+
}
|
|
92
|
+
function main() {
|
|
93
|
+
const repoRoot = argv[2] ?? cwd();
|
|
94
|
+
const pairs = discoverPairs(repoRoot);
|
|
95
|
+
const drifts = checkPairs(repoRoot);
|
|
96
|
+
if (drifts.length === 0) {
|
|
97
|
+
console.log(`OK skill-mirror: all ${pairs.length} skill pair(s) match between plugin and standalone copies.`);
|
|
98
|
+
exit(0);
|
|
99
|
+
}
|
|
100
|
+
console.error(`FAIL skill-mirror: ${drifts.length} drift(s) detected across ${pairs.length} skill pair(s).\n`);
|
|
101
|
+
for (const d of drifts) {
|
|
102
|
+
if (d.reason === "missing-standalone") {
|
|
103
|
+
console.error(` - ${d.name}: plugin copy exists but standalone copy is missing.`);
|
|
104
|
+
console.error(` plugin: ${d.pluginPath}`);
|
|
105
|
+
console.error(` standalone: ${d.standalonePath} (NOT FOUND)`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(` - ${d.name}: content drift between plugin and standalone copy (${d.pluginLines} vs ${d.standaloneLines} lines).`);
|
|
109
|
+
console.error(` plugin: ${d.pluginPath}`);
|
|
110
|
+
console.error(` standalone: ${d.standalonePath}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.error("\nFix per CONTRIBUTING.md \"Skill mirror rule\": every change to a skill must be applied to both the plugin copy and the standalone copy in the same PR. The two distribution paths (curl-install and plugin-bundle) must produce identical content.");
|
|
114
|
+
exit(1);
|
|
115
|
+
}
|
|
116
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
117
|
+
if (invokedDirectly || argv[1]?.endsWith("check-skill-mirror.mjs")) {
|
|
118
|
+
main();
|
|
119
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Tier Check
|
|
4
|
+
*
|
|
5
|
+
* Verifies that every source skill in skills/<name>.md (and the core
|
|
6
|
+
* SKILL.md at the repo root) declares a recognized `tier:` value in its
|
|
7
|
+
* YAML frontmatter. The generator groups skills into the bundled README
|
|
8
|
+
* by tier; an untagged skill silently slides into the "Other skills"
|
|
9
|
+
* bucket, which is easy to miss in review. This lint surfaces missing or
|
|
10
|
+
* unrecognized tiers as a hard failure.
|
|
11
|
+
*
|
|
12
|
+
* Recognized tier values (see src/lib/skill-tiers.mts):
|
|
13
|
+
* "core" "featured" "1" "2" "companion"
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node bin/check-skill-tiers.mjs # Check the current repo
|
|
17
|
+
* node bin/check-skill-tiers.mjs <repo-root> # Check a specific repo root
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 — every source skill declares a recognized tier
|
|
21
|
+
* 1 — at least one skill is missing or has an unrecognized tier
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { argv, cwd, exit } from "node:process";
|
|
26
|
+
import { normalizeTier, parseSkillFrontmatter, } from "../lib/skill-tiers.mjs";
|
|
27
|
+
const SKILLS_DIR = "skills";
|
|
28
|
+
const CORE_SKILL_FILE = "SKILL.md";
|
|
29
|
+
const CORE_SKILL_NAME = "continuous-improvement";
|
|
30
|
+
export function discoverSkillSources(repoRoot) {
|
|
31
|
+
const out = [];
|
|
32
|
+
// Core skill (root SKILL.md) is always present and represents the
|
|
33
|
+
// continuous-improvement framework itself; treat it as a checked source.
|
|
34
|
+
const corePath = join(repoRoot, CORE_SKILL_FILE);
|
|
35
|
+
try {
|
|
36
|
+
statSync(corePath);
|
|
37
|
+
out.push({ name: CORE_SKILL_NAME, path: corePath });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No root SKILL.md — this is a misconfigured repo, but the lint should
|
|
41
|
+
// not error here; the skill-mirror lint covers that case.
|
|
42
|
+
}
|
|
43
|
+
const skillsDir = join(repoRoot, SKILLS_DIR);
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(skillsDir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.endsWith(".md"))
|
|
53
|
+
continue;
|
|
54
|
+
if (entry === "README.md")
|
|
55
|
+
continue;
|
|
56
|
+
const fullPath = join(skillsDir, entry);
|
|
57
|
+
try {
|
|
58
|
+
const stat = statSync(fullPath);
|
|
59
|
+
if (!stat.isFile())
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out.push({ name: entry.replace(/\.md$/, ""), path: fullPath });
|
|
66
|
+
}
|
|
67
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
69
|
+
export function checkSkillTiers(repoRoot) {
|
|
70
|
+
const sources = discoverSkillSources(repoRoot);
|
|
71
|
+
const problems = [];
|
|
72
|
+
for (const src of sources) {
|
|
73
|
+
const content = readFileSync(src.path, "utf8");
|
|
74
|
+
const front = parseSkillFrontmatter(content);
|
|
75
|
+
const rawTier = front.tier;
|
|
76
|
+
if (!rawTier || rawTier.trim() === "") {
|
|
77
|
+
problems.push({ name: src.name, path: src.path, reason: "missing" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const normalized = normalizeTier(rawTier);
|
|
81
|
+
if (normalized === "unknown") {
|
|
82
|
+
problems.push({
|
|
83
|
+
name: src.name,
|
|
84
|
+
path: src.path,
|
|
85
|
+
reason: "unrecognized",
|
|
86
|
+
rawTier,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return problems;
|
|
91
|
+
}
|
|
92
|
+
function main() {
|
|
93
|
+
const repoRoot = argv[2] ?? cwd();
|
|
94
|
+
const sources = discoverSkillSources(repoRoot);
|
|
95
|
+
const problems = checkSkillTiers(repoRoot);
|
|
96
|
+
if (problems.length === 0) {
|
|
97
|
+
console.log(`OK skill-tiers: all ${sources.length} skill source(s) declare a recognized tier.`);
|
|
98
|
+
exit(0);
|
|
99
|
+
}
|
|
100
|
+
console.error(`FAIL skill-tiers: ${problems.length} skill(s) with missing or unrecognized tier (out of ${sources.length} source(s)).\n`);
|
|
101
|
+
for (const p of problems) {
|
|
102
|
+
if (p.reason === "missing") {
|
|
103
|
+
console.error(` - ${p.name}: no \`tier:\` field in frontmatter`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(` - ${p.name}: \`tier: ${p.rawTier ?? ""}\` is not a recognized value`);
|
|
107
|
+
}
|
|
108
|
+
console.error(` file: ${p.path}`);
|
|
109
|
+
}
|
|
110
|
+
console.error("\nFix: add a `tier:` field to the source skill's YAML frontmatter. Recognized values: core, featured, \"1\", \"2\", companion. The generator uses this to group skills into the bundled README by tier; an untagged skill slides into the catch-all 'Other skills' bucket and is easy to miss in review.");
|
|
111
|
+
exit(1);
|
|
112
|
+
}
|
|
113
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
114
|
+
if (invokedDirectly || argv[1]?.endsWith("check-skill-tiers.mjs")) {
|
|
115
|
+
main();
|
|
116
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Third-Party Snapshot Shape Invariant
|
|
4
|
+
*
|
|
5
|
+
* Verifies that every `third-party/<name>/` snapshot directory conforms to the
|
|
6
|
+
* vendor playbook documented in `third-party/README.md`. For each snapshot
|
|
7
|
+
* directory the check asserts:
|
|
8
|
+
*
|
|
9
|
+
* 1. `OUR_NOTES.md` exists and is non-empty
|
|
10
|
+
* 2. `LICENSE` exists
|
|
11
|
+
* 3. `third-party/MANIFEST.md` has at least one `### <heading>` block whose
|
|
12
|
+
* `Local path` row points at this snapshot directory
|
|
13
|
+
* 4. The matching MANIFEST entry has a `Pinned SHA` row containing a
|
|
14
|
+
* 40-char lowercase hex string wrapped in backticks
|
|
15
|
+
*
|
|
16
|
+
* The mapping between snapshot directory and MANIFEST heading is NOT identity
|
|
17
|
+
* (e.g. dir `superpowers` <-> heading `obra/superpowers`). The check looks up
|
|
18
|
+
* by `Local path`, not by name.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node bin/check-third-party-shape.mjs # check the current repo
|
|
22
|
+
* node bin/check-third-party-shape.mjs <repo-root> # check a specific repo root
|
|
23
|
+
*
|
|
24
|
+
* Exit codes:
|
|
25
|
+
* 0 - every snapshot is shape-compliant
|
|
26
|
+
* 1 - at least one drift found
|
|
27
|
+
*/
|
|
28
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { argv, cwd, exit } from "node:process";
|
|
31
|
+
const THIRD_PARTY_DIR = "third-party";
|
|
32
|
+
const MANIFEST_FILENAME = "MANIFEST.md";
|
|
33
|
+
const OUR_NOTES_FILENAME = "OUR_NOTES.md";
|
|
34
|
+
const LICENSE_FILENAME = "LICENSE";
|
|
35
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
36
|
+
export function discoverSnapshots(repoRoot) {
|
|
37
|
+
const dir = join(repoRoot, THIRD_PARTY_DIR);
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const snapshots = [];
|
|
46
|
+
for (const name of entries) {
|
|
47
|
+
const full = join(dir, name);
|
|
48
|
+
let entryStat;
|
|
49
|
+
try {
|
|
50
|
+
entryStat = statSync(full);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!entryStat.isDirectory())
|
|
56
|
+
continue;
|
|
57
|
+
snapshots.push({
|
|
58
|
+
name,
|
|
59
|
+
hasOurNotes: hasNonEmptyFile(join(full, OUR_NOTES_FILENAME)),
|
|
60
|
+
hasLicense: fileExists(join(full, LICENSE_FILENAME)),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return snapshots;
|
|
64
|
+
}
|
|
65
|
+
function fileExists(path) {
|
|
66
|
+
try {
|
|
67
|
+
return statSync(path).isFile();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function hasNonEmptyFile(path) {
|
|
74
|
+
try {
|
|
75
|
+
const s = statSync(path);
|
|
76
|
+
return s.isFile() && s.size > 0;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function parseManifestEntries(manifestText) {
|
|
83
|
+
const lines = manifestText.split(/\r?\n/);
|
|
84
|
+
const entries = [];
|
|
85
|
+
let current = null;
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (current && current.localPath !== null)
|
|
88
|
+
entries.push(current);
|
|
89
|
+
current = null;
|
|
90
|
+
};
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const headingMatch = line.match(/^###\s+(.+?)\s*$/);
|
|
93
|
+
if (headingMatch) {
|
|
94
|
+
flush();
|
|
95
|
+
current = { heading: headingMatch[1], pinnedSha: null, localPath: null };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (line.match(/^##\s+/) || line.match(/^#\s+/)) {
|
|
99
|
+
flush();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!current)
|
|
103
|
+
continue;
|
|
104
|
+
const shaRow = line.match(/^\|\s*Pinned SHA\s*\|\s*`?([0-9a-fA-F]+)`?\s*\|/);
|
|
105
|
+
if (shaRow) {
|
|
106
|
+
current.pinnedSha = shaRow[1].toLowerCase();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const pathRow = line.match(/^\|\s*Local path\s*\|\s*`?([^`|]+?)`?\s*\|/);
|
|
110
|
+
if (pathRow) {
|
|
111
|
+
current.localPath = pathRow[1].trim();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
flush();
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
function normalizeLocalPath(p) {
|
|
119
|
+
let out = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
120
|
+
if (out.startsWith("./"))
|
|
121
|
+
out = out.slice(2);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
export function checkThirdPartyShape(repoRoot) {
|
|
125
|
+
const snapshots = discoverSnapshots(repoRoot);
|
|
126
|
+
const manifestPath = join(repoRoot, THIRD_PARTY_DIR, MANIFEST_FILENAME);
|
|
127
|
+
let manifestText = "";
|
|
128
|
+
try {
|
|
129
|
+
manifestText = readFileSync(manifestPath, "utf8");
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* no manifest at all - every snapshot will fail no-manifest-entry */
|
|
133
|
+
}
|
|
134
|
+
const entries = parseManifestEntries(manifestText);
|
|
135
|
+
const entryByLocalPath = new Map();
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
if (e.localPath)
|
|
138
|
+
entryByLocalPath.set(normalizeLocalPath(e.localPath), e);
|
|
139
|
+
}
|
|
140
|
+
const drifts = [];
|
|
141
|
+
for (const snap of snapshots) {
|
|
142
|
+
if (!snap.hasOurNotes) {
|
|
143
|
+
drifts.push({
|
|
144
|
+
kind: "missing-our-notes",
|
|
145
|
+
snapshot: snap.name,
|
|
146
|
+
detail: `expected non-empty ${OUR_NOTES_FILENAME} at ${THIRD_PARTY_DIR}/${snap.name}/`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!snap.hasLicense) {
|
|
150
|
+
drifts.push({
|
|
151
|
+
kind: "missing-license",
|
|
152
|
+
snapshot: snap.name,
|
|
153
|
+
detail: `expected ${LICENSE_FILENAME} at ${THIRD_PARTY_DIR}/${snap.name}/`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const expectedPath = `${THIRD_PARTY_DIR}/${snap.name}`;
|
|
157
|
+
const entry = entryByLocalPath.get(expectedPath);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
drifts.push({
|
|
160
|
+
kind: "no-manifest-entry",
|
|
161
|
+
snapshot: snap.name,
|
|
162
|
+
detail: `no MANIFEST.md ### block has Local path \`${expectedPath}/\``,
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!entry.pinnedSha || !SHA_RE.test(entry.pinnedSha)) {
|
|
167
|
+
drifts.push({
|
|
168
|
+
kind: "invalid-sha",
|
|
169
|
+
snapshot: snap.name,
|
|
170
|
+
detail: `MANIFEST entry "${entry.heading}" has Pinned SHA "${entry.pinnedSha ?? "(none)"}" - expected 40-char lowercase hex`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
drifts,
|
|
176
|
+
snapshotCount: snapshots.length,
|
|
177
|
+
manifestEntryCount: entries.length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function main() {
|
|
181
|
+
const repoRoot = argv[2] ?? cwd();
|
|
182
|
+
const { drifts, snapshotCount, manifestEntryCount } = checkThirdPartyShape(repoRoot);
|
|
183
|
+
if (drifts.length === 0) {
|
|
184
|
+
console.log(`OK third-party-shape: all ${snapshotCount} snapshot(s) compliant ` +
|
|
185
|
+
`(${manifestEntryCount} MANIFEST entr${manifestEntryCount === 1 ? "y" : "ies"} with Local path).`);
|
|
186
|
+
exit(0);
|
|
187
|
+
}
|
|
188
|
+
console.error(`FAIL third-party-shape: ${drifts.length} drift(s) across ${snapshotCount} snapshot(s).\n`);
|
|
189
|
+
for (const d of drifts) {
|
|
190
|
+
console.error(` - [${d.kind}] ${d.snapshot}`);
|
|
191
|
+
if (d.detail)
|
|
192
|
+
console.error(` ${d.detail}`);
|
|
193
|
+
}
|
|
194
|
+
console.error(`\nFix: see third-party/README.md "Layout" + third-party/MANIFEST.md ` +
|
|
195
|
+
`existing entries for the playbook shape (OUR_NOTES.md, LICENSE, ` +
|
|
196
|
+
`### <heading> block with Pinned SHA + Local path rows).`);
|
|
197
|
+
exit(1);
|
|
198
|
+
}
|
|
199
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
200
|
+
if (invokedDirectly || argv[1]?.endsWith("check-third-party-shape.mjs")) {
|
|
201
|
+
main();
|
|
202
|
+
}
|