# Wake/Heartbeat Canon — FRAMEWORK PACKAGING PLAN (MS-LEAD)

**Author:** MS-LEAD (framework packaging role, named in CONVERGED-DESIGN footer) · **Date:** 2026-07-25
**Input:** `jarvis-brain@31f1f6fd5 docs/scratchpads/heartbeat-planning/CONVERGED-DESIGN.md` (GO-WITH-GATES,
RATIFIED) + `ENHANCE-CADENCE-WRITER-FIX.md` + `ENHANCE-BASELINE.md`.
**Scope:** §6 category-A framework-component items (A1–A11) + WAKE-DOCTRINE guide. NOT pilot (B*, mos-dt-0)
or operator-config values (C*). Proposal only — **no framework PR cut until Mos acks this plan** (per Mos).
**Repo facts verified** against `mosaicstack/stack` origin/main (read-only ls-tree) — not assumed.
**Rev 2 (2026-07-25):** addresses W-jarvis review at `cd11bbb` — 4 gaps resolved (component manifest/version,
schema ordering ≤ W4, blank-reset target/lifecycle, framework/operator boundaries) + #889 minor.
**Rev 3 (2026-07-25):** W-jarvis Rev-2 re-review = CONDITIONAL APPROVE; adds two acceptance gates — **Gate A**
(wake manifest is VERSION metadata only; path-ownership stays the single `framework-manifest.txt` SSOT +
Bash/TS parity tests — no second ownership truth) and **Gate B** (component manifest declares a supported
schema RANGE; detector fails loud outside it; upgrade/downgrade boundary tests). Still PROPOSED / PENDING-AUTHORITY.

---

## 1. Repo layout (grounded in verified paths)

Framework source lives in `packages/mosaic/framework/` and installs to `~/.config/mosaic/`. Verified:
`tools/<subdir>/` (git, tmux, fleet, orchestrator, lease-broker, context, …); `guides/*.md`;
`install.sh`; `systemd/`; `tools/tmux/agent-send.sh` (+ `agent-send.test.sh`).

| Item | Source path (new unless noted) | Installs to |
|---|---|---|
| A1 detector daemon | `framework/tools/wake/detector.sh` | `~/.config/mosaic/tools/wake/` |
| A2 store+drain lib | `framework/tools/wake/store.sh` (three-cursor, atomic write-tmp+rename) | XDG state `~/.local/state/mosaic/wake/` |
| A3 digest renderer | `framework/tools/wake/digest.sh` | " |
| A4 ack-wrapper | `framework/tools/wake/ack.sh` (RECEIVED/CONSUMED, local-write+async) | ledger under XDG state |
| A5 HMAC signer | `framework/tools/wake/sign.sh` (non-circular tuple, `load_credentials` by-name) | " |
| A6 FN-oracle · A7 reconciler | `framework/tools/wake/{fn-oracle,reconcile}.sh` | " |
| A8 off-host beacon | `framework/tools/wake/beacon.sh` + alarm integration | off-host monitor |
| A9 `digest` class validator | **MODIFY** `framework/tools/tmux/agent-send.sh` (+ `.test.sh`) | — |
| A10 idempotent installer | **MODIFY** `framework/install.sh` + `framework/systemd/user/mosaic-wake.service` | `~/.config/systemd/user/` |
| A11 WAKE-DOCTRINE | `framework/guides/WAKE-DOCTRINE.md` (lift §"WAKE-DOCTRINE" verbatim) | `~/.config/mosaic/guides/` |
| C1 watch-list SCHEMA (framework) | `framework/defaults/wake-watch-list.schema.json` | operator supplies values |

Note: `gitea-pr-watch` / `mos-comms` (the pollers the detector generalizes) are NOT under
`framework/tools/` on main — they are host/operator artifacts today. A1 becomes the canonical framework
generalization; it does not depend on those existing in-repo. The detector runs as a **long-lived
service** (not a timer) — the whole point is retiring unconditional timers; fallback cadence is a
per-class SLO bound inside the daemon, not a systemd interval.

## 2. PR slicing (dependency-ordered, ≤2 concurrent builders, each builder→indep-review→Mos id-11)

Dependency spine: store/ack (A2/A4) → digest/sign (A3/A5) → detector (A1) → oracle/reconciler (A6/A7);
beacon (A8), A9, A11 are near-independent; installer (A10) lands last (needs all files present).

