ai-employees 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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/TRADEMARKS.md +45 -0
  4. package/docs/COST.md +59 -0
  5. package/docs/FAQ.md +39 -0
  6. package/docs/GUARDRAILS.md +17 -0
  7. package/docs/HARNESSES.md +38 -0
  8. package/docs/HOW-EMPLOYEES-WORK.md +86 -0
  9. package/docs/INSTALL.md +148 -0
  10. package/docs/PREREQUISITES.md +91 -0
  11. package/docs/STANDARD.md +194 -0
  12. package/docs/UPGRADING.md +74 -0
  13. package/docs/WHAT-SETS-THEM-APART.md +10 -0
  14. package/employees/ad-manager-employee/AGENTS.md +46 -0
  15. package/employees/ad-manager-employee/CAPABILITIES.md +1002 -0
  16. package/employees/ad-manager-employee/CHANGELOG.md +99 -0
  17. package/employees/ad-manager-employee/CONTRACT.md +977 -0
  18. package/employees/ad-manager-employee/INSTALL-PROMPT.md +263 -0
  19. package/employees/ad-manager-employee/README.md +357 -0
  20. package/employees/ad-manager-employee/RELEASES.md +25 -0
  21. package/employees/ad-manager-employee/ROLE.md +617 -0
  22. package/employees/ad-manager-employee/SCHEDULE.md +275 -0
  23. package/employees/ad-manager-employee/VERSION +1 -0
  24. package/employees/ad-manager-employee/employee.json +185 -0
  25. package/employees/ad-manager-employee/examples/README.md +21 -0
  26. package/employees/ad-manager-employee/examples/board/LAUNCH-BOARD.md +19 -0
  27. package/employees/ad-manager-employee/examples/board/board.json +51 -0
  28. package/employees/ad-manager-employee/examples/brief-latest.md +20 -0
  29. package/employees/ad-manager-employee/examples/build/campaign-storm-repair.md +51 -0
  30. package/employees/ad-manager-employee/examples/creative/set-2026-03-04-fast-estimate/set.md +31 -0
  31. package/employees/ad-manager-employee/examples/metrics/daily.jsonl +2 -0
  32. package/employees/ad-manager-employee/examples/runlog.jsonl +3 -0
  33. package/employees/ad-manager-employee/recipes/BROWSER-RECIPES.md +558 -0
  34. package/employees/ad-manager-employee/recipes/META-ADS-RECIPES.md +100 -0
  35. package/employees/ad-manager-employee/routines/ads-account-intake/SKILL.md +929 -0
  36. package/employees/ad-manager-employee/routines/ads-account-read/SKILL.md +780 -0
  37. package/employees/ad-manager-employee/routines/ads-build-desk/SKILL.md +915 -0
  38. package/employees/ad-manager-employee/routines/ads-change-list/SKILL.md +818 -0
  39. package/employees/ad-manager-employee/routines/ads-creative-retro/SKILL.md +807 -0
  40. package/employees/ad-manager-employee/routines/ads-creative-studio/SKILL.md +802 -0
  41. package/employees/ad-manager-employee/routines/ads-desk-standup/SKILL.md +822 -0
  42. package/employees/ad-manager-employee/run/ads-account-intake.cmd.example +11 -0
  43. package/employees/ad-manager-employee/run/ads-account-read.cmd.example +11 -0
  44. package/employees/ad-manager-employee/run/ads-build-desk.cmd.example +11 -0
  45. package/employees/ad-manager-employee/run/ads-change-list.cmd.example +11 -0
  46. package/employees/ad-manager-employee/run/ads-creative-retro.cmd.example +11 -0
  47. package/employees/ad-manager-employee/run/ads-creative-studio.cmd.example +11 -0
  48. package/employees/ad-manager-employee/run/ads-desk-standup.cmd.example +11 -0
  49. package/employees/ad-manager-employee/scripts/copy-check.mjs +815 -0
  50. package/employees/ad-manager-employee/scripts/guard.mjs +447 -0
  51. package/employees/ad-manager-employee/scripts/review.mjs +443 -0
  52. package/employees/ad-manager-employee/scripts/runlog.mjs +785 -0
  53. package/employees/chief-of-staff/AGENTS.md +46 -0
  54. package/employees/chief-of-staff/CAPABILITIES.md +875 -0
  55. package/employees/chief-of-staff/CHANGELOG.md +91 -0
  56. package/employees/chief-of-staff/CONTRACT.md +1039 -0
  57. package/employees/chief-of-staff/INSTALL-PROMPT.md +206 -0
  58. package/employees/chief-of-staff/README.md +382 -0
  59. package/employees/chief-of-staff/RELEASES.md +22 -0
  60. package/employees/chief-of-staff/ROLE.md +425 -0
  61. package/employees/chief-of-staff/SCHEDULE.md +288 -0
  62. package/employees/chief-of-staff/VERSION +1 -0
  63. package/employees/chief-of-staff/employee.json +214 -0
  64. package/employees/chief-of-staff/examples/README.md +15 -0
  65. package/employees/chief-of-staff/examples/brief-latest.md +20 -0
  66. package/employees/chief-of-staff/examples/decisions/REGISTER.md +22 -0
  67. package/employees/chief-of-staff/examples/dossiers/dossier-seo-employee--seo-publish-run--silent-stop.md +43 -0
  68. package/employees/chief-of-staff/examples/fleet/fleet.json +119 -0
  69. package/employees/chief-of-staff/examples/runlog.jsonl +3 -0
  70. package/employees/chief-of-staff/recipes/BROWSER-RECIPES.md +512 -0
  71. package/employees/chief-of-staff/routines/cos-charter-and-fleet-audit/SKILL.md +1008 -0
  72. package/employees/chief-of-staff/routines/cos-decision-brief/SKILL.md +639 -0
  73. package/employees/chief-of-staff/routines/cos-decision-review/SKILL.md +644 -0
  74. package/employees/chief-of-staff/routines/cos-fault-dossier/SKILL.md +656 -0
  75. package/employees/chief-of-staff/routines/cos-fleet-reconcile/SKILL.md +942 -0
  76. package/employees/chief-of-staff/routines/cos-market-sweep/SKILL.md +655 -0
  77. package/employees/chief-of-staff/routines/cos-metrics-review/SKILL.md +746 -0
  78. package/employees/chief-of-staff/run/cos-charter-and-fleet-audit.cmd.example +11 -0
  79. package/employees/chief-of-staff/run/cos-decision-brief.cmd.example +11 -0
  80. package/employees/chief-of-staff/run/cos-decision-review.cmd.example +11 -0
  81. package/employees/chief-of-staff/run/cos-fault-dossier.cmd.example +11 -0
  82. package/employees/chief-of-staff/run/cos-fleet-reconcile.cmd.example +11 -0
  83. package/employees/chief-of-staff/run/cos-market-sweep.cmd.example +11 -0
  84. package/employees/chief-of-staff/run/cos-metrics-review.cmd.example +11 -0
  85. package/employees/chief-of-staff/scripts/copy-check.mjs +812 -0
  86. package/employees/chief-of-staff/scripts/guard.mjs +447 -0
  87. package/employees/chief-of-staff/scripts/runlog.mjs +766 -0
  88. package/employees/customer-satisfaction-employee/AGENTS.md +47 -0
  89. package/employees/customer-satisfaction-employee/CAPABILITIES.md +910 -0
  90. package/employees/customer-satisfaction-employee/CHANGELOG.md +91 -0
  91. package/employees/customer-satisfaction-employee/CONTRACT.md +1121 -0
  92. package/employees/customer-satisfaction-employee/INSTALL-PROMPT.md +290 -0
  93. package/employees/customer-satisfaction-employee/README.md +378 -0
  94. package/employees/customer-satisfaction-employee/RELEASES.md +22 -0
  95. package/employees/customer-satisfaction-employee/ROLE.md +393 -0
  96. package/employees/customer-satisfaction-employee/SCHEDULE.md +283 -0
  97. package/employees/customer-satisfaction-employee/VERSION +1 -0
  98. package/employees/customer-satisfaction-employee/employee.json +231 -0
  99. package/employees/customer-satisfaction-employee/examples/README.md +15 -0
  100. package/employees/customer-satisfaction-employee/examples/brief-latest.md +17 -0
  101. package/employees/customer-satisfaction-employee/examples/desk/DESK-BOARD.md +18 -0
  102. package/employees/customer-satisfaction-employee/examples/queue/2026-03-05-reply.md +36 -0
  103. package/employees/customer-satisfaction-employee/examples/runlog.jsonl +3 -0
  104. package/employees/customer-satisfaction-employee/examples/tickets/tickets.jsonl +2 -0
  105. package/employees/customer-satisfaction-employee/recipes/BROWSER-RECIPES.md +549 -0
  106. package/employees/customer-satisfaction-employee/routines/csat-churn-watch/SKILL.md +709 -0
  107. package/employees/customer-satisfaction-employee/routines/csat-deflection-desk/SKILL.md +671 -0
  108. package/employees/customer-satisfaction-employee/routines/csat-desk-intake/SKILL.md +1115 -0
  109. package/employees/customer-satisfaction-employee/routines/csat-desk-standup/SKILL.md +831 -0
  110. package/employees/customer-satisfaction-employee/routines/csat-inbox-sweep/SKILL.md +673 -0
  111. package/employees/customer-satisfaction-employee/routines/csat-reply-desk/SKILL.md +755 -0
  112. package/employees/customer-satisfaction-employee/routines/csat-satisfaction-report/SKILL.md +844 -0
  113. package/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh/SKILL.md +798 -0
  114. package/employees/customer-satisfaction-employee/run/csat-churn-watch.cmd.example +11 -0
  115. package/employees/customer-satisfaction-employee/run/csat-deflection-desk.cmd.example +11 -0
  116. package/employees/customer-satisfaction-employee/run/csat-desk-intake.cmd.example +11 -0
  117. package/employees/customer-satisfaction-employee/run/csat-desk-standup.cmd.example +11 -0
  118. package/employees/customer-satisfaction-employee/run/csat-inbox-sweep.cmd.example +11 -0
  119. package/employees/customer-satisfaction-employee/run/csat-reply-desk.cmd.example +11 -0
  120. package/employees/customer-satisfaction-employee/run/csat-satisfaction-report.cmd.example +11 -0
  121. package/employees/customer-satisfaction-employee/run/csat-taxonomy-refresh.cmd.example +11 -0
  122. package/employees/customer-satisfaction-employee/scripts/copy-check.mjs +888 -0
  123. package/employees/customer-satisfaction-employee/scripts/guard.mjs +447 -0
  124. package/employees/customer-satisfaction-employee/scripts/runlog.mjs +778 -0
  125. package/employees/gtm-engineer/AGENTS.md +47 -0
  126. package/employees/gtm-engineer/CAPABILITIES.md +929 -0
  127. package/employees/gtm-engineer/CHANGELOG.md +93 -0
  128. package/employees/gtm-engineer/CONTRACT.md +977 -0
  129. package/employees/gtm-engineer/INSTALL-PROMPT.md +237 -0
  130. package/employees/gtm-engineer/README.md +355 -0
  131. package/employees/gtm-engineer/RELEASES.md +22 -0
  132. package/employees/gtm-engineer/ROLE.md +551 -0
  133. package/employees/gtm-engineer/SCHEDULE.md +265 -0
  134. package/employees/gtm-engineer/VERSION +1 -0
  135. package/employees/gtm-engineer/employee.json +230 -0
  136. package/employees/gtm-engineer/examples/README.md +15 -0
  137. package/employees/gtm-engineer/examples/board/LAUNCH-BOARD.md +12 -0
  138. package/employees/gtm-engineer/examples/brief-latest.md +19 -0
  139. package/employees/gtm-engineer/examples/crm/contacts.csv +4 -0
  140. package/employees/gtm-engineer/examples/queue/2026-03-05-email.md +26 -0
  141. package/employees/gtm-engineer/examples/runlog.jsonl +3 -0
  142. package/employees/gtm-engineer/recipes/BROWSER-RECIPES.md +582 -0
  143. package/employees/gtm-engineer/routines/gtm-board-standup/SKILL.md +745 -0
  144. package/employees/gtm-engineer/routines/gtm-icp-refresh/SKILL.md +647 -0
  145. package/employees/gtm-engineer/routines/gtm-intake-and-dashboard/SKILL.md +1046 -0
  146. package/employees/gtm-engineer/routines/gtm-launch-step-runner/SKILL.md +802 -0
  147. package/employees/gtm-engineer/routines/gtm-outreach-queue/SKILL.md +708 -0
  148. package/employees/gtm-engineer/routines/gtm-paid-and-tracking-guard/SKILL.md +970 -0
  149. package/employees/gtm-engineer/routines/gtm-scoreboard/SKILL.md +740 -0
  150. package/employees/gtm-engineer/routines/gtm-signal-sweep/SKILL.md +648 -0
  151. package/employees/gtm-engineer/run/gtm-board-standup.cmd.example +11 -0
  152. package/employees/gtm-engineer/run/gtm-icp-refresh.cmd.example +11 -0
  153. package/employees/gtm-engineer/run/gtm-intake-and-dashboard.cmd.example +11 -0
  154. package/employees/gtm-engineer/run/gtm-launch-step-runner.cmd.example +11 -0
  155. package/employees/gtm-engineer/run/gtm-outreach-queue.cmd.example +11 -0
  156. package/employees/gtm-engineer/run/gtm-paid-and-tracking-guard.cmd.example +11 -0
  157. package/employees/gtm-engineer/run/gtm-scoreboard.cmd.example +11 -0
  158. package/employees/gtm-engineer/run/gtm-signal-sweep.cmd.example +11 -0
  159. package/employees/gtm-engineer/scripts/copy-check.mjs +815 -0
  160. package/employees/gtm-engineer/scripts/guard.mjs +447 -0
  161. package/employees/gtm-engineer/scripts/runlog.mjs +772 -0
  162. package/employees/sales-employee/AGENTS.md +46 -0
  163. package/employees/sales-employee/CAPABILITIES.md +880 -0
  164. package/employees/sales-employee/CHANGELOG.md +92 -0
  165. package/employees/sales-employee/CONTRACT.md +1161 -0
  166. package/employees/sales-employee/INSTALL-PROMPT.md +256 -0
  167. package/employees/sales-employee/README.md +372 -0
  168. package/employees/sales-employee/RELEASES.md +22 -0
  169. package/employees/sales-employee/ROLE.md +319 -0
  170. package/employees/sales-employee/SCHEDULE.md +263 -0
  171. package/employees/sales-employee/VERSION +1 -0
  172. package/employees/sales-employee/employee.json +202 -0
  173. package/employees/sales-employee/examples/README.md +16 -0
  174. package/employees/sales-employee/examples/brief-latest.md +17 -0
  175. package/employees/sales-employee/examples/crm/contacts.csv +4 -0
  176. package/employees/sales-employee/examples/crm/prospects.jsonl +2 -0
  177. package/employees/sales-employee/examples/pipeline/PIPELINE.md +18 -0
  178. package/employees/sales-employee/examples/queue/2026-03-05-first-touch.md +29 -0
  179. package/employees/sales-employee/examples/runlog.jsonl +3 -0
  180. package/employees/sales-employee/recipes/BROWSER-RECIPES.md +535 -0
  181. package/employees/sales-employee/routines/sales-desk-setup/SKILL.md +896 -0
  182. package/employees/sales-employee/routines/sales-desk-standup/SKILL.md +818 -0
  183. package/employees/sales-employee/routines/sales-first-touch-drafts/SKILL.md +681 -0
  184. package/employees/sales-employee/routines/sales-followup-sweep/SKILL.md +736 -0
  185. package/employees/sales-employee/routines/sales-pipeline-review/SKILL.md +765 -0
  186. package/employees/sales-employee/routines/sales-prospect-sweep/SKILL.md +696 -0
  187. package/employees/sales-employee/routines/sales-qualification-refresh/SKILL.md +743 -0
  188. package/employees/sales-employee/run/sales-desk-setup.cmd.example +11 -0
  189. package/employees/sales-employee/run/sales-desk-standup.cmd.example +11 -0
  190. package/employees/sales-employee/run/sales-first-touch-drafts.cmd.example +11 -0
  191. package/employees/sales-employee/run/sales-followup-sweep.cmd.example +11 -0
  192. package/employees/sales-employee/run/sales-pipeline-review.cmd.example +11 -0
  193. package/employees/sales-employee/run/sales-prospect-sweep.cmd.example +11 -0
  194. package/employees/sales-employee/run/sales-qualification-refresh.cmd.example +11 -0
  195. package/employees/sales-employee/scripts/copy-check.mjs +815 -0
  196. package/employees/sales-employee/scripts/guard.mjs +447 -0
  197. package/employees/sales-employee/scripts/runlog.mjs +773 -0
  198. package/employees/seo-employee/AEO-PLAYBOOK.md +51 -0
  199. package/employees/seo-employee/AGENTS.md +47 -0
  200. package/employees/seo-employee/CAPABILITIES.md +1006 -0
  201. package/employees/seo-employee/CHANGELOG.md +95 -0
  202. package/employees/seo-employee/CONTRACT.md +1020 -0
  203. package/employees/seo-employee/INSTALL-PROMPT.md +233 -0
  204. package/employees/seo-employee/README.md +368 -0
  205. package/employees/seo-employee/RELEASES.md +22 -0
  206. package/employees/seo-employee/ROLE.md +331 -0
  207. package/employees/seo-employee/SCHEDULE.md +267 -0
  208. package/employees/seo-employee/VERSION +1 -0
  209. package/employees/seo-employee/employee.json +233 -0
  210. package/employees/seo-employee/examples/README.md +14 -0
  211. package/employees/seo-employee/examples/board/WORK-BOARD.md +16 -0
  212. package/employees/seo-employee/examples/brief-latest.md +16 -0
  213. package/employees/seo-employee/examples/content/drafts.jsonl +2 -0
  214. package/employees/seo-employee/examples/content/published.jsonl +2 -0
  215. package/employees/seo-employee/examples/drafts/roof-lifespan-by-material/body.md +39 -0
  216. package/employees/seo-employee/examples/drafts/roof-lifespan-by-material/meta.json +16 -0
  217. package/employees/seo-employee/examples/runlog.jsonl +3 -0
  218. package/employees/seo-employee/recipes/BROWSER-RECIPES.md +576 -0
  219. package/employees/seo-employee/routines/seo-answer-visibility/SKILL.md +55 -0
  220. package/employees/seo-employee/routines/seo-calendar-refill/SKILL.md +614 -0
  221. package/employees/seo-employee/routines/seo-draft-run/SKILL.md +722 -0
  222. package/employees/seo-employee/routines/seo-index-sweep/SKILL.md +629 -0
  223. package/employees/seo-employee/routines/seo-intake-and-map/SKILL.md +916 -0
  224. package/employees/seo-employee/routines/seo-publish-run/SKILL.md +713 -0
  225. package/employees/seo-employee/routines/seo-rank-review/SKILL.md +795 -0
  226. package/employees/seo-employee/routines/seo-standup/SKILL.md +792 -0
  227. package/employees/seo-employee/run/seo-answer-visibility.cmd.example +11 -0
  228. package/employees/seo-employee/run/seo-calendar-refill.cmd.example +11 -0
  229. package/employees/seo-employee/run/seo-draft-run.cmd.example +11 -0
  230. package/employees/seo-employee/run/seo-index-sweep.cmd.example +11 -0
  231. package/employees/seo-employee/run/seo-intake-and-map.cmd.example +11 -0
  232. package/employees/seo-employee/run/seo-publish-run.cmd.example +11 -0
  233. package/employees/seo-employee/run/seo-rank-review.cmd.example +11 -0
  234. package/employees/seo-employee/run/seo-standup.cmd.example +11 -0
  235. package/employees/seo-employee/scripts/answer-audit.mjs +63 -0
  236. package/employees/seo-employee/scripts/copy-check.mjs +843 -0
  237. package/employees/seo-employee/scripts/guard.mjs +447 -0
  238. package/employees/seo-employee/scripts/runlog.mjs +864 -0
  239. package/employees/seo-employee/standards/PUBLISH-STANDARD.md +229 -0
  240. package/employees/social-media-employee/AGENTS.md +46 -0
  241. package/employees/social-media-employee/CAPABILITIES.md +1015 -0
  242. package/employees/social-media-employee/CHANGELOG.md +91 -0
  243. package/employees/social-media-employee/CONTRACT.md +1036 -0
  244. package/employees/social-media-employee/INSTALL-PROMPT.md +234 -0
  245. package/employees/social-media-employee/README.md +386 -0
  246. package/employees/social-media-employee/RELEASES.md +22 -0
  247. package/employees/social-media-employee/ROLE.md +376 -0
  248. package/employees/social-media-employee/SCHEDULE.md +278 -0
  249. package/employees/social-media-employee/VERSION +1 -0
  250. package/employees/social-media-employee/employee.json +195 -0
  251. package/employees/social-media-employee/examples/README.md +20 -0
  252. package/employees/social-media-employee/examples/brief-latest.md +19 -0
  253. package/employees/social-media-employee/examples/calendar/CALENDAR.md +23 -0
  254. package/employees/social-media-employee/examples/calendar/calendar.json +30 -0
  255. package/employees/social-media-employee/examples/posts/posts.jsonl +2 -0
  256. package/employees/social-media-employee/examples/queue/2026-03-05-business-network.md +28 -0
  257. package/employees/social-media-employee/examples/runlog.jsonl +3 -0
  258. package/employees/social-media-employee/recipes/BROWSER-RECIPES.md +550 -0
  259. package/employees/social-media-employee/routines/soc-calendar-standup/SKILL.md +783 -0
  260. package/employees/social-media-employee/routines/soc-draft-queue/SKILL.md +683 -0
  261. package/employees/social-media-employee/routines/soc-engagement-sweep/SKILL.md +636 -0
  262. package/employees/social-media-employee/routines/soc-intake-and-voice/SKILL.md +909 -0
  263. package/employees/social-media-employee/routines/soc-material-sweep/SKILL.md +658 -0
  264. package/employees/social-media-employee/routines/soc-performance-review/SKILL.md +795 -0
  265. package/employees/social-media-employee/routines/soc-publish-run/SKILL.md +643 -0
  266. package/employees/social-media-employee/run/soc-calendar-standup.cmd.example +11 -0
  267. package/employees/social-media-employee/run/soc-draft-queue.cmd.example +11 -0
  268. package/employees/social-media-employee/run/soc-engagement-sweep.cmd.example +11 -0
  269. package/employees/social-media-employee/run/soc-intake-and-voice.cmd.example +11 -0
  270. package/employees/social-media-employee/run/soc-material-sweep.cmd.example +11 -0
  271. package/employees/social-media-employee/run/soc-performance-review.cmd.example +11 -0
  272. package/employees/social-media-employee/run/soc-publish-run.cmd.example +11 -0
  273. package/employees/social-media-employee/scripts/copy-check.mjs +950 -0
  274. package/employees/social-media-employee/scripts/guard.mjs +447 -0
  275. package/employees/social-media-employee/scripts/runlog.mjs +786 -0
  276. package/employees/web-dev-employee/AGENTS.md +47 -0
  277. package/employees/web-dev-employee/CAPABILITIES.md +950 -0
  278. package/employees/web-dev-employee/CHANGELOG.md +91 -0
  279. package/employees/web-dev-employee/CONTRACT.md +1052 -0
  280. package/employees/web-dev-employee/INSTALL-PROMPT.md +210 -0
  281. package/employees/web-dev-employee/README.md +352 -0
  282. package/employees/web-dev-employee/RELEASES.md +22 -0
  283. package/employees/web-dev-employee/ROLE.md +383 -0
  284. package/employees/web-dev-employee/SCHEDULE.md +280 -0
  285. package/employees/web-dev-employee/VERSION +1 -0
  286. package/employees/web-dev-employee/employee.json +228 -0
  287. package/employees/web-dev-employee/examples/README.md +13 -0
  288. package/employees/web-dev-employee/examples/board/REVIEW-BOARD.md +20 -0
  289. package/employees/web-dev-employee/examples/brief-latest.md +14 -0
  290. package/employees/web-dev-employee/examples/changes/2026-03-05-fix-C-005.md +30 -0
  291. package/employees/web-dev-employee/examples/changes/changes.jsonl +2 -0
  292. package/employees/web-dev-employee/examples/health/incidents.jsonl +2 -0
  293. package/employees/web-dev-employee/examples/runlog.jsonl +3 -0
  294. package/employees/web-dev-employee/recipes/BROWSER-RECIPES.md +477 -0
  295. package/employees/web-dev-employee/routines/web-dependency-run/SKILL.md +634 -0
  296. package/employees/web-dev-employee/routines/web-fix-runner/SKILL.md +754 -0
  297. package/employees/web-dev-employee/routines/web-guardrail-review/SKILL.md +613 -0
  298. package/employees/web-dev-employee/routines/web-inventory-refresh/SKILL.md +700 -0
  299. package/employees/web-dev-employee/routines/web-platform-guard/SKILL.md +707 -0
  300. package/employees/web-dev-employee/routines/web-site-sweep/SKILL.md +732 -0
  301. package/employees/web-dev-employee/routines/web-standup/SKILL.md +738 -0
  302. package/employees/web-dev-employee/routines/web-weekly-report/SKILL.md +623 -0
  303. package/employees/web-dev-employee/run/web-dependency-run.cmd.example +11 -0
  304. package/employees/web-dev-employee/run/web-fix-runner.cmd.example +11 -0
  305. package/employees/web-dev-employee/run/web-guardrail-review.cmd.example +11 -0
  306. package/employees/web-dev-employee/run/web-inventory-refresh.cmd.example +11 -0
  307. package/employees/web-dev-employee/run/web-platform-guard.cmd.example +11 -0
  308. package/employees/web-dev-employee/run/web-site-sweep.cmd.example +11 -0
  309. package/employees/web-dev-employee/run/web-standup.cmd.example +11 -0
  310. package/employees/web-dev-employee/run/web-weekly-report.cmd.example +11 -0
  311. package/employees/web-dev-employee/scripts/copy-check.mjs +872 -0
  312. package/employees/web-dev-employee/scripts/guard.mjs +447 -0
  313. package/employees/web-dev-employee/scripts/runlog.mjs +792 -0
  314. package/installer/cli.mjs +250 -0
  315. package/installer/upgrade.mjs +255 -0
  316. package/package.json +43 -0
  317. package/skills/hire/SKILL.md +72 -0
