# Cron Job: pm-heartbeat-weekend

**Job ID:** 6864bbd32cdc
**Run Time:** 2026-07-10 22:00:59
**Schedule:** 0 22 * * 5,6

## Prompt

[IMPORTANT: The user has invoked the "pm-heartbeat" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]

---
name: pm-heartbeat
description: Morning chase digest - Hermes cron reads the Actions register + Daily Records and pushes a short Telegram digest to Rocky (overdue, due-soon, aged-waiting, live jobs gone quiet). READ-ONLY. The autonomous pulse of the /pm cockpit.
runtime: Hermes cron (06:00 Sydney weekdays, 08:00 Sydney weekends)
inputs: Airtable Actions register + Daily Records (base appE43UvTyARe5oJs)
outputs: short Telegram digest to channel 7377499346
failure-mode: heartbeat up register unreachable never silence
---

# pm-heartbeat - the morning pulse

The one thing that makes the system speak first. /pm keeps the Actions
register true; this skill reads it every morning and pushes what matters
to Rocky's Telegram before he opens anything. **READ-ONLY - this skill
never writes to Airtable. Ever.**

## Runtime

- **Schedule**: 06:00 AEST weekdays, 08:00 AEST weekends (cron, UTC).
- **Auth**: PAT loaded from `~/.hermes/config.yaml` to
  `mcp_servers.airtable.env.AIRTABLE_API_KEY` (NOT shell env). See
  `references/airtable-recipe.md`.
- **Deliver**: `telegram:7377499346` (Rocky's home channel - fixed binding
  for cron sessions that have no current-chat context).
- **Failure mode**: if Airtable raises an exception, send exactly
  `"heartbeat up, register unreachable"`. A silent morning means nothing
  to chase - NEVER that the cron died.

## The read

1. Actions register: rows with `Status = Open` or `Waiting`.
2. Jobs: Lifecycle Status (for the QUIET band) + Job Number lookup.
3. Daily Records: latest Diary Ref date per live job.

Field IDs, query recipes, and band logic: `references/airtable-recipe.md`.

## The digest (caveman-short, max 15 lines, worst first)

```
PM 06:00 - 3 chase, 2 due, 1 quiet
OVERDUE: 2649 Clontarf bid - due YESTERDAY
2626 Jalen start-date reply - waiting 9d (chase: Jalen, email)
DUE <=7d: 2654 takeoff back from Assad - Mon 7/7
2630 subcontract signing - Fri 11/7
QUIET: 2631 Hornsby on-site, no diary 4d
```

Band order (cap each at 5 lines): **OVERDUE**, then **AGED WAITING**,
then **DUE 7d or less**, then **QUIET**. Empty morning = one line:
`PM 06:00 - nothing to chase. Register clean or register blind - /pm to verify.`

**No dollar figures in the digest** (phone-lock-screen surface).
Job number + action + age only.

### Packing into 15 lines when the register is full

The "max 15 lines" budget is binding and overrides the per-band cap
when more than one band overflows (a stressed register can have 10+
overdue and 25+ due-this-week). Apply this packing:

- slot 1: header `PM HH:MM - X overdue, Y aged-waiting, Z due<=7d, W quiet`
- slot 2: band label `OVERDUE:`
- slots 3..N: top overdue items, worst first; if overflow, end with `(+(K) more)`
- next slot: next non-empty band label
- last slot(s): QUIET line if non-empty

Skip band labels for empty bands. When overall 15-line cap conflicts
with "5 per band", overall wins - trim bands proportionally
(4 OVERDUE + 2 AGED + 4 DUE + 1 QUIET = 11 + 4 headers = 15). Full
worked example and copy-pasteable packing function in
`references/digest-packing.md`.

**Meta items are not part of the digest.** Skip "Deploy pm-heartbeat"
or any "system maintenance" Action - the skill that fires the digest
should never advertise itself in its own output.

**Packing heuristic: same-day items merge (2026-07-07)**: when 2+
items share the same Due date AND both are due today (bid-week
pressure), merge them into a single line joined with `+` to save
1-2 lines. Worked example: `2649 QTE verify + 2654 Assad takeoff (today)`.

