chill-agent 1.4.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 (240) hide show
  1. package/.agents/rules/ponytail.md +30 -0
  2. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  3. package/.claude-plugin/marketplace.json +17 -0
  4. package/.claude-plugin/plugin.json +8 -3
  5. package/.cline-plugin/plugin.js +6 -0
  6. package/.clinerules/ponytail.md +30 -0
  7. package/.codex-plugin/plugin.json +4 -3
  8. package/.cursor/rules/ponytail.mdc +36 -0
  9. package/.cursor-plugin/marketplace.json +16 -0
  10. package/.cursor-plugin/plugin.json +2 -2
  11. package/.devin-plugin/plugin.json +2 -2
  12. package/.grok-plugin/marketplace.json +16 -0
  13. package/.kimi-plugin/plugin.json +11 -29
  14. package/.kiro/steering/ponytail.md +35 -0
  15. package/.omp-plugin/marketplace.json +15 -0
  16. package/.openclaw/skills/ponytail/SKILL.md +47 -0
  17. package/.openclaw/skills/ponytail-audit/SKILL.md +100 -0
  18. package/.openclaw/skills/ponytail-debt/SKILL.md +43 -0
  19. package/.openclaw/skills/ponytail-gain/SKILL.md +51 -0
  20. package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
  21. package/.openclaw/skills/ponytail-review/SKILL.md +98 -0
  22. package/.opencode/command/ponytail-audit.md +5 -0
  23. package/.opencode/command/ponytail-debt.md +5 -0
  24. package/.opencode/command/ponytail-gain.md +5 -0
  25. package/.opencode/command/ponytail-help.md +5 -0
  26. package/.opencode/command/ponytail-review.md +5 -0
  27. package/.opencode/command/ponytail.md +5 -0
  28. package/.opencode/plugins/index.js +4 -0
  29. package/.opencode/plugins/ponytail-frontmatter.cjs +63 -0
  30. package/.opencode/plugins/ponytail.mjs +206 -0
  31. package/.qoder-plugin/plugin.json +12 -26
  32. package/.windsurf/rules/ponytail.md +30 -0
  33. package/AGENTS.md +32 -0
  34. package/__init__.py +228 -0
  35. package/assets/antislop-banner.png +0 -0
  36. package/assets/band/band.png +0 -0
  37. package/assets/band/board.png +0 -0
  38. package/assets/benchmark-3model.svg +21 -0
  39. package/assets/benchmark-agentic.svg +62 -0
  40. package/assets/codex-icon.png +0 -0
  41. package/assets/compare/code/after.webp +0 -0
  42. package/assets/compare/code/before.webp +0 -0
  43. package/assets/compare/text/after.webp +0 -0
  44. package/assets/compare/text/before.webp +0 -0
  45. package/assets/compare/ui/w-all.webp +0 -0
  46. package/assets/compare/ui/w-antislop.webp +0 -0
  47. package/assets/compare/ui/w-design.webp +0 -0
  48. package/assets/compare/ui/without.webp +0 -0
  49. package/assets/logo-dark.png +0 -0
  50. package/assets/logo-dark.svg +115 -0
  51. package/assets/logo-greenpt-dark.svg +27 -0
  52. package/assets/logo-greenpt.svg +27 -0
  53. package/assets/logo.png +0 -0
  54. package/assets/photoreunion-logo-dark.svg +4 -0
  55. package/assets/photoreunion-logo-light.svg +4 -0
  56. package/assets/profile.png +0 -0
  57. package/assets/retriever-logo-dark.svg +4 -0
  58. package/assets/retriever-logo-light.svg +4 -0
  59. package/assets/stories-banner.png +0 -0
  60. package/assets/v5/audit-es.png +0 -0
  61. package/assets/v5/audit-ja.png +0 -0
  62. package/assets/v5/audit-ko.png +0 -0
  63. package/assets/v5/audit-zh-CN.png +0 -0
  64. package/assets/v5/audit.png +0 -0
  65. package/assets/v5/beforeafter-es.png +0 -0
  66. package/assets/v5/beforeafter-ja.png +0 -0
  67. package/assets/v5/beforeafter-ko.png +0 -0
  68. package/assets/v5/beforeafter-zh-CN.png +0 -0
  69. package/assets/v5/beforeafter.png +0 -0
  70. package/assets/v5/chart-es.png +0 -0
  71. package/assets/v5/chart-ja.png +0 -0
  72. package/assets/v5/chart-ko.png +0 -0
  73. package/assets/v5/chart-zh-CN.png +0 -0
  74. package/assets/v5/chart.png +0 -0
  75. package/assets/v5/hero-es.jpg +0 -0
  76. package/assets/v5/hero-ja.jpg +0 -0
  77. package/assets/v5/hero-ko.jpg +0 -0
  78. package/assets/v5/hero-zh-CN.jpg +0 -0
  79. package/assets/v5/hero.jpg +0 -0
  80. package/assets/v5/ladder-es.png +0 -0
  81. package/assets/v5/ladder-ja.png +0 -0
  82. package/assets/v5/ladder-ko.png +0 -0
  83. package/assets/v5/ladder-zh-CN.png +0 -0
  84. package/assets/v5/ladder.png +0 -0
  85. package/assets/v5/review-es.png +0 -0
  86. package/assets/v5/review-ja.png +0 -0
  87. package/assets/v5/review-ko.png +0 -0
  88. package/assets/v5/review-zh-CN.png +0 -0
  89. package/assets/v5/review.png +0 -0
  90. package/assets/v5/tests-es.png +0 -0
  91. package/assets/v5/tests-ja.png +0 -0
  92. package/assets/v5/tests-ko.png +0 -0
  93. package/assets/v5/tests-zh-CN.png +0 -0
  94. package/assets/v5/tests.png +0 -0
  95. package/assets/waitlist-banner-es.png +0 -0
  96. package/assets/waitlist-banner-ja.png +0 -0
  97. package/assets/waitlist-banner-ko.png +0 -0
  98. package/assets/waitlist-banner-zh-CN.png +0 -0
  99. package/assets/waitlist-banner.png +0 -0
  100. package/benchmarks/.env.example +2 -0
  101. package/benchmarks/agentic/README.md +201 -0
  102. package/benchmarks/agentic/answer.py +288 -0
  103. package/benchmarks/agentic/compare.py +93 -0
  104. package/benchmarks/agentic/complete.py +178 -0
  105. package/benchmarks/agentic/judge.py +198 -0
  106. package/benchmarks/agentic/run.py +543 -0
  107. package/benchmarks/agentic/tasks.py +1544 -0
  108. package/benchmarks/arms/baseline.js +2 -0
  109. package/benchmarks/arms/caveman-SKILL.md +67 -0
  110. package/benchmarks/arms/caveman.js +8 -0
  111. package/benchmarks/arms/ponytail.js +8 -0
  112. package/benchmarks/behavior.js +54 -0
  113. package/benchmarks/behavior.yaml +40 -0
  114. package/benchmarks/correctness.js +291 -0
  115. package/benchmarks/loc.js +17 -19
  116. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  117. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  118. package/benchmarks/promptfooconfig.yaml +41 -0
  119. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  120. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  121. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  122. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  123. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  124. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  125. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  126. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  127. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  128. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  129. package/cli/index.mjs +241 -0
  130. package/cli/lib/banner.mjs +21 -0
  131. package/cli/lib/install.mjs +498 -0
  132. package/cli/lib/settings.mjs +48 -0
  133. package/cli/package-lock.json +87 -0
  134. package/cli/package.json +42 -0
  135. package/cli/scripts/smoke-test.mjs +95 -0
  136. package/cli/scripts/smoke-worker.mjs +411 -0
  137. package/cli/scripts/sync-skills.mjs +81 -0
  138. package/commands/ponytail-audit.toml +2 -0
  139. package/commands/ponytail-debt.toml +2 -0
  140. package/commands/ponytail-gain.toml +2 -0
  141. package/commands/ponytail-help.toml +2 -0
  142. package/commands/ponytail-review.toml +2 -0
  143. package/commands/ponytail.toml +2 -0
  144. package/examples/email-validation.md +156 -0
  145. package/examples/group-by.md +35 -0
  146. package/examples/infinite-scroll.md +58 -0
  147. package/examples/number-formatting.md +37 -0
  148. package/examples/rate-limit.md +272 -0
  149. package/examples/react-countdown.md +390 -0
  150. package/examples/url-params.md +41 -0
  151. package/gemini-extension.json +2 -2
  152. package/hooks/band/board.ts +560 -0
  153. package/hooks/band/logo.ts +3 -0
  154. package/hooks/band/metrics.ts +311 -0
  155. package/hooks/band/register.tsx +495 -0
  156. package/hooks/band/types.d.ts +34 -0
  157. package/hooks/chill-statusline.ps1 +1 -1
  158. package/hooks/claude-band.json +6 -0
  159. package/hooks/claude-codex-hooks.json +34 -4
  160. package/hooks/copilot-hooks.json +12 -2
  161. package/hooks/cursor-hooks.json +2 -2
  162. package/hooks/ponytail-activate.js +201 -0
  163. package/hooks/ponytail-band-import.js +274 -0
  164. package/hooks/ponytail-config.js +175 -0
  165. package/hooks/ponytail-instructions.js +98 -0
  166. package/hooks/ponytail-map.js +111 -0
  167. package/hooks/ponytail-mode-tracker.js +216 -0
  168. package/hooks/ponytail-runtime.js +182 -0
  169. package/hooks/ponytail-statusline.ps1 +24 -0
  170. package/hooks/ponytail-statusline.sh +18 -0
  171. package/hooks/ponytail-subagent.js +94 -0
  172. package/hooks/qoder-hooks.json +19 -4
  173. package/i18n/README.ko.md +230 -0
  174. package/package.json +17 -6
  175. package/pi-extension/index.js +254 -0
  176. package/pi-extension/package.json +8 -0
  177. package/pi-extension/test/extension.test.js +367 -0
  178. package/pi-extension/test/helpers.test.js +179 -0
  179. package/plugin.yaml +21 -12
  180. package/rules/antislop.mdc +18 -0
  181. package/scripts/build-openclaw-skills.js +60 -0
  182. package/scripts/check-repo.mjs +389 -0
  183. package/scripts/check-rule-copies.js +74 -0
  184. package/scripts/check-versions.js +77 -0
  185. package/scripts/cursor-hooks.js +145 -0
  186. package/scripts/generate-chillslop.mjs +42 -0
  187. package/scripts/generate-examples.mjs +149 -0
  188. package/scripts/generate-plugin-doors.mjs +108 -0
  189. package/scripts/publish-openclaw-skills.js +75 -0
  190. package/scripts/skill-index.mjs +81 -0
  191. package/scripts/sync-ponytail.mjs +65 -0
  192. package/scripts/uninstall.js +92 -0
  193. package/skills/antislop/SKILL.md +700 -0
  194. package/skills/antislop/VERSION +1 -0
  195. package/skills/antislop-code/SKILL.md +128 -0
  196. package/skills/antislop-copywriting/SKILL.md +407 -0
  197. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  198. package/skills/antislop-human/SKILL.md +204 -0
  199. package/skills/antislop-human/contrast-check.py +168 -0
  200. package/skills/antislop-human/contrast-mcp.py +121 -0
  201. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  202. package/skills/antislop-human/references/WCAG.md +127 -0
  203. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  204. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  205. package/skills/antislop-ui/SKILL.md +344 -0
  206. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  207. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  208. package/skills/plugin.json +5 -0
  209. package/skills/ponytail/SKILL.md +52 -0
  210. package/skills/ponytail-audit/SKILL.md +103 -0
  211. package/skills/ponytail-debt/SKILL.md +44 -0
  212. package/skills/ponytail-gain/SKILL.md +52 -0
  213. package/skills/ponytail-help/SKILL.md +76 -0
  214. package/skills/ponytail-review/SKILL.md +101 -0
  215. package/src/package.json +3 -0
  216. package/tests/activation-map.test.js +63 -0
  217. package/tests/band-import.test.js +111 -0
  218. package/tests/band.test.tsx +520 -0
  219. package/tests/behavior.test.js +80 -0
  220. package/tests/commands.test.js +94 -0
  221. package/tests/copilot-plugin.test.js +35 -0
  222. package/tests/correctness.test.js +245 -0
  223. package/tests/cursor-hooks.test.js +343 -0
  224. package/tests/gemini-extension.test.js +91 -0
  225. package/tests/grok-plugin.test.js +39 -0
  226. package/tests/hermes-plugin.test.js +403 -0
  227. package/tests/hooks-windows.test.js +205 -0
  228. package/tests/hooks.test.js +1005 -0
  229. package/tests/kimi-plugin.test.js +25 -0
  230. package/tests/loc.test.js +18 -0
  231. package/tests/map.test.js +30 -0
  232. package/tests/openclaw-skills.test.js +26 -0
  233. package/tests/opencode-plugin.test.js +340 -0
  234. package/tests/package-scripts.test.js +35 -0
  235. package/tests/package.test.js +23 -0
  236. package/tests/qoder-plugin.test.js +84 -0
  237. package/tests/uninstall.test.js +202 -0
  238. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  239. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  240. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { spawnSync } from 'node:child_process'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const worker = path.join(__dirname, 'smoke-worker.mjs')