@@ -0,0 +1,872 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * copy-check.mjs
4
+ * The scripted judge for any text about to be written into a brief, a report,
5
+ * a change brief, a policy file, or a dashboard partial.
6
+ *
7
+ * The script is the judge, not a reading of the file. A stated preference has
8
+ * never been enough: every rule below exists because the same defect shipped
9
+ * at least once.
10
+ *
11
+ * Requires Node 18 or newer. No dependencies, no package.json, no install.
12
+ * Runs the same on Windows and POSIX.
13
+ *
14
+ * USAGE
15
+ * node scripts/copy-check.mjs --file <path> --dest <destination> [--json]
16
+ * node scripts/copy-check.mjs --file - --dest plain (reads stdin)
17
+ * node scripts/copy-check.mjs --file <path> --secrets-only [--json]
18
+ * node scripts/copy-check.mjs --selftest
19
+ * node scripts/copy-check.mjs --help
20
+ *
21
+ * --dest is one of: plain, brief, report, change, policy, dashboard
22
+ * There is no --profile, no --destination, and no bare positional path.
23
+ * Any call site using one of those is stale and this script will say so.
24
+ *
25
+ * --secrets-only is the secret.scan route. It runs rule 6 and nothing else,
26
+ * so a raw production log line can be scanned for a credential without
27
+ * being judged as prose. It takes no --dest. Use it before a log excerpt,
28
+ * a build failure reason, a package manager output line, or a diff is
29
+ * copied into any file in this kit.
30
+ *
31
+ * OPTIONS
32
+ * --json Print a machine readable verdict instead of a report.
33
+ * --quiet Print nothing. Use the exit code.
34
+ *
35
+ * THIS SCRIPT READS NO OTHER FILE. It judges the text it is handed and
36
+ * nothing else, so it behaves identically on a fresh install and on a kit
37
+ * that has been running for a year. There is no vocabulary file to fall out
38
+ * of date and no inventory of approved numbers to keep.
39
+ *
40
+ * WHAT IT FAILS, in the order it checks
41
+ * 1. An em dash or an en dash anywhere, including inside a code comment,
42
+ * and the HTML entity spellings of both.
43
+ * 2. A metric shaped digit sequence (a percentage, a currency amount, a
44
+ * multiplier, or a count of errors, incidents, checks, packages, days,
45
+ * or people) unless the line it sits on also names the file the number
46
+ * was counted in. A date, an ISO week, a clock time, and a card id are
47
+ * masked out first and are never read as a claim.
48
+ * 3. An unresolved guillemet placeholder. This kit has no sentinels: every
49
+ * guillemet in a generated file is a value that did not get filled.
50
+ * 4. A banned word, banned opener, or banned closer.
51
+ * 5. A hashtag. Nothing this Employee writes has one.
52
+ * 6. A secret shaped substring. Reports the class and the file name only,
53
+ * never the matched text.
54
+ * 7. A dotted token left bare in prose, which a publishing platform's
55
+ * autolinker rewrites into a broken link. OFF in every profile here:
56
+ * this Employee publishes nothing, and a bare domain name is its
57
+ * subject matter. See the note above rule 7 in the source.
58
+ *
59
+ * WHICH RULES APPLY WHERE
60
+ * rule plain brief report change policy dashboard
61
+ * dash y y y y y y
62
+ * metric y y y y y y
63
+ * placeholder y y y y y y
64
+ * secret y y y y y y
65
+ * voice y y y y n y
66
+ * hashtag y y y y n n
67
+ * autolink n n n n n n
68
+ *
69
+ * Every routine in this kit calls --dest plain, which is the strictest
70
+ * profile and the right default. The other five exist so a member can
71
+ * loosen one surface without loosening all of them.
72
+ *
73
+ * policy skips the voice rules because policy/safe-fix-rules.md is a table
74
+ * of rung names rather than prose, and a checker that fails a boundary file
75
+ * is a checker that gets turned off. dashboard skips hashtag and autolink
76
+ * because a hex colour and a markup attribute are not prose.
77
+ *
78
+ * EXIT CODES
79
+ * 0 clean
80
+ * 1 one or more violations
81
+ * 2 bad usage, or the input could not be read
82
+ */
83
+
84
+ import fs from "node:fs";
85
+ import { fileURLToPath } from "node:url";
86
+
87
+ const SELF = fileURLToPath(import.meta.url);
88
+
89
+ const DESTINATIONS = ["plain", "brief", "report", "change", "policy", "dashboard"];
90
+
91
+ // Rule 7, the autolinker rule, is off in every profile in this kit, and that
92
+ // is the one rule this Employee deliberately does not run. See the note above
93
+ // rule 7 below for why: a bare dotted token is this Employee's subject matter,
94
+ // not a hazard, because nothing it writes is ever published anywhere an
95
+ // autolinker could reach.
96
+ const PROFILE = {
97
+ plain: { voice: true, hashtag: true, autolink: false, markup: false },
98
+ brief: { voice: true, hashtag: true, autolink: false, markup: false },
99
+ report: { voice: true, hashtag: true, autolink: false, markup: false },
100
+ change: { voice: true, hashtag: true, autolink: false, markup: false },
101
+ policy: { voice: false, hashtag: false, autolink: false, markup: false },
102
+ dashboard: { voice: true, hashtag: false, autolink: false, markup: true },
103
+ };
104
+
105
+ // Destinations that belong to a sibling Employee rather than to this one.
106
+ // Naming the replacement is cheaper than a member reading the contract to
107
+ // work out why a call site stopped working.
108
+ const RETIRED_DESTINATIONS = {
109
+ email: "this Employee writes no outbound copy. Use plain",
110
+ dm: "this Employee writes no outbound copy. Use plain",
111
+ form: "this Employee fills no listing forms. Use plain",
112
+ strategy: "there is no strategy folder in this kit. Use policy, or plain",
113
+ };
114
+
115
+ /* ------------------------------------------------------------------ *
116
+ * Rule 1. Dashes, by code point.
117
+ *
118
+ * Checked by code point and not by a shell regex on purpose: a regex handed
119
+ * through a shell gets mangled by the locale and the check silently passes.
120
+ * The source below carries escapes, never a literal dash character, so this
121
+ * file passes its own rule 1.
122
+ * ------------------------------------------------------------------ */
123
+
124
+ const DASH_CODEPOINTS = new Map([
125
+ [0x2012, "figure dash"],
126
+ [0x2013, "en dash"],
127
+ [0x2014, "em dash"],
128
+ [0x2015, "horizontal bar"],
129
+ ]);
130
+
131
+ // The HTML spellings of the same characters. A code point scan misses these
132
+ // entirely, and a dashboard partial is exactly where they turn up. Built from
133
+ // the code points above rather than written out, so this source file carries
134
+ // no literal entity for its own rule 1 to trip over.
135
+ const DASH_ENTITIES = [];
136
+ for (const [cp, name] of DASH_CODEPOINTS) {
137
+ DASH_ENTITIES.push(["&#" + cp + ";", name + ", numeric entity"]);
138
+ DASH_ENTITIES.push(["&#x" + cp.toString(16) + ";", name + ", hex entity"]);
139
+ }
140
+ // Split across the concatenation for the same reason.
141
+ DASH_ENTITIES.push(["&" + "mdash;", "em dash, named entity"]);
142
+ DASH_ENTITIES.push(["&" + "ndash;", "en dash, named entity"]);
143
+
144
+ /* ------------------------------------------------------------------ *
145
+ * Rule 2. Metric shaped digit sequences.
146
+ * ------------------------------------------------------------------ */
147
+
148
+ // The nouns this Employee actually writes numbers against. A count of one of
149
+ // these in front of a member reads as a measurement, so it carries its source
150
+ // or it does not go in.
151
+ //
152
+ // Four nouns are deliberately absent and every one of them would misfire.
153
+ // "lines" is a limit in policy/safe-fix-rules.md rather than a claim.
154
+ // "observations" only ever appears inside the n/a string that says a metric
155
+ // was below its floor, which is the honest answer this rule exists to
156
+ // encourage. "minutes" and "hours" are almost always a duration this run
157
+ // measured off its own clock, and a clock is a source no line can name.
158
+ const COUNT_NOUNS = [
159
+ "errors?", "incidents?", "fingerprints?", "regressions?", "failures?",
160
+ "blockers?", "findings?", "advisories", "advisory", "vulnerabilities",
161
+ "projects?", "packages?", "dependencies", "branches", "branch",
162
+ "changes?", "commits?", "cards?", "checks?", "migrations?",
163
+ "domains?", "certificates?", "hostnames?", "builds?", "deploys?",
164
+ "customers?", "clients?", "members?", "users?", "people", "teams?",
165
+ "companies", "accounts?", "visitors?", "sessions?", "views?", "clicks?",
166
+ "requests?", "links?",
167
+ "days?", "weeks?", "months?", "years?",
168
+ ];
169
+
170
+ const METRIC_PATTERNS = [
171
+ ["percentage", new RegExp("\\b\\d[\\d,.]*\\s*%", "g")],
172
+ ["percentage", new RegExp("\\b\\d[\\d,.]*\\s*percent\\b", "gi")],
173
+ ["currency", new RegExp("[$£€¥]\\s?\\d[\\d,.]*\\s*(?:k|m|bn?|million|billion)?\\b", "gi")],
174
+ ["currency", new RegExp("\\b\\d[\\d,.]*\\s*(?:usd|eur|gbp|dollars?|euros?|pounds?)\\b", "gi")],
175
+ ["multiplier", new RegExp("\\b\\d[\\d,.]*x\\b", "gi")],
176
+ // Up to two words may sit between the number and the noun, because "4 new
177
+ // errors" and "3 consecutive clean months" are exactly the shapes that read
178
+ // as a claim, and a pattern that only matches a noun touching the digit
179
+ // misses both.
180
+ ["count", new RegExp("\\b\\d[\\d,.]*\\+?\\s*(?:[A-Za-z]{2,12}\\s+){0,2}(?:" + COUNT_NOUNS.join("|") + ")\\b", "gi")],
181
+ ];
182
+
183
+ // Masked out before the metric scan so a date, a time, or a card id is never
184
+ // read as a claim.
185
+ const NOT_A_METRIC = [
186
+ /\b\d{4}-\d{2}-\d{2}\b/g, // ISO date
187
+ /\b\d{4}-W\d{2}\b/g, // ISO week
188
+ /\b\d{4}-\d{2}\b/g, // calendar month
189
+ /\b\d{1,2}:\d{2}(?::\d{2})?\b/g, // clock time
190
+ /\b[Cc]-\d+\b/g, // card id
191
+ /\bstep\s*\u00ab?n?\u00bb?\s*\d*\b/gi,
192
+ /\bstep:\s*\d+\b/gi,
193
+ /\bHTTP\/\d(?:\.\d)?\b/gi,
194
+ /\b[1-5]\d{2}\s*(?:status|response)?\b(?=\s*(?:status|code|response|returned|and|,|\.|$))/g,
195
+ /\bU\+[0-9A-Fa-f]{4,6}\b/g,
196
+ /\bNode\s*\d+\b/gi,
197
+ ];
198
+
199
+ /* ------------------------------------------------------------------ *
200
+ * Rule 3. Placeholders.
201
+ *
202
+ * This kit has no sentinels and that is deliberate. A sibling Employee needs
203
+ * one because a member has to paste a credential into a draft by hand. This
204
+ * Employee never writes a draft anybody sends, so every guillemet left in a
205
+ * generated file is a value the run failed to fill, and the honest fix is to
206
+ * fill it or to write one of the n/a strings the routines carry.
207
+ * ------------------------------------------------------------------ */
208
+
209
+ const SENTINELS = [];
210
+
211
+ /* ------------------------------------------------------------------ *
212
+ * Rule 4. Voice. These lists ship inside this script and there is no file
213
+ * that overrides them, because this Employee writes reports and briefs rather
214
+ * than marketing copy and has no voice to configure. A member who wants one
215
+ * of these words allowed writes a line in the Corrections section of the
216
+ * routine that keeps tripping on it.
217
+ * ------------------------------------------------------------------ */
218
+
219
+ const SHIPPED_VOICE = {
220
+ bannedWords: [
221
+ "revolutionary", "10x", "unlock", "unlocks", "unlocking",
222
+ "supercharge", "supercharges", "supercharging",
223
+ "game-changing", "game changing", "game-changer", "game changer",
224
+ "seamless", "seamlessly", "effortless", "effortlessly", "frictionless",
225
+ "robust", "powerful", "elevate", "elevates", "elevating",
226
+ "transform", "transforms", "transforming", "transformative",
227
+ "cutting-edge", "cutting edge", "best-in-class", "world-class",
228
+ "skyrocket", "turbocharge", "revolutionize", "revolutionise",
229
+ "leverage", "leveraging", "delve", "harnessing",
230
+ ],
231
+ bannedOpeners: [
232
+ "Most people", "Most founders", "Most operators", "Most companies",
233
+ "Most teams", "Most of us", "Everyone is", "Everyone's",
234
+ "In today's", "In a world", "Let's face it", "Picture this",
235
+ "Imagine a world", "It's no secret", "We've all been",
236
+ "Here's the thing", "The truth is",
237
+ ],
238
+ bannedClosers: [
239
+ "That shift feels less", "The real shift is this", "The real shift is",
240
+ "And that changes everything", "The bottom line is this",
241
+ "At the end of the day", "That's the whole game",
242
+ ],
243
+ hashtagPolicy: "none",
244
+ };
245
+
246
+ /* ------------------------------------------------------------------ *
247
+ * Rule 6. Secrets. The class and the file name are reported. Never the text.
248
+ * ------------------------------------------------------------------ */
249
+
250
+ const SECRET_CLASSES = [
251
+ ["stripe-key", /\bsk_(?:live|test)?_?[A-Za-z0-9]{8,}/g],
252
+ ["webhook-secret", /\bwhsec_[A-Za-z0-9]{8,}/g],
253
+ ["json-web-token", /\beyJ[A-Za-z0-9_-]{10,}/g],
254
+ ["github-token", /\bgh[pousr]_[A-Za-z0-9]{16,}/g],
255
+ ["google-api-key", /\bAIza[A-Za-z0-9_-]{20,}/g],
256
+ ["aws-access-key", /\bAKIA[A-Z0-9]{12,}/g],
257
+ ["slack-token", /\bxox[abprs]-[A-Za-z0-9-]{8,}/g],
258
+ ["bearer-token", /\bBearer\s+[A-Za-z0-9._~+/-]{16,}/gi],
259
+ ["webhook-url", /\bhook\.[a-z][\w.-]*\//gi],
260
+ ["password-literal", /\bpass(?:word|wd)\s*[:=]\s*\S/gi],
261
+ ["private-key-block", /-----BEGIN [A-Z ]*PRIVATE KEY-----/g],
262
+ ["credential-in-url", /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@]+:[^\s/@]+@/gi],
263
+ ];
264
+
265
+ /* ------------------------------------------------------------------ *
266
+ * Rule 7. The autolinker rule, which is OFF in every profile in this kit.
267
+ *
268
+ * The rule itself is real: one publishing platform auto-links any dotted
269
+ * token sitting bare in prose, and anchor text carrying a dot-TLD gets
270
+ * silently rewritten into a broken link. It once left dozens of dead links on
271
+ * a live page.
272
+ *
273
+ * It is off here because this Employee has no publishing surface at all.
274
+ * Every file it writes is a local markdown file the member opens, and no
275
+ * autolinker ever sees one. Meanwhile a bare domain name is this Employee's
276
+ * actual subject matter: "«domain» expires 2026-04-02 and auto renew reads
277
+ * off" is the exact line the platform guard and the standup are required to
278
+ * write, and failing it would fail the deliverable rather than protect it.
279
+ *
280
+ * The code stays, with every profile set to false, because the knowledge is
281
+ * worth keeping and a member who repurposes this script for a surface that
282
+ * does publish can turn it back on in one line. FILE_EXTENSIONS below is not
283
+ * dead either: SOURCE_ON_LINE is built from it.
284
+ * ------------------------------------------------------------------ */
285
+
286
+ const AUTOLINK_TLDS = new Set([
287
+ "com", "net", "org", "io", "ai", "co", "dev", "app", "me", "ly", "sh", "gg",
288
+ "tv", "xyz", "site", "online", "store", "shop", "tech", "cloud", "live",
289
+ "blog", "page", "link", "email", "domains", "agency", "studio", "design",
290
+ "media", "work", "run", "new", "now", "one", "club", "top", "vip", "fun",
291
+ "art", "pro", "name", "mobi", "asia", "biz", "info", "edu", "gov", "int",
292
+ "us", "uk", "ca", "de", "fr", "es", "nl", "se", "jp", "au", "nz", "ie",
293
+ "ch", "at", "be", "pt", "in", "it", "no", "is", "so", "to", "cc", "md",
294
+ ]);
295
+
296
+ // A dotted token whose suffix is a file extension is a path, not a link.
297
+ // Checked before the TLD list, because md, sh, so, and in collide.
298
+ const FILE_EXTENSIONS = new Set([
299
+ "md", "mjs", "cjs", "js", "jsx", "ts", "tsx", "json", "jsonl", "csv", "tsv",
300
+ "html", "htm", "css", "scss", "txt", "log", "tmp", "bak", "old", "lock",
301
+ "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "pdf", "zip", "gz", "tar",
302
+ "yml", "yaml", "toml", "ini", "env", "xml", "sql", "db", "map", "min",
303
+ "py", "rb", "go", "rs", "java", "php", "sh", "bat", "ps1", "exe", "dll",
304
+ "mp4", "mp3", "wav", "so", "d",
305
+ ]);
306
+
307
+ /* ------------------------------------------------------------------ *
308
+ * Rule 2, the second half. A number passes when the line it sits on names
309
+ * the file the number was counted in.
310
+ *
311
+ * Every figure this Employee produces was folded out of one of its own
312
+ * ledgers this run, so naming the ledger is always possible and it is always
313
+ * the better sentence. "health/incidents.jsonl, 4 fingerprints new today"
314
+ * says where to look. "4 new errors on your site" is a claim about the
315
+ * member's business that nothing in the file supports.
316
+ *
317
+ * A source is a path or a bare filename carrying one of the extensions above,
318
+ * or the URL of the screen the value was read off. The platform guard reads
319
+ * its values off a provider screen rather than out of a ledger, so a screen
320
+ * is a source in exactly the way a ledger path is.
321
+ * ------------------------------------------------------------------ */
322
+
323
+ const SOURCE_ON_LINE = new RegExp(
324
+ "(?:^|[\\s(\\[`|/\"'])[A-Za-z0-9_][A-Za-z0-9_.-]*(?:/[A-Za-z0-9_.-]+)*\\.(?:" +
325
+ [...FILE_EXTENSIONS].join("|") + ")(?![A-Za-z0-9])" +
326
+ "|https?://[^\\s<>()\"']+"
327
+ );
328
+
329
+ /* ------------------------------------------------------------------ *
330
+ * Text helpers. Masking preserves offsets so every violation keeps a real
331
+ * line and column: masked characters become spaces, newlines stay newlines.
332
+ * ------------------------------------------------------------------ */
333
+
334
+ function maskRanges(text, ranges) {
335
+ if (!ranges.length) return text;
336
+ const chars = text.split("");
337
+ for (const [start, end] of ranges) {
338
+ for (let i = start; i < end && i < chars.length; i++) {
339
+ if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
340
+ }
341
+ }
342
+ return chars.join("");
343
+ }
344
+
345
+ function rangesOf(text, patterns, group) {
346
+ const ranges = [];
347
+ for (const pattern of patterns) {
348
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
349
+ let m;
350
+ while ((m = re.exec(text)) !== null) {
351
+ if (m[0].length === 0) { re.lastIndex++; continue; }
352
+ if (group && m[group] !== undefined) {
353
+ const offset = m[0].indexOf(m[group]);
354
+ ranges.push([m.index + offset, m.index + offset + m[group].length]);
355
+ } else {
356
+ ranges.push([m.index, m.index + m[0].length]);
357
+ }
358
+ }
359
+ }
360
+ return ranges;
361
+ }
362
+
363
+ const CODE_AND_LINK_PATTERNS = [
364
+ /```[\s\S]*?(?:```|$)/g, // fenced code block
365
+ /~~~[\s\S]*?(?:~~~|$)/g,
366
+ /`[^`\n]*`/g, // inline code
367
+ /\b(?:https?:\/\/|www\.|mailto:)[^\s<>()"'\]}]+/gi, // a real URL
368
+ /\]\([^)\s]+/g, // a markdown link target
369
+ /\b(?:href|src|action|content|srcset|data-[\w-]+)\s*=\s*["'][^"']*["']/gi,
370
+ /^\s*(?:search_url|source_url|account_url|url|buy_url|landing_url|linkedin_url)\s*:.*$/gim,
371
+ ];
372
+
373
+ const MARKUP_PATTERNS = [
374
+ /<style[\s\S]*?<\/style>/gi,
375
+ /<script[\s\S]*?<\/script>/gi,
376
+ /\sstyle\s*=\s*["'][^"']*["']/gi,
377
+ /<!--[\s\S]*?-->/g,
378
+ ];
379
+
380
+ function lineStarts(text) {
381
+ const starts = [0];
382
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
383
+ return starts;
384
+ }
385
+
386
+ function locate(starts, index) {
387
+ let lo = 0;
388
+ let hi = starts.length - 1;
389
+ while (lo < hi) {
390
+ const mid = (lo + hi + 1) >> 1;
391
+ if (starts[mid] <= index) lo = mid; else hi = mid - 1;
392
+ }
393
+ return { line: lo + 1, col: index - starts[lo] + 1 };
394
+ }
395
+
396
+ function snippet(text, index, length) {
397
+ const lineEnd = text.indexOf("\n", index);
398
+ const end = lineEnd === -1 ? text.length : lineEnd;
399
+ let lineStart = text.lastIndexOf("\n", index);
400
+ lineStart = lineStart === -1 ? 0 : lineStart + 1;
401
+ let out = text.slice(Math.max(lineStart, index - 24), Math.min(end, index + Math.max(length, 1) + 24));
402
+ out = out.replace(/\s+/g, " ").trim();
403
+ return out.length > 72 ? out.slice(0, 69) + "..." : out;
404
+ }
405
+
406
+ /* ------------------------------------------------------------------ *
407
+ * The one line lookup rule 2 needs
408
+ * ------------------------------------------------------------------ */
409
+
410
+ function lineTextAt(text, starts, index) {
411
+ let lo = 0;
412
+ let hi = starts.length - 1;
413
+ while (lo < hi) {
414
+ const mid = (lo + hi + 1) >> 1;
415
+ if (starts[mid] <= index) lo = mid; else hi = mid - 1;
416
+ }
417
+ const from = starts[lo];
418
+ const nl = text.indexOf("\n", from);
419
+ return text.slice(from, nl === -1 ? text.length : nl);
420
+ }
421
+
422
+ /* ------------------------------------------------------------------ *
423
+ * The checks
424
+ * ------------------------------------------------------------------ */
425
+
426
+ function check(text, dest, ctx) {
427
+ const profile = PROFILE[dest];
428
+ const v = [];
429
+ const starts = lineStarts(text);
430
+ const at = (index, length, rule, detail, showText) => {
431
+ const where = locate(starts, index);
432
+ v.push({
433
+ rule,
434
+ detail,
435
+ line: where.line,
436
+ col: where.col,
437
+ text: showText === false ? null : snippet(text, index, length),
438
+ });
439
+ };
440
+
441
+ /* Rule 1. Dashes, scanned by code point over the raw text, including code
442
+ comments, and then the entity spellings. */
443
+ for (let i = 0; i < text.length; i++) {
444
+ const cp = text.codePointAt(i);
445
+ const name = DASH_CODEPOINTS.get(cp);
446
+ if (name) {
447
+ at(i, 1, "dash", name + " (U+" + cp.toString(16).toUpperCase().padStart(4, "0") + "). Use a period, a comma, or split the sentence.");
448
+ }
449
+ if (cp > 0xffff) i++; // step over the low surrogate so columns stay right
450
+ }
451
+ const lowered = text.toLowerCase();
452
+ for (const [entity, name] of DASH_ENTITIES) {
453
+ let from = 0;
454
+ for (;;) {
455
+ const idx = lowered.indexOf(entity, from);
456
+ if (idx === -1) break;
457
+ at(idx, entity.length, "dash", name + ". Use a period, a comma, or split the sentence.");
458
+ from = idx + entity.length;
459
+ }
460
+ }
461
+
462
+ /* Build the prose view once: code, real URLs, and markup are not prose. */
463
+ let prose = maskRanges(text, rangesOf(text, CODE_AND_LINK_PATTERNS));
464
+ if (profile.markup) prose = maskRanges(prose, rangesOf(prose, MARKUP_PATTERNS));
465
+
466
+ /* Rule 2. Metrics, and the source that redeems one. */
467
+ const metricView = maskRanges(prose, rangesOf(prose, NOT_A_METRIC));
468
+ for (const [kind, pattern] of METRIC_PATTERNS) {
469
+ const re = new RegExp(pattern.source, pattern.flags);
470
+ let m;
471
+ while ((m = re.exec(metricView)) !== null) {
472
+ if (m[0].length === 0) { re.lastIndex++; continue; }
473
+ const claim = m[0].trim();
474
+ if (SOURCE_ON_LINE.test(lineTextAt(text, starts, m.index))) continue;
475
+ at(m.index, m[0].length, "metric",
476
+ "\"" + claim + "\" reads as a " + kind + " and the line it sits on names no source. " +
477
+ "Put the path of the file you counted it in on the same line, or write the date instead of the count.");
478
+ }
479
+ }
480
+
481
+ /* Rule 3. Unresolved placeholders, minus the two sentinels. */
482
+ let placeholderView = text;
483
+ for (const sentinel of SENTINELS) {
484
+ const ranges = [];
485
+ let from = 0;
486
+ for (;;) {
487
+ const idx = placeholderView.indexOf(sentinel, from);
488
+ if (idx === -1) break;
489
+ ranges.push([idx, idx + sentinel.length]);
490
+ from = idx + sentinel.length;
491
+ }
492
+ placeholderView = maskRanges(placeholderView, ranges);
493
+ }
494
+ for (let i = 0; i < placeholderView.length; i++) {
495
+ const cp = placeholderView.codePointAt(i);
496
+ if (cp === 0x00ab || cp === 0x00bb) {
497
+ at(i, 1, "placeholder", "an unresolved guillemet placeholder. Fill it, or use one of the two sentinels.");
498
+ }
499
+ }
500
+
501
+ /* Rule 4. Voice. */
502
+ if (profile.voice) {
503
+ for (const word of ctx.voice.bannedWords) {
504
+ const re = new RegExp("(^|[^\\w-])(" + escapeRe(word) + ")(?![\\w-])", "gi");
505
+ let m;
506
+ while ((m = re.exec(prose)) !== null) {
507
+ const idx = m.index + m[1].length;
508
+ at(idx, m[2].length, "banned-word", "\"" + m[2] + "\" is on the banned list in " + ctx.voice.source);
509
+ }
510
+ }
511
+
512
+ for (const opener of ctx.voice.bannedOpeners) {
513
+ const needle = opener.toLowerCase();
514
+ let cursor = 0;
515
+ for (const rawLine of prose.split("\n")) {
516
+ // Strip list markers, headings, quote markers, and bold so an opener
517
+ // inside a bullet is still an opener.
518
+ const stripped = rawLine.replace(/^[\s>]*(?:[-*+]\s+|\d+[.)]\s+)?#{0,6}\s*(?:\*\*|__)?/, "");
519
+ const offset = rawLine.length - stripped.length;
520
+ if (stripped.toLowerCase().startsWith(needle)) {
521
+ at(cursor + offset, opener.length, "banned-opener",
522
+ "\"" + opener + "\" is a banned opener in " + ctx.voice.source + ". Readers pattern match it.");
523
+ }
524
+ cursor += rawLine.length + 1;
525
+ }
526
+ }
527
+
528
+ for (const closer of ctx.voice.bannedClosers) {
529
+ const re = new RegExp(escapeRe(closer), "gi");
530
+ let m;
531
+ while ((m = re.exec(prose)) !== null) {
532
+ at(m.index, m[0].length, "banned-closer", "\"" + closer + "\" is a banned closer in " + ctx.voice.source);
533
+ }
534
+ }
535
+ }
536
+
537
+ /* Rule 5. Hashtags. */
538
+ if (profile.hashtag && ctx.voice.hashtagPolicy === "none") {
539
+ const re = /(^|[^\w&#])#([A-Za-z][\w]*)/g;
540
+ let m;
541
+ while ((m = re.exec(prose)) !== null) {
542
+ const tag = m[2];
543
+ if (/^(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(tag)) continue; // a hex colour
544
+ at(m.index + m[1].length, tag.length + 1, "hashtag",
545
+ "#" + tag + ". The hashtag policy in " + ctx.voice.source + " is none.");
546
+ }
547
+ }
548
+
549
+ /* Rule 6. Secrets. Class and location only. Never the matched text. */
550
+ for (const [cls, pattern] of SECRET_CLASSES) {
551
+ const re = new RegExp(pattern.source, pattern.flags);
552
+ let m;
553
+ while ((m = re.exec(text)) !== null) {
554
+ if (m[0].length === 0) { re.lastIndex++; continue; }
555
+ at(m.index, m[0].length, "secret",
556
+ "a substring matching the " + cls + " class. Move it to env or secret storage and reference the account by name.",
557
+ false);
558
+ }
559
+ }
560
+
561
+ /* Rule 7. Autolinker bait. */
562
+ if (profile.autolink) {
563
+ // Email addresses and the anchor text of a markdown link are masked first: a dotted token inside
564
+ // [anchor](url) is a real link already, not bait for an autolinker.
565
+ const linkView = maskRanges(prose, rangesOf(prose, [/[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+\.[A-Za-z.]{2,}/g]).concat(rangesOf(text, [/\[[^\]\n]{1,200}\]\(/g])));
566
+ const re = /(^|[^\w@/\\.])([A-Za-z0-9][\w-]{1,})\.([A-Za-z]{2,24})(?![\w-])/g;
567
+ let m;
568
+ while ((m = re.exec(linkView)) !== null) {
569
+ const suffix = m[3].toLowerCase();
570
+ if (FILE_EXTENSIONS.has(suffix)) continue;
571
+ if (!AUTOLINK_TLDS.has(suffix)) continue;
572
+ const token = m[2] + "." + m[3];
573
+ at(m.index + m[1].length, token.length, "autolink",
574
+ "\"" + token + "\" sits bare in prose. An autolinker rewrites it into a link, and that link is usually broken. " +
575
+ "Write it as a real link, or break the dotted token.");
576
+ }
577
+ }
578
+
579
+ v.sort((a, b) => (a.line - b.line) || (a.col - b.col));
580
+ return v;
581
+ }
582
+
583
+ function escapeRe(s) {
584
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
585
+ }
586
+
587
+ function shippedVoice() {
588
+ return { ...SHIPPED_VOICE, source: "the shipped list in scripts/copy-check.mjs" };
589
+ }
590
+
591
+ /* The secret.scan route. Rule 6 and nothing else, so a raw production log
592
+ line, a build failure reason, a package manager output line, or a diff can
593
+ be scanned for a credential without being judged as prose. It reports the
594
+ class and the location and never the matched text, on purpose: a scanner
595
+ that echoes what it found has written the secret into a second file. */
596
+ function checkSecretsOnly(text) {
597
+ const v = [];
598
+ const starts = lineStarts(text);
599
+ for (const [cls, pattern] of SECRET_CLASSES) {
600
+ const re = new RegExp(pattern.source, pattern.flags);
601
+ let m;
602
+ while ((m = re.exec(text)) !== null) {
603
+ if (m[0].length === 0) { re.lastIndex++; continue; }
604
+ const where = locate(starts, m.index);
605
+ v.push({
606
+ rule: "secret",
607
+ detail: "a substring matching the " + cls + " class. Withhold the line, name the class and the screen, " +
608
+ "and never rotate the key yourself.",
609
+ line: where.line, col: where.col, text: null,
610
+ });
611
+ }
612
+ }
613
+ v.sort((a, b) => (a.line - b.line) || (a.col - b.col));
614
+ return v;
615
+ }
616
+
617
+ /* ------------------------------------------------------------------ *
618
+ * Reporting
619
+ * ------------------------------------------------------------------ */
620
+
621
+ function report(violations, label, dest, ctx, opts) {
622
+ if (opts.json) {
623
+ const counts = {};
624
+ for (const item of violations) counts[item.rule] = (counts[item.rule] || 0) + 1;
625
+ process.stdout.write(JSON.stringify({
626
+ verdict: violations.length ? "FAIL" : "PASS",
627
+ file: label,
628
+ dest,
629
+ violation_count: violations.length,
630
+ counts,
631
+ voice_source: ctx.voice.source,
632
+ metric_rule: "a number carries the path of the file it was counted in, on the same line",
633
+ violations: violations.map((item) => ({
634
+ rule: item.rule, detail: item.detail, line: item.line, col: item.col, text: item.text,
635
+ })),
636
+ }, null, 2) + "\n");
637
+ return;
638
+ }
639
+
640
+ if (opts.quiet) return;
641
+
642
+ if (!violations.length) {
643
+ process.stdout.write(
644
+ "copy-check: PASS " + label + " dest=" + dest + " (voice: " + ctx.voice.source + ")\n"
645
+ );
646
+ return;
647
+ }
648
+
649
+ process.stdout.write("copy-check: FAIL " + label + " dest=" + dest + "\n");
650
+ for (const item of violations) {
651
+ const where = item.line ? ("line " + item.line + " col " + item.col) : "file";
652
+ // padEnd must exceed the longest rule name, or the columns run together.
653
+ const gutter = 18 + 16;
654
+ process.stdout.write(
655
+ " " + where.padEnd(18) + item.rule.padEnd(16) + item.detail + "\n" +
656
+ (item.text ? " " + " ".repeat(gutter) + item.text + "\n" : "")
657
+ );
658
+ }
659
+ const counts = {};
660
+ for (const item of violations) counts[item.rule] = (counts[item.rule] || 0) + 1;
661
+ const summary = Object.entries(counts).map(([k, n]) => k + " " + n).join(", ");
662
+ process.stdout.write(" " + violations.length + " violation(s): " + summary + "\n");
663
+ }
664
+
665
+ /* ------------------------------------------------------------------ *
666
+ * Self test. This script reads no file at all, so the context is the shipped
667
+ * one and the test is the same on every machine.
668
+ * ------------------------------------------------------------------ */
669
+
670
+ function selftest() {
671
+ const ctx = { voice: shippedVoice() };
672
+
673
+ const cases = [
674
+ ["a clean brief line", "# 2026-03-06\n\n## Today\nC-041 on acme-site, changes/2026-03-05-fix-C-041.md\n", "brief", ctx, []],
675
+ ["empty file", "", "plain", ctx, []],
676
+ ["no trailing newline", "One clean line with nothing wrong in it", "plain", ctx, []],
677
+ ["unicode and emoji", "Cafe owners ship fast and it holds up fine.\nSecond line is clean.\n", "plain", ctx, []],
678
+ // The dash fixtures use escapes, never a literal dash character, so this
679
+ // source file passes its own rule 1.
680
+ ["em dash", "This is a sentence\u2014and it continues.", "plain", ctx, ["dash"]],
681
+ ["en dash", "Pages 10\u201312 of the doc.", "plain", ctx, ["dash"]],
682
+ ["figure dash", "Rows 3\u20124 are stale.", "plain", ctx, ["dash"]],
683
+ ["dash inside a code comment", "```\n// a comment \u2014 with a dash\n```\n", "policy", ctx, ["dash"]],
684
+ ["banned word", "This will supercharge the build.", "report", ctx, ["banned-word"]],
685
+ ["banned opener at a line start", "Most people never check their logs.", "report", ctx, ["banned-opener"]],
686
+ ["banned opener inside a bullet", "- Most teams skip this step.", "report", ctx, ["banned-opener"]],
687
+ ["banned closer", "So it goes. The real shift is this: nobody checks.", "report", ctx, ["banned-closer"]],
688
+ ["hashtag", "Shipped it today. #founders", "brief", ctx, ["hashtag"]],
689
+ ["hex colour is not a hashtag", "<div style=\"color:#fff\">ok</div>", "dashboard", ctx, []],
690
+ ["an unsourced count", "4 new errors on your site today.", "brief", ctx, ["metric"]],
691
+ ["the same count with its ledger passes", "health/incidents.jsonl, 4 new errors today.", "brief", ctx, []],
692
+ ["an unsourced elapsed count", "That blocker has been open 9 days.", "brief", ctx, ["metric"]],
693
+ ["the date instead of the count passes", "That blocker has been open since 2026-02-24.", "brief", ctx, []],
694
+ ["an unsourced percentage", "Uptime was 99.4% last week.", "report", ctx, ["metric"]],
695
+ ["a value sourced to a screen passes", "auto renew reads off, read on https://example.com/domains/acme", "report", ctx, []],
696
+ ["a gate duration is not a claim", "ran at: 2026-03-05T08:31:07+07:00 took: 4 minutes", "change", ctx, []],
697
+ ["a sourced percentage passes", "Uptime 99.4% | acme-site | health/checks.jsonl", "report", ctx, []],
698
+ ["an unsourced currency", "The plan upgrade costs $20 a month.", "report", ctx, ["metric"]],
699
+ ["a line count limit is not a claim", "content one-file 12 lines", "policy", ctx, []],
700
+ ["an observation floor is not a claim", "median response n/a (2 observations, floor 3)", "report", ctx, []],
701
+ ["an ISO date is not a metric", "Observed on 2026-03-04 and merged 2026-03-05.", "plain", ctx, []],
702
+ ["an ISO week is not a metric", "Window 2026-W11 to 2026-W12.", "report", ctx, []],
703
+ ["a card id is not a metric", "C-041 is ready and C-038 is done.", "plain", ctx, []],
704
+ ["a clock time is not a metric", "It fires at 08:10 every weekday.", "plain", ctx, []],
705
+ ["an unresolved placeholder", "The domain \u00abdomain\u00bb expires soon.", "report", ctx, ["placeholder"]],
706
+ ["a secret", "Set the key to sk_" + "live_51H8xQ2ePlaceholderValue here.", "policy", ctx, ["secret"]],
707
+ ["a bare domain in prose is fine here", "The host serves example.com from that project.", "report", ctx, []],
708
+ ["the expiry line the platform guard has to write", "example.com expires 2026-04-02 and auto renew reads off", "report", ctx, []],
709
+ ["a real link is fine", "Open [the screen](https://example.com/projects) to change it.", "report", ctx, []],
710
+ ["a path in backticks is fine", "The ledger is `health/incidents.jsonl` and it appends.", "policy", ctx, []],
711
+ ["a bare kit path is fine", "Write it to health/incidents.jsonl every run.", "plain", ctx, []],
712
+ ["a filename is not a link", "Open changes/2026-03-05-fix-C-041.md and read the gate.", "plain", ctx, []],
713
+ ["policy skips the voice rules", "Banned words: supercharge, unlock, revolutionary.", "policy", ctx, []],
714
+ ];
715
+
716
+ let failures = 0;
717
+ for (const [name, text, dest, context, expected] of cases) {
718
+ const got = check(text, dest, context);
719
+ const gotRules = [...new Set(got.map((x) => x.rule))].sort();
720
+ const wantRules = [...new Set(expected)].sort();
721
+ const pass = gotRules.join(",") === wantRules.join(",");
722
+ if (pass) {
723
+ process.stdout.write(" ok " + name + "\n");
724
+ } else {
725
+ failures++;
726
+ process.stdout.write(" FAIL " + name + "\n" +
727
+ " want [" + wantRules.join(", ") + "] got [" + gotRules.join(", ") + "]\n");
728
+ for (const item of got) process.stdout.write(" line " + item.line + " " + item.rule + ": " + item.detail + "\n");
729
+ }
730
+ }
731
+
732
+ // The secret.scan route. It runs rule 6 and nothing else, so a raw log line
733
+ // carrying a dash and an unsourced count is clean unless it carries a key.
734
+ const rawLogLine = "2026-03-04 ERROR 37 errors \u2014 cannot read properties of undefined";
735
+ if (checkSecretsOnly(rawLogLine).length === 0) {
736
+ process.stdout.write(" ok secrets-only ignores prose rules on a raw log line\n");
737
+ } else {
738
+ failures++;
739
+ process.stdout.write(" FAIL secrets-only ignores prose rules on a raw log line\n");
740
+ }
741
+ const leakyLogLine = "connect failed for postgres://user:hunter2@db.internal:5432/app";
742
+ const leaks = checkSecretsOnly(leakyLogLine);
743
+ if (leaks.length === 1 && leaks[0].rule === "secret" && leaks[0].text === null) {
744
+ process.stdout.write(" ok secrets-only catches a credential and never echoes it\n");
745
+ } else {
746
+ failures++;
747
+ process.stdout.write(" FAIL secrets-only catches a credential and never echoes it\n");
748
+ }
749
+
750
+ // Line numbers must survive a surrogate pair and a masked region.
751
+ const multiline = "line one is clean\nline two has 🚀 an emoji\nline three has a dash \u2014 here\n";
752
+ const found = check(multiline, "plain", ctx);
753
+ if (found.length === 1 && found[0].line === 3) {
754
+ process.stdout.write(" ok line numbers survive a surrogate pair\n");
755
+ } else {
756
+ failures++;
757
+ process.stdout.write(" FAIL line numbers survive a surrogate pair (got " +
758
+ found.map((f) => f.rule + "@" + f.line).join(", ") + ")\n");
759
+ }
760
+
761
+ process.stdout.write(failures === 0
762
+ ? "copy-check: selftest PASS (" + (cases.length + 3) + " checks)\n"
763
+ : "copy-check: selftest FAIL (" + failures + " of " + (cases.length + 3) + ")\n");
764
+ process.exit(failures === 0 ? 0 : 1);
765
+ }
766
+
767
+ /* ------------------------------------------------------------------ *
768
+ * Argument parsing and main
769
+ * ------------------------------------------------------------------ */
770
+
771
+ function die(message) {
772
+ process.stderr.write("copy-check: " + message + "\n");
773
+ process.exit(2);
774
+ }
775
+
776
+ function usage() {
777
+ const text = fs.readFileSync(SELF, "utf8");
778
+ const block = text.slice(text.indexOf("/*") + 2, text.indexOf("*/"));
779
+ process.stdout.write(block.split("\n").map((l) => l.replace(/^ \* ?/, "")).join("\n").trim() + "\n");
780
+ }
781
+
782
+ const CANONICAL = "node scripts/copy-check.mjs --file <path> --dest <" + DESTINATIONS.join("|") + "> [--json]";
783
+
784
+ function parseArgs(argv) {
785
+ const opts = { file: null, dest: null, json: false, quiet: false, selftest: false, help: false, secretsOnly: false };
786
+
787
+ for (let i = 0; i < argv.length; i++) {
788
+ const arg = argv[i];
789
+ const next = () => {
790
+ const value = argv[i + 1];
791
+ if (value === undefined) die(arg + " needs a value\n " + CANONICAL);
792
+ i++;
793
+ return value;
794
+ };
795
+
796
+ switch (arg) {
797
+ case "--help": case "-h": opts.help = true; break;
798
+ case "--selftest": opts.selftest = true; break;
799
+ case "--json": opts.json = true; break;
800
+ case "--quiet": opts.quiet = true; break;
801
+ case "--file": opts.file = next(); break;
802
+ case "--dest": opts.dest = next(); break;
803
+ case "--secrets-only": opts.secretsOnly = true; break;
804
+
805
+ // Flags earlier drafts of this kit invented. Fail loudly and name the
806
+ // one real interface, so the stale call site gets fixed rather than
807
+ // silently doing something else.
808
+ case "--destination":
809
+ die("there is no --destination. The flag is --dest.\n " + CANONICAL);
810
+ break;
811
+ case "--profile":
812
+ die("there is no --profile. The flag is --dest, one of: " + DESTINATIONS.join(", ") + "\n " + CANONICAL);
813
+ break;
814
+ case "--root":
815
+ die("there is no --root. This script reads no other file.\n " + CANONICAL);
816
+ break;
817
+
818
+ default:
819
+ if (arg.startsWith("-") && arg !== "-") die("unknown option " + arg + "\n " + CANONICAL);
820
+ if (opts.file === null) {
821
+ die("this script takes no bare positional path. Pass it as --file.\n " + CANONICAL);
822
+ }
823
+ die("unexpected argument " + arg + "\n " + CANONICAL);
824
+ }
825
+ }
826
+ return opts;
827
+ }
828
+
829
+ function main() {
830
+ const opts = parseArgs(process.argv.slice(2));
831
+ if (opts.help) { usage(); process.exit(0); }
832
+ if (opts.selftest) { selftest(); return; }
833
+
834
+ if (opts.secretsOnly) {
835
+ if (opts.dest !== null) {
836
+ die("--secrets-only takes no --dest. It runs rule 6 and nothing else.\n " + CANONICAL);
837
+ }
838
+ } else if (opts.dest === null) {
839
+ die("--dest is required, one of: " + DESTINATIONS.join(", ") +
840
+ "\n Or pass --secrets-only to scan a raw log line for a credential.\n " + CANONICAL);
841
+ } else if (!DESTINATIONS.includes(opts.dest)) {
842
+ const retired = RETIRED_DESTINATIONS[opts.dest];
843
+ const guess = { html: "dashboard", text: "plain", markdown: "plain", file: "policy", internal: "policy" }[opts.dest];
844
+ die("unknown dest \"" + opts.dest + "\"." +
845
+ (retired ? " " + retired + "." : (guess ? " Did you mean \"" + guess + "\"?" : "")) +
846
+ "\n Valid: " + DESTINATIONS.join(", ") + "\n " + CANONICAL);
847
+ }
848
+
849
+ let text;
850
+ let label;
851
+ if (opts.file === null || opts.file === "-") {
852
+ label = "(stdin)";
853
+ try { text = fs.readFileSync(0, "utf8"); }
854
+ catch (err) { die("could not read stdin: " + err.message + "\n " + CANONICAL); }
855
+ } else {
856
+ label = opts.file;
857
+ try { text = fs.readFileSync(opts.file, "utf8"); }
858
+ catch (err) { die("could not read " + opts.file + ": " + err.message); }
859
+ }
860
+
861
+ // A byte order mark is not a violation, but leaving it in shifts every
862
+ // column by one.
863
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
864
+
865
+ const ctx = { voice: shippedVoice() };
866
+ const dest = opts.secretsOnly ? "secrets-only" : opts.dest;
867
+ const violations = opts.secretsOnly ? checkSecretsOnly(text) : check(text, opts.dest, ctx);
868
+ report(violations, label, dest, ctx, opts);
869
+ process.exit(violations.length ? 1 : 0);
870
+ }
871
+
872
+ main();