continuous-improvement 3.0.0 → 3.8.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.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +78 -0
  2. package/CHANGELOG.md +210 -0
  3. package/LICENSE +21 -21
  4. package/QUICKSTART.md +101 -81
  5. package/README.md +224 -198
  6. package/SKILL.md +87 -9
  7. package/action.yml +33 -0
  8. package/bin/analyze.sh +161 -153
  9. package/bin/backfill.mjs +172 -0
  10. package/bin/check-docs-substrings.mjs +333 -0
  11. package/bin/check-everything-mirror.mjs +145 -0
  12. package/bin/check-routing-targets.mjs +151 -0
  13. package/bin/check-skill-law-tag.mjs +128 -0
  14. package/bin/check-skill-mirror.mjs +119 -0
  15. package/bin/check-skill-tiers.mjs +116 -0
  16. package/bin/check-third-party-shape.mjs +202 -0
  17. package/bin/generate-plugin-manifests.mjs +169 -0
  18. package/bin/harvest-friction.mjs +279 -0
  19. package/bin/hook-stats.mjs +258 -0
  20. package/bin/install.mjs +418 -456
  21. package/bin/lint-transcript.mjs +239 -0
  22. package/bin/mcp-server.mjs +842 -499
  23. package/bin/observe.mjs +148 -0
  24. package/bin/pre-commit-block-strays.sh +49 -0
  25. package/bin/refresh-third-party.mjs +416 -0
  26. package/bin/unified-cli.mjs +533 -0
  27. package/commands/continuous-improvement.md +115 -74
  28. package/commands/dashboard.md +56 -0
  29. package/commands/discipline.md +51 -0
  30. package/commands/harvest.md +76 -0
  31. package/commands/learn-eval.md +117 -0
  32. package/commands/planning-with-files.md +66 -0
  33. package/commands/proceed-with-the-recommendation.md +62 -0
  34. package/commands/ralph.md +103 -0
  35. package/commands/release-train.md +81 -0
  36. package/commands/seven-laws.md +16 -0
  37. package/commands/superpowers.md +153 -0
  38. package/commands/swarm.md +101 -0
  39. package/commands/workspace-surface-audit.md +77 -0
  40. package/hooks/observe.sh +172 -134
  41. package/hooks/session.sh +106 -106
  42. package/hooks/three-section-close.mjs +181 -0
  43. package/instinct-packs/go.json +58 -0
  44. package/instinct-packs/meta.json +16 -0
  45. package/instinct-packs/python.json +58 -0
  46. package/instinct-packs/react.json +58 -0
  47. package/lib/cli-anything.mjs +401 -0
  48. package/lib/compound-engineering.mjs +831 -0
  49. package/lib/observe-event.mjs +128 -0
  50. package/lib/plugin-metadata.mjs +432 -0
  51. package/lib/pm-marketplace.mjs +61 -0
  52. package/lib/pm-skills.mjs +1274 -0
  53. package/lib/resolve-home-dir.mjs +43 -0
  54. package/lib/skill-tiers.mjs +137 -0
  55. package/lib/unified-plugin.mjs +924 -0
  56. package/llms.txt +68 -0
  57. package/package.json +38 -15
  58. package/plugins/beginner.json +17 -6
  59. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +20 -0
  60. package/plugins/continuous-improvement/.claude-plugin/plugin.json +26 -0
  61. package/plugins/continuous-improvement/LICENSE +21 -0
  62. package/plugins/continuous-improvement/README.md +56 -0
  63. package/plugins/continuous-improvement/bin/backfill.mjs +172 -0
  64. package/plugins/continuous-improvement/bin/mcp-server.mjs +886 -0
  65. package/plugins/continuous-improvement/bin/observe.mjs +148 -0
  66. package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -0
  67. package/plugins/continuous-improvement/commands/dashboard.md +56 -0
  68. package/plugins/continuous-improvement/commands/discipline.md +51 -0
  69. package/plugins/continuous-improvement/commands/harvest.md +76 -0
  70. package/plugins/continuous-improvement/commands/learn-eval.md +117 -0
  71. package/plugins/continuous-improvement/commands/planning-with-files.md +66 -0
  72. package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +62 -0
  73. package/plugins/continuous-improvement/commands/ralph.md +103 -0
  74. package/plugins/continuous-improvement/commands/release-train.md +81 -0
  75. package/plugins/continuous-improvement/commands/seven-laws.md +16 -0
  76. package/plugins/continuous-improvement/commands/superpowers.md +153 -0
  77. package/plugins/continuous-improvement/commands/swarm.md +101 -0
  78. package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -0
  79. package/plugins/continuous-improvement/hooks/hooks.json +60 -0
  80. package/plugins/continuous-improvement/hooks/observe.sh +172 -0
  81. package/plugins/continuous-improvement/hooks/session.sh +106 -0
  82. package/plugins/continuous-improvement/hooks/three-section-close.mjs +181 -0
  83. package/plugins/continuous-improvement/instinct-packs/go.json +58 -0
  84. package/plugins/continuous-improvement/instinct-packs/meta.json +16 -0
  85. package/plugins/continuous-improvement/instinct-packs/python.json +58 -0
  86. package/plugins/continuous-improvement/instinct-packs/react.json +58 -0
  87. package/plugins/continuous-improvement/lib/observe-event.mjs +128 -0
  88. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +432 -0
  89. package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +43 -0
  90. package/plugins/continuous-improvement/skills/README.md +34 -0
  91. package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +249 -0
  92. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +131 -0
  93. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +155 -0
  94. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +108 -0
  95. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +454 -0
  96. package/plugins/continuous-improvement/skills/ralph/SKILL.md +221 -0
  97. package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +76 -0
  98. package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +104 -0
  99. package/plugins/continuous-improvement/skills/superpowers/SKILL.md +212 -0
  100. package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -0
  101. package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +136 -0
  102. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +192 -0
  103. package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +191 -0
  104. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +147 -0
  105. package/plugins/continuous-improvement/templates/planning-with-files/findings.md +8 -0
  106. package/plugins/continuous-improvement/templates/planning-with-files/progress.md +7 -0
  107. package/plugins/continuous-improvement/templates/planning-with-files/task_plan.md +23 -0
  108. package/plugins/expert.json +34 -5
  109. package/skills/README.md +79 -0
  110. package/skills/deploy-receipt.md +131 -0
  111. package/skills/gateguard.md +155 -0
  112. package/skills/para-memory-files.md +108 -0
  113. package/skills/proceed-with-the-recommendation.md +454 -0
  114. package/skills/ralph.md +221 -0
  115. package/skills/safety-guard.md +76 -0
  116. package/skills/strategic-compact.md +104 -0
  117. package/skills/superpowers.md +212 -0
  118. package/skills/tdd-workflow.md +411 -0
  119. package/skills/token-budget-advisor.md +136 -0
  120. package/skills/verification-loop.md +192 -0
  121. package/skills/wild-risa-balance.md +191 -0
  122. package/skills/workspace-surface-audit.md +147 -0
  123. package/templates/planning-with-files/findings.md +8 -0
  124. package/templates/planning-with-files/progress.md +7 -0
  125. package/templates/planning-with-files/task_plan.md +23 -0
  126. package/templates/verify-ladder.example.json +47 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Everything Mirror Check
