# MeshKore standard — changelog

The MeshKore standard is the plain-text protocol that describes the
`.meshkore/` folder layout, the cluster bootstrap, the agent docs
contract, and the daemon API surface. As the standard evolves, each
version gets a section here. Every running daemon checks
`https://meshkore.com/standard/version` periodically; when the local
`.meshkore/STANDARD_VERSION` falls behind, the operator (or the LLM
agent acting on their behalf) reads this changelog and applies the
catch-up manually.

The catch-up engine is the LLM. No declarative migration format. No
squash logic. Prose entries describe what changed and how to apply,
the LLM does the edit. This is on purpose — see
[`spec-evolution`](../../../.meshkore/modules/meshkore-spec/tasks/10-spec-evolution.md)
for the rationale.

---

## v12 — 2026-06-02 — Commit attribution trailers (`Agent:` + `Model:`)

Every LLM-authored commit now MUST carry two new git trailers in
addition to the existing `Co-Authored-By:` line:

```
Agent: <agent-role>      # e.g. master, roadmap-architect, work-I13-CAT2
Model: <model-id>        # e.g. claude-opus-4-7, claude-sonnet-4-6
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
```

The `Agent:` value comes from the daemon's `agent_type` field or the
conv slug. The `Model:` value is the exact vendor model id. Human-
only commits MAY omit both. See `commit_attribution` in
`standard.json` and §9.1 of `standard.md` for the full spec.

**Why now.** Field-reported 2026-06-02: a 2-minute "resync" pass
from a roadmap-architect agent committed nothing but rewrote 21 files
to kick the daemon's stale in-memory state. From the git log alone
there's no way to tell whether a commit was a fast probe by a thin
agent or substantive work by a heavyweight one. The trailers make
role + model legible to `git log` / `blame` / analytics; survive
merges + cherry-picks unchanged; parseable via
`git interpret-trailers --parse`.

**Catch-up procedure** for a v11 cluster:

1. Append the v12 prose from §9.1 of `webapp/standard.md` into the
   local cluster's `closure-protocol.md` R2 (or wherever the local
   commit convention lives). Don't copy the rule body — link to
   <https://meshkore.com/standard#91-commit-attribution--agent--model-trailers-v12>.
2. If the cluster runs `meshkore-py`, bump to **py-1.12.7** or later
   — the daemon's spawn-time briefing includes the agent slug + model
   so subagents stamp commits automatically (no operator action).
3. Update the cluster's `CLAUDE.md` boot block: add a line under
   "Conventions" pointing at §9.1.
4. Set `.meshkore/STANDARD_VERSION` to `12`.

**Anti-pattern.** Embedding `Agent:` / `Model:` inside the commit
title or body prose ("// committed by roadmap-architect on Opus 4.7")
defeats the parser. Always use the trailer form at the end of the
body, after a blank line.

**Backfill.** Old commits stay unchanged. Don't rewrite history to
add trailers retroactively — the rule applies to commits authored
after the cluster bumps `.meshkore/STANDARD_VERSION` to 12.

---

## v11 — 2026-05-30 — Cycles (optional task timebox)

Linear-style **Cycles** land as an optional third dimension on the
roadmap. The two existing primitives — Initiative (work-stream) and
Task (one week of work) — are unchanged. Operators who don't want
sprint cadence ignore cycles entirely; nothing breaks.

New folder convention:

```
.meshkore/cycles/
  <YYYY>-w<ISO week>.md       # weekly cycles
  <YYYY>-<ID>.md              # ad-hoc cycles
```

Cycle file frontmatter:

```yaml
---
id: 2026-w22
title: "Sprint 22 — Cluster Cloud P0"
starts: 2026-05-25     # ISO date
ends:   2026-06-07
status: active         # planned | active | done
goal:   "Cerrar CC01..CC06 antes del cierre de junio."
---
```

Body is free-form markdown (planning notes, retrospective, links).

New optional task frontmatter key:

```yaml
cycle: 2026-w22
```

Missing key = task is not in any cycle (default). Present with an id
that doesn't match an existing `.meshkore/cycles/<id>.md` file =
the daemon emits a `task_cycle_broken` integrity hint on the next
briefing (warn, never block).

Daemon work (py-1.10.25 → next bump):
- `/state` payload gains a top-level `cycles:` slice:
  `[{ id, title, starts, ends, status, goal, tasks: [<id>...], path }]`.
  `tasks:` is the list of task ids whose frontmatter has
  `cycle: <this id>`.