| PR | Contents | Notes |
|---|---|---|
| **W0** | A11 WAKE-DOCTRINE.md | docs-only, independent; lands first as normative reference. Supersedes/informs #889; closure Mos-owned (see §5). |
| **W1** | A9 `agent-send.sh` digest-class validator + test | tiny, independent, red-first (like Patch 2b). Good opener. |
| **W2** | A2 store+drain lib **+** A4 ack-wrapper **+** watch-list SCHEMA contract (framework `.schema.json` + `schema_version`) | core three-cursor transaction **+ the public schema W4/W5 validate against** (W-jarvis gap 2 — schema lands ≤ its first consumer, not W7); red-first heavy. |
| **W3** | A3 digest renderer **+** A5 HMAC signer | depends on W2 store; hard-locator + scrub + non-circular MAC tests. |
| **W4** | A1 detector daemon | depends on W2 (incl. schema)/W3; **checks watch-list `schema_version` ∈ component supported-range → FAIL-LOUD (Gate B)** + boundary tests; repo-section/anchor-scoped hashing; flock; in mos-comms loop. |
| **W5** | A6 FN-oracle **+** A7 reconciler | depends on W4; the non-optional G3/§4 cost, built explicitly. |
| **W6** | A8 off-host beacon + **pluggable alarm-sink adapter** | semi-independent; independent liveness leg (G1). Framework = emitter+adapter iface; operator = target (§5 boundaries). |
| **W7** | A10 idempotent install.sh (**component-manifest install, metadata-only; path-ownership stays framework-manifest SSOT + parity tests — Gate A** + snapshot-guard + blank-reset on **legacy timer** drop-ins during retire) + `mosaic-wake.service` | LAST; **absorbs batch (d)**; #869 publish-gate. Operator schema VALUES deploy here; schema CONTRACT is W2. |

~8 PRs pipelined; at ≤2 lanes this is several convergence cycles. Each: fresh-clone builder
(`mosaic-coder`), author≠reviewer independent review at exact head, RoR packet, Mos id-11 stamp+merge.
Red-first mandatory on all code PRs; `test:framework-shell` chain extended per PR.

## 3. Versioning + #869 compatibility

- **Two independent version axes** (resolves the inherit-vs-adopt-independently contradiction — W-jarvis gap 1):
  1. **Component artifact version — metadata only (Gate A).** The wake component ships its own
     manifest+semver (`framework/tools/wake/manifest.txt`) as **VERSION metadata ONLY**. **Path-ownership
     stays the single SSOT `framework/framework-manifest.txt`** (consumed by BOTH the Bash and TS
     installers with parity tests) — the wake manifest does NOT independently authorize file writes/prunes
     (no second ownership truth). A component-scoped install/update (`install.sh --component wake`) selects
     the component's file set **intersected-and-validated against framework-manifest ownership**, and
     **extends the Bash/TS parity tests**. Net: independent *addressability/versioning* of the component
     WITHOUT a second path-ownership authority (design §5.4 enabler). **Acceptance: parity tests green +
     no wake-manifest-authorized write outside framework-manifest ownership.**
  2. **Watch-list `schema_version` — orthogonal; compat via explicit RANGE (Gate B).** Component semver
     and `schema_version` are different axes and are **NOT compared directly**. The **component manifest
     declares a supported schema range** (`schema_min`/`schema_max`, or a supported list); the detector
     checks the watch-list `schema_version` **∈ range → else FAIL LOUD** (#869 discipline, Patch-2b
     posture). **Acceptance: upgrade/downgrade boundary tests** (in-range pass, out-of-range fail-loud).
- WAKE-DOCTRINE is version-agnostic doctrine.
- **A10 installer is an enforcement path** → #869 publish-gate: fail-closed, idempotent, snapshot-guard
  blocks a reap/clean-checkout without a snapshot; post-apply verify = exactly one `OnUnitActiveUSec`.
- **web1 rides #869 Point-2** (currently HELD/non-autonomous). **dragon-lin pilots NOW** (separate host,
  outside the #869 freeze) — so canon PRs can land + be piloted without touching the web1 release hold.

## 4. Rollout sequence (§5, retire-net-LAST)

1. **Snapshot-first** (B5, mos-dt-0/pilot): dragon-lin `jarvis-heartbeat.{service,timer}` — snapshot
   already present at `heartbeat-planning/pilot/snapshot-dragon-lin/`. Canon is version-controlled from
   day one (the failure class — deployed-from-uncommitted — cannot recur).
