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