- Each task in `/state.roadmap.tasks[]` gains the `cycle` field
  (string id or null).
- The FS-signature watcher now includes `.meshkore/cycles/` so
  adding / editing a cycle file fires `state.rebuilt` and the
  cockpit refreshes.
- StateIntegrityChecker gains `task_cycle_broken` (warn-not-block,
  symmetric with `task_initiative_broken`).

No new HTTP endpoints. Cycles are pure data — no scheduler, no
auto-rollover of unfinished work. The LLM rolls over on demand from
chat (same posture as `standard-versioning`).

Note on numbering: the original spec body (written 2026-05-26) said
"bump to v3", but the standard had already advanced to v10.1 by then.
This entry lands as v11 — the next additive bump after v10.1 — to
preserve monotonic numbering.

`STANDARD_VERSION` 10 → 11. No `DAEMON_VERSION` schema requirement
(daemon binary still serves the same endpoints; only payload shape
gains the additive `cycles:` slice).

---

## v10.1 — 2026-05-27 — Challenge-response auth (`/auth/challenge`)

Daemon adds `GET /auth/challenge?nonce=<n>` returning
`{ nonce, sig: HMAC-SHA256(portal-token, nonce), alg, version, ts }`.
Cockpit runs this handshake on every `switchToPort` to a new instance
when a token for that cluster is already in local storage, and refuses
to attach on mismatch. Defeats MITM-by-cert-leak: an attacker who
serves a valid TLS cert for `daemon.meshkore.com` (our wildcard is
public by design) still can't fake the daemon because they don't have
the operator's `portal-token`.

Backwards-compatible patch on top of v10. Daemons advertise the
capability via the `auth.challenge` feature flag; cockpits without
support fall back silently. **No `STANDARD_VERSION` bump** — additive
endpoint, no schema change. `DAEMON_VERSION` bumped `py-1.8.0 → py-1.8.1`.

Limitations:
- Doesn't protect first-time connection to a NEW cluster (no shared
  secret yet). Operator must verify `cluster_id` by hand on that
  initial bind.
- Future: per-cluster ACME (`<cluster_id>.daemon.meshkore.com` with
  its own cert) closes this fully. See initiative
  `local-tls-subdomain` → `daemon-multi-tenant-tls`.

## v10 — 2026-05-27 — Loopback TLS (daemon.meshkore.com)

The Python daemon now serves HTTPS + WSS on its port range
(5570-5589) when a bundled TLS cert is present alongside `daemon.py`.
The wildcard cert is for `*.daemon.meshkore.com`, which resolves to
`127.0.0.1` via a public Cloudflare DNS A record. This lets the
cockpit at `architect.meshkore.com` reach the local daemon from any
HTTPS origin without:

- mixed-content rejections (HTTPS page → `ws://localhost`)
- Chrome Local Network Access "Issues" (every public→loopback fetch
  was flagged; sessions accumulated thousands)
- the operator having to grant per-site permission via Chrome flags

The pattern is the same one Plex (`*.plex.direct`), Caddy local
HTTPS, and Tailscale Funnel use. Cert + key are deliberately public:
the only thing an attacker can do with them is impersonate
`daemon.meshkore.com` on their own loopback, which gives access to
nothing.

### What changed

- `daemon.py` (`DAEMON_VERSION = py-1.8.0`):
  - New `_find_tls_bundle()` + `_build_tls_context()` helpers.
  - `serve_forever()` wraps the listening socket with `ssl.SSLContext`
    when `tls/fullchain.pem` + `tls/privkey.pem` exist next to
    `daemon.py`. Falls back to plain HTTP otherwise (backwards
    compatible — operators who don't pull the `tls/` directory keep
    working unchanged).
  - `/health` exposes `tls: bool` and an `endpoint` field
    (`https://daemon.meshkore.com:<port>` when TLS is on,
    `http://localhost:<port>` otherwise).
  - `_features()` adds `tls.loopback` when TLS is enabled.
- `daemon/tls/` ships the wildcard cert + key + `README.md` +
  `renew.sh` (certbot DNS-01 against Cloudflare).
- Cockpit `MIN_DAEMON_VERSION = py-1.8.0`. The V47 upgrade modal
  fires on any daemon at `py-1.7.x` or below — daemons keep working
  on HTTP, but the operator is nudged to upgrade so the cockpit can
  flip TLS on by default.
