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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/TRADEMARKS.md +45 -0
- package/docs/COST.md +59 -0
- package/docs/FAQ.md +39 -0
- package/docs/GUARDRAILS.md +17 -0
- package/docs/HARNESSES.md +38 -0
- package/docs/HOW-EMPLOYEES-WORK.md +86 -0
- package/docs/INSTALL.md +148 -0
- package/docs/PREREQUISITES.md +91 -0
- package/docs/STANDARD.md +194 -0
- package/docs/UPGRADING.md +74 -0
- package/docs/WHAT-SETS-THEM-APART.md +10 -0
- package/employees/ad-manager-employee/AGENTS.md +46 -0
- package/employees/ad-manager-employee/CAPABILITIES.md +1002 -0
- package/employees/ad-manager-employee/CHANGELOG.md +99 -0
- package/employees/ad-manager-employee/CONTRACT.md +977 -0
- package/employees/ad-manager-employee/INSTALL-PROMPT.md +263 -0
- package/employees/ad-manager-employee/README.md +357 -0
- package/employees/ad-manager-employee/RELEASES.md +25 -0
- package/employees/ad-manager-employee/ROLE.md +617 -0
- package/employees/ad-manager-employee/SCHEDULE.md +275 -0
- package/employees/ad-manager-employee/VERSION +1 -0
- package/employees/ad-manager-employee/employee.json +185 -0
- package/employees/ad-manager-employee/examples/README.md +21 -0
- package/employees/ad-manager-employee/examples/board/LAUNCH-BOARD.md +19 -0
- package/employees/ad-manager-employee/examples/board/board.json +51 -0
- package/employees/ad-manager-employee/examples/brief-latest.md +20 -0
- package/employees/ad-manager-employee/examples/build/campaign-storm-repair.md +51 -0
- package/employees/ad-manager-employee/examples/creative/set-2026-03-04-fast-estimate/set.md +31 -0
- package/employees/ad-manager-employee/examples/metrics/daily.jsonl +2 -0
- package/employees/ad-manager-employee/examples/runlog.jsonl +3 -0
- package/employees/ad-manager-employee/recipes/BROWSER-RECIPES.md +558 -0
- package/employees/ad-manager-employee/recipes/META-ADS-RECIPES.md +100 -0
- package/employees/ad-manager-employee/routines/ads-account-intake/SKILL.md +929 -0
- package/employees/ad-manager-employee/routines/ads-account-read/SKILL.md +780 -0
- package/employees/ad-manager-employee/routines/ads-build-desk/SKILL.md +915 -0
- package/employees/ad-manager-employee/routines/ads-change-list/SKILL.md +818 -0
- package/employees/ad-manager-employee/routines/ads-creative-retro/SKILL.md +807 -0
- package/employees/ad-manager-employee/routines/ads-creative-studio/SKILL.md +802 -0
- package/employees/ad-manager-employee/routines/ads-desk-standup/SKILL.md +822 -0
- package/employees/ad-manager-employee/run/ads-account-intake.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-account-read.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-build-desk.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-change-list.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-creative-retro.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-creative-studio.cmd.example +11 -0
- package/employees/ad-manager-employee/run/ads-desk-standup.cmd.example +11 -0
- package/employees/ad-manager-employee/scripts/copy-check.mjs +815 -0
- package/employees/ad-manager-employee/scripts/guard.mjs +447 -0
- package/employees/ad-manager-employee/scripts/review.mjs +443 -0
- package/employees/ad-manager-employee/scripts/runlog.mjs +785 -0
- package/employees/chief-of-staff/AGENTS.md +46 -0
- package/employees/chief-of-staff/CAPABILITIES.md +875 -0
- package/employees/chief-of-staff/CHANGELOG.md +91 -0
- package/employees/chief-of-staff/CONTRACT.md +1039 -0
- package/employees/chief-of-staff/INSTALL-PROMPT.md +206 -0
- package/employees/chief-of-staff/README.md +382 -0
- package/employees/chief-of-staff/RELEASES.md +22 -0
- package/employees/chief-of-staff/ROLE.md +425 -0
- package/employees/chief-of-staff/SCHEDULE.md +288 -0
- package/employees/chief-of-staff/VERSION +1 -0
- package/employees/chief-of-staff/employee.json +214 -0
- package/employees/chief-of-staff/examples/README.md +15 -0
- package/employees/chief-of-staff/examples/brief-latest.md +20 -0
- package/employees/chief-of-staff/examples/decisions/REGISTER.md +22 -0
- package/employees/chief-of-staff/examples/dossiers/dossier-seo-employee--seo-publish-run--silent-stop.md +43 -0
- package/employees/chief-of-staff/examples/fleet/fleet.json +119 -0
- package/employees/chief-of-staff/examples/runlog.jsonl +3 -0
- package/employees/chief-of-staff/recipes/BROWSER-RECIPES.md +512 -0
- package/employees/chief-of-staff/routines/cos-charter-and-fleet-audit/SKILL.md +1008 -0
- package/employees/chief-of-staff/routines/cos-decision-brief/SKILL.md +639 -0
- package/employees/chief-of-staff/routines/cos-decision-review/SKILL.md +644 -0
- package/employees/chief-of-staff/routines/cos-fault-dossier/SKILL.md +656 -0
- package/employees/chief-of-staff/routines/cos-fleet-reconcile/SKILL.md +942 -0
- package/employees/chief-of-staff/routines/cos-market-sweep/SKILL.md +655 -0
- package/employees/chief-of-staff/routines/cos-metrics-review/SKILL.md +746 -0
- package/employees/chief-of-staff/run/cos-charter-and-fleet-audit.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-decision-brief.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-decision-review.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-fault-dossier.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-fleet-reconcile.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-market-sweep.cmd.example +11 -0
- package/employees/chief-of-staff/run/cos-metrics-review.cmd.example +11 -0
- package/employees/chief-of-staff/scripts/copy-check.mjs +812 -0
- package/employees/chief-of-staff/scripts/guard.mjs +447 -0
- package/employees/chief-of-staff/scripts/runlog.mjs +766 -0
- package/employees/customer-satisfaction-employee/AGENTS.md +47 -0
- package/employees/customer-satisfaction-employee/CAPABILITIES.md +910 -0
- package/employees/customer-satisfaction-employee/CHANGELOG.md +91 -0
- package/employees/customer-satisfaction-employee/CONTRACT.md +1121 -0
- package/employees/customer-satisfaction-employee/INSTALL-PROMPT.md +290 -0
- package/employees/customer-satisfaction-employee/README.md +378 -0
- package/employees/customer-satisfaction-employee/RELEASES.md +22 -0
- package/employees/customer-satisfaction-employee/ROLE.md +393 -0
- package/employees/customer-satisfaction-employee/SCHEDULE.md +283 -0
- package/employees/customer-satisfaction-employee/VERSION +1 -0
- package/employees/customer-satisfaction-employee/employee.json +231 -0
- package/employees/customer-satisfaction-employee/examples/README.md +15 -0
- package/employees/customer-satisfaction-employee/examples/brief-latest.md +17 -0
- package/employees/customer-satisfaction-employee/examples/desk/DESK-BOARD.md +18 -0
- package/employees/customer-satisfaction-employee/examples/queue/2026-03-05-reply.md +36 -0
- package/employees/customer-satisfaction-employee/examples/runlog.jsonl +3 -0
- package/employees/customer-satisfaction-employee/examples/tickets/tickets.jsonl +2 -0
- package/employees/customer-satisfaction-employee/recipes/BROWSER-RECIPES.md +549 -0
- package/employees/customer-satisfaction-employee/routines/csat-churn-watch/SKILL.md +709 -0
- package/employees/customer-satisfaction-employee/routines/csat-deflection-desk/SKILL.md +671 -0
- package/employees/customer-satisfaction-employee/routines/csat-desk-intake/SKILL.md +1115 -0
- package/employees/customer-satisfaction-employee/routines/csat-desk-standup/SKILL.md +831 -0
- package/employees/customer-satisfaction-employee/routines/csat-inbox-sweep/SKILL.md +673 -0
- package/employees/customer-satisfaction-employee/routines/csat-reply-desk/SKILL.md +755 -0
- package/employees/customer-satisfaction-employee/routines/csat-satisfaction-report/SKILL.md +844 -0
- package/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh/SKILL.md +798 -0
- package/employees/customer-satisfaction-employee/run/csat-churn-watch.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-deflection-desk.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-desk-intake.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-desk-standup.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-inbox-sweep.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-reply-desk.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-satisfaction-report.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/run/csat-taxonomy-refresh.cmd.example +11 -0
- package/employees/customer-satisfaction-employee/scripts/copy-check.mjs +888 -0
- package/employees/customer-satisfaction-employee/scripts/guard.mjs +447 -0
- package/employees/customer-satisfaction-employee/scripts/runlog.mjs +778 -0
- package/employees/gtm-engineer/AGENTS.md +47 -0
- package/employees/gtm-engineer/CAPABILITIES.md +929 -0
- package/employees/gtm-engineer/CHANGELOG.md +93 -0
- package/employees/gtm-engineer/CONTRACT.md +977 -0
- package/employees/gtm-engineer/INSTALL-PROMPT.md +237 -0
- package/employees/gtm-engineer/README.md +355 -0
- package/employees/gtm-engineer/RELEASES.md +22 -0
- package/employees/gtm-engineer/ROLE.md +551 -0
- package/employees/gtm-engineer/SCHEDULE.md +265 -0
- package/employees/gtm-engineer/VERSION +1 -0
- package/employees/gtm-engineer/employee.json +230 -0
- package/employees/gtm-engineer/examples/README.md +15 -0
- package/employees/gtm-engineer/examples/board/LAUNCH-BOARD.md +12 -0
- package/employees/gtm-engineer/examples/brief-latest.md +19 -0
- package/employees/gtm-engineer/examples/crm/contacts.csv +4 -0
- package/employees/gtm-engineer/examples/queue/2026-03-05-email.md +26 -0
- package/employees/gtm-engineer/examples/runlog.jsonl +3 -0
- package/employees/gtm-engineer/recipes/BROWSER-RECIPES.md +582 -0
- package/employees/gtm-engineer/routines/gtm-board-standup/SKILL.md +745 -0
- package/employees/gtm-engineer/routines/gtm-icp-refresh/SKILL.md +647 -0
- package/employees/gtm-engineer/routines/gtm-intake-and-dashboard/SKILL.md +1046 -0
- package/employees/gtm-engineer/routines/gtm-launch-step-runner/SKILL.md +802 -0
- package/employees/gtm-engineer/routines/gtm-outreach-queue/SKILL.md +708 -0
- package/employees/gtm-engineer/routines/gtm-paid-and-tracking-guard/SKILL.md +970 -0
- package/employees/gtm-engineer/routines/gtm-scoreboard/SKILL.md +740 -0
- package/employees/gtm-engineer/routines/gtm-signal-sweep/SKILL.md +648 -0
- package/employees/gtm-engineer/run/gtm-board-standup.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-icp-refresh.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-intake-and-dashboard.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-launch-step-runner.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-outreach-queue.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-paid-and-tracking-guard.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-scoreboard.cmd.example +11 -0
- package/employees/gtm-engineer/run/gtm-signal-sweep.cmd.example +11 -0
- package/employees/gtm-engineer/scripts/copy-check.mjs +815 -0
- package/employees/gtm-engineer/scripts/guard.mjs +447 -0
- package/employees/gtm-engineer/scripts/runlog.mjs +772 -0
- package/employees/sales-employee/AGENTS.md +46 -0
- package/employees/sales-employee/CAPABILITIES.md +880 -0
- package/employees/sales-employee/CHANGELOG.md +92 -0
- package/employees/sales-employee/CONTRACT.md +1161 -0
- package/employees/sales-employee/INSTALL-PROMPT.md +256 -0
- package/employees/sales-employee/README.md +372 -0
- package/employees/sales-employee/RELEASES.md +22 -0
- package/employees/sales-employee/ROLE.md +319 -0
- package/employees/sales-employee/SCHEDULE.md +263 -0
- package/employees/sales-employee/VERSION +1 -0
- package/employees/sales-employee/employee.json +202 -0
- package/employees/sales-employee/examples/README.md +16 -0
- package/employees/sales-employee/examples/brief-latest.md +17 -0
- package/employees/sales-employee/examples/crm/contacts.csv +4 -0
- package/employees/sales-employee/examples/crm/prospects.jsonl +2 -0
- package/employees/sales-employee/examples/pipeline/PIPELINE.md +18 -0
- package/employees/sales-employee/examples/queue/2026-03-05-first-touch.md +29 -0
- package/employees/sales-employee/examples/runlog.jsonl +3 -0
- package/employees/sales-employee/recipes/BROWSER-RECIPES.md +535 -0
- package/employees/sales-employee/routines/sales-desk-setup/SKILL.md +896 -0
- package/employees/sales-employee/routines/sales-desk-standup/SKILL.md +818 -0
- package/employees/sales-employee/routines/sales-first-touch-drafts/SKILL.md +681 -0
- package/employees/sales-employee/routines/sales-followup-sweep/SKILL.md +736 -0
- package/employees/sales-employee/routines/sales-pipeline-review/SKILL.md +765 -0
- package/employees/sales-employee/routines/sales-prospect-sweep/SKILL.md +696 -0
- package/employees/sales-employee/routines/sales-qualification-refresh/SKILL.md +743 -0
- package/employees/sales-employee/run/sales-desk-setup.cmd.example +11 -0
- package/employees/sales-employee/run/sales-desk-standup.cmd.example +11 -0
- package/employees/sales-employee/run/sales-first-touch-drafts.cmd.example +11 -0
- package/employees/sales-employee/run/sales-followup-sweep.cmd.example +11 -0
- package/employees/sales-employee/run/sales-pipeline-review.cmd.example +11 -0
- package/employees/sales-employee/run/sales-prospect-sweep.cmd.example +11 -0
- package/employees/sales-employee/run/sales-qualification-refresh.cmd.example +11 -0
- package/employees/sales-employee/scripts/copy-check.mjs +815 -0
- package/employees/sales-employee/scripts/guard.mjs +447 -0
- package/employees/sales-employee/scripts/runlog.mjs +773 -0
- package/employees/seo-employee/AEO-PLAYBOOK.md +51 -0
- package/employees/seo-employee/AGENTS.md +47 -0
- package/employees/seo-employee/CAPABILITIES.md +1006 -0
- package/employees/seo-employee/CHANGELOG.md +95 -0
- package/employees/seo-employee/CONTRACT.md +1020 -0
- package/employees/seo-employee/INSTALL-PROMPT.md +233 -0
- package/employees/seo-employee/README.md +368 -0
- package/employees/seo-employee/RELEASES.md +22 -0
- package/employees/seo-employee/ROLE.md +331 -0
- package/employees/seo-employee/SCHEDULE.md +267 -0
- package/employees/seo-employee/VERSION +1 -0
- package/employees/seo-employee/employee.json +233 -0
- package/employees/seo-employee/examples/README.md +14 -0
- package/employees/seo-employee/examples/board/WORK-BOARD.md +16 -0
- package/employees/seo-employee/examples/brief-latest.md +16 -0
- package/employees/seo-employee/examples/content/drafts.jsonl +2 -0
- package/employees/seo-employee/examples/content/published.jsonl +2 -0
- package/employees/seo-employee/examples/drafts/roof-lifespan-by-material/body.md +39 -0
- package/employees/seo-employee/examples/drafts/roof-lifespan-by-material/meta.json +16 -0
- package/employees/seo-employee/examples/runlog.jsonl +3 -0
- package/employees/seo-employee/recipes/BROWSER-RECIPES.md +576 -0
- package/employees/seo-employee/routines/seo-answer-visibility/SKILL.md +55 -0
- package/employees/seo-employee/routines/seo-calendar-refill/SKILL.md +614 -0
- package/employees/seo-employee/routines/seo-draft-run/SKILL.md +722 -0
- package/employees/seo-employee/routines/seo-index-sweep/SKILL.md +629 -0
- package/employees/seo-employee/routines/seo-intake-and-map/SKILL.md +916 -0
- package/employees/seo-employee/routines/seo-publish-run/SKILL.md +713 -0
- package/employees/seo-employee/routines/seo-rank-review/SKILL.md +795 -0
- package/employees/seo-employee/routines/seo-standup/SKILL.md +792 -0
- package/employees/seo-employee/run/seo-answer-visibility.cmd.example +11 -0
- package/employees/seo-employee/run/seo-calendar-refill.cmd.example +11 -0
- package/employees/seo-employee/run/seo-draft-run.cmd.example +11 -0
- package/employees/seo-employee/run/seo-index-sweep.cmd.example +11 -0
- package/employees/seo-employee/run/seo-intake-and-map.cmd.example +11 -0
- package/employees/seo-employee/run/seo-publish-run.cmd.example +11 -0
- package/employees/seo-employee/run/seo-rank-review.cmd.example +11 -0
- package/employees/seo-employee/run/seo-standup.cmd.example +11 -0
- package/employees/seo-employee/scripts/answer-audit.mjs +63 -0
- package/employees/seo-employee/scripts/copy-check.mjs +843 -0
- package/employees/seo-employee/scripts/guard.mjs +447 -0
- package/employees/seo-employee/scripts/runlog.mjs +864 -0
- package/employees/seo-employee/standards/PUBLISH-STANDARD.md +229 -0
- package/employees/social-media-employee/AGENTS.md +46 -0
- package/employees/social-media-employee/CAPABILITIES.md +1015 -0
- package/employees/social-media-employee/CHANGELOG.md +91 -0
- package/employees/social-media-employee/CONTRACT.md +1036 -0
- package/employees/social-media-employee/INSTALL-PROMPT.md +234 -0
- package/employees/social-media-employee/README.md +386 -0
- package/employees/social-media-employee/RELEASES.md +22 -0
- package/employees/social-media-employee/ROLE.md +376 -0
- package/employees/social-media-employee/SCHEDULE.md +278 -0
- package/employees/social-media-employee/VERSION +1 -0
- package/employees/social-media-employee/employee.json +195 -0
- package/employees/social-media-employee/examples/README.md +20 -0
- package/employees/social-media-employee/examples/brief-latest.md +19 -0
- package/employees/social-media-employee/examples/calendar/CALENDAR.md +23 -0
- package/employees/social-media-employee/examples/calendar/calendar.json +30 -0
- package/employees/social-media-employee/examples/posts/posts.jsonl +2 -0
- package/employees/social-media-employee/examples/queue/2026-03-05-business-network.md +28 -0
- package/employees/social-media-employee/examples/runlog.jsonl +3 -0
- package/employees/social-media-employee/recipes/BROWSER-RECIPES.md +550 -0
- package/employees/social-media-employee/routines/soc-calendar-standup/SKILL.md +783 -0
- package/employees/social-media-employee/routines/soc-draft-queue/SKILL.md +683 -0
- package/employees/social-media-employee/routines/soc-engagement-sweep/SKILL.md +636 -0
- package/employees/social-media-employee/routines/soc-intake-and-voice/SKILL.md +909 -0
- package/employees/social-media-employee/routines/soc-material-sweep/SKILL.md +658 -0
- package/employees/social-media-employee/routines/soc-performance-review/SKILL.md +795 -0
- package/employees/social-media-employee/routines/soc-publish-run/SKILL.md +643 -0
- package/employees/social-media-employee/run/soc-calendar-standup.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-draft-queue.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-engagement-sweep.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-intake-and-voice.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-material-sweep.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-performance-review.cmd.example +11 -0
- package/employees/social-media-employee/run/soc-publish-run.cmd.example +11 -0
- package/employees/social-media-employee/scripts/copy-check.mjs +950 -0
- package/employees/social-media-employee/scripts/guard.mjs +447 -0
- package/employees/social-media-employee/scripts/runlog.mjs +786 -0
- package/employees/web-dev-employee/AGENTS.md +47 -0
- package/employees/web-dev-employee/CAPABILITIES.md +950 -0
- package/employees/web-dev-employee/CHANGELOG.md +91 -0
- package/employees/web-dev-employee/CONTRACT.md +1052 -0
- package/employees/web-dev-employee/INSTALL-PROMPT.md +210 -0
- package/employees/web-dev-employee/README.md +352 -0
- package/employees/web-dev-employee/RELEASES.md +22 -0
- package/employees/web-dev-employee/ROLE.md +383 -0
- package/employees/web-dev-employee/SCHEDULE.md +280 -0
- package/employees/web-dev-employee/VERSION +1 -0
- package/employees/web-dev-employee/employee.json +228 -0
- package/employees/web-dev-employee/examples/README.md +13 -0
- package/employees/web-dev-employee/examples/board/REVIEW-BOARD.md +20 -0
- package/employees/web-dev-employee/examples/brief-latest.md +14 -0
- package/employees/web-dev-employee/examples/changes/2026-03-05-fix-C-005.md +30 -0
- package/employees/web-dev-employee/examples/changes/changes.jsonl +2 -0
- package/employees/web-dev-employee/examples/health/incidents.jsonl +2 -0
- package/employees/web-dev-employee/examples/runlog.jsonl +3 -0
- package/employees/web-dev-employee/recipes/BROWSER-RECIPES.md +477 -0
- package/employees/web-dev-employee/routines/web-dependency-run/SKILL.md +634 -0
- package/employees/web-dev-employee/routines/web-fix-runner/SKILL.md +754 -0
- package/employees/web-dev-employee/routines/web-guardrail-review/SKILL.md +613 -0
- package/employees/web-dev-employee/routines/web-inventory-refresh/SKILL.md +700 -0
- package/employees/web-dev-employee/routines/web-platform-guard/SKILL.md +707 -0
- package/employees/web-dev-employee/routines/web-site-sweep/SKILL.md +732 -0
- package/employees/web-dev-employee/routines/web-standup/SKILL.md +738 -0
- package/employees/web-dev-employee/routines/web-weekly-report/SKILL.md +623 -0
- package/employees/web-dev-employee/run/web-dependency-run.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-fix-runner.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-guardrail-review.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-inventory-refresh.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-platform-guard.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-site-sweep.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-standup.cmd.example +11 -0
- package/employees/web-dev-employee/run/web-weekly-report.cmd.example +11 -0
- package/employees/web-dev-employee/scripts/copy-check.mjs +872 -0
- package/employees/web-dev-employee/scripts/guard.mjs +447 -0
- package/employees/web-dev-employee/scripts/runlog.mjs +792 -0
- package/installer/cli.mjs +250 -0
- package/installer/upgrade.mjs +255 -0
- package/package.json +43 -0
- 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();
|