chill-agent 1.5.0 → 1.6.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 (187) hide show
  1. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  2. package/.claude-plugin/plugin.json +8 -3
  3. package/.cline-plugin/plugin.js +6 -0
  4. package/.codex-plugin/plugin.json +4 -3
  5. package/.cursor-plugin/marketplace.json +16 -0
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.devin-plugin/plugin.json +2 -2
  8. package/.kimi-plugin/plugin.json +11 -29
  9. package/.omp-plugin/marketplace.json +15 -0
  10. package/.openclaw/skills/ponytail/SKILL.md +2 -7
  11. package/.openclaw/skills/ponytail-audit/SKILL.md +3 -6
  12. package/.openclaw/skills/ponytail-debt/SKILL.md +3 -4
  13. package/.openclaw/skills/ponytail-gain/SKILL.md +3 -4
  14. package/.openclaw/skills/ponytail-help/SKILL.md +3 -3
  15. package/.openclaw/skills/ponytail-review/SKILL.md +3 -6
  16. package/.opencode/plugins/ponytail.mjs +1 -1
  17. package/.qoder-plugin/plugin.json +12 -26
  18. package/AGENTS.md +32 -19
  19. package/__init__.py +1 -1
  20. package/assets/antislop-banner.png +0 -0
  21. package/assets/band/band.png +0 -0
  22. package/assets/band/board.png +0 -0
  23. package/assets/benchmark-3model.svg +21 -0
  24. package/assets/benchmark-agentic.svg +62 -0
  25. package/assets/codex-icon.png +0 -0
  26. package/assets/compare/code/after.webp +0 -0
  27. package/assets/compare/code/before.webp +0 -0
  28. package/assets/compare/text/after.webp +0 -0
  29. package/assets/compare/text/before.webp +0 -0
  30. package/assets/compare/ui/w-all.webp +0 -0
  31. package/assets/compare/ui/w-antislop.webp +0 -0
  32. package/assets/compare/ui/w-design.webp +0 -0
  33. package/assets/compare/ui/without.webp +0 -0
  34. package/assets/logo-dark.png +0 -0
  35. package/assets/logo-dark.svg +115 -0
  36. package/assets/logo-greenpt-dark.svg +27 -0
  37. package/assets/logo-greenpt.svg +27 -0
  38. package/assets/logo.png +0 -0
  39. package/assets/photoreunion-logo-dark.svg +4 -0
  40. package/assets/photoreunion-logo-light.svg +4 -0
  41. package/assets/profile.png +0 -0
  42. package/assets/retriever-logo-dark.svg +4 -0
  43. package/assets/retriever-logo-light.svg +4 -0
  44. package/assets/stories-banner.png +0 -0
  45. package/assets/v5/audit-es.png +0 -0
  46. package/assets/v5/audit-ja.png +0 -0
  47. package/assets/v5/audit-ko.png +0 -0
  48. package/assets/v5/audit-zh-CN.png +0 -0
  49. package/assets/v5/audit.png +0 -0
  50. package/assets/v5/beforeafter-es.png +0 -0
  51. package/assets/v5/beforeafter-ja.png +0 -0
  52. package/assets/v5/beforeafter-ko.png +0 -0
  53. package/assets/v5/beforeafter-zh-CN.png +0 -0
  54. package/assets/v5/beforeafter.png +0 -0
  55. package/assets/v5/chart-es.png +0 -0
  56. package/assets/v5/chart-ja.png +0 -0
  57. package/assets/v5/chart-ko.png +0 -0
  58. package/assets/v5/chart-zh-CN.png +0 -0
  59. package/assets/v5/chart.png +0 -0
  60. package/assets/v5/hero-es.jpg +0 -0
  61. package/assets/v5/hero-ja.jpg +0 -0
  62. package/assets/v5/hero-ko.jpg +0 -0
  63. package/assets/v5/hero-zh-CN.jpg +0 -0
  64. package/assets/v5/hero.jpg +0 -0
  65. package/assets/v5/ladder-es.png +0 -0
  66. package/assets/v5/ladder-ja.png +0 -0
  67. package/assets/v5/ladder-ko.png +0 -0
  68. package/assets/v5/ladder-zh-CN.png +0 -0
  69. package/assets/v5/ladder.png +0 -0
  70. package/assets/v5/review-es.png +0 -0
  71. package/assets/v5/review-ja.png +0 -0
  72. package/assets/v5/review-ko.png +0 -0
  73. package/assets/v5/review-zh-CN.png +0 -0
  74. package/assets/v5/review.png +0 -0
  75. package/assets/v5/tests-es.png +0 -0
  76. package/assets/v5/tests-ja.png +0 -0
  77. package/assets/v5/tests-ko.png +0 -0
  78. package/assets/v5/tests-zh-CN.png +0 -0
  79. package/assets/v5/tests.png +0 -0
  80. package/assets/waitlist-banner-es.png +0 -0
  81. package/assets/waitlist-banner-ja.png +0 -0
  82. package/assets/waitlist-banner-ko.png +0 -0
  83. package/assets/waitlist-banner-zh-CN.png +0 -0
  84. package/assets/waitlist-banner.png +0 -0
  85. package/benchmarks/.env.example +2 -0
  86. package/benchmarks/agentic/README.md +201 -0
  87. package/benchmarks/agentic/answer.py +288 -0
  88. package/benchmarks/agentic/compare.py +93 -0
  89. package/benchmarks/agentic/complete.py +178 -0
  90. package/benchmarks/agentic/judge.py +198 -0
  91. package/benchmarks/agentic/run.py +543 -0
  92. package/benchmarks/agentic/tasks.py +1544 -0
  93. package/benchmarks/arms/baseline.js +2 -0
  94. package/benchmarks/arms/caveman-SKILL.md +67 -0
  95. package/benchmarks/arms/caveman.js +8 -0
  96. package/benchmarks/arms/ponytail.js +8 -0
  97. package/benchmarks/behavior.js +54 -0
  98. package/benchmarks/behavior.yaml +40 -0
  99. package/benchmarks/correctness.js +291 -0
  100. package/benchmarks/loc.js +17 -19
  101. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  102. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  103. package/benchmarks/promptfooconfig.yaml +41 -0
  104. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  105. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  106. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  107. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  108. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  109. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  110. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  111. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  112. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  113. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  114. package/cli/index.mjs +241 -0
  115. package/cli/lib/banner.mjs +21 -0
  116. package/cli/lib/install.mjs +498 -0
  117. package/cli/lib/settings.mjs +48 -0
  118. package/cli/package-lock.json +87 -0
  119. package/cli/package.json +42 -0
  120. package/cli/scripts/smoke-test.mjs +95 -0
  121. package/cli/scripts/smoke-worker.mjs +411 -0
  122. package/cli/scripts/sync-skills.mjs +81 -0
  123. package/examples/email-validation.md +156 -0
  124. package/examples/group-by.md +35 -0
  125. package/examples/infinite-scroll.md +58 -0
  126. package/examples/number-formatting.md +37 -0
  127. package/examples/rate-limit.md +272 -0
  128. package/examples/react-countdown.md +390 -0
  129. package/examples/url-params.md +41 -0
  130. package/gemini-extension.json +2 -2
  131. package/hooks/band/board.ts +560 -0
  132. package/hooks/band/logo.ts +3 -0
  133. package/hooks/band/metrics.ts +311 -0
  134. package/hooks/band/register.tsx +495 -0
  135. package/hooks/band/types.d.ts +34 -0
  136. package/hooks/claude-band.json +6 -0
  137. package/hooks/claude-codex-hooks.json +34 -4
  138. package/hooks/copilot-hooks.json +12 -2
  139. package/hooks/cursor-hooks.json +2 -2
  140. package/hooks/ponytail-band-import.js +274 -0
  141. package/hooks/ponytail-config.js +175 -0
  142. package/hooks/ponytail-map.js +111 -0
  143. package/hooks/ponytail-subagent.js +94 -0
  144. package/hooks/qoder-hooks.json +19 -4
  145. package/i18n/README.ko.md +230 -0
  146. package/package.json +12 -7
  147. package/pi-extension/test/extension.test.js +367 -0
  148. package/pi-extension/test/helpers.test.js +179 -0
  149. package/plugin.yaml +21 -12
  150. package/rules/antislop.mdc +18 -0
  151. package/scripts/build-openclaw-skills.js +60 -0
  152. package/scripts/check-repo.mjs +389 -0
  153. package/scripts/publish-openclaw-skills.js +75 -0
  154. package/scripts/skill-index.mjs +81 -0
  155. package/skills/antislop/SKILL.md +700 -0
  156. package/skills/antislop/VERSION +1 -0
  157. package/skills/antislop-code/SKILL.md +128 -0
  158. package/skills/antislop-copywriting/SKILL.md +407 -0
  159. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  160. package/skills/antislop-human/SKILL.md +204 -0
  161. package/skills/antislop-human/contrast-check.py +168 -0
  162. package/skills/antislop-human/contrast-mcp.py +121 -0
  163. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  164. package/skills/antislop-human/references/WCAG.md +127 -0
  165. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  166. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  167. package/skills/antislop-ui/SKILL.md +344 -0
  168. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  169. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  170. package/skills/plugin.json +5 -0
  171. package/src/package.json +3 -0
  172. package/tests/activation-map.test.js +63 -0
  173. package/tests/band-import.test.js +111 -0
  174. package/tests/band.test.tsx +520 -0
  175. package/tests/behavior.test.js +80 -0
  176. package/tests/gemini-extension.test.js +2 -2
  177. package/tests/grok-plugin.test.js +1 -1
  178. package/tests/hermes-plugin.test.js +1 -1
  179. package/tests/hooks-windows.test.js +205 -0
  180. package/tests/kimi-plugin.test.js +1 -1
  181. package/tests/map.test.js +30 -0
  182. package/tests/package-scripts.test.js +35 -0
  183. package/tests/package.test.js +23 -0
  184. package/tests/uninstall.test.js +202 -0
  185. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  186. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  187. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The rules antislop applies to other people's repos, applied to this one.