10
+
11
+ function tree(dir, prefix = '') {
12
+ const out = []
13
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
14
+ const p = path.join(dir, entry.name)
15
+ if (entry.isDirectory()) {
16
+ out.push(prefix + entry.name + '/')
17
+ out.push(...tree(p, prefix + ' '))
18
+ } else {
19
+ out.push(prefix + entry.name)
20
+ }
21
+ }
22
+ return out
23
+ }
24
+
25
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'antislop-smoke-'))
26
+
27
+ // Every child gets a throwaway home from launch: Bun's os.homedir() ignores a later HOME
28
+ // change, so the worker's global install and --update would write into the real home.
29
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'antislop-home-'))
30
+ const env = { ...process.env, HOME: home, USERPROFILE: home }
31
+ for (const k of ['KIMI_CODE_HOME', 'CLINE_DIR', 'XDG_DATA_HOME']) delete env[k]
32
+
33
+ const result = spawnSync(process.execPath, [worker], { cwd: tmp, env, encoding: 'utf8' })
34
+ console.log('--- worker output ---')
35
+ console.log(result.stdout.trim())
36
+ if (result.status !== 0) console.error('worker stderr:', result.stderr)
37
+
38
+ console.log('\n--- files written to temp project ---')
39
+ console.log(tree(tmp).join('\n'))
40
+
41
+ for (const name of ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']) {
42
+ const entry = path.join(tmp, name)
43
+ if (!fs.existsSync(entry)) continue
44
+ console.log(`\n--- ${name} ---`)
45
+ console.log(fs.readFileSync(entry, 'utf8'))
46
+ }
47
+
48
+ const coreSkill = path.join(tmp, '.claude', 'skills', 'antislop', 'SKILL.md')
49
+ const coreInstalled = fs.existsSync(coreSkill)
50
+ console.log('\ncore SKILL.md exists:', coreInstalled)
51
+
52
+ // Every failure is collected here and decided before cleanup, so a failing run
53
+ // still leaves a tidy temp dir behind.
54
+ const reasons = []
55
+ if (result.status !== 0) reasons.push(`worker exited ${result.status}`)
56
+ if (!coreInstalled) reasons.push('core SKILL.md was not installed')
57
+
58
+ // The wizard needs a terminal. With stdin closed it used to print its prompts, install
59
+ // nothing, and still exit 0, so callers saw success. It has to refuse and say why.
60
+ // Loading it needs the cli's dependencies, so a bare checkout reports a skip instead.
61
+ const cliDeps = fs.existsSync(path.join(__dirname, '..', 'node_modules', '@clack'))
62
+ if (cliDeps) {
63
+ const invalidMode = spawnSync(process.execPath, [path.join(__dirname, '..', 'index.mjs'), '--mode', 'invalid'], {
64
+ cwd: tmp,
65
+ env,
66
+ encoding: 'utf8',
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ })
69
+ const cleanError = /^antislop: /.test(invalidMode.stderr) && !/\n\s+at /.test(invalidMode.stderr)
70
+ if (invalidMode.status !== 1 || !/Usage: antislop-ai --mode/.test(invalidMode.stderr) || !cleanError) {
71
+ reasons.push('the mode command must exit 1 on one clean line, not a stack trace')
72
+ }
73
+ const noTty = spawnSync(process.execPath, [path.join(__dirname, '..', 'index.mjs')], {
74
+ cwd: tmp,
75
+ env,
76
+ encoding: 'utf8',
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ })
79
+ const refused = noTty.status === 1 && /needs a terminal/.test(noTty.stderr)
80
+ console.log('no-terminal run: exit', noTty.status, '| refused:', refused)
81
+ if (!refused) reasons.push(`a no-terminal run should exit 1 and say why, got exit ${noTty.status}`)
82
+
83
+ } else {
84
+ console.log('no-terminal run: skipped, cli dependencies are not installed')
85
+ }
86
+
87
+ fs.rmSync(tmp, { recursive: true, force: true })
88
+ fs.rmSync(home, { recursive: true, force: true })
89
+ console.log('\ncleaned up temp project.')
90
+
91
+ if (reasons.length > 0) {
92
+ console.error('\nsmoke test failed: ' + reasons.join('; '))
93
+ process.exit(1)
94
+ }
95
+ console.log('smoke test passed.')
@@ -0,0 +1,411 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import assert from 'node:assert/strict'
5
+ import { configureMode, readSettings, resolveSettingsPath } from '../lib/settings.mjs'
6
+ import {
7
+ AGENTS,
8
+ CORE,
9
+ PLUGIN_DOORS,
10
+ skillSourceDir,
11
+ resolveTargets,
12
+ detectAgents,
13
+ detectConflicts,
14
+ detectDuplicateReads,
15
+ detectPluginDoors,
16
+ installSkills,
17
+ installedVersion,
18
+ updateAll,
19
+ updatePointers,
20
+ VERSION,
21
+ } from '../lib/install.mjs'
22
+
23
+ const skills = ['antislop', 'antislop-ui']
24
+
25
+ assert.equal(
26
+ resolveSettingsPath({ platform: 'linux', env: {}, home: '/home/ada' }),
27
+ '/home/ada/.config/antislop/settings.json'
28
+ )
29
+ assert.equal(
30
+ resolveSettingsPath({ platform: 'darwin', env: {}, home: '/Users/ada' }),
31
+ '/Users/ada/.config/antislop/settings.json'
32
+ )
33
+ assert.equal(
34
+ resolveSettingsPath({ platform: 'win32', env: { APPDATA: 'C:\\Users\\Ada\\AppData\\Roaming' }, home: 'C:\\Users\\Ada' }),
35
+ 'C:\\Users\\Ada\\AppData\\Roaming\\antislop\\settings.json'
36
+ )
37
+ assert.equal(
38
+ resolveSettingsPath({ platform: 'win32', env: {}, home: 'C:\\Users\\Ada' }),
39
+ 'C:\\Users\\Ada\\.config\\antislop\\settings.json'
40
+ )
41
+
42
+ const settingsFile = path.join(process.cwd(), 'preferences', 'settings.json')
43
+ assert.match(configureMode([], settingsFile), /ask \(default\)/)
44
+ assert.equal(fs.existsSync(settingsFile), false)
45
+ for (const mode of ['during', 'after', 'ask']) {
46
+ assert.match(configureMode([mode], settingsFile), new RegExp(`global mode: ${mode}`))
47
+ assert.equal(readSettings(settingsFile).mode, mode)
48
+ assert.match(configureMode([], settingsFile), new RegExp(`global mode: ${mode}`))
49
+ }
50
+ fs.writeFileSync(settingsFile, '{"other":true,"mode":"during"}')
51
+ configureMode(['after'], settingsFile)
52
+ assert.deepEqual(readSettings(settingsFile), { other: true, mode: 'after' })
53
+ assert.throws(() => configureMode(['invalid'], settingsFile), /Usage:/)
54
+ assert.throws(() => configureMode(['during', 'extra'], settingsFile), /Usage:/)
55
+ assert.equal(readSettings(settingsFile).mode, 'after')
56
+ for (const content of ['{broken', 'null', '[]', '"during"']) {
57
+ fs.writeFileSync(settingsFile, content)
58
+ // The refusal is right, but it has to name the way out: --mode is the only command
59
+ // that can fix this file, so a message with no instruction is a dead end.
60
+ assert.throws(() => configureMode(['during'], settingsFile), /Fix or delete/)
61
+ assert.equal(fs.readFileSync(settingsFile, 'utf8'), content)
62
+ }
63
+ for (const mode of ['invalid', null, 1]) {
64
+ fs.writeFileSync(settingsFile, JSON.stringify({ mode }))
65
+ assert.throws(() => configureMode([], settingsFile), /Invalid mode/)
66
+ configureMode(['during'], settingsFile)
67
+ assert.equal(readSettings(settingsFile).mode, 'during')
68
+ }
69
+ console.log('ok global mode preferences: persistence, validation, and preservation')
70
+
71
+ const failures = []
72
+
73
+ /** Prints the line this always printed and records whether it held, so one run
74
+ * reports every break rather than stopping at the first.
75
+ */
76
+ const check = (label, actual, expected) => {
77
+ const ok = JSON.stringify(actual) === JSON.stringify(expected)
78
+ console.log(ok ? `ok ${label}:` : `FAIL ${label}:`, actual, ok ? '' : `(expected ${JSON.stringify(expected)})`)
79
+ if (!ok) failures.push(label)
80
+ }
81
+
82
+ // Fresh state: nothing detected, and the default selection is every agent.
83
+ const fresh = detectAgents('project')
84
+ check('A detected (fresh project)', fresh, [])
85
+ const defaultTargets = resolveTargets('project')
86
+ console.log('A default targets:', defaultTargets.map((t) => `${t.agents.map((a) => a.id).join('+')}@${t.path} exists=${t.exists}`).join(' | '))
87
+ // Copilot, Kimi Code, and Amp share Antigravity's folder, so twelve resolve to nine.
88
+ check('A twelve agents over nine folders', [AGENTS.length, defaultTargets.length], [12, 9])
89
+ check('A copilot and kimi share the antigravity folder', resolveTargets('project', ['antigravity', 'copilot', 'kimi']).length, 1)
90
+
91
+ // Claude Code only, via explicit selection (old behavior preserved).
92
+ const targets = resolveTargets('project', ['claude'])
93
+ let written = installSkills({ skills, targets, overwrite: false })
94
+ let pointers = updatePointers({ targets, skills })
95
+ console.log('B targets:', targets.map((t) => `${t.agents.map((a) => a.id).join('+')}@${t.path} exists=${t.exists}`).join(' | '))
96
+ console.log('B written:', written.map((w) => `${w.agents.join('+')}:${w.skill}`).join(', '))
97
+ check('B pointers', pointers.map((p) => path.basename(p)), ['CLAUDE.md'])
98
+ const pointer = fs.readFileSync(pointers[0], 'utf8')
99
+ assert.match(pointer, /explicit session instruction first, then global preference, then ask/)
100
+ assert.match(pointer, /A session instruction always wins/)
101
+ assert.ok(pointer.includes('antislop active: <mode> (session override).'))
102
+ assert.ok(pointer.includes('antislop active: <mode> (global preference).'))
103
+ assert.ok(pointer.includes('ask during/after and end the response'))
104
+ assert.ok(pointer.includes('only at the start of the final answer, never in progress messages'))
105
+ assert.ok(pointer.includes('announce before the first edit and omit it from the final answer'))
106
+ assert.ok(pointer.includes('`.claude/skills/antislop/SKILL.md`'))
107
+ assert.ok(pointer.includes('`.claude/skills/antislop-ui/SKILL.md`'))
108
+
109
+ const conflicts = detectConflicts({ skills, targets })
110
+ written = installSkills({ skills, targets, overwrite: false })
111
+ check('C conflicts', conflicts.length, 2)
112
+ check('C written without overwrite', written.length, 0)
113
+ written = installSkills({ skills, targets, overwrite: true })
114
+ check('C overwritten', written.length, 2)
115
+
116
+ // Antigravity on a fresh project: .agents/ does not exist yet, install creates it.
117
+ const agTargets = resolveTargets('project', ['antigravity'])
118
+ console.log('D antigravity targets:', agTargets.map((t) => `${t.agents.map((a) => a.id).join('+')}@${t.path} exists=${t.exists}`).join(' | '), '(exists=false before install)')
119
+ const agWritten = installSkills({ skills, targets: agTargets, overwrite: false })
120
+ check('D antigravity written', agWritten.length, 2)
121
+ const agPointers = updatePointers({ targets: agTargets, skills })
122
+ check('D antigravity pointers', agPointers.map((p) => path.basename(p)), ['AGENTS.md'])
123
+ check('D .agents/skills/antislop/SKILL.md exists', fs.existsSync(path.join(process.cwd(), '.agents', 'skills', 'antislop', 'SKILL.md')), true)
124
+
125
+ // OpenCode, Cursor, and Gemini each install and point to their own entry file.
126
+ for (const agent of ['opencode', 'cursor', 'gemini']) {
127
+ const t = resolveTargets('project', [agent])
128
+ const w = installSkills({ skills, targets: t, overwrite: false })
129
+ const pointers = updatePointers({ targets: t, skills })
130
+ check(`D2 ${agent} written`, w.length, 2)
131
+ check(`D2 ${agent} pointer`, pointers.map((p) => path.basename(p)), [agent === 'gemini' ? 'GEMINI.md' : 'AGENTS.md'])
132
+ check(`D2 ${agent} folder exists`, fs.existsSync(path.join(process.cwd(), agent === 'gemini' ? '.gemini' : agent === 'cursor' ? '.cursor' : '.opencode', 'skills', 'antislop', 'SKILL.md')), true)
133
+ }
134
+
135
+ // Cline keeps a folder of its own and reads .claude/skills beside it, so both fill up.
136
+ const clineProject = resolveTargets('project', ['cline'])
137
+ check('D8 cline project target', clineProject[0].path, path.join(process.cwd(), '.cline', 'skills'))
138
+ check('D8 cline global target', resolveTargets('global', ['cline'])[0].path, path.join(os.homedir(), '.cline', 'skills'))
139
+ check('D8 cline written', installSkills({ skills, targets: clineProject, overwrite: false }).length, 2)
140
+ check('D8 cline pointer', updatePointers({ targets: clineProject, skills }).map((p) => path.basename(p)), ['AGENTS.md'])
141
+ check('D8 cline folder exists', fs.existsSync(path.join(process.cwd(), '.cline', 'skills', 'antislop', 'SKILL.md')), true)
142
+
143
+ // Amp shares the project's .agents/skills, which the Antigravity block already filled,
144
+ // but its global folder is a different path.
145
+ const ampProject = resolveTargets('project', ['amp'])
146
+ check('D9 amp shares the agents folder', ampProject[0].path, path.join(process.cwd(), '.agents', 'skills'))
147
+ check('D9 amp global target', resolveTargets('global', ['amp'])[0].path, path.join(os.homedir(), '.config', 'agents', 'skills'))
148
+ check('D9 amp writes nothing twice', installSkills({ skills, targets: ampProject, overwrite: false }).length, 0)
149
+ check('D9 amp pointer', updatePointers({ targets: ampProject, skills }).map((p) => path.basename(p)), ['AGENTS.md'])
150
+
151
+ // Pi keeps a folder of its own at both scopes, and walks the shared .agents/skills
152
+ // beside it, which the Antigravity block already filled.
153
+ const piProject = resolveTargets('project', ['pi'])
154
+ check('D10 pi project target', piProject[0].path, path.join(process.cwd(), '.pi', 'skills'))
155
+ check('D10 pi global target', resolveTargets('global', ['pi'])[0].path, path.join(os.homedir(), '.pi', 'agent', 'skills'))
156
+ check('D10 pi written', installSkills({ skills, targets: piProject, overwrite: false }).length, 2)
157
+ check('D10 pi pointer', updatePointers({ targets: piProject, skills }).map((p) => path.basename(p)), ['AGENTS.md'])
158
+ check('D10 pi folder exists', fs.existsSync(path.join(process.cwd(), '.pi', 'skills', 'antislop', 'SKILL.md')), true)
159
+ // Pi loads the shared folder too, so Pi plus Antigravity holds the same names twice.
160
+ check('D10 pi duplicate read named', detectDuplicateReads({ targets: resolveTargets('project', ['antigravity', 'pi']), location: 'project' }).map((d) => [d.agent.id, d.paths.length]), [['pi', 2]])
161
+
162
+ // Hermes reads a project's .hermes/skills, and ~/.hermes/skills for a global install.
163
+ const hermesProject = resolveTargets('project', ['hermes'])
164
+ const hermesGlobal = resolveTargets('global', ['hermes'])
165
+ check('D3 hermes project target', hermesProject[0].path, path.join(process.cwd(), '.hermes', 'skills'))
166
+ check('D3 hermes global target', hermesGlobal[0].path, path.join(os.homedir(), '.hermes', 'skills'))
167
+ const hermesWritten = installSkills({ skills, targets: hermesProject, overwrite: false })
168
+ check('D3 hermes written into the project', hermesWritten.length, 2)
169
+ check('D3 hermes detected in project', detectAgents('project').includes('hermes'), true)
170
+
171
+ // OpenCode splits its scopes: project folder for a project install, ~/.config for global.
172
+ const ocProject = resolveTargets('project', ['opencode'])
173
+ const ocGlobal = resolveTargets('global', ['opencode'])
174
+ check('D4 opencode project target', ocProject[0].path, path.join(process.cwd(), '.opencode', 'skills'))
175
+ check('D4 opencode global target', ocGlobal[0].path, path.join(os.homedir(), '.config', 'opencode', 'skills'))
176
+
177
+ // Antigravity reads a project's .agents/skills but not the one under the home dir.
178
+ const agGlobal = resolveTargets('global', ['antigravity'])
179
+ check('D5 antigravity project target', resolveTargets('project', ['antigravity'])[0].path, path.join(process.cwd(), '.agents', 'skills'))
180
+ check('D5 antigravity global target', agGlobal[0].path, path.join(os.homedir(), '.gemini', 'config', 'skills'))
181
+
182
+ // Kimi Code has no folder of its own here: both scopes are the shared .agents/skills.
183
+ check('D7 kimi project target', resolveTargets('project', ['kimi'])[0].path, path.join(process.cwd(), '.agents', 'skills'))
184
+
185
+ // updatePointers has no other source for the entry file, so no row may omit it.
186
+ check('D6 every agent names an entry file', AGENTS.filter((a) => !a.entry).map((a) => a.id), [])
187
+
188
+ // Hermes reads a project's AGENTS.md, so a project install of it must write the pointer.
189
+ check('D6 hermes writes a project pointer', updatePointers({ targets: hermesProject, skills }).map((p) => path.basename(p)), ['AGENTS.md'])
190
+
191
+ // Detection now sees the agents that were installed.
192
+ const after = detectAgents('project')
193
+ check('E detected after installs', [...after].sort(), ['amp', 'antigravity', 'claude', 'cline', 'copilot', 'cursor', 'gemini', 'hermes', 'kimi', 'opencode', 'pi'])
194
+
195
+ // OpenCode reads .claude/skills and .agents/skills too, so a project that installs into
196
+ // two of them holds the same names twice and OpenCode picks between them unpredictably.
197
+ const dupReads = detectDuplicateReads({ targets: resolveTargets('project', ['claude', 'opencode']), location: 'project' })
198
+ check('E duplicate read named', dupReads.map((d) => [d.agent.id, d.paths.length]), [['opencode', 2]])
199
+ check('E one folder alone is not a duplicate', detectDuplicateReads({ targets: resolveTargets('project', ['opencode']), location: 'project' }), [])
200
+ // Cline reads .claude/skills beside its own folder, so Claude Code plus Cline collides.
201
+ check('E cline duplicate read named', detectDuplicateReads({ targets: resolveTargets('project', ['claude', 'cline']), location: 'project' }).map((d) => [d.agent.id, d.paths.length]), [['cline', 2]])
202
+ // Amp loads .claude/skills beside the shared folder, so Claude Code plus Amp collides too.
203
+ check('E amp duplicate read named', detectDuplicateReads({ targets: resolveTargets('project', ['claude', 'amp']), location: 'project' }).map((d) => [d.agent.id, d.paths.length]), [['amp', 2]])
204
+ // Copilot reads .claude/skills beside the shared folder, so Claude Code plus Copilot collides.
205
+ check('E copilot duplicate read named', detectDuplicateReads({ targets: resolveTargets('project', ['claude', 'copilot']), location: 'project' }).map((d) => [d.agent.id, d.paths.length]), [['copilot', 2]])
206
+ check('E global scope is not checked', detectDuplicateReads({ targets: resolveTargets('global', ['claude', 'opencode']), location: 'global' }), [])
207
+
208
+ // Codex's global scope is the shared folder, not ~/.codex/skills, which Codex calls deprecated.
209
+ // Kimi Code lands there too, so the grouping has to hold at global scope as well.
210
+ const globalTargets = resolveTargets('global', ['claude', 'codex', 'kimi'])
211
+ check('F global targets', globalTargets.map((t) => `${t.agents.map((a) => a.id).join('+')}@${t.path}`), [
212
+ `claude@${path.join(os.homedir(), '.claude', 'skills')}`,
213
+ `codex+kimi@${path.join(os.homedir(), '.agents', 'skills')}`,
214
+ ])
215
+
216
+ // Copies are identical and the pointer block dedupes.
217
+ // A shared entry file must point to an installed copy, even with several agents.
218
+ const sharedTargets = resolveTargets('project', ['codex', 'opencode'])
219
+ installSkills({ skills, targets: sharedTargets, overwrite: false })
220
+ updatePointers({ targets: sharedTargets, skills })
221
+ const sharedPointer = fs.readFileSync('AGENTS.md', 'utf8')
222
+ for (const skill of skills) {
223
+ const installedPath = `.codex/skills/${skill}/SKILL.md`
224
+ assert.ok(sharedPointer.includes(`\`${installedPath}\``))
225
+ assert.equal(fs.readFileSync(installedPath, 'utf8'), fs.readFileSync(path.join(skillSourceDir(), skill, 'SKILL.md'), 'utf8'))
226
+ }
227
+
228
+ const src = fs.readFileSync(path.join(skillSourceDir(), 'antislop-ui', 'SKILL.md'), 'utf8')
229
+ const dst = fs.readFileSync(path.join(process.cwd(), '.claude', 'skills', 'antislop-ui', 'SKILL.md'), 'utf8')
230
+ check('G antislop-ui SKILL.md identical', src === dst, true)
231
+
232
+ // The conflict prompt reports the release on disk from this file alone.
233
+ check('G installed version is stamped', installedVersion(targets[0].path), VERSION)
234
+ check('G no install means no version', installedVersion(path.join(process.cwd(), 'nowhere')), null)
235
+
236
+ updatePointers({ targets, skills })
237
+ const entry = fs.readFileSync(path.join(process.cwd(), 'CLAUDE.md'), 'utf8')
238
+ check('H blocks after a second run', (entry.match(/antislop:start/g) || []).length, 1)
239
+
240
+ // The entry file belongs to the author. Each fixture below used to be damaged.
241
+ const entryPath = path.join(process.cwd(), 'CLAUDE.md')
242
+ const write = (text) => fs.writeFileSync(entryPath, text)
243
+ const read = () => fs.readFileSync(entryPath, 'utf8')
244
+
245
+ write('# Mine\r\n\r\nNotes.\r\n')
246
+ updatePointers({ targets, skills })
247
+ check('I CRLF kept', read().includes('\r\n<!-- antislop:start -->'), true)
248
+ check('I bare LF count', (read().match(/(?<!\r)\n/g) || []).length, 0)
249
+
250
+ write('# Mine\n\n```md\n<!-- antislop:start -->\n<!-- antislop:end -->\n```\n')
251
+ updatePointers({ targets, skills })
252
+ check('I fenced example untouched', read().includes('```md\n<!-- antislop:start -->\n<!-- antislop:end -->\n```'), true)
253
+ check('I real block added beside it', (read().match(/antislop:start/g) || []).length, 2)
254
+
255
+ write('# Mine\n<!-- antislop:end -->\n')
256
+ updatePointers({ targets, skills })
257
+ const stable = read()
258
+ updatePointers({ targets, skills })
259
+ check('I orphan marker does not grow', read() === stable, true)
260
+ check('I orphan marker is gone', (read().match(/antislop:end/g) || []).length, 1)
261
+
262
+ // A fence left open at EOF is a typo, not a boundary: the block lands inside it and has
263
+ // to be found again, or every run appends another copy.
264
+ write('# Mine\n\nNotes:\n\n```bash\nnpm i\n')
265
+ updatePointers({ targets, skills })
266
+ const fenced = read()
267
+ updatePointers({ targets, skills })
268
+ updatePointers({ targets, skills })
269
+ check('J unclosed fence stable', read() === fenced, true)
270
+ check('J unclosed fence keeps code', read().includes('npm i'), true)
271
+ check('J unclosed fence one block', (read().match(/antislop:start/g) || []).length, 1)
272
+
273
+ // A mistyped end marker is the author's text, so the block is appended, not swapped in.
274
+ write('# Mine\n\n<!-- antislop:start -->\nold\n<!-- antislop:End -->\n\n## Notes\nKeep this line.\n')
275
+ updatePointers({ targets, skills })
276
+ check('J mistyped end keeps the tail', read().includes('Keep this line.'), true)
277
+ check('J mistyped end one block', (read().match(/antislop:start/g) || []).length, 1)
278
+
279
+ // A longer fence run is not closed by a shorter one inside the example.
280
+ write('# Mine\n\n````md\n```md\n<!-- antislop:start -->\n<!-- antislop:end -->\n```\n````\n')
281
+ updatePointers({ targets, skills })
282
+ check('J four-backtick example untouched', read().includes('````md\n```md\n<!-- antislop:start -->'), true)
283
+
284
+ // A fence-like line with trailing text is code, not a closing fence.
285
+ for (const fence of ['```', '~~~']) {
286
+ const example = `${fence}md\n${fence}js\n<!-- antislop:start -->\nKeep this example.\n<!-- antislop:end -->\n${fence}`
287
+ write(`# Mine\n\n${example}\n`)
288
+ updatePointers({ targets, skills })
289
+ check(`J ${fence} with info string keeps the example`, read().includes(example), true)
290
+ check(`J ${fence} real block added outside the example`, (read().match(/antislop:start/g) || []).length, 2)
291
+ const first = read()
292
+ updatePointers({ targets, skills })
293
+ check(`J ${fence} example stays intact on reinstall`, read() === first, true)
294
+ }
295
+
296
+ // A stray end marker below the block is ours too, and must not survive.
297
+ write('# Mine\n<!-- antislop:start -->\nold\n<!-- antislop:end -->\nTail text\n<!-- antislop:end -->\n')
298
+ updatePointers({ targets, skills })
299
+ check('J stray end below cleared', (read().match(/antislop:end/g) || []).length, 1)
300
+ check('J text below the block kept', read().includes('Tail text'), true)
301
+
302
+ // Dominant line ending wins, so two CRLF lines do not flip a mostly-LF file.
303
+ write('# Mine\n' + 'line\n'.repeat(10) + 'a\r\nb\r\n')
304
+ updatePointers({ targets, skills })
305
+ check('J mostly-LF stays LF', (read().match(/\r\n/g) || []).length, 0)
306
+
307
+ // A second marker pair leaves a stray start behind unless every marker line is cleared.
308
+ write('# Mine\n<!-- antislop:start -->\nX\n<!-- antislop:end -->\nKeep this too.\n<!-- antislop:start -->\nY\n<!-- antislop:end -->\n')
309
+ updatePointers({ targets, skills })
310
+ check('J duplicate pair settles to one', (read().match(/antislop:start/g) || []).length, 1)
311
+ check('J duplicate pair keeps text', read().includes('Keep this too.'), true)
312
+
313
+ // --update replaces what is already on disk, keeps each folder's skill selection, and
314
+ // names the release it replaced. The scope list keeps a test out of the home directory.
315
+ const present = resolveTargets('project').filter((t) => fs.existsSync(path.join(t.path, CORE)))
316
+ check('K update finds the installed folders', present.length > 0, true)
317
+ const upd = updateAll({ locations: ['project'] })
318
+ check('K update reports every folder it found', upd.results.length, present.length)
319
+ check('K update lands on this release', upd.results.every((r) => r.to === VERSION), true)
320
+ check('K update keeps the skill selection', upd.results.every((r) => r.skills.includes(CORE)), true)
321
+ check('K update writes a pointer', upd.pointers.length > 0, true)
322
+ check('K pointer carries the update line', fs.readFileSync(path.join(process.cwd(), 'AGENTS.md'), 'utf8').includes('antislop-ai --update'), true)
323
+ const again = updateAll({ locations: ['project'] })
324
+ check('K update is repeatable', again.results.map((r) => r.from), upd.results.map((r) => r.to))
325
+
326
+ // A clean directory has nothing to replace, and saying so is the whole answer.
327
+ const clean = fs.mkdtempSync(path.join(os.tmpdir(), 'antislop-clean-'))
328
+ const here = process.cwd()
329
+ process.chdir(clean)
330
+ check('K update says nothing when nothing is installed', updateAll({ locations: ['project'] }).results.length, 0)
331
+ check('K update writes no pointer when nothing is installed', updateAll({ locations: ['project'] }).pointers.length, 0)
332
+ process.chdir(here)
333
+ fs.rmSync(clean, { recursive: true, force: true })
334
+
335
+ // --update can combine folders with different extras. Every pointer must resolve,
336
+ // and a separate entry file must not list another agent's unavailable extras.
337
+ const mixed = fs.mkdtempSync(path.join(os.tmpdir(), 'antislop-mixed-'))
338
+ try {
339
+ process.chdir(mixed)
340
+ installSkills({ skills: [CORE], targets: resolveTargets('project', ['codex']), overwrite: false })
341
+ installSkills({ skills: [CORE, 'antislop-ui'], targets: resolveTargets('project', ['opencode']), overwrite: false })
342
+ installSkills({ skills: [CORE, 'antislop-copywriting'], targets: resolveTargets('project', ['claude']), overwrite: false })
343
+ updateAll({ locations: ['project'] })
344
+ const shared = fs.readFileSync('AGENTS.md', 'utf8')
345
+ const separate = fs.readFileSync('CLAUDE.md', 'utf8')
346
+ assert.ok(shared.includes('`.codex/skills/antislop/SKILL.md`'))
347
+ assert.ok(shared.includes('`.opencode/skills/antislop-ui/SKILL.md`'))
348
+ assert.equal(shared.includes('antislop-copywriting'), false)
349
+ assert.ok(separate.includes('`.claude/skills/antislop-copywriting/SKILL.md`'))
350
+ assert.equal(separate.includes('antislop-ui'), false)
351
+ for (const pointer of [shared, separate]) {
352
+ for (const match of pointer.matchAll(/`([^`]+\/SKILL\.md)`/g)) assert.ok(fs.existsSync(match[1]))
353
+ }
354
+ console.log('ok K mixed selections retain valid per-skill paths and separate entry files')
355
+ } finally {
356
+ process.chdir(here)
357
+ fs.rmSync(mixed, { recursive: true, force: true })
358
+ }
359
+
360
+ // A door dropped from the list stops being named after an install, and nothing else notices.
361
+ check(
362
+ 'K all seven plugin doors are probed',
363
+ PLUGIN_DOORS.map((d) => d.id).sort(),
364
+ ['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kimi', 'omp']
365
+ )
366
+
367
+ // A door row without a command would print a blank line where the answer belongs.
368
+ check('K every plugin door names itself and its command', PLUGIN_DOORS.filter((d) => !d.id || !d.label || !d.update).map((d) => d.id), [])
369
+
370
+ // A store holding an old copy beside the new one used to report whichever folder listed
371
+ // first. The old copy is named to list first, so only a version sort reports 3.2.22.
372
+ // The home is the throwaway one smoke-test.mjs launched this worker with. Writing here
373
+ // outside that run would touch a real install, so refuse instead.
374
+ const fakeHome = os.homedir()
375
+ const inTmp = path.relative(fs.realpathSync(os.tmpdir()), fs.realpathSync(fakeHome))
376
+ if (inTmp.startsWith('..') || path.isAbsolute(inTmp)) {
377
+ console.error(`refusing to write into ${fakeHome}: run this worker through smoke-test.mjs`)
378
+ process.exit(1)
379
+ }
380
+ try {
381
+ const copy = (dir, version) => {
382
+ fs.mkdirSync(path.join(fakeHome, dir, 'skills', CORE), { recursive: true })
383
+ fs.writeFileSync(path.join(fakeHome, dir, 'skills', CORE, 'VERSION'), version + '\n')
384
+ }
385
+ for (const [name, version] of [['aa-old', '3.2.9'], ['zz-new', '3.2.22']]) {
386
+ copy(path.join('.kimi-code', 'plugins', 'managed', name), version)
387
+ copy(path.join('.cline', 'plugins', '_installed', name, 'hash'), version)
388
+ copy(path.join('.omp', 'plugins', name), version)
389
+ fs.mkdirSync(path.join(fakeHome, '.codex', 'plugins', 'cache', 'anti-slop', CORE, version), { recursive: true })
390
+ }
391
+ check(
392
+ 'L plugin doors report the newest copy in a store',
393
+ detectPluginDoors().map((d) => `${d.door.id} ${d.version}`).sort(),
394
+ ['cline 3.2.22', 'codex 3.2.22', 'kimi 3.2.22', 'omp 3.2.22']
395
+ )
396
+
397
+ // Run from home, --update must treat it as the global scope only.
398
+ installSkills({ skills: [CORE], targets: resolveTargets('global', ['claude']), overwrite: false })
399
+ process.chdir(fakeHome)
400
+ check('L update from home touches each folder once', updateAll().results.map((r) => r.location), ['global'])
401
+ check('L update from home writes no pointer', fs.existsSync(path.join(fakeHome, 'CLAUDE.md')), false)
402
+ check('L install from home writes no pointer', updatePointers({ targets: resolveTargets('project', ['claude']), skills: [CORE] }), [])
403
+ } finally {
404
+ process.chdir(here)
405
+ }
406
+
407
+ if (failures.length > 0) {
408
+ console.error(`\n${failures.length} check(s) failed: ${failures.join(', ')}`)
409
+ process.exit(1)
410
+ }
411
+ console.log('\nall checks passed')
@@ -0,0 +1,81 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
+ const repoRoot = path.join(__dirname, '..', '..')
7
+ const repoSkills = path.join(repoRoot, 'skills')
8
+ const cliSkills = path.join(__dirname, '..', 'skills')
9
+
10
+ const CORE_FRONTMATTER = [
11
+ '---',
12
+ 'name: antislop',
13
+ 'description: "Anti Slop: Rules for AI Coding Agents. The core filter. Load always to stop generic AI slop."',
14
+ 'allowed-tools: Read Write Edit Glob Grep',
15
+ '---',
16
+ '',
17
+ ].join('\n')
18
+
19
+ const version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version
20
+
21
+ const coreBody = fs.readFileSync(path.join(repoRoot, 'antislop.md'), 'utf8')
22
+ .replace(/\r\n/g, '\n')
23
+ .trim()
24
+
25
+ // Snyk W012: a shipped skill must not tell the agent to download its own instructions.
26
+ if (/https?:\/\/raw\.githubusercontent\.com/.test(coreBody)) {
27
+ throw new Error('antislop.md carries a runtime download URL; remove it before syncing (Snyk W012)')
28
+ }
29
+
30
+ // The wizard installs the single-file route. Every route that ships this SKILL.md has already
31
+ // placed the folders, so the wizard would only cost context there; the update steps stay.
32
+ const WIZARD = /^## First-Run Install Wizard\n[\s\S]*?^### (Already installed, and the user asks how to update)$/m
33
+ if (!WIZARD.test(coreBody)) throw new Error('antislop.md: install wizard headings not found')
34
+ const skillBody = coreBody.replace(WIZARD, '## $1')
35
+ fs.writeFileSync(path.join(repoSkills, 'antislop', 'SKILL.md'), CORE_FRONTMATTER + skillBody + '\n')
36
+
37
+ // An installed copy is a snapshot with nothing in it that names the release it came from,
38
+ // so an update could not say what was on disk. Every install route copies this folder.
39
+ fs.writeFileSync(path.join(repoSkills, 'antislop', 'VERSION'), version + '\n')
40
+
41
+ // Python leaves a __pycache__ beside the contrast checker; it must not reach the tarball.
42
+ fs.rmSync(cliSkills, { recursive: true, force: true })
43
+ fs.cpSync(repoSkills, cliSkills, {
44
+ recursive: true,
45
+ filter: (src) => path.basename(src) !== '__pycache__',
46
+ })
47
+
48
+ // npm only ships a README and LICENSE found in cli/, so the package page and Socket saw none.
49
+ // Relative links point at files that are not in the tarball, so aim them at the repo.
50
+ const REPO = 'https://github.com/miqdadbadjuber/anti-slop'
51
+ const readme = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8')
52
+ .replace(/src="(?:\.\/)?(?!https?:|#)([^"]+)"/g, `src="${REPO}/raw/main/$1"`)
53
+ .replace(/href="(?:\.\/)?(?!https?:|#|mailto:)([^"]+)"/g, `href="${REPO}/blob/main/$1"`)
54
+ .replace(/\]\((?:\.\/)?(?!https?:|#|mailto:)([^)]+)\)/g, `](${REPO}/blob/main/$1)`)
55
+ fs.writeFileSync(path.join(__dirname, '..', 'README.md'), readme)
56
+ fs.copyFileSync(path.join(repoRoot, 'LICENSE'), path.join(__dirname, '..', 'LICENSE'))
57
+
58
+ // A Windows checkout hands back CRLF, and a shipped skill would then carry "name: x\r"
59
+ // into any registry that splits on \n alone. Ship LF whatever the checkout did.
60
+ const TEXT = new Set(['.md', '.py', '.json', '.txt', '.mjs'])
61
+ let normalised = 0
62
+ const normalise = (dir) => {
63
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
64
+ const file = path.join(dir, e.name)
65
+ if (e.isDirectory()) {
66
+ normalise(file)
67
+ continue
68
+ }
69
+ if (!TEXT.has(path.extname(e.name))) continue
70
+ const buf = fs.readFileSync(file)
71
+ if (!buf.includes(0x0d)) continue
72
+ fs.writeFileSync(file, buf.toString('utf8').replace(/\r\n/g, '\n'), 'utf8')
73
+ normalised++
74
+ }
75
+ }
76
+ normalise(cliSkills)
77
+
78
+ console.log(
79
+ `Regenerated skills/antislop/SKILL.md and VERSION (${version}) and synced to cli/skills/` +
80
+ (normalised ? ` (${normalised} file(s) normalised to LF)` : '')
81
+ )
@@ -0,0 +1,2 @@
1
+ description = "Quality audit of the whole repo: bugs, security, load, tests, speed, bloat"
2
+ prompt = "Audit the whole repository (or the folder the user names) like the senior developer who just inherited it. Map first: README, deploy and build config, dependencies, entry points, tests; find the expected load (one person or many users and processes); trace the main flows and read fully the paths with user input, money, auth, data writes, background jobs and shared state. Look for, in this order: bugs, risk (security holes, data loss), scale (races, the same work done by every process, memory that only grows, a query per item, O(n^2)), risky logic without a test, slow code (big = problem, small = suggestion), lean (dead code, duplicate helpers, stdlib or native instead of custom code or a dependency, abstractions with one implementation, near-copies to merge, a function doing several unrelated jobs to split, never split by line count). Before calling code unused, grep the whole tree including tests, fixtures, config and string references. Every finding needs a concrete case. A shortcut marked with a shortcut: (or older ponytail:) comment that names its limit is a decision, not a finding. Propose the smallest fix, prefer deleting code, never add layers. Very simple English. Start with 'What this repo does:' and the load you assumed. Then at most 20 numbered findings grouped Must fix / Should fix / Nice to have, most important first; each: a bold title with file:lines, then four bullets: What this is, Problem, Fix, If we skip it. End with 'Verdict:', 'Lean: -N lines, -M dependencies possible.' and 'Not checked:' for the parts you did not read. Change no code."
@@ -0,0 +1,2 @@
1
+ description = "Harvest shortcut: comments into a tracked debt ledger"
2
+ prompt = "Harvest every `shortcut:` comment (and older `ponytail:` ones) in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (git grep --no-index --exclude-standard -nE '(#|//|/[*]) ?(shortcut|ponytail):', skipping .git and everything .gitignore ignores; if the user names their own marker word, grep for that instead; skip hits that are not a deferral, like a keyboard-shortcut note). One row per marker, grouped by file: <file>:<line> — <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No shortcut debt. Clean ledger.' Report only, change nothing."
@@ -0,0 +1,2 @@
1
+ description = "Show ponytail's measured impact scoreboard (less code, cost, time)"
2
+ prompt = "Show the ponytail gain scoreboard. One shot, change nothing: do not switch mode, write flag files, or persist anything. Render the published agentic benchmark of Ponytail 5 (39 tasks on a real FastAPI + React repo plus bug fixes, security and privacy cases and small apps, Opus 5.5, 5 runs each, against the same agent without the skill; geometric mean of per-task medians; source benchmarks/results/2026-10-07-agentic.md and the README) as plain ASCII bars, ponytail as a share of the no-skill baseline (100%): Lines of code 47% (down 53%); Output tokens 55% (down 45%); Cost 74% (down 26%); Time 59% (down 41%); Hidden checks passed 97% (no-skill 96%; 18 of the tasks have hidden checks for correctness and safety); Tests where the logic needs one 98% (no-skill 68%). The bar length shows the share, the label carries the exact figure. These are benchmark averages, not this repo. NEVER print a per-repo savings number: the unbuilt version was never written, so there is no real baseline to subtract from in a live repo. For real per-repo figures, point to /ponytail-debt (the counted shortcut ledger) and /ponytail-audit (what is still cuttable). Report only."
@@ -0,0 +1,2 @@
1
+ description = "Quick reference for ponytail levels, skills, and commands"
2
+ prompt = "Show the ponytail quick reference. One shot, change nothing: do not switch mode, write flag files, or persist anything. Levels: /ponytail lite (build what's asked, name the lazier alternative in one line), /ponytail (full, the default ladder: YAGNI then stdlib then native then one line then minimum), /ponytail ultra (deletion before addition, challenges the requirement before building). Commands: /ponytail-review (over-engineering review of the current changes), /ponytail-audit (whole-repo over-engineering audit), /ponytail-debt (harvest shortcut: comments into a tracked ledger), /ponytail-gain (measured-impact scoreboard from the benchmark), /ponytail-help (this card). Deactivate with 'stop ponytail', 'normal mode', or /ponytail off; resume anytime with /ponytail. Default mode is full; change it with the PONYTAIL_DEFAULT_MODE environment variable (off|lite|full|ultra) or a config file at ~/.config/ponytail/config.json (Windows: %APPDATA%\\ponytail\\config.json) with {\"defaultMode\": \"lite\"}. Resolution order: env var, then config file, then full."
@@ -0,0 +1,2 @@
1
+ description = "Quality review of the current changes: bugs, security, load, tests, speed, bloat"
2
+ prompt = "Review the current code changes like the senior developer who will be paged when they break. Review what the user names (uncommitted or staged changes, a branch, a PR link, files); nothing named: the uncommitted changes, or the last commit if there are none. First read the code the change touches: callers of every changed function, the functions it calls, the tests, the README (find the expected load: one person or many users and processes). A change can break code it does not touch. Look for, in this order: bugs (wrong result, crash, missed edge case, a caller broken by the change, a fix in one caller while the shared function stays broken), risk (security holes, data loss), scale (check-then-write races, the same work done by every process, memory that only grows, a query per item, O(n^2) on big input), risky logic without a test, slow code (big = problem, small = suggestion), lean (dead code, a helper the repo already has, stdlib or native instead of custom code or a new dependency, abstractions with one implementation, near-copies to merge, a function doing several unrelated jobs to split, never split by line count). Every finding needs a concrete case; re-read the lines to confirm it. A shortcut marked with a shortcut: (or older ponytail:) comment that names its limit is a decision, not a finding. Propose the smallest fix, prefer deleting code, never add layers. No style taste, no vague worries. Very simple English, explain technical terms. Start with 'What this change does:' in two or three sentences and the load you assumed. Then numbered findings grouped Must fix / Should fix / Nice to have; each finding: a bold title with file:lines, then four bullets: What this is, Problem, Fix, If we skip it (one or two short sentences each). End with 'Verdict:', 'Lean: -N lines possible.' when lean findings exist, and 'Not checked:' if something mattered and you could not check it. Nothing found: 'Looks good. Ship.' Change no code."
@@ -0,0 +1,2 @@
1
+ description = "Switch ponytail intensity level (lite/full/ultra/off)"
2
+ prompt = "Switch to ponytail {{args}} mode. With no level, keep the current level, or switch on at the default level if ponytail was off. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a shortcut: comment that names the ceiling and upgrade path."