- Cockpit feature flag `localStorage['mc-daemon-via-tls']` (or
  `?tls=1`) toggles the cockpit's URL scheme between
  `https://daemon.meshkore.com:<port>` and the legacy
  `http://localhost:<port>`. Surfaced in the Header About modal.

### How to apply (catch-up from v9)

1. `printf '10' > .meshkore/STANDARD_VERSION`
2. Run the daemon-upgrade flow:
   ```bash
   curl -fsS https://meshkore.com/reference/cluster/scripts/daemon.py \
     -o .meshkore/scripts/daemon.py
   curl -fsS https://meshkore.com/reference/cluster/scripts/tls/fullchain.pem \
     -o .meshkore/scripts/tls/fullchain.pem
   curl -fsS https://meshkore.com/reference/cluster/scripts/tls/privkey.pem \
     -o .meshkore/scripts/tls/privkey.pem
   chmod 600 .meshkore/scripts/tls/privkey.pem
   ```
3. Restart the daemon:
   ```bash
   curl -X POST http://localhost:$(cat .meshkore/.runtime/port)/shutdown \
     -H "Authorization: Bearer $(cat .meshkore/credentials/portal-token)"
   python3 .meshkore/scripts/daemon.py &
   ```
   Confirm the boot log says `tls=on (daemon.meshkore.com)`.
4. In the cockpit's Header → click the logo → About modal → toggle
   "TLS mode" to on. Reload. The cockpit will now hit
   `https://daemon.meshkore.com:<port>` for everything.

The catch-up is safe to skip — operators who don't apply it stay on
plain HTTP and continue to work, just with the mixed-content + LNA
Issues as before. The bump is mandatory only for operators who want
the silent / real-time cockpit experience over HTTPS.

### What this enables

- Real-time cockpit (`wss://` WebSocket events) when cockpit is
  served from any HTTPS origin (`architect.meshkore.com` today).
- Zero Chrome LNA Issues on session — no more 1k+ Issue
  accumulation while debugging.
- Foundation for the cockpit to talk to **multiple parallel
  daemons** without each one having to mint its own self-signed
  cert.

### See also

- [`local-tls-subdomain`](../../../.meshkore/roadmap/initiatives/local-tls-subdomain.md) initiative.
- [`P4-daemon-upgrade`](../../../.meshkore/protocols/P4-daemon-upgrade.md) protocol for daemon-only updates that don't touch the standard schema.

---

## v9 — 2026-05-26 — Project repo convention (multi-repo `.meshkore/` backup)

New §17 of the standard. Codifies what the operator should do with
their `.meshkore/` folder so it doesn't only live on the laptop:

- **Single-repo projects**: `.meshkore/` stays inside the source repo,
  gitignored selectively (same as today's behaviour, just documented).
- **Multi-repo projects**: `.meshkore/` becomes its **own private git
  repo** at `<org>/project` (or `<org>/<project-slug>`). The `.git/`
  lives **inside** the `.meshkore/` folder — the daemon stays unaware
  that the folder happens to be a git working copy.

A reference script `operator-sync.sh` plus a `launchctl` plist
template ships at
`meshkore.com/reference/cluster/scripts/operator-sync.sh` — default
cadence 30 min, single-instance-locked, drops a log under
`.meshkore/.runtime/logs/operator-sync.log`. Optional but recommended
until cluster cloud lands.

### Mandatory `.gitignore` patterns in any project repo

```
credentials/        # NEVER commit
.runtime/           # ephemeral daemon state
roadmap/state.json  # regenerable
roadmap/state.js    # regenerable
```

### Apply manually

For existing multi-repo projects:

```bash
cd <workspace>/.meshkore
git init -b main
# ... add gitignore from §17.3 ...
git add -A
git commit -m "Initial: import project workspace"
gh repo create <org>/project --private
git remote add origin git@github.com:<org>/project.git
git push -u origin main

# Install the auto-sync
curl -o scripts/operator-sync.sh \
  https://meshkore.com/reference/cluster/scripts/operator-sync.sh
chmod +x scripts/operator-sync.sh
# (install your platform's scheduler — see §17.4)
```

For single-repo projects: no action; existing gitignore rule from
v6 (`.meshkore/*` with `!.meshkore/public/` exceptions) is already
the right shape.

### Renaming from v8 pre-release

In an internal iteration the repo name `<org>/operator` was floated
before this changelog landed. The canonical name is `<org>/project`
from v9 onwards. If a cluster already created `<org>/operator`, just
`gh repo rename project --repo <org>/operator --yes` and update the
remote URL in `.meshkore/.git/config`.

### What did NOT change

- No daemon API change.
- No `.meshkore/` schema change — same folder shape.
- `cluster.yaml` schema unchanged.
- The L1/L2/L3 Quality Gates from v8 are unaffected.

---

## v8 — 2026-05-26 — Quality Gates (L1/L2/L3 mechanical safety net)

New §15 of the standard. Every code repo in a MeshKore cluster
opts in to three independent gate layers that catch mechanical
issues (types, lint, format, secrets, build, basic tests) before
they reach `main`.

### What changed

- New §15 — Quality Gates. Three layers documented:
  L1 (pre-commit, <10s, local), L2 (CI on push, GitHub Actions,
  1-5min), L3 (nightly drift, optional).
- New repo contract: every code repo MUST have `.github/workflows/ci.yml`
  + `.git/hooks/pre-commit` + `.meshkore.repo.yaml` with declared
  `stack` and `quality_gates_version`.
- Four stack templates shipped at
  `meshkore.com/reference/standards/quality-gates/`:
  `typescript/`, `cloudflare-worker/`, `rust/`, `python/`. Each has
  `ci.yml` + `pre-commit` + (where useful) nightly audit job.
- One-line installer:
  `curl … install.sh | bash -s <stack>`. Idempotent.
- Agent contract prompt at
  `meshkore.com/reference/standards/quality-gates/agent-prompt.md`.
  The daemon prepends this to every chat-dispatch in repos with
  Quality Gates installed. Forbids `--no-verify`, forbids
  `meshkore.skipL1`, forbids deleting gates to make red turn green.
- §16 takes the old §15 ("Where everything else points") slot —
  one-line rename, no semantic change.

### What did NOT change

- No daemon API change — the daemon doesn't need to know about
  Quality Gates yet. The hooks live on disk, the workflows live on
  GitHub. Integration with the prompt queue (L2 failure → agent's
  next dispatch) is deferred to the `quality-gates-standard`
  initiative, which depends on `context-versioning-and-agent-sync`.
- No `.meshkore/` schema change.
- No `cluster.yaml` schema change (`stack:` already declarable, just
  encouraged now).

### Apply manually (existing repos)

For each existing MeshKore-managed code repo:

```bash
cd <repo>
curl -sSf https://meshkore.com/reference/standards/quality-gates/install.sh \
  | bash -s <stack>     # typescript | rust | python | cloudflare-worker
git checkout -b chore/install-quality-gates
git add .github .meshkore.repo.yaml
git commit -m "chore: install meshkore quality gates v1 (<stack>)"
git push -u origin chore/install-quality-gates
# open PR, merge after L2 passes
```

Skip this for repos that are deliberately gate-less (scratchpads,
archived). Architect's repo panel will badge them yellow.

### Note on v7

v7 was an in-flight bump that didn't reach the public version file
before v8 work landed. The standard sequence ships v6 → v8 to keep
the public version monotonic. Nothing functional was added in v7.

---

## v6 — 2026-05-20 — Adoption polish (timeline vocab + migrate wizard + verify + token clarity)

This is a polish bump driven by operator feedback after a real-world
migration of an external repo (charms-wallet) hit eight distinct
friction points. No new files in `.meshkore/`; the changes are
documentation completeness + script hygiene + redirect cleanup.

### What changed

- **New §6.3 in the standard** — full timeline event vocabulary
  (`task.*`, `chat.*`, `tool.*`, `commit.*`, `deploy.*`, `cron.*`,
  `state.rebuilt`, `links.updated`, `protocols.updated`,
  `bookmarks.updated`, `daemon.shutdown`). §6.2 previously said "see
  spec v1 §11" but §11 is versioning — now fixed.
- **§3 clarification** — `.meshkore/STANDARD_VERSION` is the
  canonical answer to "which standard does this repo apply"; new
  table documents the spec ↔ manifest ↔ daemon relationship (three
  independent versions that may drift).
- **New wizard prompt** at
  `/reference/prompts/migrate-existing-repo.md` — the missing
  counterpart to `project-from-scratch`, covering ad-hoc legacy layouts
  (`_rjj/`, `_docs/`, `notes/`, …) with deterministic legacy → canonical
  mappings.
- **New `verify.py`** at `/reference/cluster/scripts/verify.py` — runs
  the §2/§3/§4/§5 conformance checks (layout, cluster.yaml fields,
  frontmatter, gitignore contract, STANDARD_VERSION freshness vs the
  published version, links.yaml shape, protocol filenames). Stdlib-only.
- **`migrate-tasklist.py` fixed** — was emitting to the pre-v3 layout
  (`roadmap/tasks/<module>/`); now emits to the §2 canonical
  (`modules/<module>/tasks/`).
- **`/changelog` aliases land** — `/changelog`, `/changelog.md`,
  `/standard/changelog`, `/standard/changelog.md` all 301 to
  `/standard/CHANGELOG.md`. Previously every variant 404'd.
- **Token policy clarified in `cluster/operate.md`** — explicit list
  of read endpoints (no auth) vs write endpoints (bearer required);
  the architect's localhost-only trust boundary is documented.
- **Editor boot blocks self-identify** — CLAUDE.md, .cursorrules,
  .windsurfrules, copilot-instructions.md each open with a comment
  naming the source URL + "kept local, gitignored" so an operator
  arriving at a repo recognises them instantly instead of asking "is
  this ours or someone else's?".
- **`/reference/manifest.json` cross-refs** — gains `version`,
  `changelog`, and `version_relationship` fields under
  `canonical_standard`, pointing at the integer endpoint + per-version
  blocks. Bumped to manifest schema v5.

### How to apply (catch-up from v5)

Backward compatible. Nothing in this bump forces a folder reshape.

1. **Pull the latest daemon**: `webapp/reference/cluster/scripts/daemon.py`.
   No new endpoints; only consistency.

2. **Pull the new `verify.py`** and run it once:
   ```bash
   curl -fsSL https://meshkore.com/reference/cluster/scripts/verify.py \
     -o .meshkore/scripts/verify.py
   python3 .meshkore/scripts/verify.py
   ```
   Address every ERROR; treat WARN as discretionary.

3. **Update your editor boot block** if you keep a local copy at repo
   root — the new template starts with a self-identifying comment.
   Drop your old `CLAUDE.md` / `.cursorrules` / `.windsurfrules` and
   re-curl from `/reference/cluster/editor-rules/`.

4. **Bump the marker**: `echo 6 > .meshkore/STANDARD_VERSION`.

### What this enables

- A second wizard (`migrate-existing-repo`) means the *"apply the
  standard"* prompt now has the right entry point for both empty
  repos and pre-existing ones.
- Agents finally have a programmatic conformance check (`verify.py`).
- Timeline event vocabulary is exhaustive enough that the architect's
  Diary panel (V43) can render every event type without surprises.
- No more *"which version am I on?"* ambiguity — the integer in
  `.meshkore/STANDARD_VERSION` is the single source of truth.

### Operator feedback this addressed

The full migration feedback from 2026-05-15 is preserved in
`.meshkore/log/2026-05-20.md`. Friction items #1–#8 are now closed
(token contradiction, missing changelog URL, stale migrate script,
missing event vocab, no self-identifying headers, no verify script,
no existing-repo wizard, no spec/manifest/daemon relationship doc).
The two open items — "bootstrap script for any legacy layout" and
"daemon-side `applied_standard` check at boot" — are covered
respectively by the new `migrate-existing-repo` prompt + `verify.py`
combo and by `verify.py`'s network check against `/standard/version`.

---

## v5 — 2026-05-20 — Protocols (reusable multi-scope runbooks)

### What changed

- New top-level folder **`.meshkore/protocols/`** holds named,
  indexed, reusable runbooks for multi-step work that crosses scopes
  (docs + code + commit + deploy).
- Identifier scheme **`P<N>`** added to the cluster's identifier
  family (P joins T, V, D, C, N, AG). One global numbering.
- New standard section **§14** describes the file shape, run-log
  shape, daemon surface, and "apply P<N>" semantics.
- Python daemon gains four endpoints:
  - `GET  /protocols`            no-auth, full list
  - `GET  /protocols/<id>`       no-auth, raw markdown + frontmatter
  - `GET  /protocols/<id>/runs`  no-auth, last 50 run entries
  - WS broadcast: `protocols.updated` on file change
- Gitignore exception added: protocols + INDEX are committed; run
  logs stay per-machine.
- Two protocols ship with the standard so projects have working
  references:
  - **P1** — bump-standard-version (the very protocol that produced
    this section).
  - **P2** — deploy-project (build + lint + clean-workspace check +
    per-module deploy via `links.yaml` + post-deploy patch + verify).
- Editor boot blocks (CLAUDE.md / .cursorrules / .windsurfrules /
  copilot-instructions.md) teach agents the "apply P<N>" pattern.

### How to apply (catch-up from v4)

**Backward compatible** — projects without `.meshkore/protocols/`
work exactly as before; the daemon treats a missing folder as an
empty protocol set.

1. **Create the folder** + index:
   ```bash
   mkdir -p .meshkore/protocols/log
   curl -fsSL https://meshkore.com/reference/cluster/templates/protocol.md \
     -o .meshkore/protocols/_template.md     # reference template (delete later)
   ```

2. **Update `.gitignore`** so protocols are committed but run logs
   stay local:
   ```
   !.meshkore/protocols/
   .meshkore/protocols/log/
   ```

3. **Write your first protocol** if useful (deploy, release, audit,
   data migration — whatever you re-explain to your agents most
   often). Follow the schema in §14.3.

4. **Bump the marker**: `echo 5 > .meshkore/STANDARD_VERSION`.

5. **Pull the latest daemon** so `/protocols` endpoints work. Older
   daemons simply don't expose them — no breakage.

### What this enables

- "Apply protocol P<N>" replaces "let me explain again how we ship
  a standard version / deploy the project / run a release".
- Cron-driven scheduled work can reference a protocol id instead of
  inlining a long command.
- The architect's cockpit (next iteration) renders the protocols
  list, lets the operator launch a run, and streams step-by-step
  outcome from the WS.
- Audit: every protocol run leaves a markdown log entry with commit
  sha + deploy IDs + per-step verdict. No more "what did we change in
  the last release" archaeology.

---

## v4 — 2026-05-20 — Links registry (`.meshkore/public/links.yaml`)

### What changed

- New committed file **`.meshkore/public/links.yaml`** declares, per
  module: local URL + start command, production URL + provider +
  deployed sha/version/timestamp, current repo branch + head sha.
- New standard section **§13** describes the schema, the
  agent-maintenance contract, and the daemon surface.
- Python daemon gains four endpoints:
  - `GET  /links`         no-auth, full registry as JSON
  - `GET  /links/<id>`    no-auth, single module
  - `POST /links/<id>`    bearer-auth, patches the file atomically
  - WS broadcast: `links.updated` on every rewrite
- Commit convention §6 extended: any commit that changes where a module
  runs, where it deploys, what branch it lives on, or what version it
  carries MUST update `links.yaml` in the same commit.
- Editor boot blocks (CLAUDE.md / .cursorrules / .windsurfrules /
  copilot-instructions.md) extended to remind agents to keep
  `links.yaml` in sync on every meaningful module change.

### How to apply (catch-up from v3)

**Backward compatible** — projects that don't deploy anything may
skip the file entirely; the daemon treats a missing file as
`{ version: 1, modules: [] }`.

1. **Create `.meshkore/public/links.yaml`** with at minimum:

   ```yaml
   version: 1
   modules: []
   ```

2. **For every module that has a reachable surface** (local port,
   production URL, or both), add an entry. Use the template at
   <https://meshkore.com/reference/cluster/templates/links.yaml> or
   read §13.1 of the standard.

3. **Bump the marker**: `echo 4 > .meshkore/STANDARD_VERSION`.

4. **Pull the latest daemon** at
   `webapp/reference/cluster/scripts/daemon.py` so the `/links`
   endpoints work. Older daemons will simply ignore the file — no
   breakage.

5. **Going forward**: every time you deploy a module, switch its
   active branch, or bump its version, update its entry in
   `links.yaml` in the same commit. The architect's cockpit will
   surface the registry in a future release (the file already drives
   the daemon today).

### What this enables

- Single source of truth for "where is everything deployed and at what
  version" — no more ad-hoc `links.md` lists or scattered per-module
  README sections.
- Cron-driven deploy agents can patch `prod.deployed_*` fields via
  `POST /links/<id>` immediately after a successful deploy.
- The cockpit gains a Deployments view (future task) that reads
  `/links` over the WS event stream.

---

## v3 — 2026-05-19 — Daemon-owned cron scheduler

### What changed

- `cluster.yaml` gains two **optional** top-level keys:
  - `crons:` — list of cron jobs. Each entry: `id`, `name`,
    `schedule` (5-field POSIX cron expression), `cmd`, optional
    `cwd`, `env`, `enabled`, `max_runtime_sec`, `restart_policy`,
    `retention_runs`, `destructive`.
  - `crons_owner:` — the `device_id` of the **coordinator** daemon.
    Only this daemon fires jobs; peers tick at 10 s intervals and
    emit `cron.would_have_fired` events. (Coordinator failover is
    deferred to Cluster Cloud P1.)
- `.meshkore/.runtime/crons.json` is the per-machine runtime state
  (last_run, next_run, history). Gitignored, recreated on first
  scheduler tick.
- `.meshkore/.runtime/logs/cron/<job_id>/<ts>.log` holds the captured
  stdout/stderr of each run.
- The Python daemon (`webapp/reference/cluster/scripts/daemon.py`)
  validates the `crons:` block on load. Unknown / malformed entries
  are skipped with a logged warning; the rest of the daemon's
  features (state, agents, WS) continue normally.
- Schema reference:
  [`docs/conventions/cluster-yaml-crons.md`](../../../.meshkore/docs/conventions/cluster-yaml-crons.md).
- Architecture diagram:
  [`docs/architecture/daemon.md § Cron scheduler`](../../../.meshkore/docs/architecture/daemon.md).

### How to apply (catch-up from v2)

**Backward compatible** — a v2 daemon ignores the new keys; a v3
daemon validates and processes them. No `.meshkore/` reshape needed.
The catch-up is purely additive:

1. **Pull the latest daemon** at `webapp/reference/cluster/scripts/daemon.py`
   (or re-clone the repo) so your local daemon has the v3 validator.
   The TypedDicts and `_validate_crons_block` were added in commit
   `eb1ce40` on branch `daemon/cron-scheduler` and merged to `main`
   when the cron scheduler is feature-complete.
2. **Optional**: add a `crons:` block + `crons_owner:` field to your
   `cluster.yaml`. If you don't want crons yet, do nothing — the
   daemon parses an empty `crons:` set without warning.
3. **Bump the marker**: `echo 3 > .meshkore/STANDARD_VERSION`.
4. **Restart the daemon** so it re-reads `cluster.yaml`. The new
   block is visible at `GET /cron/list` (when D-CRON-04 ships).

If you have an older daemon (no v3 validator) and the upstream
operator has already added a `crons:` block: nothing breaks — your
daemon's `parse_simple_yaml` reads the keys but `Cluster` doesn't
expose them. Upgrade the daemon when you want the feature.

### What this enables

- Replacement of macOS LaunchAgent / cron-tab / GH-Actions cron with
  one portable, stdlib-only Python scheduler that works on every OS
  Python runs on.
- Single-coordinator firing — no double-run when the same repo lives
  on multiple machines.
- Cockpit observability (CronsPanel + log viewer + manual trigger
  arriving with the cron-dashboard initiative).

---

## v2 — pre-changelog anchor

The `STANDARD_VERSION` marker was bumped from 1 to 2 between
2026-05-12 (the spec-evolution task's "today is 1" anchor) and the
creation of this file. Specific changes weren't tracked in changelog
form because this file didn't exist yet. From v3 forward, every bump
carries a section here.

---

## v1 — initial anchor (2026-05-12)

The first formally-versioned shape of `.meshkore/`:

- `.meshkore/public/cluster.yaml` (committed) — cluster identity,
  bootstrap URLs, admission policy, module list.
- `.meshkore/credentials/` (gitignored) — per-machine secrets.
- `.meshkore/agents/` (gitignored) — declared agent identities.
- `.meshkore/modules/<id>/tasks/*.md` — work units, scoped per
  module.
- `.meshkore/docs/{architecture,conventions,deploy,ops,security}/`
  — internal documentation.
- `.meshkore/roadmap/{initiatives,log,manual-tasks.md}` — planning.
- `.meshkore/log/<YYYY-MM-DD>.md` — daily activity log (operator
  convention added 2026-05-12).
- Standard discovery endpoint at `meshkore.com/standard/version`
  (this file's sibling).