2. Land W0→W7 to main (reviewed framework PRs).
3. dragon-lin: enable canon **alongside** ad-hoc (B1) → dual-ledger compare (B2) → run §4 vector (B3) →
   delete ad-hoc units **after pass** (B4). Every step leaves old-or-new live (sol F7).
4. web1: adopt component + watch-list **on #869 resolution**.
- **Do NOT land the ad-hoc `infra/fleet/*` subsystem to main first** (§5.1, Mos ruling) — it is exactly
  what canon retires.

## 5. Disposition of adjacent items

- **Batch (d) — ABSORBED into A10.** The blank-reset bug is in `infra/fleet/install.sh`
  (deployed-from-uncommitted, NOT on main — confirms my "could not verify on main"). Host drop-ins
  already corrected + live-verified 2026-07-25 (planner-opus/planner-sol now fire 30min not 15min).
  **Nothing standalone to land**; the proven idiom (byte-identical to `installer-7`) becomes an A10
  requirement + a post-apply test. Batch item (d) is therefore closed-as-absorbed, not a separate PR.
  **Blank-reset TARGET + lifecycle (W-jarvis gap 3):** the component is a long-lived `mosaic-wake.service`
  (NOT a timer), so blank-reset does NOT apply to the service. It applies to the **legacy
  `mosaic-heartbeat@<agent>.timer` cadence drop-ins** (`.../interval.conf`) during the §5 overlap→retire
  lifecycle: A10 installer (1) **snapshots** the legacy unit; (2) IF a per-class **fallback-cadence**
  timer is used, writes its drop-in in the blank-reset form (byte-identical to `installer-7`; verify =
  exactly one `OnUnitActiveUSec`); (3) on §4-vector pass, **removes the legacy `mosaic-heartbeat@`
  units** (retire-last). Named units + reset→verify→retire steps make the invariant a **testable
  acceptance criterion**, not an orphaned idiom.
- **Framework/operator boundaries + fail-closed criteria (W-jarvis gap 4):**
  - **W6 off-host alarm:** framework ships the beacon emitter + a **pluggable alarm-sink adapter
    interface**; the operator owns the **target endpoint** (off-host monitor address/credential, by-name
    via `load_credentials`). **Fail-closed:** an unconfigured/unreachable alarm target **fails loud at
    install** — no silent no-alarm host (G1/G2a).
  - **W3/W7 HMAC key:** operator-owned secret, provisioned **by-name via `load_credentials`**, NEVER
    inlined, NEVER in units or digests. Framework ships the signer using the named key. **Fail-closed:**
    missing key → **fail loud, no unsigned wake emitted.** Same-uid key-read boundary documented
    (§2.5/G6); off-uid signer tracked as a gate, not assumed away.
- **PR #889 (my layering note) — W0 supersedes/informs it; CLOSURE remains Mos-owned.** Its constraints
  (one liveness substrate, framework-vs-product split, state location, blank-reset invariant) are now
  canonized in CONVERGED-DESIGN §0/§1.1/§1.4/§5.2. The canonical heartbeat docs live in jarvis-brain
  `heartbeat-planning/`; a `mosaicstack/stack docs/scratchpads/` duplicate is cross-repo drift. **My rec:
  close #889 without merge** (design input delivered + absorbed) — but W0 does not itself resolve #889;
  Mos decides close-superseded vs merge-as-record.

## 6. Open questions for Mos (before I cut W0)

1. **Repo home** — `framework/tools/wake/` for A1–A8 + `framework/defaults/` for C1 schema: confirm names.
2. **#889** — close-superseded (my rec) vs merge-as-record?
3. **PR granularity** — ~8 pipelined PRs OK, or coarser (fewer, larger) / finer?
4. **Pilot boundary** — design assigns pilot (B1–B5) to **mos-dt-0**; I own framework packaging (A*). I do
   NOT build pilot items unless you reassign. Confirm.
5. **Sequencing vs tooling batch** — #890 (a) in flight for id-11; (b) held on scopes; (c) HALTED. Does W0
   start now (dragon-lin outside #869), or after the tooling batch clears? My lean: **W0/W1 can start
   now** (doc + tiny validator, no #869 dependency), heavier W2+ after (b)/(c) resolve to keep ≤2 lanes.
6. **Tracking issue/EPIC** — create a `mosaicstack/stack` EPIC for the canon component (parent for W0–W7),
   analogous to Matrix EPIC #887 / tooling umbrella #891? I'll open it on your nod.
