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,772 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * runlog.mjs
4
+ * The only sanctioned way to append a run record to «GTM_ROOT»/runlog.jsonl.
5
+ *
6
+ * Why this script exists: a shell redirect, an append cmdlet, or a hand rolled
7
+ * write prepends a byte order mark on Windows, and that corrupts the first line
8
+ * of the log for every reader after it. This script writes UTF-8 with no mark,
9
+ * repairs a stray mark already at the head of the file, and refuses a record
10
+ * that carries a secret, a draft, or personal data.
11
+ *
12
+ * Requires Node 18 or newer. No dependencies, no package.json, no install.
13
+ * Runs the same on Windows and POSIX.
14
+ *
15
+ * USAGE
16
+ * These three work on every shell, so a routine should reach for one of them:
17
+ *
18
+ * <the JSON> | node scripts/runlog.mjs --stdin
19
+ * node scripts/runlog.mjs --file <path to a .json file>
20
+ * node scripts/runlog.mjs --routine gtm-outreach-queue --period 2026-03-04 \
21
+ * --start <ISO> --end <ISO> --status ok \
22
+ * --output "queue/2026-03-04-email.md (4 drafts)" \
23
+ * --blocker "..." --notes "one line"
24
+ *
25
+ * This one works on POSIX shells only. PowerShell strips every double quote
26
+ * out of an argument on its way to a native command, so the object arrives
27
+ * unquoted and unparseable. Do not put it in a routine that has to be
28
+ * portable:
29
+ *
30
+ * node scripts/runlog.mjs '<one JSON object>'
31
+ * node scripts/runlog.mjs --json '<one JSON object>'
32
+ *
33
+ * node scripts/runlog.mjs --failed-run gtm-board-standup --exit-code 1
34
+ * (the launcher's form: appends a failed record when the harness exited
35
+ * before the routine could write one, which is what a missing login looks like)
36
+ *
37
+ * node scripts/runlog.mjs --selftest
38
+ * node scripts/runlog.mjs --help
39
+ *
40
+ * OPTIONS
41
+ * --root <path> The kit working folder. Defaults to this script's parent
42
+ * folder, then the GTM_ROOT environment variable.
43
+ * --log <path> Write to this file instead of «GTM_ROOT»/runlog.jsonl.
44
+ * --dry-run Validate and print the line. Write nothing.
45
+ * --once Refuse if a record for this routine and period already
46
+ * exists. Off by default, because the once per period guard
47
+ * legitimately writes a second record with the status
48
+ * skipped-already-ran.
49
+ *
50
+ * THE RECORD
51
+ * Every field is required. outputs and blockers are always arrays, empty
52
+ * rather than absent. notes is one line.
53
+ *
54
+ * Nine optional fields are accepted when present, counts and prices only:
55
+ * model, harness (strings), turns, input_tokens, output_tokens,
56
+ * cache_write_tokens, cache_read_tokens (whole numbers), cost_usd (number or
57
+ * null), cost_basis (api-list | subscription | unknown).
58
+ *
59
+ * {"routine":"gtm-outreach-queue","period":"2026-03-04",
60
+ * "start":"2026-03-04T08:15:11+07:00","end":"2026-03-04T08:34:40+07:00",
61
+ * "status":"ok",
62
+ * "outputs":["queue/2026-03-04-email.md (4 drafts)"],
63
+ * "blockers":[],
64
+ * "notes":"follow-up interval 4 days"}
65
+ *
66
+ * EXIT CODES
67
+ * 0 appended (or validated, under --dry-run)
68
+ * 1 could not write the file
69
+ * 2 bad input: bad usage, bad JSON, bad shape, bad status, bad period
70
+ * 3 refused: the record carries a secret, a draft, or personal data
71
+ * 4 refused by --once: a record for this routine and period already exists
72
+ */
73
+
74
+ import fs from "node:fs";
75
+ import path from "node:path";
76
+ import { fileURLToPath } from "node:url";
77
+
78
+ const SELF = fileURLToPath(import.meta.url);
79
+ const SCRIPT_DIR = path.dirname(SELF);
80
+
81
+ /* ------------------------------------------------------------------ *
82
+ * The closed vocabularies. These come from CONTRACT.md sections 1 and 4.
83
+ * ------------------------------------------------------------------ */
84
+
85
+ const REQUIRED = ["routine", "period", "start", "end", "status", "outputs", "blockers", "notes"];
86
+
87
+ // Optional usage fields, validated when present and never required, so older records stay valid.
88
+ // Counts and prices only, never prose. They exist so cost is a measured field rather than a guess.
89
+ const OPTIONAL = {
90
+ model: "string", harness: "string", turns: "integer", input_tokens: "integer", output_tokens: "integer",
91
+ cache_write_tokens: "integer", cache_read_tokens: "integer", cost_usd: "number-or-null", cost_basis: "cost-basis",
92
+ };
93
+ const COST_BASIS = new Set(["api-list", "subscription", "unknown"]);
94
+
95
+ // CONTRACT.md 4.1, plus skipped-paused, which the pause switch in section 5 writes.
96
+ const STATUSES = new Set([
97
+ "ok",
98
+ "partial",
99
+ "failed",
100
+ "skipped-out-of-window",
101
+ "skipped-already-ran",
102
+ "blocked-login",
103
+ "blocked-browser-busy",
104
+ "skipped-paused",
105
+ ]);
106
+
107
+ // Statuses earlier drafts used. Naming the replacement is cheaper than a
108
+ // member reading the contract to work out what went wrong.
109
+ const RETIRED_STATUSES = {
110
+ "blocked-approval": "nothing in this kit waits for an approval. Use partial, or ok",
111
+ "blocked-machine": "use partial when file work was done, failed when it was not",
112
+ "skipped-browser-busy": "use blocked-browser-busy",
113
+ "blocked-capability": "use partial when file work was done, failed when it was not",
114
+ };
115
+
116
+ // CONTRACT.md 1.3. Which period key shape each routine writes.
117
+ const CADENCE = {
118
+ "gtm-signal-sweep": "day",
119
+ "gtm-board-standup": "day",
120
+ "gtm-outreach-queue": "day",
121
+ "gtm-launch-step-runner": "day",
122
+ "gtm-paid-and-tracking-guard": "week",
123
+ "gtm-scoreboard": "week",
124
+ "gtm-intake-and-dashboard": "month",
125
+ "gtm-icp-refresh": "month",
126
+ };
127
+
128
+ // CONTRACT.md Appendix A. A stale id fails on its first line forever and the
129
+ // member never sees an error, so catch it here.
130
+ const STALE_IDS = {
131
+ "icp-signal-sweep": "gtm-signal-sweep",
132
+ "gtm-list-build": "gtm-signal-sweep",
133
+ "list-build": "gtm-signal-sweep",
134
+ "outreach-drafts": "gtm-outreach-queue",
135
+ "gtm-outbound-draft-queue": "gtm-outreach-queue",
136
+ "launch-step-runner": "gtm-launch-step-runner",
137
+ "gtm-directory-and-press": "gtm-launch-step-runner",
138
+ "directory-and-press": "gtm-launch-step-runner",
139
+ "paid-ops-run": "gtm-paid-and-tracking-guard",
140
+ "weekly-gtm-review": "gtm-scoreboard",
141
+ "launch-dashboard-build": "gtm-intake-and-dashboard",
142
+ "monthly-icp-refresh": "gtm-icp-refresh",
143
+ "board-standup": "gtm-board-standup",
144
+ };
145
+
146
+ const PERIOD_SHAPE = {
147
+ day: /^\d{4}-\d{2}-\d{2}$/,
148
+ week: /^\d{4}-W\d{2}$/,
149
+ month: /^\d{4}-\d{2}$/,
150
+ };
151
+
152
+ /* ------------------------------------------------------------------ *
153
+ * What a run record may never carry. CONTRACT.md 4.2.
154
+ * The record holds the shape. The detail lives in the queue files.
155
+ * ------------------------------------------------------------------ */
156
+
157
+ const SECRET_CLASSES = [
158
+ ["stripe-key", /\bsk_(live|test)?_?[A-Za-z0-9]{8,}/],
159
+ ["webhook-secret", /\bwhsec_[A-Za-z0-9]{8,}/],
160
+ ["json-web-token", /\beyJ[A-Za-z0-9_-]{10,}/],
161
+ ["github-token", /\bgh[pousr]_[A-Za-z0-9]{16,}/],
162
+ ["google-api-key", /\bAIza[A-Za-z0-9_-]{20,}/],
163
+ ["aws-access-key", /\bAKIA[A-Z0-9]{12,}/],
164
+ ["slack-token", /\bxox[abprs]-[A-Za-z0-9-]{8,}/],
165
+ ["bearer-token", /\bBearer\s+[A-Za-z0-9._~+/-]{16,}/i],
166
+ ["webhook-url", /\bhook\.[a-z]/i],
167
+ ["password-literal", /\bpass(word|wd)\s*[:=]/i],
168
+ ["private-key-block", /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
169
+ ["credential-in-url", /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@]+:[^\s/@]+@/i],
170
+ ];
171
+
172
+ const EMAIL_ADDRESS = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+\.[A-Za-z]{2,}/;
173
+ const ANY_URL = /\bhttps?:\/\/\S/i;
174
+ const PLACEHOLDER = /[«»]/; // guillemets, the kit's unresolved marker
175
+ const SALUTATION = /^(hi|hey|hello|dear)\b[^\n]{0,40}[,!]/i;
176
+ // Two halves on purpose. A trailing \b after a comma never matches, because a
177
+ // comma and the space after it are both non-word characters, so the sign offs
178
+ // that end in a comma need their own alternation.
179
+ const SIGN_OFF = /\b(?:best regards|kind regards|warm regards|sincerely)\b|\b(?:cheers|best|thanks|regards|warmly),/i;
180
+ const SUBJECT_LINE = /\bsubject\s*:/i;
181
+
182
+ // Length caps. A run record carries counts and paths, never prose.
183
+ const CAPS = { notes: 400, output: 220, blocker: 320, routine: 80, period: 16, stamp: 40 };
184
+
185
+ /* ------------------------------------------------------------------ *
186
+ * Small helpers
187
+ * ------------------------------------------------------------------ */
188
+
189
+ function die(code, message) {
190
+ process.stderr.write("runlog: " + message + "\n");
191
+ process.exit(code);
192
+ }
193
+
194
+ function warn(message) {
195
+ process.stderr.write("runlog: note: " + message + "\n");
196
+ }
197
+
198
+ function isPlainObject(value) {
199
+ return value !== null && typeof value === "object" && !Array.isArray(value);
200
+ }
201
+
202
+ function readStdin() {
203
+ try {
204
+ return fs.readFileSync(0, "utf8");
205
+ } catch {
206
+ return "";
207
+ }
208
+ }
209
+
210
+ function stripBom(text) {
211
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
212
+ }
213
+
214
+ /* ------------------------------------------------------------------ *
215
+ * Argument parsing
216
+ * ------------------------------------------------------------------ */
217
+
218
+ function parseArgs(argv) {
219
+ const opts = {
220
+ failedRun: null, exitCode: null,
221
+ root: null,
222
+ log: null,
223
+ json: null,
224
+ file: null,
225
+ stdin: false,
226
+ dryRun: false,
227
+ once: false,
228
+ selftest: false,
229
+ help: false,
230
+ fields: {},
231
+ outputs: [],
232
+ blockers: [],
233
+ positional: [],
234
+ };
235
+
236
+ const takesValue = new Set([
237
+ "--root", "--log", "--json", "--file",
238
+ "--routine", "--period", "--start", "--end", "--status", "--notes",
239
+ "--output", "--blocker",
240
+ ]);
241
+
242
+ for (let i = 0; i < argv.length; i++) {
243
+ const arg = argv[i];
244
+
245
+ if (arg === "--help" || arg === "-h") { opts.help = true; continue; }
246
+ if (arg === "--selftest") { opts.selftest = true; continue; }
247
+ if (arg === "--dry-run") { opts.dryRun = true; continue; }
248
+ if (arg === "--once") { opts.once = true; continue; }
249
+ if (arg === "--failed-run") { opts.failedRun = argv[++i]; if (opts.failedRun === undefined) die(2, "--failed-run needs a routine id"); continue; }
250
+ if (arg === "--exit-code") { opts.exitCode = argv[++i]; if (opts.exitCode === undefined) die(2, "--exit-code needs a number"); continue; }
251
+ if (arg === "--stdin" || arg === "-") { opts.stdin = true; continue; }
252
+
253
+ if (takesValue.has(arg)) {
254
+ const value = argv[i + 1];
255
+ if (value === undefined) die(2, arg + " needs a value");
256
+ i++;
257
+ switch (arg) {
258
+ case "--root": opts.root = value; break;
259
+ case "--log": opts.log = value; break;
260
+ case "--json": opts.json = value; break;
261
+ case "--file": opts.file = value; break;
262
+ case "--output": opts.outputs.push(value); break;
263
+ case "--blocker": opts.blockers.push(value); break;
264
+ default: opts.fields[arg.slice(2)] = value; break;
265
+ }
266
+ continue;
267
+ }
268
+
269
+ if (arg.startsWith("--")) {
270
+ die(2, "unknown option " + arg + ". Run with --help for the interface.");
271
+ }
272
+ opts.positional.push(arg);
273
+ }
274
+
275
+ return opts;
276
+ }
277
+
278
+ function resolveRoot(opts) {
279
+ if (opts.root) return path.resolve(opts.root);
280
+ if (process.env.GTM_ROOT) return path.resolve(process.env.GTM_ROOT);
281
+ // The script lives at «GTM_ROOT»/scripts/runlog.mjs, so its parent is the
282
+ // root no matter which folder the routine was launched from.
283
+ return path.resolve(SCRIPT_DIR, "..");
284
+ }
285
+
286
+ /* ------------------------------------------------------------------ *
287
+ * Building the record from whatever the caller gave us
288
+ * ------------------------------------------------------------------ */
289
+
290
+ function recordFromFlags(opts) {
291
+ const f = opts.fields;
292
+ const record = {
293
+ routine: f.routine,
294
+ period: f.period,
295
+ start: f.start,
296
+ end: f.end,
297
+ status: f.status,
298
+ outputs: opts.outputs,
299
+ blockers: opts.blockers,
300
+ notes: f.notes === undefined ? "" : f.notes,
301
+ };
302
+ for (const key of ["routine", "period", "start", "end", "status"]) {
303
+ if (record[key] === undefined) {
304
+ die(2, "missing --" + key + ". Run with --help for the interface.");
305
+ }
306
+ }
307
+ return record;
308
+ }
309
+
310
+ /*
311
+ * The launcher's record. run/<id>.cmd calls this when the harness exits non zero
312
+ * before the routine wrote anything, which is what a missing login looks like.
313
+ */
314
+ function failedRecord(opts) {
315
+ const id = String(opts.failedRun).trim();
316
+ const code = String(opts.exitCode === null ? "unknown" : opts.exitCode).trim();
317
+ const now = new Date();
318
+ const pad = (n) => String(n).padStart(2, "0");
319
+ const off = -now.getTimezoneOffset();
320
+ const stamp = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) + "T" + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds()) +
321
+ (off >= 0 ? "+" : "-") + pad(Math.floor(Math.abs(off) / 60)) + ":" + pad(Math.abs(off) % 60);
322
+ const cadence = CADENCE[id] || "day";
323
+ let period = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate());
324
+ if (cadence === "month") period = now.getFullYear() + "-" + pad(now.getMonth() + 1);
325
+ if (cadence === "week") {
326
+ const t = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
327
+ const day = t.getUTCDay() || 7;
328
+ t.setUTCDate(t.getUTCDate() + 4 - day);
329
+ const ys = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
330
+ period = t.getUTCFullYear() + "-W" + pad(Math.ceil(((t - ys) / 86400000 + 1) / 7));
331
+ }
332
+ return {
333
+ routine: id, period, start: stamp, end: stamp, status: "failed", outputs: [],
334
+ blockers: ["launcher: the harness exited with code " + code + " before this routine wrote a run record. Check the login first (claude auth status), then the path to the binary in run/" + id + ".cmd"],
335
+ notes: "written by the launcher, not by the routine",
336
+ };
337
+ }
338
+
339
+ function loadPayload(opts) {
340
+ const usedFlagForm = Object.keys(opts.fields).length > 0 || opts.outputs.length > 0 || opts.blockers.length > 0;
341
+
342
+ let raw = null;
343
+ let source = "";
344
+
345
+ if (opts.json !== null) { raw = opts.json; source = "--json"; }
346
+ else if (opts.file !== null) {
347
+ source = opts.file;
348
+ try { raw = fs.readFileSync(opts.file, "utf8"); }
349
+ catch (err) { die(2, "cannot read " + opts.file + ": " + err.message); }
350
+ } else if (opts.positional.length > 0) { raw = opts.positional.join(" "); source = "argument"; }
351
+ else if (usedFlagForm) { return { record: recordFromFlags(opts), source: "flags" }; }
352
+ else if (opts.stdin || !process.stdin.isTTY) { raw = readStdin(); source = "stdin"; }
353
+ else {
354
+ die(2, "no payload. Pass a JSON object, --file <path>, --stdin, or the --routine flag set. Run with --help.");
355
+ }
356
+
357
+ raw = stripBom(raw).trim();
358
+ if (!raw) die(2, "empty payload from " + source);
359
+
360
+ let parsed;
361
+ try { parsed = JSON.parse(raw); }
362
+ catch (err) {
363
+ // PowerShell strips every double quote out of an argument on its way to a
364
+ // native command, so a JSON object passed positionally arrives with no
365
+ // quotes at all and cannot be recovered without guessing. The signature is
366
+ // unmistakable, so name it rather than letting the member re-read the docs.
367
+ const quoteStripped = raw.startsWith("{") && raw.includes(":") && !raw.includes("\"");
368
+ if (quoteStripped) {
369
+ die(2,
370
+ "the JSON arrived with every double quote removed, which is what PowerShell does to an\n" +
371
+ " argument handed to a native command. The object cannot be rebuilt from it.\n" +
372
+ " Three routes work on every shell. Any of them is fine:\n" +
373
+ " 1. pipe it in: $record | node scripts/runlog.mjs --stdin\n" +
374
+ " 2. write a file: node scripts/runlog.mjs --file <path to a .json file>\n" +
375
+ " 3. use flags: node scripts/runlog.mjs --routine <id> --period <key> --status ok ...\n" +
376
+ " Passing the object as a positional argument works on POSIX shells only."
377
+ );
378
+ }
379
+ die(2, "payload from " + source + " is not valid JSON: " + err.message +
380
+ "\n Pipe it to --stdin, or pass --file, if your shell is rewriting the quotes.");
381
+ }
382
+ if (!isPlainObject(parsed)) die(2, "payload must be one JSON object, not an array or a scalar");
383
+ return { record: parsed, source };
384
+ }
385
+
386
+ /* ------------------------------------------------------------------ *
387
+ * Validation: shape first, then content
388
+ * ------------------------------------------------------------------ */
389
+
390
+ function validateShape(record) {
391
+ const problems = [];
392
+
393
+ for (const key of REQUIRED) {
394
+ if (!(key in record)) problems.push("missing key " + key);
395
+ }
396
+ if (problems.length) return problems;
397
+
398
+ const extra = Object.keys(record).filter((k) => !REQUIRED.includes(k) && !(k in OPTIONAL));
399
+ for (const [key, kind] of Object.entries(OPTIONAL)) {
400
+ if (!(key in record)) continue;
401
+ const v = record[key];
402
+ const ok = kind === "string" ? typeof v === "string"
403
+ : kind === "integer" ? Number.isInteger(v) && v >= 0
404
+ : kind === "number-or-null" ? (v === null || (typeof v === "number" && Number.isFinite(v) && v >= 0))
405
+ : COST_BASIS.has(v);
406
+ if (!ok) problems.push(key + " must be " + (kind === "cost-basis" ? "one of api-list, subscription, unknown" : kind === "number-or-null" ? "a number or null" : kind === "integer" ? "a whole number" : "a string"));
407
+ }
408
+ if (extra.length) problems.push("unexpected key(s): " + extra.join(", ") + ". The schema is closed.");
409
+
410
+ for (const key of ["routine", "period", "start", "end", "status", "notes"]) {
411
+ if (typeof record[key] !== "string") problems.push(key + " must be a string");
412
+ }
413
+ for (const key of ["outputs", "blockers"]) {
414
+ if (!Array.isArray(record[key])) problems.push(key + " must be an array, empty rather than absent");
415
+ else if (record[key].some((v) => typeof v !== "string")) problems.push("every entry in " + key + " must be a string");
416
+ }
417
+ if (problems.length) return problems;
418
+
419
+ // Routine id
420
+ const routine = record.routine.trim();
421
+ if (!routine) problems.push("routine is empty");
422
+ else if (STALE_IDS[routine]) {
423
+ problems.push("routine \"" + routine + "\" is a stale id. Use \"" + STALE_IDS[routine] + "\" (CONTRACT.md appendix A).");
424
+ } else if (!CADENCE[routine] && !routine.startsWith("gtm-")) {
425
+ problems.push("routine \"" + routine + "\" is not one of the eight and does not carry the gtm- prefix");
426
+ }
427
+
428
+ // Status
429
+ if (!STATUSES.has(record.status)) {
430
+ const hint = RETIRED_STATUSES[record.status];
431
+ problems.push(
432
+ "status \"" + record.status + "\" is not one of the seven" +
433
+ (hint ? ": " + hint : "") +
434
+ "\n allowed: " + [...STATUSES].join(" | ")
435
+ );
436
+ }
437
+
438
+ // Period shape
439
+ const period = record.period.trim();
440
+ const shapeOk = Object.values(PERIOD_SHAPE).some((re) => re.test(period));
441
+ if (!shapeOk) {
442
+ problems.push("period \"" + period + "\" is not a period key. Use YYYY-MM-DD, YYYY-Www, or YYYY-MM (CONTRACT.md 1.3).");
443
+ }
444
+
445
+ // Timestamps
446
+ for (const key of ["start", "end"]) {
447
+ const value = record[key].trim();
448
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) || Number.isNaN(Date.parse(value))) {
449
+ problems.push(key + " \"" + value + "\" is not an ISO 8601 timestamp");
450
+ }
451
+ }
452
+
453
+ // Length caps
454
+ if (record.notes.length > CAPS.notes) problems.push("notes is " + record.notes.length + " characters. It is one line, cap " + CAPS.notes + ".");
455
+ record.outputs.forEach((v, i) => {
456
+ if (v.length > CAPS.output) problems.push("outputs[" + i + "] is " + v.length + " characters, cap " + CAPS.output);
457
+ });
458
+ record.blockers.forEach((v, i) => {
459
+ if (v.length > CAPS.blocker) problems.push("blockers[" + i + "] is " + v.length + " characters, cap " + CAPS.blocker);
460
+ });
461
+
462
+ return problems;
463
+ }
464
+
465
+ function stringFields(record) {
466
+ const out = [["routine", record.routine], ["period", record.period], ["start", record.start], ["end", record.end], ["status", record.status], ["notes", record.notes]];
467
+ for (const k of ["model", "harness", "cost_basis"]) if (typeof record[k] === "string") out.push([k, record[k]]);
468
+ (record.outputs || []).forEach((v, i) => out.push(["outputs[" + i + "]", v]));
469
+ (record.blockers || []).forEach((v, i) => out.push(["blockers[" + i + "]", v]));
470
+ return out.filter(([, v]) => typeof v === "string");
471
+ }
472
+
473
+ /*
474
+ * Refusals. A secret, a draft, or personal data in a run record is not a style
475
+ * problem: the run log is the file most likely to be pasted into a support
476
+ * thread or a screenshot. The report names the field and the class, never the
477
+ * matched text.
478
+ */
479
+ function findRefusals(record) {
480
+ const refusals = [];
481
+
482
+ for (const [field, value] of stringFields(record)) {
483
+ for (const [cls, re] of SECRET_CLASSES) {
484
+ if (re.test(value)) refusals.push({ field, cls: "secret:" + cls });
485
+ }
486
+ if (EMAIL_ADDRESS.test(value)) refusals.push({ field, cls: "personal-data:email-address" });
487
+ if (ANY_URL.test(value)) refusals.push({ field, cls: "personal-data:url" });
488
+ if (value.includes("\n") || value.includes("\r")) refusals.push({ field, cls: "draft-text:multiple-lines" });
489
+ if (SUBJECT_LINE.test(value)) refusals.push({ field, cls: "draft-text:subject-line" });
490
+ if (SALUTATION.test(value.trim())) refusals.push({ field, cls: "draft-text:salutation" });
491
+ if (SIGN_OFF.test(value)) refusals.push({ field, cls: "draft-text:sign-off" });
492
+ if (PLACEHOLDER.test(value)) refusals.push({ field, cls: "template:unresolved-placeholder" });
493
+ }
494
+
495
+ // De-duplicate on field plus class.
496
+ const seen = new Set();
497
+ return refusals.filter((r) => {
498
+ const key = r.field + "|" + r.cls;
499
+ if (seen.has(key)) return false;
500
+ seen.add(key);
501
+ return true;
502
+ });
503
+ }
504
+
505
+ /* Advisory only. Never lose a record over one of these. */
506
+ function advisories(record) {
507
+ const notes = [];
508
+ const cadence = CADENCE[record.routine];
509
+ if (cadence && !PERIOD_SHAPE[cadence].test(record.period.trim())) {
510
+ const want = { day: "YYYY-MM-DD", week: "YYYY-Www", month: "YYYY-MM" }[cadence];
511
+ notes.push(record.routine + " runs on a " + cadence + " cadence, so its period key should look like " + want +
512
+ ". Got \"" + record.period + "\". The once per period guard will not match.");
513
+ }
514
+ for (const key of ["start", "end"]) {
515
+ const value = String(record[key] || "");
516
+ if (!/(Z|[+-]\d{2}:?\d{2})$/.test(value)) {
517
+ notes.push(key + " carries no timezone offset. Read the machine timezone and stamp it, never assume one.");
518
+ }
519
+ }
520
+ if (Date.parse(record.end) < Date.parse(record.start)) notes.push("end is earlier than start");
521
+ for (const [field, value] of stringFields(record)) {
522
+ for (const ch of value) {
523
+ const cp = ch.codePointAt(0);
524
+ if (cp === 0x2013 || cp === 0x2014 || cp === 0x2012 || cp === 0x2015) {
525
+ notes.push(field + " carries a dash character at code point U+" + cp.toString(16).toUpperCase() + ". Use a comma or a period.");
526
+ break;
527
+ }
528
+ }
529
+ }
530
+ return notes;
531
+ }
532
+
533
+ /* ------------------------------------------------------------------ *
534
+ * Writing
535
+ * ------------------------------------------------------------------ */
536
+
537
+ function repairBom(logPath) {
538
+ // A byte order mark left by an earlier shell append corrupts the first line
539
+ // for every reader. Strip it through a temp file plus rename so a crash
540
+ // mid repair cannot truncate the log.
541
+ let head;
542
+ try {
543
+ const fd = fs.openSync(logPath, "r");
544
+ head = Buffer.alloc(3);
545
+ fs.readSync(fd, head, 0, 3, 0);
546
+ fs.closeSync(fd);
547
+ } catch { return false; }
548
+ if (!(head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf)) return false;
549
+
550
+ const body = fs.readFileSync(logPath).slice(3);
551
+ const tmp = logPath + ".bomfix." + process.pid + ".tmp";
552
+ fs.writeFileSync(tmp, body);
553
+ fs.renameSync(tmp, logPath);
554
+ return true;
555
+ }
556
+
557
+ function needsLeadingNewline(logPath) {
558
+ // A previous writer that died mid line leaves no trailing newline. Our line
559
+ // has to start on a fresh one or it silently merges into the broken record.
560
+ try {
561
+ const size = fs.statSync(logPath).size;
562
+ if (size === 0) return false;
563
+ const fd = fs.openSync(logPath, "r");
564
+ const last = Buffer.alloc(1);
565
+ fs.readSync(fd, last, 0, 1, size - 1);
566
+ fs.closeSync(fd);
567
+ return last[0] !== 0x0a;
568
+ } catch {
569
+ return false;
570
+ }
571
+ }
572
+
573
+ function alreadyRecorded(logPath, routine, period) {
574
+ if (!fs.existsSync(logPath)) return false;
575
+ let text;
576
+ try { text = stripBom(fs.readFileSync(logPath, "utf8")); } catch { return false; }
577
+ for (const line of text.split("\n")) {
578
+ const trimmed = line.trim();
579
+ if (!trimmed) continue;
580
+ try {
581
+ const row = JSON.parse(trimmed);
582
+ if (row && row.routine === routine && row.period === period) return true;
583
+ } catch { /* a malformed line is not our problem here */ }
584
+ }
585
+ return false;
586
+ }
587
+
588
+ function appendRecord(logPath, line) {
589
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
590
+ if (fs.existsSync(logPath)) repairBom(logPath);
591
+
592
+ const payload = (needsLeadingNewline(logPath) ? "\n" : "") + line + "\n";
593
+ const buf = Buffer.from(payload, "utf8");
594
+
595
+ // One write to a descriptor opened in append mode. The seek to end and the
596
+ // write are a single operation, so a concurrent routine cannot interleave
597
+ // half of its line into the middle of ours.
598
+ const fd = fs.openSync(logPath, "a");
599
+ try {
600
+ fs.writeSync(fd, buf, 0, buf.length, null);
601
+ try { fs.fsyncSync(fd); } catch { /* some filesystems refuse fsync, the write still landed */ }
602
+ } finally {
603
+ fs.closeSync(fd);
604
+ }
605
+ }
606
+
607
+ /* ------------------------------------------------------------------ *
608
+ * Self test
609
+ * ------------------------------------------------------------------ */
610
+
611
+ function good() {
612
+ return {
613
+ routine: "gtm-outreach-queue",
614
+ period: "2026-03-04",
615
+ start: "2026-03-04T08:15:11+07:00",
616
+ end: "2026-03-04T08:34:40+07:00",
617
+ status: "ok",
618
+ outputs: ["queue/2026-03-04-email.md (4 drafts)", "crm/contacted.jsonl (+10 queued)"],
619
+ blockers: [],
620
+ notes: "1 draft dropped by copy-check, unsourced number; follow-up interval 4 days",
621
+ };
622
+ }
623
+
624
+ function selftest() {
625
+ const cases = [];
626
+ const add = (name, mutate, expectShape, expectRefusal) => {
627
+ const record = good();
628
+ if (mutate) mutate(record);
629
+ cases.push({ name, record, expectShape, expectRefusal });
630
+ };
631
+
632
+ add("a valid record", null, false, false);
633
+ add("weekly key on a weekly routine", (r) => { r.routine = "gtm-scoreboard"; r.period = "2026-W10"; }, false, false);
634
+ add("monthly key on a monthly routine", (r) => { r.routine = "gtm-icp-refresh"; r.period = "2026-03"; }, false, false);
635
+ add("missing key", (r) => { delete r.notes; }, true, false);
636
+ add("extra key", (r) => { r.extra = "x"; }, true, false);
637
+ add("blockers as a string", (r) => { r.blockers = "none"; }, true, false);
638
+ add("retired status", (r) => { r.status = "blocked-approval"; }, true, false);
639
+ add("invented status", (r) => { r.status = "done"; }, true, false);
640
+ add("stale routine id", (r) => { r.routine = "outreach-drafts"; }, true, false);
641
+ add("routine without the prefix", (r) => { r.routine = "my-routine"; }, true, false);
642
+ add("bad period shape", (r) => { r.period = "March 4"; }, true, false);
643
+ add("bad timestamp", (r) => { r.start = "this morning"; }, true, false);
644
+ add("overlong notes", (r) => { r.notes = "x".repeat(CAPS.notes + 1); }, true, false);
645
+ add("the pause status", (r) => { r.status = "skipped-paused"; }, false, false);
646
+ add("optional usage fields", (r) => { r.model = "claude-sonnet-5"; r.harness = "claude-cli 2.1.255"; r.turns = 12; r.output_tokens = 5000; r.cost_usd = 1.23; r.cost_basis = "api-list"; }, false, false);
647
+ add("optional field of the wrong type", (r) => { r.turns = "twelve"; }, true, false);
648
+ add("optional cost basis outside the three", (r) => { r.cost_basis = "guess"; }, true, false);
649
+ // The fake key is split across a concatenation so a repository secret
650
+ // scanner does not flag this test fixture. It is one string at runtime.
651
+ add("a secret", (r) => { r.notes = "used key sk_" + "live_51H8xQ2ePlacehold"; }, false, true);
652
+ add("an email address", (r) => { r.blockers = ["could not reach sam@example.com"]; }, false, true);
653
+ add("a URL", (r) => { r.notes = "see https://example.com/page"; }, false, true);
654
+ add("a draft body", (r) => { r.notes = "Hi Sam, I saw your job post and wanted to reach out."; }, false, true);
655
+ add("a subject line", (r) => { r.outputs = ["subject: quick question about hiring"]; }, false, true);
656
+ add("a sign off", (r) => { r.notes = "closed with Best, Mark"; }, false, true);
657
+ add("an unresolved placeholder", (r) => { r.period = "2026-03-04"; r.notes = "swept «source»"; }, false, true);
658
+ add("a multi line field", (r) => { r.notes = "line one\nline two"; }, false, true);
659
+
660
+ let failures = 0;
661
+ for (const c of cases) {
662
+ const shape = validateShape(c.record);
663
+ const refusals = shape.length ? [] : findRefusals(c.record);
664
+ const gotShape = shape.length > 0;
665
+ const gotRefusal = refusals.length > 0;
666
+ const pass = gotShape === c.expectShape && gotRefusal === c.expectRefusal;
667
+ if (!pass) {
668
+ failures++;
669
+ process.stdout.write(" FAIL " + c.name +
670
+ " (shape " + gotShape + " want " + c.expectShape +
671
+ ", refusal " + gotRefusal + " want " + c.expectRefusal + ")\n");
672
+ if (gotShape) process.stdout.write(" " + shape.join(" / ") + "\n");
673
+ } else {
674
+ process.stdout.write(" ok " + c.name + "\n");
675
+ }
676
+ }
677
+
678
+ // Round trip an append into a temp file so the write path is exercised too.
679
+ const tmpDir = fs.mkdtempSync(path.join(process.env.TMPDIR || process.env.TEMP || ".", "runlog-selftest-"));
680
+ const tmpLog = path.join(tmpDir, "nested", "runlog.jsonl");
681
+ try {
682
+ fs.mkdirSync(path.dirname(tmpLog), { recursive: true });
683
+ // A byte order mark and no trailing newline: the two states a shell
684
+ // append leaves behind, and the two this script has to repair.
685
+ fs.writeFileSync(tmpLog, "\uFEFF{\"routine\":\"gtm-board-standup\",\"period\":\"2026-03-03\"}");
686
+ appendRecord(tmpLog, JSON.stringify(good()));
687
+ const raw = fs.readFileSync(tmpLog);
688
+ const text = raw.toString("utf8");
689
+ const lines = text.split("\n").filter((l) => l.trim());
690
+ const bomGone = raw[0] !== 0xef;
691
+ const twoLines = lines.length === 2;
692
+ const parses = lines.every((l) => { try { JSON.parse(l); return true; } catch { return false; } });
693
+ const endsNewline = text.endsWith("\n");
694
+ if (bomGone && twoLines && parses && endsNewline) {
695
+ process.stdout.write(" ok append repairs a byte order mark and a missing trailing newline\n");
696
+ } else {
697
+ failures++;
698
+ process.stdout.write(" FAIL append round trip (bom removed " + bomGone + ", two lines " + twoLines +
699
+ ", all parse " + parses + ", trailing newline " + endsNewline + ")\n");
700
+ }
701
+ } finally {
702
+ fs.rmSync(tmpDir, { recursive: true, force: true });
703
+ }
704
+
705
+ process.stdout.write(failures === 0
706
+ ? "runlog: selftest PASS (" + (cases.length + 1) + " checks)\n"
707
+ : "runlog: selftest FAIL (" + failures + " of " + (cases.length + 1) + ")\n");
708
+ process.exit(failures === 0 ? 0 : 1);
709
+ }
710
+
711
+ function usage() {
712
+ const text = fs.readFileSync(SELF, "utf8");
713
+ const block = text.slice(text.indexOf("/*") + 2, text.indexOf("*/"));
714
+ process.stdout.write(block.split("\n").map((l) => l.replace(/^ \* ?/, "")).join("\n").trim() + "\n");
715
+ }
716
+
717
+ /* ------------------------------------------------------------------ *
718
+ * Main
719
+ * ------------------------------------------------------------------ */
720
+
721
+ function main() {
722
+ const opts = parseArgs(process.argv.slice(2));
723
+ if (opts.help) { usage(); process.exit(0); }
724
+ if (opts.selftest) { selftest(); return; }
725
+
726
+ const { record } = opts.failedRun ? { record: failedRecord(opts) } : loadPayload(opts);
727
+
728
+ const shape = validateShape(record);
729
+ if (shape.length) {
730
+ die(2, "record rejected, " + shape.length + " problem(s):\n " + shape.join("\n "));
731
+ }
732
+
733
+ const refusals = findRefusals(record);
734
+ if (refusals.length) {
735
+ const lines = refusals.map((r) => r.field + ": " + r.cls);
736
+ die(3,
737
+ "refused to write this record. A run record carries counts, paths, and blockers, never a secret,\n" +
738
+ " a draft, or personal data (CONTRACT.md 4.2). The detail belongs in the queue file.\n" +
739
+ " " + lines.join("\n ") +
740
+ "\n The matched text is deliberately not printed."
741
+ );
742
+ }
743
+
744
+ for (const note of advisories(record)) warn(note);
745
+
746
+ const root = resolveRoot(opts);
747
+ const logPath = opts.log ? path.resolve(opts.log) : path.join(root, "runlog.jsonl");
748
+ const line = JSON.stringify(record);
749
+
750
+ if (opts.dryRun) {
751
+ process.stdout.write(line + "\n");
752
+ process.stderr.write("runlog: dry run, nothing written. Target was " + logPath + "\n");
753
+ process.exit(0);
754
+ }
755
+
756
+ if (opts.once && alreadyRecorded(logPath, record.routine, record.period)) {
757
+ die(4, "a record for " + record.routine + " and period " + record.period + " already exists. Nothing written.");
758
+ }
759
+ if (!opts.once && alreadyRecorded(logPath, record.routine, record.period)) {
760
+ warn("a record for " + record.routine + " and period " + record.period + " already exists. Appending anyway, which is correct for skipped-already-ran and a defect otherwise.");
761
+ }
762
+
763
+ try {
764
+ appendRecord(logPath, line);
765
+ } catch (err) {
766
+ die(1, "could not write " + logPath + ": " + err.message);
767
+ }
768
+
769
+ process.stdout.write("runlog: appended " + record.routine + " " + record.period + " " + record.status + "\n");
770
+ }
771
+
772
+ main();