**Packing heuristic: drop QUIET label when only 1 item (2026-07-07)**:
at the cap, a lone QUIET item can run on the same line as its label
(`QUIET: 2631 Hornsby on-site, no diary 16 wd - DR sync gap`) to save
one line. Use only when QUIET is exactly 1 item.

**Terminal heredoc home-dir pitfall (cron, 2026-07-07)**: piping a
Python heredoc straight into `terminal` with no explicit workdir
fails with "Could not determine home directory" - the cron session
has no $HOME for the implicit cwd. Always `cd /tmp` first (or pass
`workdir=/tmp`) before the heredoc, then run.

### Cron-mode tool restrictions (pitfall)

`execute_code` is BLOCKED in cron sessions ("approve only if this cron
profile is intentionally trusted"). Use Airtable MCP tools directly
for reads and `terminal` for any local processing (heredoc Python works
fine through terminal). Do not waste a turn discovering this.

`telegram` delivery is automatic on the final response - do not call
`send_message` yourself; the cron harness delivers the final reply.

### Worked QUIET-band fallback

When Lifecycle Status is empty on a job (22 of 25 as of 2026-07-05) and
Daily Records is thin, infer "live" from active Action presence and use
the latest DR date as the silence clock. Worked example from 2026-07-05:
job 2631 (Hornsby) Status=On-site, latest DR Date=2026-06-16, today
2026-07-05, elapsed = 19 calendar days = ~13 working days. Surfaces in
QUIET band as `2631 Hornsby on-site, no diary ~13 wd - DR sync gap`.

## Deploy-time binding

- Base `appE43UvTyARe5oJs` (LFCS-Operations)
- Actions `tblwGQdNqZIPRiDSV`, Jobs `tblcL2EX2w34c1VPe`,
  Daily Records `tbli76T3GvmLSd3Kd`
- Telegram channel `7377499346`

## Smoke-test before arming cron

Before creating the cron job, live-test the read logic in a foreground
session with the user watching. See `lfcs/references/cron-smoke-test.md`
for the full protocol and reusable Python snippets. Five-minute check;
prevents silent misses.

## Deploy note (2026-07-03)

Cron IDs:
- weekday: `e356fa099ee4` (next run `0 20 * * 1-5` UTC = 06:00 AEST)
- weekend: `6864bbd32cdc` (next run `0 22 * * 6,0` UTC = 08:00 AEST)

Sydney is UTC+10 (no DST). Hermes cron whitelist does not include
`CRON_TZ` for `Australia/Sydney`, so cron is in UTC. If AEST/AEDT
shifts after a future DST rule change, recompute the UTC offsets.

Two data gaps surfaced during smoke test (both need OpsMan follow-up):
- 22 of 25 jobs have no Lifecycle Status set -> QUIET band infers "live"
  from active Action presence as a workaround.
- Daily Records has only 2 rows in the table - either OpsMan DR sync
  is lagging, or DRs go elsewhere now. Either way, the digest falls
  back to "active Actions = live job".

The user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]

You are running the pm-heartbeat skill. Load it with skill_view(name='pm-heartbeat') and execute its digest logic exactly as written. Read Airtable base appE43UvTyARe5oJs (PAT loads from ~/.hermes/config.yaml → mcp.servers[airtable-mcp-server].env.AIRTABLE_API_KEY, do NOT print it). Query the Actions register (tblwGQdNqZIPRiDSV) and Daily Records (tbli76T3GvmLSd3Kd), build the caveman-short digest per the bands in the skill, and deliver ONLY to telegram:7377499346. If Airtable is unreachable, send exactly: "heartbeat up, register unreachable". Read-only - never write Airtable. Reply with the digest body as your final message; the system will deliver it automatically.

## Response

Airtable MCP is unreachable. Per the skill's failure mode, I must send exactly: "heartbeat up, register unreachable" — not silence, not a digest.

heartbeat up, register unreachable