4
+ *
5
+ * Verifies that every non-skill, non-manifest file under
6
+ * plugins/continuous-improvement/ has a byte-identical sibling at the same
7
+ * relative path under the repo root. Catches drift across commands/, hooks/
8
+ * (excluding the plugin-only hooks.json), instinct-packs/, templates/, lib/,
9
+ * bin/, and the LICENSE file — surfaces that check-skill-mirror.mjs does not
10
+ * cover.
11
+ *
12
+ * Skills are intentionally excluded because their flat-vs-plugin layout is
13
+ * structurally different (skills/<name>.md vs plugins/.../skills/<name>/SKILL.md)
14
+ * and check-skill-mirror.mjs already enforces that pair. A few plugin-only
15
+ * surfaces are explicitly allowlisted: the .claude-plugin/ manifests, the
16
+ * generated bundle README.md and skills/README.md, and hooks/hooks.json.
17
+ *
18
+ * Usage:
19
+ * node bin/check-everything-mirror.mjs # Check the current repo
20
+ * node bin/check-everything-mirror.mjs <repo-root> # Check a specific repo root
21
+ *
22
+ * Exit codes:
23
+ * 0 — every mirrored file matches its flat-tree sibling
24
+ * 1 — at least one drift detected (missing flat sibling or content mismatch)
25
+ */
26
+ import { readFileSync, readdirSync, statSync } from "node:fs";
27
+ import { join, relative, sep } from "node:path";
28
+ import { argv, cwd, exit } from "node:process";
29
+ const PLUGIN_ROOT_REL = "plugins/continuous-improvement";
30
+ // Paths under PLUGIN_ROOT_REL that are intentionally plugin-only (no flat
31
+ // sibling expected) or are covered by another lint. Stored with forward
32
+ // slashes; matched against the file's relative-to-plugin-root path normalized
33
+ // the same way.
34
+ const EXCLUDE_FILES = new Set([
35
+ ".claude-plugin/plugin.json",
36
+ ".claude-plugin/marketplace.json",
37
+ "README.md",
38
+ "hooks/hooks.json",
39
+ "skills/README.md",
40
+ ]);
41
+ // Whole directories under PLUGIN_ROOT_REL that this lint never walks into.
42
+ const EXCLUDE_DIRS = new Set(["skills", ".claude-plugin"]);
43
+ function toForward(p) {
44
+ return p.split(sep).join("/");
45
+ }
46
+ export function discoverMirrorPairs(repoRoot) {
47
+ const pluginRoot = join(repoRoot, PLUGIN_ROOT_REL);
48
+ const out = [];
49
+ function walk(dir) {
50
+ let entries;
51
+ try {
52
+ entries = readdirSync(dir);
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ for (const name of entries) {
58
+ const full = join(dir, name);
59
+ let stat;
60
+ try {
61
+ stat = statSync(full);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ const relFromPlugin = toForward(relative(pluginRoot, full));
67
+ if (stat.isDirectory()) {
68
+ if (EXCLUDE_DIRS.has(relFromPlugin))
69
+ continue;
70
+ walk(full);
71
+ continue;
72
+ }
73
+ if (!stat.isFile())
74
+ continue;
75
+ if (EXCLUDE_FILES.has(relFromPlugin))
76
+ continue;
77
+ out.push({
78
+ relPath: relFromPlugin,
79
+ pluginPath: full,
80
+ flatPath: join(repoRoot, relFromPlugin),
81
+ });
82
+ }
83
+ }
84
+ walk(pluginRoot);
85
+ return out.sort((a, b) => a.relPath.localeCompare(b.relPath));
86
+ }
87
+ export function checkMirrorPairs(repoRoot) {
88
+ const pairs = discoverMirrorPairs(repoRoot);
89
+ const drifts = [];
90
+ for (const pair of pairs) {
91
+ let flatContent;
92
+ try {
93
+ flatContent = readFileSync(pair.flatPath);
94
+ }
95
+ catch {
96
+ drifts.push({
97
+ relPath: pair.relPath,
98
+ reason: "missing-flat",
99
+ pluginPath: pair.pluginPath,
100
+ flatPath: pair.flatPath,
101
+ });
102
+ continue;
103
+ }
104
+ const pluginContent = readFileSync(pair.pluginPath);
105
+ if (!pluginContent.equals(flatContent)) {
106
+ drifts.push({
107
+ relPath: pair.relPath,
108
+ reason: "content-drift",
109
+ pluginPath: pair.pluginPath,
110
+ flatPath: pair.flatPath,
111
+ pluginBytes: pluginContent.length,
112
+ flatBytes: flatContent.length,
113
+ });
114
+ }
115
+ }
116
+ return drifts;
117
+ }
118
+ function main() {
119
+ const repoRoot = argv[2] ?? cwd();
120
+ const pairs = discoverMirrorPairs(repoRoot);
121
+ const drifts = checkMirrorPairs(repoRoot);
122
+ if (drifts.length === 0) {
123
+ console.log(`OK everything-mirror: all ${pairs.length} mirrored file(s) match their flat-tree sibling.`);
124
+ exit(0);
125
+ }
126
+ console.error(`FAIL everything-mirror: ${drifts.length} drift(s) detected across ${pairs.length} mirrored file(s).\n`);
127
+ for (const d of drifts) {
128
+ if (d.reason === "missing-flat") {
129
+ console.error(` - ${d.relPath}: plugin copy exists but flat sibling is missing.`);
130
+ console.error(` plugin: ${d.pluginPath}`);
131
+ console.error(` flat: ${d.flatPath} (NOT FOUND)`);
132
+ }
133
+ else {
134
+ console.error(` - ${d.relPath}: byte drift between plugin and flat copy (${d.pluginBytes} vs ${d.flatBytes} bytes).`);
135
+ console.error(` plugin: ${d.pluginPath}`);
136
+ console.error(` flat: ${d.flatPath}`);
137
+ }
138
+ }
139
+ console.error("\nFix: every change to a mirrored file under plugins/continuous-improvement/ must be applied to its flat-tree sibling at the same relative path in the same PR. Plugin-only files (.claude-plugin/manifests, hooks/hooks.json, the bundle README.md, skills/README.md) are allowlisted; skills are covered by check-skill-mirror.mjs.");
140
+ exit(1);
141
+ }
142
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
143
+ if (invokedDirectly || argv[1]?.endsWith("check-everything-mirror.mjs")) {
144
+ main();
145
+ }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Routing-Target Invariant Check
4
+ *
5
+ * Verifies that every routing target named in the orchestrator skill at
6
+ * skills/proceed-with-the-recommendation.md § "Routing Table (with Inline
7
+ * Fallbacks)" either:
8
+ * (a) ships bundled at plugins/continuous-improvement/skills/<name>/SKILL.md, or
9
+ * (b) is declared in the root-level optional-companions.json file.
10
+ *
11
+ * Catches: a routing-table row that names a skill the bundle does not ship and
12
+ * the maintainer has not declared as an optional companion. Without this gate,
13
+ * such drift only surfaces at runtime when the orchestrator routes to a target
14
+ * that resolves to neither a bundled skill nor an inline fallback the
15
+ * maintainer was tracking.
16
+ *
17
+ * Usage:
18
+ * node bin/check-routing-targets.mjs # Check the current repo
19
+ * node bin/check-routing-targets.mjs <repo-root> # Check a specific repo root
20
+ *
21
+ * Exit codes:
22
+ * 0 — every routing target is accounted for (bundled or optional-declared)
23
+ * 1 — at least one routing target is unaccounted for
24
+ */
25
+ import { readdirSync, readFileSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { argv, cwd, exit } from "node:process";
28
+ const ORCHESTRATOR_SKILL_PATH = "skills/proceed-with-the-recommendation.md";
29
+ const OPTIONAL_COMPANIONS_PATH = "optional-companions.json";
30
+ const PLUGIN_SKILLS_DIR = "plugins/continuous-improvement/skills";
31
+ const SECTION_HEADER = "### Routing Table (with Inline Fallbacks)";
32
+ export function discoverBundledSkills(repoRoot) {
33
+ const dir = join(repoRoot, PLUGIN_SKILLS_DIR);
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(dir);
37
+ }
38
+ catch {
39
+ return new Set();
40
+ }
41
+ const names = new Set();
42
+ for (const name of entries) {
43
+ const skillFile = join(dir, name, "SKILL.md");
44
+ try {
45
+ const stat = statSync(skillFile);
46
+ if (stat.isFile())
47
+ names.add(name);
48
+ }
49
+ catch {
50
+ /* not a skill dir */
51
+ }
52
+ }
53
+ return names;
54
+ }
55
+ export function loadOptionalCompanions(repoRoot) {
56
+ const path = join(repoRoot, OPTIONAL_COMPANIONS_PATH);
57
+ const raw = readFileSync(path, "utf8");
58
+ const data = JSON.parse(raw);
59
+ if (!Array.isArray(data.optional_companions)) {
60
+ throw new Error(`${OPTIONAL_COMPANIONS_PATH}: missing or non-array "optional_companions" field`);
61
+ }
62
+ return new Set(data.optional_companions);
63
+ }
64
+ export function extractRoutingTargets(orchestratorMarkdown) {
65
+ const lines = orchestratorMarkdown.split(/\r?\n/);
66
+ const headerIdx = lines.findIndex((line) => line.trim() === SECTION_HEADER);
67
+ if (headerIdx === -1) {
68
+ throw new Error(`Routing-table section header not found: "${SECTION_HEADER}"`);
69
+ }
70
+ const targets = [];
71
+ let inTable = false;
72
+ let sawHeaderRow = false;
73
+ let sawDividerRow = false;
74
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
75
+ const line = lines[i];
76
+ const trimmed = line.trim();
77
+ if (trimmed.startsWith("## ") || trimmed.startsWith("### "))
78
+ break;
79
+ if (!trimmed.startsWith("|")) {
80
+ if (inTable)
81
+ break;
82
+ continue;
83
+ }
84
+ if (!sawHeaderRow) {
85
+ sawHeaderRow = true;
86
+ inTable = true;
87
+ continue;
88
+ }
89
+ if (!sawDividerRow) {
90
+ sawDividerRow = true;
91
+ continue;
92
+ }
93
+ const cells = trimmed.split("|").map((c) => c.trim());
94
+ // cells[0] and cells[last] are empty strings from leading/trailing pipes.
95
+ // cells[1] = "Recommendation type", cells[2] = "Preferred skill", cells[3] = "Inline fallback".
96
+ const preferredCell = cells[2] ?? "";
97
+ const tokens = [...preferredCell.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
98
+ for (const token of tokens) {
99
+ targets.push({
100
+ rowIndex: targets.length,
101
+ recommendationType: cells[1] ?? "",
102
+ target: token,
103
+ });
104
+ }
105
+ }
106
+ return targets;
107
+ }
108
+ export function checkRoutingTargets(repoRoot) {
109
+ const orchestratorPath = join(repoRoot, ORCHESTRATOR_SKILL_PATH);
110
+ const orchestratorContent = readFileSync(orchestratorPath, "utf8");
111
+ const targets = extractRoutingTargets(orchestratorContent);
112
+ const bundled = discoverBundledSkills(repoRoot);
113
+ const optional = loadOptionalCompanions(repoRoot);
114
+ const drifts = [];
115
+ for (const t of targets) {
116
+ if (bundled.has(t.target))
117
+ continue;
118
+ if (optional.has(t.target))
119
+ continue;
120
+ drifts.push(t);
121
+ }
122
+ return {
123
+ targets,
124
+ drifts,
125
+ bundledCount: bundled.size,
126
+ optionalCount: optional.size,
127
+ };
128
+ }
129
+ function main() {
130
+ const repoRoot = argv[2] ?? cwd();
131
+ const { targets, drifts, bundledCount, optionalCount } = checkRoutingTargets(repoRoot);
132
+ if (drifts.length === 0) {
133
+ console.log(`OK routing-targets: all ${targets.length} routing target(s) accounted for ` +
134
+ `(${bundledCount} bundled skill(s), ${optionalCount} optional companion(s) declared).`);
135
+ exit(0);
136
+ }
137
+ console.error(`FAIL routing-targets: ${drifts.length} unaccounted target(s) in ${ORCHESTRATOR_SKILL_PATH}.\n`);
138
+ for (const d of drifts) {
139
+ console.error(` - "${d.target}" — referenced by row "${d.recommendationType}"`);
140
+ console.error(` Not bundled at ${PLUGIN_SKILLS_DIR}/${d.target}/SKILL.md`);
141
+ console.error(` Not declared in ${OPTIONAL_COMPANIONS_PATH}`);
142
+ }
143
+ console.error(`\nFix: either bundle the skill (add plugins/continuous-improvement/skills/<name>/SKILL.md ` +
144
+ `via the source skill + 'npm run build'), OR declare it in ${OPTIONAL_COMPANIONS_PATH} ` +
145
+ `with a pointer note in docs/audits/.`);
146
+ exit(1);
147
+ }
148
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
149
+ if (invokedDirectly || argv[1]?.endsWith("check-routing-targets.mjs")) {
150
+ main();
151
+ }
@@ -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
+ }