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,443 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * review.mjs
4
+ * The member's review page for creative sets, and the only writer of
5
+ * creative/approvals.jsonl other than the member's own editor.
6
+ *
7
+ * Why this script exists: a creative set used to wait on a board tick, and a
8
+ * set the member did not like sat in the only open slot until somebody
9
+ * archived it by hand. An approval is now a row bound to the exact content
10
+ * it approved, a rejection closes the slot, and three facts that used to
11
+ * collapse into one word are shown apart: approved, published, delivering.
12
+ *
13
+ * Requires Node 18 or newer. No dependencies, no package.json, no install.
14
+ * Runs the same on Windows and POSIX. Makes no network call of its own.
15
+ *
16
+ * USAGE
17
+ * node scripts/review.mjs --serve [--port 8770]
18
+ * Serves the review page on the loopback address only. Approve, reject,
19
+ * needs revision and withdraw are one click each, and every label on the
20
+ * page is read from the files on disk after each click. Nothing on this
21
+ * page calls an ad platform.
22
+ *
23
+ * node scripts/review.mjs --catalog [--json]
24
+ * Prints every set with its revision, its review status, its publication
25
+ * status and its delivery status. Routines read this instead of parsing
26
+ * the ledgers themselves.
27
+ *
28
+ * node scripts/review.mjs --record --set creative/set-YYYY-MM-DD-slug \
29
+ * --status approved|rejected|needs-revision|withdrawn [--note "..."]
30
+ * Appends one review row from a terminal, for a member who prefers the
31
+ * keyboard. The revision is computed from the files as they are now.
32
+ *
33
+ * node scripts/review.mjs --selftest
34
+ * node scripts/review.mjs --help
35
+ *
36
+ * OPTIONS
37
+ * --root <path> The kit working folder. Defaults to this script's parent
38
+ * folder, then the ADS_ROOT environment variable.
39
+ *
40
+ * WHAT A ROW LOOKS LIKE, in creative/approvals.jsonl
41
+ * {"set":"creative/set-2026-03-04-slug","revision":"<sha256>",
42
+ * "status":"approved","at":"<ISO>","by":"member","note":""}
43
+ *
44
+ * status is one of approved, rejected, needs-revision, withdrawn.
45
+ * by is always "member": this page is the member's hand, and no routine
46
+ * ever appends a row here. A routine that finds a row it cannot trace to
47
+ * the member treats the set as awaiting review.
48
+ *
49
+ * WHAT THE REVISION COVERS
50
+ * The manifest set.md, every image file the manifest names, in sorted
51
+ * order, and the four money and destination lines of plan/offer.md. Any
52
+ * change to any of those gives a new revision, and an approval for an
53
+ * earlier revision reads as "changed" until the member reviews again.
54
+ *
55
+ * WHAT NEVER HAPPENS HERE
56
+ * No routine is started, no platform is called, no file other than
57
+ * creative/approvals.jsonl is written, and a published set cannot be
58
+ * withdrawn from this page: pausing a live ad is a change list line and a
59
+ * build desk action, not a review.
60
+ */
61
+
62
+ import fs from "node:fs";
63
+ import path from "node:path";
64
+ import os from "node:os";
65
+ import crypto from "node:crypto";
66
+ import http from "node:http";
67
+ import { fileURLToPath } from "node:url";
68
+
69
+ const REVIEW_STATUSES = ["approved", "rejected", "needs-revision", "withdrawn"];
70
+ const OFFER_HEADINGS = ["Landing URL", "Countries sold into", "Daily cap", "Currency"];
71
+
72
+ function readText(p) {
73
+ if (!fs.existsSync(p)) return "";
74
+ return fs.readFileSync(p, "utf8").replace(/^/, "");
75
+ }
76
+
77
+ function readRows(p) {
78
+ const out = [];
79
+ const text = readText(p);
80
+ if (!text) return out;
81
+ const lines = text.split(/\r?\n/);
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i].trim();
84
+ if (!line) continue;
85
+ try { out.push(JSON.parse(line)); } catch { out.push({ __malformed: i + 1 }); }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function section(text, heading) {
91
+ const m = new RegExp("^## " + heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\r?\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))", "m").exec(text);
92
+ return m ? m[1].trim() : "";
93
+ }
94
+
95
+ function scopeOf(root) {
96
+ const offer = readText(path.join(root, "plan", "offer.md"));
97
+ const scope = {};
98
+ for (const h of OFFER_HEADINGS) scope[h] = section(offer, h);
99
+ return scope;
100
+ }
101
+
102
+ function setFolders(root) {
103
+ const dir = path.join(root, "creative");
104
+ if (!fs.existsSync(dir)) return [];
105
+ return fs.readdirSync(dir)
106
+ .filter((n) => /^set-\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/.test(n))
107
+ .filter((n) => fs.existsSync(path.join(dir, n, "set.md")))
108
+ .sort()
109
+ .map((n) => "creative/" + n);
110
+ }
111
+
112
+ function imagesOf(root, set) {
113
+ const folder = path.join(root, set);
114
+ return fs.readdirSync(folder)
115
+ .filter((n) => /\.(png|jpe?g|webp)$/i.test(n))
116
+ .sort();
117
+ }
118
+
119
+ function slotsOf(manifest) {
120
+ const body = section(manifest, "Slots");
121
+ return body.split(/\r?\n/).map((l) => l.replace(/^[-*]\s+/, "").trim()).filter(Boolean);
122
+ }
123
+
124
+ function revisionOf(root, set, scope) {
125
+ const folder = path.join(root, set);
126
+ const h = crypto.createHash("sha256");
127
+ h.update(readText(path.join(folder, "set.md")));
128
+ for (const img of imagesOf(root, set)) {
129
+ h.update("\n" + img + "\n");
130
+ h.update(fs.readFileSync(path.join(folder, img)));
131
+ }
132
+ h.update(JSON.stringify(scope));
133
+ return h.digest("hex");
134
+ }
135
+
136
+ /*
137
+ * The three facts, derived and never stored.
138
+ * review: from creative/approvals.jsonl, bound to the revision
139
+ * publication: from build/publication-receipts.jsonl, the last receipt for the set
140
+ * delivery: from metrics/daily.jsonl, the last row carrying the ad id
141
+ */
142
+ export function catalog(root) {
143
+ const scope = scopeOf(root);
144
+ const approvals = readRows(path.join(root, "creative", "approvals.jsonl")).filter((r) => !r.__malformed);
145
+ const receipts = readRows(path.join(root, "build", "publication-receipts.jsonl")).filter((r) => !r.__malformed);
146
+ const metrics = readRows(path.join(root, "metrics", "daily.jsonl")).filter((r) => !r.__malformed);
147
+ return setFolders(root).map((set) => {
148
+ const manifest = readText(path.join(root, set, "set.md"));
149
+ const revision = revisionOf(root, set, scope);
150
+ const rows = approvals.filter((r) => r.set === set && r.by === "member");
151
+ const latest = rows.length ? rows[rows.length - 1] : null;
152
+ let review = "awaiting-review";
153
+ if (latest) review = latest.revision === revision ? latest.status : "changed";
154
+ const receipt = receipts.filter((r) => r.set === set).pop() || null;
155
+ let publication = "unpublished";
156
+ if (receipt && receipt.ad_id) publication = String(receipt.configured_status || "").toUpperCase() === "ACTIVE" ? "published" : "prepared";
157
+ else if (receipt) publication = "in-progress";
158
+ let delivery = { state: "not-observed", on: null, impressions: null };
159
+ if (receipt && receipt.ad_id) {
160
+ const rowsForAd = metrics.filter((m) => String(m.object_id) === String(receipt.ad_id)).sort((a, b) => String(a.date).localeCompare(String(b.date)));
161
+ const last = rowsForAd.pop();
162
+ if (last) delivery = { state: Number(last.impressions) > 0 ? "delivering" : "no-delivery", on: last.date || null, impressions: last.impressions ?? null };
163
+ }
164
+ const images = imagesOf(root, set);
165
+ const slots = slotsOf(manifest);
166
+ return {
167
+ set,
168
+ title: (manifest.split(/\r?\n/)[0] || "").replace(/^#\s*/, ""),
169
+ revision,
170
+ review,
171
+ review_at: latest ? latest.at : null,
172
+ review_note: latest && latest.revision === revision ? (latest.note || "") : "",
173
+ publication,
174
+ effective_status: receipt ? receipt.effective_status || null : null,
175
+ receipt_url: receipt ? receipt.url || null : null,
176
+ delivery,
177
+ slots,
178
+ images,
179
+ reviewable: images.length > 0 && slots.length > 0,
180
+ open: review !== "rejected" && review !== "withdrawn" && publication !== "published",
181
+ };
182
+ });
183
+ }
184
+
185
+ export function record(root, { set, status, note = "", revision = null }) {
186
+ if (!REVIEW_STATUSES.includes(status)) throw new Error("status must be one of " + REVIEW_STATUSES.join(", "));
187
+ const entry = catalog(root).find((c) => c.set === set);
188
+ if (!entry) throw new Error("no such set: " + set);
189
+ if (!entry.reviewable) throw new Error("set has no images or no slots yet, nothing to review");
190
+ if (revision && revision !== entry.revision) throw new Error("the set changed since the page loaded; reload and review the new revision");
191
+ if (status === "withdrawn" && entry.publication === "published") throw new Error("a published set cannot be withdrawn here; pausing a live ad is a change list line");
192
+ if (status === "withdrawn" && entry.review !== "approved") throw new Error("only an approved set can be withdrawn");
193
+ if (entry.review === status) return entry;
194
+ const row = { set, revision: entry.revision, status, at: new Date().toISOString(), by: "member", note: String(note || "").slice(0, 500) };
195
+ const file = path.join(root, "creative", "approvals.jsonl");
196
+ fs.mkdirSync(path.dirname(file), { recursive: true });
197
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
198
+ const prefix = existing.length && !existing.endsWith("\n") ? "\n" : "";
199
+ fs.appendFileSync(file, prefix + JSON.stringify(row) + "\n", "utf8");
200
+ return catalog(root).find((c) => c.set === set);
201
+ }
202
+
203
+ function esc(s) {
204
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
205
+ }
206
+
207
+ function page(root) {
208
+ const items = catalog(root);
209
+ const chip = (label, value) => `<span class="chip ${esc(value)}">${esc(label)}: ${esc(value)}</span>`;
210
+ const cards = items.map((it) => {
211
+ const imgs = it.images.map((f) => `<img src="/${esc(it.set)}/${esc(f)}" alt="">`).join("");
212
+ const slots = it.slots.map((s) => `<li>${esc(s)}</li>`).join("");
213
+ const canReview = it.reviewable && it.publication !== "published";
214
+ const buttons = canReview ? `
215
+ <div class="actions">
216
+ <button data-status="approved">Approve</button>
217
+ <button data-status="needs-revision">Needs revision</button>
218
+ <button data-status="rejected">Reject</button>
219
+ ${it.review === "approved" ? `<button data-status="withdrawn">Withdraw approval</button>` : ""}
220
+ <input type="text" placeholder="Note, one line, optional" maxlength="500">
221
+ </div>` : `<p class="muted">${it.publication === "published" ? "Published. Pausing a live ad is a change list line, not a review." : "Not reviewable yet: the set needs at least one image and one slot."}</p>`;
222
+ const delivery = it.delivery.state === "delivering" ? `delivering (${it.delivery.impressions} impressions on ${it.delivery.on})` : it.delivery.state;
223
+ return `<article data-set="${esc(it.set)}" data-revision="${esc(it.revision)}">
224
+ <h2>${esc(it.title || it.set)}</h2>
225
+ <p class="path">${esc(it.set)} <code>${esc(it.revision.slice(0, 12))}</code></p>
226
+ <p>${chip("review", it.review)} ${chip("publication", it.publication)}${it.effective_status ? " " + chip("platform", it.effective_status) : ""} ${chip("delivery", delivery)}</p>
227
+ ${it.review_note ? `<p class="note">Note: ${esc(it.review_note)}</p>` : ""}
228
+ ${it.receipt_url ? `<p><a href="${esc(it.receipt_url)}" target="_blank" rel="noopener">Open in the ad account</a></p>` : ""}
229
+ <div class="images">${imgs}</div>
230
+ <ul class="slots">${slots}</ul>
231
+ ${buttons}
232
+ </article>`;
233
+ }).join("\n");
234
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Creative review</title>
235
+ <meta name="viewport" content="width=device-width, initial-scale=1">
236
+ <style>
237
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;padding:24px;background:#f6f6f4;color:#1b1b1b}
238
+ h1{font-size:20px;margin:0 0 4px}
239
+ .lede{color:#555;margin:0 0 20px;font-size:14px}
240
+ article{background:#fff;border:1px solid #e2e2df;border-radius:10px;padding:16px 18px;margin:0 0 16px;max-width:980px}
241
+ h2{font-size:16px;margin:0 0 4px}
242
+ .path{color:#666;font-size:12px;margin:0 0 8px}
243
+ .chip{display:inline-block;font-size:12px;padding:2px 8px;border-radius:999px;background:#eee;margin-right:4px}
244
+ .chip.approved,.chip.published,.chip.delivering{background:#dff3e4}
245
+ .chip.rejected,.chip.withdrawn{background:#f9dede}
246
+ .chip.needs-revision,.chip.changed,.chip.awaiting-review,.chip.prepared{background:#fff0c7}
247
+ .images{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0}
248
+ .images img{max-width:300px;max-height:300px;border:1px solid #ddd;border-radius:6px}
249
+ .slots{font-size:14px;padding-left:18px}
250
+ .actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px}
251
+ .actions input{flex:1;min-width:220px;padding:6px 8px;border:1px solid #ccc;border-radius:6px}
252
+ button{padding:6px 12px;border:1px solid #bbb;border-radius:6px;background:#fafafa;cursor:pointer}
253
+ button:hover{background:#eee}
254
+ .muted{color:#777;font-size:13px}
255
+ .note{font-size:13px;color:#444}
256
+ #msg{color:#a00;font-size:13px;min-height:18px}
257
+ </style></head><body>
258
+ <h1>Creative review</h1>
259
+ <p class="lede">Three facts, kept apart: review is your decision, publication is what the build desk did, delivery is what the account read observed. Every label here is read from the files after each click.</p>
260
+ <div id="msg"></div>
261
+ <main>${cards || "<p class='muted'>No creative sets yet.</p>"}</main>
262
+ <script>
263
+ document.addEventListener("click", async (e) => {
264
+ const b = e.target.closest("button[data-status]"); if (!b) return;
265
+ const a = b.closest("article"); const note = a.querySelector("input") ? a.querySelector("input").value : "";
266
+ b.disabled = true;
267
+ try {
268
+ const r = await fetch("/api/review", { method: "POST", headers: { "content-type": "application/json" },
269
+ body: JSON.stringify({ set: a.dataset.set, revision: a.dataset.revision, status: b.dataset.status, note }) });
270
+ const j = await r.json();
271
+ if (!r.ok) { document.getElementById("msg").textContent = j.error || "Could not record the review."; b.disabled = false; return; }
272
+ location.reload();
273
+ } catch (err) { document.getElementById("msg").textContent = "The review page could not reach its own server."; b.disabled = false; }
274
+ });
275
+ </script></body></html>`;
276
+ }
277
+
278
+ function serve(root, port) {
279
+ const host = "127.0.0.1";
280
+ const origin = "http://" + host + ":" + port;
281
+ const mime = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
282
+ const server = http.createServer(async (req, res) => {
283
+ const send = (code, body, type) => {
284
+ res.writeHead(code, { "Content-Type": type || "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
285
+ res.end(type ? body : JSON.stringify(body));
286
+ };
287
+ try {
288
+ const hostHeader = String(req.headers.host || "");
289
+ if (hostHeader !== host + ":" + port && hostHeader !== "localhost:" + port) return send(403, { error: "local access only" });
290
+ const url = new URL(req.url, origin);
291
+ if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) return send(200, page(root), "text/html; charset=utf-8");
292
+ if (req.method === "GET" && url.pathname === "/api/catalog") return send(200, catalog(root));
293
+ if (req.method === "POST" && url.pathname === "/api/review") {
294
+ const o = String(req.headers.origin || "");
295
+ if (o !== origin && o !== "http://localhost:" + port) return send(403, { error: "same origin only" });
296
+ let body = "";
297
+ for await (const chunk of req) { body += chunk; if (body.length > 8192) return send(413, { error: "too large" }); }
298
+ let parsed;
299
+ try { parsed = JSON.parse(body); } catch { return send(400, { error: "bad JSON" }); }
300
+ try { return send(200, record(root, parsed)); } catch (err) { return send(409, { error: err.message }); }
301
+ }
302
+ if (req.method !== "GET") return send(405, { error: "method not allowed" });
303
+ const rel = decodeURIComponent(url.pathname).replace(/^\/+/, "");
304
+ const m = /^creative\/(set-\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+)\/([A-Za-z0-9_.-]+\.(?:png|jpe?g|webp))$/i.exec(rel);
305
+ if (!m) return send(404, { error: "not found" });
306
+ const file = path.join(root, "creative", m[1], m[2]);
307
+ if (!fs.existsSync(file)) return send(404, { error: "not found" });
308
+ return send(200, fs.readFileSync(file), mime[path.extname(file).toLowerCase()] || "application/octet-stream");
309
+ } catch {
310
+ return send(500, { error: "the review page could not read its files" });
311
+ }
312
+ });
313
+ server.listen(port, host, () => process.stdout.write("review page: " + origin + "\n"));
314
+ return server;
315
+ }
316
+
317
+ function selftest() {
318
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ads-review-"));
319
+ let failures = 0;
320
+ const check = (name, ok) => { process.stdout.write((ok ? " ok " : " FAIL ") + name + "\n"); if (!ok) failures++; };
321
+ try {
322
+ fs.mkdirSync(path.join(root, "plan"), { recursive: true });
323
+ fs.writeFileSync(path.join(root, "plan", "offer.md"), "# Offer\n\n## Landing URL\nhttps://example.com/pricing\n\n## Countries sold into\nUS, GB\n\n## Daily cap\n20\n\n## Currency\nUSD\n");
324
+ const set = "creative/set-2026-03-04-fast-estimate";
325
+ fs.mkdirSync(path.join(root, set), { recursive: true });
326
+ fs.writeFileSync(path.join(root, set, "set.md"), "# Creative set: fast estimate, produced 2026-03-04\n\n## Slots\n- Primary text, a: Get a quote in a day, 20, cap 125\n- Headline, a: Same day estimate, 17, cap 40\n\n## Images\n- hero.png, 1080x1080, 12 bytes, 1x1\n");
327
+ fs.writeFileSync(path.join(root, set, "hero.png"), Buffer.from("not-an-image"));
328
+
329
+ let c = catalog(root);
330
+ check("catalog lists the set", c.length === 1 && c[0].set === set);
331
+ check("a set with no row awaits review", c[0].review === "awaiting-review" && c[0].publication === "unpublished" && c[0].delivery.state === "not-observed");
332
+ check("a set with no row is open", c[0].open === true);
333
+
334
+ let r = record(root, { set, status: "approved", note: "run it" });
335
+ check("approve writes a member row bound to the revision", r.review === "approved" && readRows(path.join(root, "creative", "approvals.jsonl"))[0].by === "member");
336
+ check("approving twice appends nothing", (record(root, { set, status: "approved" }), readRows(path.join(root, "creative", "approvals.jsonl")).length === 1));
337
+
338
+ const before = c[0].revision;
339
+ fs.writeFileSync(path.join(root, "plan", "offer.md"), "# Offer\n\n## Landing URL\nhttps://example.com/pricing\n\n## Countries sold into\nUS, GB\n\n## Daily cap\n30\n\n## Currency\nUSD\n");
340
+ c = catalog(root);
341
+ check("a budget change gives a new revision", c[0].revision !== before);
342
+ check("an approval for an earlier revision reads changed", c[0].review === "changed");
343
+ let threw = false;
344
+ try { record(root, { set, status: "approved", revision: before }); } catch { threw = true; }
345
+ check("a stale page cannot approve the new revision", threw);
346
+
347
+ r = record(root, { set, status: "rejected", note: "dated imagery" });
348
+ check("reject closes the slot", r.review === "rejected" && r.open === false && r.review_note === "dated imagery");
349
+
350
+ r = record(root, { set, status: "needs-revision", note: "shorter headline" });
351
+ check("needs revision keeps the slot open with the note", r.review === "needs-revision" && r.open === true);
352
+
353
+ record(root, { set, status: "approved" });
354
+ fs.mkdirSync(path.join(root, "build"), { recursive: true });
355
+ fs.writeFileSync(path.join(root, "build", "publication-receipts.jsonl"), JSON.stringify({ set, revision: c[0].revision, ad_id: "1", campaign_id: "2", configured_status: "ACTIVE", effective_status: "PENDING_REVIEW", url: "https://example.com/ads" }) + "\n");
356
+ c = catalog(root);
357
+ check("a receipt with an ad id reads published, not queued", c[0].publication === "published" && c[0].effective_status === "PENDING_REVIEW");
358
+ check("published is still not delivering", c[0].delivery.state === "not-observed" && c[0].open === false);
359
+ threw = false;
360
+ try { record(root, { set, status: "withdrawn" }); } catch { threw = true; }
361
+ check("a published set cannot be withdrawn from the page", threw);
362
+
363
+ fs.mkdirSync(path.join(root, "metrics"), { recursive: true });
364
+ fs.writeFileSync(path.join(root, "metrics", "daily.jsonl"), JSON.stringify({ object_id: "1", level: "ad", date: "2026-03-05", impressions: 0 }) + "\n" + JSON.stringify({ object_id: "1", level: "ad", date: "2026-03-06", impressions: 140 }) + "\n");
365
+ c = catalog(root);
366
+ check("delivery comes from the latest metrics row for the ad", c[0].delivery.state === "delivering" && c[0].delivery.on === "2026-03-06");
367
+
368
+ const html = page(root);
369
+ check("the page renders every label from disk", html.includes("review: approved") && html.includes("publication: published") && html.includes("delivering (140 impressions"));
370
+ const banned = new RegExp("[" + String.fromCharCode(0x2013, 0x2014, 0xab, 0xbb) + "]");
371
+ check("the page carries no dash and no guillemet", !banned.test(html));
372
+
373
+ fs.writeFileSync(path.join(root, "creative", "approvals.jsonl"), JSON.stringify({ set, revision: c[0].revision, status: "approved", at: "x", by: "ads-build-desk" }) + "\n");
374
+ c = catalog(root);
375
+ check("a row not by the member is ignored", c[0].review === "awaiting-review");
376
+ } catch (err) {
377
+ failures++;
378
+ process.stdout.write(" FAIL selftest threw: " + err.message + "\n");
379
+ } finally {
380
+ fs.rmSync(root, { recursive: true, force: true });
381
+ }
382
+ process.stdout.write(failures === 0 ? "review: selftest PASS (17 checks)\n" : "review: selftest FAIL (" + failures + " of 17)\n");
383
+ process.exit(failures === 0 ? 0 : 1);
384
+ }
385
+
386
+ function usage() {
387
+ process.stdout.write([
388
+ "usage:",
389
+ " node scripts/review.mjs --serve [--port 8770] [--root <path>]",
390
+ " node scripts/review.mjs --catalog [--json] [--root <path>]",
391
+ " node scripts/review.mjs --record --set <creative/set-...> --status <approved|rejected|needs-revision|withdrawn> [--note \"...\"]",
392
+ " node scripts/review.mjs --selftest",
393
+ "",
394
+ ].join("\n"));
395
+ }
396
+
397
+ function main() {
398
+ const argv = process.argv.slice(2);
399
+ const opts = { serve: false, catalog: false, record: false, json: false, selftest: false, help: false, port: 8770, root: null, set: null, status: null, note: "" };
400
+ for (let i = 0; i < argv.length; i++) {
401
+ const a = argv[i];
402
+ switch (a) {
403
+ case "--serve": opts.serve = true; break;
404
+ case "--catalog": opts.catalog = true; break;
405
+ case "--record": opts.record = true; break;
406
+ case "--json": opts.json = true; break;
407
+ case "--selftest": opts.selftest = true; break;
408
+ case "--help": case "-h": opts.help = true; break;
409
+ case "--port": opts.port = Number(argv[++i]); break;
410
+ case "--root": opts.root = argv[++i]; break;
411
+ case "--set": opts.set = argv[++i]; break;
412
+ case "--status": opts.status = argv[++i]; break;
413
+ case "--note": opts.note = argv[++i]; break;
414
+ default:
415
+ process.stderr.write("review: unknown option " + a + "\n");
416
+ usage();
417
+ process.exit(2);
418
+ }
419
+ }
420
+ if (opts.help) { usage(); process.exit(0); }
421
+ if (opts.selftest) { selftest(); return; }
422
+ const root = path.resolve(opts.root || process.env.ADS_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), ".."));
423
+ if (opts.serve) { serve(root, opts.port); return; }
424
+ if (opts.record) {
425
+ try {
426
+ const r = record(root, { set: opts.set, status: opts.status, note: opts.note });
427
+ process.stdout.write(JSON.stringify(r, null, 2) + "\n");
428
+ process.exit(0);
429
+ } catch (err) {
430
+ process.stderr.write("review: " + err.message + "\n");
431
+ process.exit(1);
432
+ }
433
+ }
434
+ const items = catalog(root);
435
+ if (opts.json) { process.stdout.write(JSON.stringify(items, null, 2) + "\n"); process.exit(0); }
436
+ if (!items.length) process.stdout.write("no creative sets\n");
437
+ for (const it of items) {
438
+ process.stdout.write(it.set + "\n review " + it.review + (it.review_at ? " (" + it.review_at + ")" : "") + " | publication " + it.publication + (it.effective_status ? " (" + it.effective_status + ")" : "") + " | delivery " + it.delivery.state + "\n revision " + it.revision.slice(0, 12) + " | open " + it.open + "\n");
439
+ }
440
+ process.exit(0);
441
+ }
442
+
443
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();