4
+ * Run it before opening a PR:
5
+ *
6
+ * node scripts/check-repo.mjs
7
+ */
8
+ import fs from 'node:fs'
9
+ import path from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { buildIndex, START, END } from './skill-index.mjs'
12
+
13
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
14
+ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8')
15
+ // Split on /\r?\n/, not '\n': on a Windows checkout a trailing \r survives and
16
+ // every ^...$ regex below silently stops matching.
17
+ const lines = (p) => read(p).split(/\r?\n/)
18
+
19
+ const SKILLS = fs
20
+ .readdirSync(path.join(root, 'skills'), { withFileTypes: true })
21
+ .filter((e) => e.isDirectory())
22
+ .map((e) => `skills/${e.name}/SKILL.md`)
23
+
24
+ const MANIFESTS = [
25
+ 'package.json',
26
+ '.claude-plugin/plugin.json',
27
+ '.claude-plugin/marketplace.json',
28
+ '.codex-plugin/plugin.json',
29
+ '.cursor-plugin/plugin.json',
30
+ '.cursor-plugin/marketplace.json',
31
+ '.kimi-plugin/plugin.json',
32
+ '.agents/plugins/marketplace.json',
33
+ '.omp-plugin/marketplace.json',
34
+ 'plugin.json',
35
+ 'cli/package.json',
36
+ ]
37
+
38
+ /** The community files GitHub reads. Held to the rules the documents above already follow. */
39
+ const COMMUNITY = [
40
+ 'CONTRIBUTING.md',
41
+ 'CODE_OF_CONDUCT.md',
42
+ '.github/PULL_REQUEST_TEMPLATE.md',
43
+ '.github/ISSUE_TEMPLATE/bug_report.yml',
44
+ '.github/ISSUE_TEMPLATE/rule_gap.yml',
45
+ '.github/ISSUE_TEMPLATE/config.yml',
46
+ ]
47
+
48
+ /** R-02's carve-out, encoded so a new em dash in ordinary prose still fails. */
49
+ function emDashes() {
50
+ const bad = []
51
+ // The rules/ pointers ship to plugin users, so the ban covers them like any other doc.
52
+ const files = [
53
+ 'antislop.md',
54
+ 'README.md',
55
+ 'GUIDE.md',
56
+ 'ROADMAP.md',
57
+ 'SECURITY.md',
58
+ 'rules/antislop.md',
59
+ 'rules/antislop.mdc',
60
+ ...COMMUNITY,
61
+ ...SKILLS,
62
+ ]
63
+
64
+ for (const file of files) {
65
+ let section = ''
66
+ lines(file).forEach((line, i) => {
67
+ const heading = line.match(/^#{2,4}\s+(.*)$/)
68
+ if (heading) section = heading[1]
69
+ if (!/[—–]/.test(line)) return
70
+
71
+ const exempt =
72
+ // "#### R-02 — Copywriting" and "### C-1 — Intentionality"
73
+ /^#{2,4}\s+(R-\d{2}|C-\d)\s+—\s/.test(line) ||
74
+ // The rule that defines the ban, and the skill section that teaches it.
75
+ /^(R-02|Em Dashes)\b/.test(section) ||
76
+ // The Part 1 table row that shows the pattern.
77
+ /^\|\s*\*\*Em Dash/.test(line) ||
78
+ // The Delivery Gate item that quotes the character it checks for.
79
+ (/^\s*- \[ \]/.test(line) && /\*\(R-02\)\*/.test(line))
80
+
81
+ if (!exempt) bad.push(`${file}:${i + 1}: ${line.trim().slice(0, 90)}`)
82
+ })
83
+ }
84
+ return bad
85
+ }
86
+
87
+ /** Every rule the core defines must have a Delivery Gate item citing it, and vice versa. */
88
+ function gateCoverage() {
89
+ const core = read('antislop.md')
90
+ const defined = new Set([...core.matchAll(/^#### (R-\d{2}) —/gm)].map((m) => m[1]))
91
+ const gate = core.split('## Delivery Gate (Mandatory)')[1] ?? ''
92
+ const cited = new Set([...gate.matchAll(/\((R-\d{2})\)/g)].map((m) => m[1]))
93
+
94
+ const bad = []
95
+ for (const r of [...defined].sort()) {
96
+ if (!cited.has(r)) bad.push(`${r} is defined in Part 2 but no Delivery Gate item cites it`)
97
+ }
98
+ for (const r of [...cited].sort()) {
99
+ if (!defined.has(r)) bad.push(`${r} is cited in the Delivery Gate but Part 2 does not define it`)
100
+ }
101
+ return bad
102
+ }
103
+
104
+ /** A skill may reference core rules by number. It may not invent one. */
105
+ function skillReferences() {
106
+ const defined = new Set([...read('antislop.md').matchAll(/^#### (R-\d{2}) —/gm)].map((m) => m[1]))
107
+ const bad = []
108
+ for (const file of SKILLS) {
109
+ for (const r of new Set([...read(file).matchAll(/\bR-\d{2}\b/g)].map((m) => m[0]))) {
110
+ if (!defined.has(r)) bad.push(`${file} cites ${r}, which antislop.md does not define`)
111
+ }
112
+ }
113
+ return bad
114
+ }
115
+
116
+ /** The version is hand-written in every manifest and stamp. They have to agree. */
117
+ function versions() {
118
+ // A malformed file is already reported by the manifest check; do not crash here.
119
+ const json = (p) => {
120
+ try {
121
+ return JSON.parse(read(p))
122
+ } catch {
123
+ return null
124
+ }
125
+ }
126
+ const want = json('cli/package.json')?.version
127
+ if (!want) return ['cli/package.json has no readable version']
128
+
129
+ const found = [
130
+ // The lock file carries the version twice, and npm ci will not correct a stale one.
131
+ ['cli/package-lock.json', json('cli/package-lock.json')?.version],
132
+ ['cli/package-lock.json (packages "")', json('cli/package-lock.json')?.packages?.['']?.version],
133
+ // The Cline plugin manifest and, from v3.2.17, the Pi package. Same release.
134
+ ['package.json', json('package.json')?.version],
135
+ ['.claude-plugin/plugin.json', json('.claude-plugin/plugin.json')?.version],
136
+ ['.claude-plugin/marketplace.json', json('.claude-plugin/marketplace.json')?.plugins?.[0]?.version],
137
+ ['.codex-plugin/plugin.json', json('.codex-plugin/plugin.json')?.version],
138
+ ['.cursor-plugin/plugin.json', json('.cursor-plugin/plugin.json')?.version],
139
+ ['.omp-plugin/marketplace.json', json('.omp-plugin/marketplace.json')?.plugins?.[0]?.version],
140
+ ['.kimi-plugin/plugin.json', json('.kimi-plugin/plugin.json')?.version],
141
+ // Stamped into every install, so a stale one makes the installer misreport what is on disk.
142
+ ['skills/antislop/VERSION', read('skills/antislop/VERSION').trim()],
143
+ ['cli/lib/banner.mjs', read('cli/lib/banner.mjs').match(/installer v(\d+\.\d+\.\d+)/)?.[1]],
144
+ ['skills/antislop-human/contrast-mcp.py', read('skills/antislop-human/contrast-mcp.py').match(/SERVER_VERSION = "(\d+\.\d+\.\d+)"/)?.[1]],
145
+ // The contributors image is cached per URL, so a changed version is what makes it redraw.
146
+ ['README.md (contributors image)', read('README.md').match(/contrib\.rocks\/image\?repo=[^"'&\s]+&v=(\d+\.\d+\.\d+)/)?.[1]],
147
+ ]
148
+
149
+ return found
150
+ .filter(([, got]) => got !== want)
151
+ .map(([file, got]) => `${file} says ${got ?? 'nothing'}, cli/package.json says ${want}`)
152
+ }
153
+
154
+ /** A manifest that does not parse takes its whole install path down. */
155
+ function manifests() {
156
+ const bad = []
157
+ for (const file of MANIFESTS) {
158
+ try {
159
+ JSON.parse(read(file))
160
+ } catch (err) {
161
+ bad.push(`${file} is not valid JSON: ${err.message}`)
162
+ }
163
+ }
164
+ return bad
165
+ }
166
+
167
+ /** An agent finds a skill by its frontmatter. Without it the folder is inert. */
168
+ function frontmatter() {
169
+ const bad = []
170
+ for (const file of SKILLS) {
171
+ const head = read(file).split('---')[1] ?? ''
172
+ // [ \t] not \s: \s spans newlines, so an empty value would match the next key.
173
+ if (!/^name:[ \t]*\S/m.test(head)) bad.push(`${file} has no name in its frontmatter`)
174
+ if (!/^description:[ \t]*\S/m.test(head)) bad.push(`${file} has no description in its frontmatter`)
175
+ }
176
+ return bad
177
+ }
178
+
179
+ /** A new skill has to be registered in all four places below or it cannot install. */
180
+ function registration() {
181
+ const names = SKILLS.map((f) => f.split('/')[1])
182
+ const core = 'antislop'
183
+ const places = [
184
+ ['rules/antislop.md', read('rules/antislop.md')],
185
+ ['rules/antislop.mdc', read('rules/antislop.mdc')],
186
+ ["cli/index.mjs (EXTRA_SKILLS)", read('cli/index.mjs')],
187
+ ['cli/lib/install.mjs (SKILL_LINES)', read('cli/lib/install.mjs')],
188
+ ]
189
+
190
+ const bad = []
191
+ for (const name of names) {
192
+ for (const [label, text] of places) {
193
+ // The core is always on, so the picker's optional-extras menu never lists it.
194
+ if (name === core && label.startsWith('cli/index.mjs')) continue
195
+ if (!text.includes(name)) bad.push(`${name} is not registered in ${label}`)
196
+ }
197
+ }
198
+ return bad
199
+ }
200
+
201
+ /** A rule's tier decides its gate block: Hard Gate 1, Purpose-Gate 2, Quality Locks 4. */
202
+ function tiers() {
203
+ const core = read('antislop.md')
204
+ const part2 = core.split('## Part 2:')[1]?.split('## Part 3:')[0] ?? ''
205
+
206
+ const group = {}
207
+ let current = null
208
+ for (const line of part2.split('\n')) {
209
+ const g = line.match(/^### Group \d: ([^(]+)/)
210
+ if (g) current = g[1].trim()
211
+ const r = line.match(/^#### (R-\d{2}) —/)
212
+ if (r && current) group[r[1]] = current
213
+ }
214
+
215
+ const block = {}
216
+ let currentBlock = null
217
+ for (const line of (core.split('## Delivery Gate (Mandatory)')[1] ?? '').split('\n')) {
218
+ const b = line.match(/^### (Block \d):/)
219
+ if (b) currentBlock = b[1]
220
+ for (const m of line.matchAll(/\((R-\d{2})\)/g)) {
221
+ if (!block[m[1]]) block[m[1]] = currentBlock
222
+ }
223
+ }
224
+
225
+ const wants = { 'Hard Gate': 'Block 1', 'Purpose-Gate': 'Block 2', 'Quality Locks': 'Block 4' }
226
+ const bad = []
227
+ for (const [rule, grp] of Object.entries(group).sort()) {
228
+ const want = wants[grp]
229
+ const got = block[rule]
230
+ if (want && got && want !== got) bad.push(`${rule} is a ${grp} rule but the gate cites it from ${got}`)
231
+ }
232
+ return bad
233
+ }
234
+
235
+ /** A manifest that points at a file that is gone breaks its install path quietly. */
236
+ function manifestPaths() {
237
+ const bad = []
238
+ for (const file of MANIFESTS) {
239
+ let text
240
+ try {
241
+ text = read(file)
242
+ } catch {
243
+ continue
244
+ }
245
+ for (const m of text.matchAll(/"(\.\/[^"]+)"/g)) {
246
+ const target = m[1].replace(/^\.\//, '')
247
+ if (!existsExactly(target)) bad.push(`${file} points at ${m[1]}, which does not exist`)
248
+ }
249
+ // A plugin-root path is absolute only once the agent expands it, so check the tail.
250
+ for (const m of text.matchAll(/"\$\{CLAUDE_PLUGIN_ROOT\}\/([^"]+)"/g)) {
251
+ if (!existsExactly(m[1])) bad.push(`${file} points at \${CLAUDE_PLUGIN_ROOT}/${m[1]}, which does not exist`)
252
+ }
253
+ }
254
+ return bad
255
+ }
256
+
257
+ /** existsSync misses a case-only rename on macOS; compare the real dir entry. */
258
+ const existsExactly = (target) => {
259
+ const full = path.join(root, target)
260
+ const dir = path.dirname(full)
261
+ if (!fs.existsSync(dir)) return false
262
+ return fs.readdirSync(dir).includes(path.basename(full))
263
+ }
264
+
265
+ /** GitHub's anchor rule: lowercase, drop punctuation, spaces to hyphens, duplicates numbered. */
266
+ function anchorsIn(text) {
267
+ const taken = new Map()
268
+ const out = new Set()
269
+ for (const line of text.split(/\r?\n/)) {
270
+ const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/)
271
+ if (!m) continue
272
+ const base = m[1].toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, '').trim().replace(/\s+/g, '-')
273
+ const seen = taken.get(base) ?? 0
274
+ taken.set(base, seen + 1)
275
+ out.add(seen === 0 ? base : `${base}-${seen}`)
276
+ }
277
+ return out
278
+ }
279
+
280
+ function docLinks() {
281
+ const bad = []
282
+ for (const file of ['README.md', 'GUIDE.md', 'ROADMAP.md', 'SECURITY.md', ...COMMUNITY]) {
283
+ // Markdown links, plus the raw HTML images and badges the README is built from.
284
+ for (const m of read(file).matchAll(/\]\(([^)\s]+)\)|(?:src|href)="([^"]+)"/g)) {
285
+ const [target, anchor] = (m[1] ?? m[2]).split('#')
286
+ if (/^(https?:|mailto:)/.test(target)) continue
287
+ // Every table of contents is a list of these, and a renamed heading breaks one silently.
288
+ if (!target) {
289
+ if (anchor && !anchorsIn(read(file)).has(anchor)) {
290
+ bad.push(`${file} links to #${anchor}, which is not a heading in it`)
291
+ }
292
+ continue
293
+ }
294
+ if (!existsExactly(target)) {
295
+ bad.push(`${file} links to ${target}, which does not exist`)
296
+ continue
297
+ }
298
+ if (anchor && target.endsWith('.md') && !anchorsIn(read(target)).has(anchor)) {
299
+ bad.push(`${file} links to ${target}#${anchor}, which is not a heading in it`)
300
+ }
301
+ }
302
+ }
303
+ return bad
304
+ }
305
+
306
+ /** A YAML file that routes people by URL rots the same way a markdown link does. */
307
+ function repoUrls() {
308
+ const bad = []
309
+ const prefix = 'https://github.com/miqdadbadjuber/anti-slop/blob/main/'
310
+ for (const file of COMMUNITY) {
311
+ for (const m of read(file).matchAll(/url:\s*(\S+)/g)) {
312
+ // A URL that leaves this repository is not this check's to resolve.
313
+ if (!m[1].startsWith(prefix)) continue
314
+ const target = m[1].slice(prefix.length)
315
+ if (!existsExactly(target)) bad.push(`${file} points at ${target}, which does not exist`)
316
+ }
317
+ }
318
+ return bad
319
+ }
320
+
321
+ /** The WCAG formula is duplicated, and only contrast-check.py has a selftest. */
322
+ function contrastTwins() {
323
+ const check = read('skills/antislop-human/contrast-check.py')
324
+ const mcp = read('skills/antislop-human/contrast-mcp.py')
325
+ const constants = ['0.03928', '12.92', '1.055', '0.2126', '0.7152', '0.0722', '0.05']
326
+
327
+ const bad = []
328
+ for (const c of constants) {
329
+ if (check.includes(c) !== mcp.includes(c)) {
330
+ bad.push(`the WCAG constant ${c} appears in only one of contrast-check.py and contrast-mcp.py`)
331
+ }
332
+ }
333
+ return bad
334
+ }
335
+
336
+ /** The guide carries a generated catalogue, so a skill that adds a pattern cannot leave it behind. */
337
+ function skillIndex() {
338
+ const guide = read('GUIDE.md').replace(/\r\n/g, '\n')
339
+ const from = guide.indexOf(START)
340
+ const to = guide.indexOf(END)
341
+ if (from === -1 || to === -1) return [`GUIDE.md has no ${START} ... ${END} pair`]
342
+
343
+ let generated
344
+ try {
345
+ generated = buildIndex()
346
+ } catch (e) {
347
+ return [e.message]
348
+ }
349
+ const shipped = guide.slice(from + START.length, to).replace(/^\n+|\n+$/g, '')
350
+ if (shipped !== generated.replace(/\n+$/, '')) {
351
+ return ['the skill catalogue in GUIDE.md is stale: run node scripts/skill-index.mjs']
352
+ }
353
+ return []
354
+ }
355
+
356
+ const CHECKS = [
357
+ // Manifests first: the later checks read them as data.
358
+ ['every manifest is valid JSON', manifests],
359
+ ['no em dash outside the R-02 carve-out', emDashes],
360
+ ['every rule has a Delivery Gate item', gateCoverage],
361
+ ['skills cite only rules that exist', skillReferences],
362
+ ['the version agrees everywhere', versions],
363
+ ['every skill has usable frontmatter', frontmatter],
364
+ ['every skill is registered everywhere', registration],
365
+ ['each rule sits in the gate block its tier implies', tiers],
366
+ ['every manifest path resolves', manifestPaths],
367
+ ['every doc link resolves', docLinks],
368
+ ['every URL in a community file resolves', repoUrls],
369
+ ['both contrast implementations use the same formula', contrastTwins],
370
+ ['the skill index in the guide is current', skillIndex],
371
+ ]
372
+
373
+ let failed = 0
374
+ for (const [label, run] of CHECKS) {
375
+ const problems = run()
376
+ if (problems.length === 0) {
377
+ console.log(`ok ${label}`)
378
+ continue
379
+ }
380
+ failed += 1
381
+ console.log(`FAIL ${label}`)
382
+ for (const p of problems) console.log(` ${p}`)
383
+ }
384
+
385
+ if (failed > 0) {
386
+ console.error(`\n${failed} of ${CHECKS.length} checks failed`)
387
+ process.exit(1)
388
+ }
389
+ console.log(`\nall ${CHECKS.length} checks passed`)
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Publish the generated OpenClaw skills (.openclaw/skills/) to ClawHub.
3
+ //
4
+ // ClawHub does not sync from GitHub: each skill is pushed explicitly with the
5
+ // clawhub CLI and carries its own version. This publishes every generated skill
6
+ // in one pass, versioned from the repo's package.json so ClawHub tracks the repo
7
+ // instead of drifting (the same drift that hit the plugin manifests in #260).
8
+ //
9
+ // Prereqs:
10
+ // - `clawhub login` once (registry auth persists)
11
+ // - skills must be current: run `node scripts/build-openclaw-skills.js` first
12
+ // if you changed a skill (CI fails if the committed copies are stale)
13
+ //
14
+ // Usage:
15
+ // node scripts/publish-openclaw-skills.js # publish all as latest
16
+ // node scripts/publish-openclaw-skills.js --dry-run # preview, upload nothing
17
+ // (any extra args are passed through to `clawhub skill publish`)
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+
23
+ const root = path.join(__dirname, '..');
24
+ const skillsDir = path.join(root, '.openclaw', 'skills');
25
+
26
+ const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
27
+
28
+ // Every generated skill dir with a SKILL.md is publishable. Reading the dir
29
+ // (instead of a hardcoded list) covers whatever build-openclaw-skills emits,
30
+ // with nothing to keep in sync.
31
+ const slugs = fs.readdirSync(skillsDir, { withFileTypes: true })
32
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
33
+ .map((e) => e.name)
34
+ .sort();
35
+
36
+ if (slugs.length === 0) {
37
+ console.error(`No skills under ${path.relative(root, skillsDir)}; run build-openclaw-skills.js first.`);
38
+ process.exit(1);
39
+ }
40
+
41
+ // "ponytail-review" -> "Ponytail Review"
42
+ const displayName = (slug) =>
43
+ slug.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
44
+
45
+ // Minimal quoting that satisfies both POSIX sh and cmd.exe: only display names
46
+ // (which contain a space) need wrapping; slugs, versions, paths, and flags don't.
47
+ const quote = (a) => (/[^\w./-]/.test(a) ? `"${a}"` : a);
48
+
49
+ const passthrough = process.argv.slice(2);
50
+ const extra = passthrough.length ? ` (${passthrough.join(' ')})` : '';
51
+ console.log(`Publishing ${slugs.length} skills to ClawHub at version ${version}${extra}:`);
52
+
53
+ for (const slug of slugs) {
54
+ const args = [
55
+ 'clawhub', 'skill', 'publish', `.openclaw/skills/${slug}`,
56
+ '--slug', slug,
57
+ '--name', displayName(slug),
58
+ '--version', version,
59
+ '--tags', 'latest',
60
+ ...passthrough,
61
+ ];
62
+ const cmdline = args.map(quote).join(' ');
63
+ console.log(`\n$ ${cmdline}`);
64
+ const res = spawnSync(cmdline, { stdio: 'inherit', cwd: root, shell: true });
65
+ if (res.status !== 0) {
66
+ console.error(
67
+ `\nPublish failed for "${slug}" (exit ${res.status}). ` +
68
+ `Check that the clawhub CLI is installed and you have run \`clawhub login\`, then re-run. ` +
69
+ `Skills already published in this run are unaffected.`,
70
+ );
71
+ process.exit(res.status || 1);
72
+ }
73
+ }
74
+
75
+ console.log(`\nDone. Published ${slugs.length} skills at ${version}.`);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Builds the per-skill catalogue that GUIDE.md carries, from the skills themselves.
4
+ *
5
+ * node scripts/skill-index.mjs
6
+ */
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
12
+
13
+ export const START = '<!-- antislop:skills:start -->'
14
+ export const END = '<!-- antislop:skills:end -->'
15
+
16
+ // One plain-English line per skill, and the order the guide lists them in. The frontmatter
17
+ // description is written for an agent to match on, not for a person to read.
18
+ const NOTES = {
19
+ antislop:
20
+ 'The core, and it is always loaded. All 40 rules and the Delivery Gate live here, so every task gets them whether or not you pick anything else. The five skills below add depth to it.',
21
+ 'antislop-ui':
22
+ 'Load it when the AI builds or restyles anything you can see: color, layout, cards, buttons, motion. Sized media and load priority are built in.',
23
+ 'antislop-copywriting':
24
+ 'Load it when the AI writes or edits text: headlines, buttons, emails, or a page of prose. It removes the excited, empty voice and the made-up numbers.',
25
+ 'antislop-human':
26
+ 'Load it when real people have to use the result. Readable colors, keyboard use, visible focus, button states, the pointer cursor and other interaction affordances, WCAG 2.2 references, and the contrast checker.',
27
+ 'antislop-layoutmobile':
28
+ 'Load it when the same page has to work on a phone and on a desktop. Reflow, breakpoints, overflow, tap targets, mobile navigation.',
29
+ 'antislop-code':
30
+ 'Load it when the AI writes or reviews code comments. It removes the ones an AI adds out of habit and keeps the ones that carry information.',
31
+ }
32
+
33
+ // A pattern entry opens with one of these labels; every other heading under a skill is prose.
34
+ const ENTRY = /^\s*-\s*\*\*(?:Tell|The pattern):\*\*/m
35
+ // A fenced example can hold a '### ' line of its own, so read with the fences taken out.
36
+ const unfenced = (text) => text.replace(/^[ \t]*(?:```|~~~)[\s\S]*?^[ \t]*(?:```|~~~)[ \t]*$/gm, '')
37
+
38
+ const blocksOf = (body, level) => body.split(new RegExp(`^${level} `, 'm')).slice(1)
39
+
40
+ const HEAD = '| What it covers | The patterns it names |\n|----------------|-----------------------|'
41
+
42
+ export function buildIndex() {
43
+ const names = fs
44
+ .readdirSync(path.join(root, 'skills'), { withFileTypes: true })
45
+ .filter((e) => e.isDirectory())
46
+ .map((e) => e.name)
47
+
48
+ const unlisted = names.filter((n) => !NOTES[n])
49
+ if (unlisted.length) throw new Error(`scripts/skill-index.mjs has no intro for: ${unlisted.join(', ')}`)
50
+ const gone = Object.keys(NOTES).filter((n) => !names.includes(n))
51
+ if (gone.length) throw new Error(`scripts/skill-index.mjs describes a skill that is gone: ${gone.join(', ')}`)
52
+
53
+ const out = []
54
+ for (const name of Object.keys(NOTES)) {
55
+ const raw = fs.readFileSync(path.join(root, 'skills', name, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n')
56
+ const rows = []
57
+ for (const section of blocksOf(unfenced(raw), '##')) {
58
+ const title = section.split('\n')[0].trim()
59
+ const found = blocksOf(section, '###')
60
+ .filter((block) => ENTRY.test(block))
61
+ .map((block) => block.split('\n')[0].trim())
62
+ if (found.length) rows.push(`| ${title} | ${found.join(', ')} |`)
63
+ }
64
+ out.push(`### ${name}`, '', NOTES[name], '')
65
+ if (rows.length) out.push(HEAD, ...rows, '')
66
+ }
67
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
68
+ }
69
+
70
+ // Run directly to write it into the guide; imported by check-repo to prove the guide is current.
71
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
72
+ const file = path.join(root, 'GUIDE.md')
73
+ const guide = fs.readFileSync(file, 'utf8')
74
+ const from = guide.indexOf(START)
75
+ const to = guide.indexOf(END)
76
+ if (from === -1 || to === -1) throw new Error(`GUIDE.md has no ${START} ... ${END} pair`)
77
+ const eol = guide.includes('\r\n') ? '\r\n' : '\n'
78
+ const block = buildIndex().replace(/\n/g, eol)
79
+ fs.writeFileSync(file, guide.slice(0, from + START.length) + eol + block + guide.slice(to))
80
+ console.log('Regenerated the skill catalogue in GUIDE.md')
81
+ }