LD4 (HOMELAB-1713): The Loops — intake, El Director, routines, readiness, plan-freshness, meetings, alert audit → live #591

Merged
aaron merged 112 commits from epic/LD4 into live 2026-10-03 18:18:11 +00:00
Owner

Epic integration PR for LD4 · The Loops (HOMELAB-1713). Operator-gated: merging rolls a new dispatcher image into the fleet backbone via ArgoCD — do not merge without the operator's review session.

What lands (all PRs reviewed by fresh-context landing workers, CI green)

Piece PRs State
LD4.4 routines (registry 22+R-24/R-25, watchdog, R-06 cost rollup) #567 + fix merged
LD4.1 intake (PlaneContextSource, sweep arm, RC notifier, bridge spool) #568 merged
LD4.3 El Director (persona, warranted memory, veto, comms ladder) #569 merged
LD4.9 plan-freshness (boundary arm, zero-LLM diff, R-25 paused) #575 merged
LD4.5a alert audit (42.9%→19.1% page fraction, contract gate) #573 merged
LD4.8 readiness (R-24 7-line score, per-class test_inventory) #576 merged
LD4.2 meetings (whisper-asr, ingest, PlaneConvener; arms OFF) #577 merged
R-06 leg A (sandbox_env attribution, unspoofable) pi-extensions #57 merged

What this merge triggers

  • The dispatcher image build (live-only trigger) publishes a new image — the prod manifest PRs then need the digest re-pin (#868 F1: the pinned digest is the old dispatcher-only image; re-pin before #868/#870/#871/#875/#876 merge — the KnownFields strict loader refuses unknown config keys, so prod config PRs MUST land after this image).

Deliberately OFF / deferred (each named on its ticket)

  • All new arms OFF by default (arms.intake/meetings/comms/director false); R-04 nudge scan not invoked (C1 on #568); heartbeat CronJob suspended (ledger reader not wired); R-25 paused (no executor); whisper-asr App unsynced (2-edit activation documented); roundtable record = Plane item (sandbox egress follow-up HOMELAB-1859).
  • Voice notes → HOMELAB-1858; hermes tree → HOMELAB-1851; readiness gate enforcement → HOMELAB-1857.

Operator review queue attached to this merge

① §11 spool ruling (HOMELAB-1685) · ② Signal demotion gate (HOMELAB-1687) · ③ OpenBao policies create-only + token seeding (HOMELAB-1688) · ④ prod queue #868/#870/#871/#875/#876 in that order after the image · ⑤ meetings: create the Outline Meetings collection + RC ld-el-director account before arming.

Known non-blockers

Atlantis orphaned statuses (HOMELAB-1861) may leave phantom pending contexts — CI is the required leg; the phantom is tracked infra-side. The alert-audit base lag (148 lines of monitoring rules behind live) means one page-eligible rule could not be annotated from this branch — a live merge-forward before the next census window closes it.

Epic integration PR for **LD4 · The Loops** (HOMELAB-1713). **Operator-gated**: merging rolls a new dispatcher image into the fleet backbone via ArgoCD — do not merge without the operator's review session. ## What lands (all PRs reviewed by fresh-context landing workers, CI green) | Piece | PRs | State | |---|---|---| | LD4.4 routines (registry 22+R-24/R-25, watchdog, R-06 cost rollup) | #567 + fix | merged | | LD4.1 intake (PlaneContextSource, sweep arm, RC notifier, bridge spool) | #568 | merged | | LD4.3 El Director (persona, warranted memory, veto, comms ladder) | #569 | merged | | LD4.9 plan-freshness (boundary arm, zero-LLM diff, R-25 paused) | #575 | merged | | LD4.5a alert audit (42.9%→19.1% page fraction, contract gate) | #573 | merged | | LD4.8 readiness (R-24 7-line score, per-class test_inventory) | #576 | merged | | LD4.2 meetings (whisper-asr, ingest, PlaneConvener; arms OFF) | #577 | merged | | R-06 leg A (sandbox_env attribution, unspoofable) | pi-extensions #57 | merged | ## What this merge triggers - The dispatcher image build (live-only trigger) publishes a new image — **the prod manifest PRs then need the digest re-pin** (#868 F1: the pinned digest is the old dispatcher-only image; re-pin before #868/#870/#871/#875/#876 merge — the KnownFields strict loader refuses unknown config keys, so prod config PRs MUST land after this image). ## Deliberately OFF / deferred (each named on its ticket) - All new arms OFF by default (arms.intake/meetings/comms/director false); R-04 nudge scan not invoked (C1 on #568); heartbeat CronJob suspended (ledger reader not wired); R-25 paused (no executor); whisper-asr App unsynced (2-edit activation documented); roundtable record = Plane item (sandbox egress follow-up HOMELAB-1859). - Voice notes → HOMELAB-1858; hermes tree → HOMELAB-1851; readiness gate enforcement → HOMELAB-1857. ## Operator review queue attached to this merge ① §11 spool ruling (HOMELAB-1685) · ② Signal demotion gate (HOMELAB-1687) · ③ OpenBao policies create-only + token seeding (HOMELAB-1688) · ④ prod queue #868/#870/#871/#875/#876 in that order after the image · ⑤ meetings: create the Outline `Meetings` collection + RC ld-el-director account before arming. ## Known non-blockers Atlantis orphaned statuses (HOMELAB-1861) may leave phantom pending contexts — CI is the required leg; the phantom is tracked infra-side. The alert-audit base lag (148 lines of monitoring rules behind live) means one page-eligible rule could not be annotated from this branch — a live merge-forward before the next census window closes it.
Adds the control-plane event vocabulary LD4.3 needs: EventKind with the
meeting.completed trigger (LD4.2 owns the producer) and the memory.candidate
record a routine promotes into a reviewed PR. MeetingCompleted.Validate
refuses a chat trigger with no warrant and a pipeline trigger that claims one;
Gaps() names the artifacts a half-recorded meeting is missing so the arm opens
ONE decision-queue item instead of convening on it.
ChatClient grows exactly one method: EnsureDM(username) -> roomID, over
POST /api/v1/im.create. It is the transport half of the Director's RC-DM
primary path (contract §7): a DM room is addressed by roomId like any channel,
so Post/History/Members are reused verbatim and there is no second transport.

The four-method surface assertion in credential_test.go moves to five
deliberately (that test is the reason a new method is a reviewable act), and
FixtureChatClient models the DM room so 'the DM is opened once' is provable
from the call log. No new exported struct, so the struct-walk guard is
unchanged. Tests keep using a stub RoundTripper: no test opens a socket.
Adds the el-director identity entries the persona cross-checks against:
plane, rocketchat and herdr. NO forgejo entry and NOT in activeRoles, both
deliberately — the completeness rule requires an ACTIVE role to bind every
surface with a tokenRef, and OQ-7 gives the fleet's voice no Forgejo token.
v1 runs the Director as a dispatcher arm (OQ-3), not a spawned sandbox, so
the dormant state is honest rather than a deferral.

This is the first entry set in the example that exercises 'a dormant role's
entries may be partial'; the pinned counts in the identity tests move 6->7
roles, 28->31 entries, 7->8 chat accounts, and the test now asserts the
OQ-7 property directly (no forgejo binding).
New package persona/: el-director.yaml (the canonical persona, CONTENT not
config) plus a strict loader that refuses what the contract forbids:

  * an unknown key (KnownFields) or an out-of-vocabulary tone/behavior/tier
    value — a silently dropped setting behaves like one that never existed;
  * identity.account that is not the name identity.AccountNameFor derives, or
    that the committed registry does not bind on Plane and RocketChat (§8);
  * boundaries.never that is not EXACTLY the contract §2 set for the role,
    in that order — adding a prohibition is refused (a standing rule is a
    mechanism, a measurement or a deletion, §5) and removing one is refused.

Persona.Render() emits the fixed block and PersonaSource wraps the role-lore
corpus source so ONE sha256 pin covers persona + lore: a changed tone
re-derives a different digest, which is the point of pinning it. The persona
carries tone, never authority — enforcement lives where the Director is not
(design §2.4), which is the structural answer to risk R-1.
The LD4.1 data model and mechanism, fixture-first and pure where it can be:

- direction/: Item (verbatim body, open source enum, DedupKey, priority
  refused without a citation), the append-only ledger value
  (ld.direction/v1) with a log-only v1 default, the idempotent sink seam,
  the pure R-11 window planner (WindowKey + PlanWindow), and the R-04 SLA
  ladder (24h FYI / 72h ACT, class E pages immediately per O5).
- notifier/: the human-notification seam that discharges FINDING V2. v1 rail
  is RocketChat (reachable today); the DM gap in rcbridge.ChatClient is
  documented and resolved through an explicit optional DirectMessenger
  extension (O4). LogNotifier is the loud default.
- intake/: the sweep arm (rcTopology's non-fatal shape, injectable clock and
  timer), the Plane read surface (marker wire format shared with the Node
  bridge), the label/state resolvers (O1 labels, dispatchable digest state),
  the read-before-write digest guard, and the narrow HTTP digest writer.

No hot loop: one writer per item class, window-key idempotency, and the
read-before-write guard (all three written into the package header).
Package routines/ (LD4.4) with the design brief's three homes: the registry is
GitOps YAML (registry.yaml, all 22 routines registered, 3 active with the ledger
obligation and 19 paused with machine-checkable reasons), the run ledger is a
RoutineRun receipt behind a Ledger port (JetStream Publisher seam with a
nil=log-only mode, plus an offline file backend so the watchdog is readable
before the bus reader lands), and the claim lock is a Plane execution issue
(Forbid = no second execution issue while one is unclosed).

Implements the seven refusal classes (P-ROUT-005) as loader faults with line
numbers, the receipt contract (P-ROUT-004: no receipt is 'missed', never
'success'), the watchdog evaluation (P-ROUT-001/002/010, fail-closed on an
unreadable ledger), the routines policy book as data (policy.md, parsed back by
a test), and R-06's three legs: the sandbox_env attribution key from the
dispatch binding (unspoofable by the child, 'unbound' is a value and a finding),
the read-only redacted session-transcript reader, and the ticket->PR->run
denominator join key.

HOMELAB-1688
New package comms/: MessageClass (attention.push, nudge.r04, escalation.p1,
fallback.notice, director.reply, direction-ack, refusal), Message, Health, the
Ladder, the SignalClient seam and FixtureSignalClient, and the probe loop the
arm runs.

The ladder is where contract §7 ('Signal = outage-fallback + attention-push
only') becomes code, with two anti-storm properties that are the reason it is a
package and not three if-statements in an arm:

  * ONE fallback.notice per degradation episode (edge-latched, emitted by the
    first message that actually needs the fallback — a probe failure alone
    pages nobody);
  * a degraded ladder never touches RocketChat, so an outage costs one failed
    probe per interval rather than one timeout per message.

escalation.p1 dual-posts unconditionally; the 24h soft nudge never does; the
72h ACT nudge does because the caller marks it, not because of its class.
Deliver returns what actually happened (route, room, signal calls, notice,
degraded) and reports a failed attention leg rather than claiming delivery.

rccbridge stays the transport and holds the credential: comms is handed a
DMClient (EnsureDM+Post) and a SignalClient, so 'the ladder can only post' is
structural. Tests use the committed chat fixture: no socket is opened.
- A1: every refusal class (the seven from the brief §2.1) plus the structural
  faults, each as a mutation of one valid entry, asserting the fault CLASS and
  the line number rather than message text; a planted credential is refused as
  ErrSecretInFile.
- A2: corpus <-> registry parity, parsing the R-01..R-22 catalog table
  transcribed from the corpus (testdata/corpus-table.md) and comparing kind,
  cadence and runs-as; the dangerous-class rows are asserted gated.
- A3: Forbid concurrency is Plane state — a second run while an issue is
  unclosed is skipped-concurrent, creates no issue, and the routine resumes when
  the lock is released; an unreadable lock refuses.
- A4: the catch-up policy defaults (derivations backfill, judgment does not) and
  the loader's refusal of the forbidden combination.
- A5: the receipt contract (no receipt => missed, never success; a visible
  no-op; a budget kill) plus the escalation ladder's reset on success.
- A6: the watchdog fires on a dead scheduler (finding, then episode), reports
  never-ran outside the first period, is silent inside it, and PAGES on an
  unreadable ledger (including the log-only ledger).
- A7/C1-C5: census, per-sandbox-environment cost, the leg A/B reconcile
  tolerance, unbound spend visible as a value plus a finding, the budget kill
  citing P-CUST-007, and the in-process pin freshness check (a changed
  derivation fails until its pin is updated).
- The schema accommodation for 4.7-4.11 is itself a fixture document
  (testdata/registry-4x-shapes.yaml) the loader accepts.

HOMELAB-1688
The watchdog must be a separate process from the scheduler (the design's whole
point: the hermes ticker died silently for ten weeks). cmd/routines is the
CronJob's contract: -check prints the registry census, -watchdog evaluates
routine health against the run ledger, and the exit codes are 0 healthy /
1 findings / 2 ledger unreadable — a page, never a pass.

The README records what is wired, what is not (JetStream adapter, live Plane
executor seam, live script/agent executors), the two documented refinements of
the brief's refusal classes (active-only pin/role obligations, multi-token
pausedReason), why analyze-sessions is not vendored (the upstream tool does not
exist on this machine — leg B is an in-package implementation of the same
contract, so C4's '5 scripts at a pinned revision' line is explicitly NOT
satisfied), and the territory boundary with 4.5d.

HOMELAB-1688
HOMELAB-1688: build(dispatcher image): ship the routines binary and the registry, checked at build time
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 2m0s
82e5021cb0
Additive Dockerfile hunk (shared territory — listed in the PR body): the build
now also produces /usr/local/bin/routines and bakes routines/registry.yaml at
/etc/lab-director-routines/registry.yaml, and it RUNS 'routines -check' against
that registry inside the build. A registry the loader refuses now fails the image
build instead of a suspended CronJob nobody looked at.

HOMELAB-1688
R1 (the critical-path slice): cmd/dispatcher/context_plane.go implements the
live per-ticket ContextSource whose absence made the dispatcher refuse every
claim. The ticket-and-acceptance-criteria section is authored from the Plane
item (verbatim) and pinned to that revision's digest; the other seven sections
come from the mounted tree, exactly as context.author: files reads them, and are
NOT fabricated from the ticket body. Staleness fails loud twice: compiler.Compile
re-derives every pin, and the author re-reads the item after compiling and
refuses with a *compiler.CompileError (ReasonStale, section named) when the
revision moved.

Routing: the lane resolver reads the item's own declaration (the ld:direction
marker + the intake label) and maps a digest execution issue to the `intake`
template key and an authored direction digest to `direction` — template keys,
not A-E class letters (the config's own two-taxonomies note). An unbound lane is
refused BEFORE the claim, like an unbound class.

Wiring: arms.intake registered (non-fatal), intake.* config keys (interval,
softAfter/hardAfter, labels, digestState, channel, dmUser), context.author:
plane, templates.direction/intake (defaulting to the generic lane until the
role sandbox templates exist), Options intake seams, README-free (the package
headers carry the design).

Acceptance evidence in cmd/dispatcher/intake_pipeline_test.go: plant an
undigested direction item -> one sweep -> exactly one digest execution issue ->
second sweep creates none; the routing end to end through the core loop; the
plane author's eight sections + pin; the staleness refusal; an unbound lane
refused before the claim.
R6 (the cheap-to-fix, easy-to-forget risk): ActiveRolesInV1 omits intake and the
registry loader refuses an active role that is missing a surface. The role is
activated as DATA — four bindings (plane, forgejo, rocketchat, herdr) with
tokenRefs into OpenBao — exactly the way model.go documents ('adding a role is a
registry edit, never a code change here'). Empty role lore is NOT part of LD4.1
(O6). The three count assertions that pin the artifact (described roles, entries,
chat invite list) move with it.
New package director/: the El Director's warranted machinery, fixture-first.

  * warrants: a closed source vocabulary, a per-class ref grammar, and a
    Resolver seam. Resolvability is CHECKED, not assumed — a record whose
    citation does not resolve is excluded from the active set with a fault
    naming file + line (the anti-hallucination boundary, AC-5).
  * memory: the git-backed record store (preference / lore /
    conversation-state), the maturity ladder (established needs two
    INDEPENDENT warrants — provenance, not count; canonical needs an
    operator-ruling on top), the 90-day TTL, contradiction = ESCALATE not
    coin-flip, and Compose() which renders only the active set. A
    quarantined or contradicted record is composed into no package.
  * veto: named gap + resolvable warrant, or refused with a teaching message
    and NOTHING written to Plane (AC-2). A stop veto releases the claim and
    leaves the rest to the TTL reaper (R-6: no sentinel scope). Recording one
    veto is exactly one ld.veto/v1 comment.
  * hand-off: exactly ONE Plane item, zero spawn/claim calls (AC-1), asserted
    from the fixture's whole call log; plus the OQ-6 direction intake that
    mints an inbox item quoting the operator's own words.
  * meeting: a complete meeting.completed convenes the roundtable once; a gap
    opens exactly ONE decision-queue item naming every missing artifact and
    convenes nothing (AC-6).
  * counters: veto_rate = N/(N+M) with refusals counted SEPARATELY, the
    overturn rate as the calibration input, and the OQ-5 band
    (0.05-0.35/quarter, with a proposal floor so a zero rate reads
    'unmeasured' rather than 'in band') (AC-7).

The package has no spawn, claim, merge, delete or transition surface at all —
PlaneSink is exactly CreateItem + Comment — and boundary_test.go asserts that
over the package's own syntax tree. The live Plane writer is HTTPPlaneSink
(item create + comment, redacting String, stub-transport tests: no socket is
opened anywhere in this package).
HOMELAB-1685: feat(signal-bridge): durable inbox — queue, replay, drain
Some checks are pending
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 2m7s
30d867b47d
The ticket's premise: lib/router.js answered an orphan inbound with an apology and
dropped it. Now the message is persisted BEFORE the operator is told anything, and
the ack is a fact rather than a hope.

- lib/spool.js: append-only JSONL spool, fsync'd before the ack returns, bounded by
  records AND bytes, mode 0600, pruned once every record has landed. The invariant
  that matters: a NON-DRAINED record is never evicted — on overflow the append is
  REFUSED and the operator is told, because silent loss is the failure this ticket
  exists to remove. Dispositions: queued / drained_to_plane:<id> /
  delivered_to_session:<target>.
- lib/plane.js: the minimum Plane write. One writer per item class (the hot-loop
  guard), idempotent on the dedup key (read-before-create, since Plane has no
  constraint this service can use), credential as a header and never a payload.
  The description wire format (marker line + one JSON line) mirrors
  intake.EncodeDirectionItem byte for byte, and BOTH suites assert the same rendered
  string.
- lib/inbox.js: spool -> Plane -> mark, in that order: a record is marked only
  after Plane confirmed it, so a crash mid-drain re-delivers (idempotently) instead
  of losing. Failures stop the pass and keep the spool's order.
- lib/router.js: deliverOrQueue() replaces the drop; QUEUED_REPLY is asserted
  VERBATIM by the suite; the spool-full refusal is loud; /inbox <text> forces the
  inbox path; LINK replays the backlog in spool order as [signal] …; the spool
  stores a sender digest, never the number.
- bridge.js: drain BEFORE the Signal/intercom streams start (a restart replays
  first) and on DRAIN_INTERVAL; disabled-with-a-named-reason when PLANE_* is unset.
- README + env.example: the contract, the disposition ladder, the boundedness, the
  honest residual risk, and the new events.

23 Node tests, no socket: mkdtemp spool, injected fetchImpl, injected timers.
Wires El Director v1 into the process as two NON-FATAL arms (the rcTopology
posture: a chat outage is an observability outage, not a dispatch outage):

  * director  the DM listener. Four intents and no fifth: a !decision marker
              in a DM maps to no ticket, so the R-7 rail replies with ONE
              escalation question (and names the item when the operator scoped
              it with ticket=<KEY>); a meeting-done DM becomes the manual
              meeting.completed trigger; an utterance taken as direction mints
              an inbox item and is acknowledged with its identifier (OQ-6);
              conversation is recorded, not answered (the model surface is not
              wired in v1, and a template reply would be worse than silence).
  * comms     the ladder's health probe, one observation per interval, so 'is
              RocketChat up?' is answered once per probe instead of once per
              message.

Config: a director block (personaPath, memoryRoot, userEnv/tokenEnv for the
SECOND chat account, operator, signalFallback, fallbackProbeInterval,
pollInterval), arms.director/arms.comms/arms.directorArchive, a Secrets pair
for the el-director account, and the example document. The keys are named
userEnv/tokenEnv rather than the design's dmUserEnv/dmTokenEnv because
load.go's credential scan refuses a credential-shaped key that is not on its
exact-match allowlist and dmTokenEnv is not.

Two refusals are deliberate and loud, not stubs:

  * director.signalFallback: true is refused (FaultUnwiredArm). The fleet has
    exactly one Signal egress — the pi-signal-bridge systemd USER unit on the
    mgmt VM, reached over the pi-intercom socket — and a dispatcher pod cannot
    reach it. comms.UnwiredSignalClient fails with that reason instead of
    reporting pages that never arrive.
  * arms.directorArchive: true is refused for the same reason: the warrant
    archive reader does not exist yet, so the loader validates warrant SHAPES
    and says so (a nil Resolver).

buildDirector fails loud on a persona that does not validate or a corpus that
cannot be read, and REPORTS memory faults rather than dying on them: one
record whose warrant does not resolve must not take the Director off the air.
The veto stop is a claim release via the ordinary Plane writer (R-6) — no
sentinel scope, and the arm cannot stop a sandbox it does not own.

Fixture-driven arm tests use the committed persona, registry and chat fixture:
no socket is opened.
HOMELAB-1687: docs(role-lore): seed the El Director corpora and document the slice's limits
Some checks failed
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Failing after 53s
0/0 projects applied successfully.
22b5a93253
The memory corpus ships EMPTY and says why: a record is a claim about a human
backed by a citation, so seeding one would mean inventing the preference and
the warrant — the failure the store exists to prevent. Its README carries the
record schema, the maturity/warrant rules and the O6-analog TODO (the first
record lands from the first ratified DM exchange). The lore corpus is the same
shape, with the pin reminder.

director/seed_test.go pins the shipped corpus mechanically: zero faults, ZERO
records, no .yaml files, and an explicit empty section when composed. It also
checks the lore directory exists as real files, because compiler.FileSource
refuses a non-regular entry (a ConfigMap mount is a symlink farm until it is
materialised).

The dispatcher README gets the package map entries and an honest LD4.3 status:
the ticket's three Verify items and where each stands, plus the two gaps this
slice does not paper over — the Signal egress is unreachable from the cluster
(pi-signal-bridge is a systemd USER unit on the mgmt VM), and a meeting has no
ticket so the roundtable cannot be convened yet. Both refuse loudly and name
their follow-up instead of reporting an effect that did not happen.
HOMELAB-1687: fix(director): commit the unwired convener (which buildDirector wires) and ignore the dispatcher binary
All checks were successful
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 22s
CI Review / dispatcher-test (pull_request) Successful in 2m3s
b185a9a99d
Two things the previous commits missed, found by checking the branch out
clean rather than trusting the working tree:

1. director/meeting.go's UnwiredConvener was staged but not committed by the
   previous pathspec-scoped commits, while cmd/dispatcher/app.go (committed)
   calls director.NewUnwiredConvener(). The pushed branch therefore did not
   compile. This commits it.
2. The executable a local build writes at lab-director/dispatcher/dispatcher
   is build output, never an artifact; .gitignore now names that one path so
   it cannot be staged by accident.

The convener refuses with the structural reason: the ordinary spawn path is
keyed on a Plane ticket (spawn.Spec.TicketID, compiler.Spec.Ticket) and a
meeting has none, so a complete meeting.completed event fails loudly instead of
reporting a roundtable that never sat.
HOMELAB-1687: fix(comms): refuse a ladder with no primary transport before it panics
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 1m59s
e4ea1746da
A nil Client would panic inside the P1 delivery path the ladder exists to
serve, and a panic in the escalation rail is the worst possible failure mode:
the page that never arrives because the process died trying to send it. The
ladder now refuses up front, like it already does for a missing Signal leg, and
TestLadderWithoutAPrimaryRefuses pins that nothing is delivered on either leg.
HOMELAB-1688: fix(routines): source the first-period anchor instead of inventing 'now', and make the read path fail closed
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 2m12s
998e825f81
BLOCKER (PR #567 review, 2026-10-02): cmd/routines passed RegisteredAt:
time.Now(), so watchdog.go's first-period guard was permanently true and the
shipped binary could never emit routine-never-ran (P-ROUT-002). An empty
ledger over three active routines printed 0 findings / exit 0.

- watchdog.go: RegisteredAt is data. Zero now derives the anchor from the
  ledger's oldest receipt (the half the doc comment promised) and, when even
  that is absent, the watchdog grants NO first period it cannot source: a
  zero-row active routine is reported (SeverityFinding, exit 1) rather than
  assumed new.
- watchdog.go: RegistryLandedAt is the reviewer's anchor-independent backstop
  — a floor, from the registry revision's landing time, that revokes a grace
  and sizes the ladder but never grants one (a file's mtime is a lower bound
  on the age of what it holds).
- registry.go: LoadFile records the file's mtime as Registry.LandedAt().
- cmd/routines: -registered-at (RFC3339 / ROUTINES_REGISTERED_AT) is the real
  source the deployment wires to the registry ConfigMap's creationTimestamp;
  an unparsable anchor is a usage error, never silently dropped.
- ledger.go: NewFileLedgerReader never creates the ledger (Concern 1). The
  read path O_CREATE made a wiped emptyDir/PVC read as an empty healthy
  ledger; a missing ledger is now ErrLedgerUnreadable -> exit 2 (P-ROUT-010).
- Tests: CLI-level regressions (TestWatchdogNeverRanFromTheCLI,
  TestWatchdogCreditedAnchorIsRealData, TestWatchdogMissingLedgerIsAPage)
  plus the library backstop and no-anchor tests.
HOMELAB-1685: merge epic/LD4 (LD4.3 el-director) into the intake branch
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 2m10s
b5a806a1a2
#569 (el-director) landed on epic/LD4 first and made this branch unmergeable:
both PRs appended to the same shared files. Resolution, file by file:

- cmd/dispatcher/app.go: union of the import block, the Options seams, the App
  fields and the New() arm-construction blocks. buildRunners needed no edit --
  the two runner registrations merged cleanly (intake at :645, comms :672,
  director :686).
- config/config.go: union of the Arm* consts, the Arms struct fields, and the
  Config.String() summary (one format string, both arms).
- config/load.go: union of the intake and director validation blocks.
- config/example.go: union of the arms keys.
- identity/example-registry.yaml auto-merged; both roles survive (intake and
  el-director).
- identity count assertions recomputed against the MERGED artifact, not taken
  from either side: the example now describes 8 roles (6 active + scout +
  el-director), 35 entries (7x4 + sendrast 4 + el-director 3) and 9 chat
  accounts. `len(roles) != 7` was shared context in the conflict hunk (both
  PRs happened to assert 7) and had to be corrected to 8 separately.

Verified in the merged tree before committing: go build ./... , go vet ./... ,
go test -race -count=1 ./... (20 packages ok) and node --test (23/23).
planeIssue gains a flex `parent` field (UUID string or expanded object) and
Issue.Parent carries it, so a caller that needs an issue's CHILDREN can scan
pages and match client-side. That is the only correct read: the self-hosted
proxy ignores a `?parent=` query filter (verified live 2026-10-02: the filtered
page is the unfiltered newest-first page and `count` is the page length, not a
total). Fixture and both wire shapes (list + detail) are covered.
WIP insurance commit. alert_census.py is functional and reproduces the brief's
repo-owned numbers exactly (63 rules / 27 page-eligible / 42.86%) against the
live retention-bounded 10.0d window; two pinned-window runs are byte-identical.

Also records the raw probe findings so a replacement builder can reconstruct the
derivations without re-discovering them: window measurement, the PromQL that
works, the route-tree walk, the 6 dead rules whose metrics do not exist, and the
open ControlPlaneStaticPodsCrashLooping anomaly (notified but no ALERTS samples).

Still WIP: tests, dispositions, silence reconciler, contract CI gate, PR bodies.
The LD4.9 mechanism as a pure, fixture-testable package:

  scope.go  the plan of record: the epic description's <h2>Scope</h2>
            checklist, parsed and normalised (entities, tags, three citation
            shapes; a citation key must be >=3 chars so `R-25` cannot become
            a manufactured finding)
  diff.go   the 4-class diff. D1 (planned, unticketed) and D2 (unplanned
            child) are identifier mismatches -> drift. D3 (stale tick) and D4
            (uncited scope item) are advisory lines only. That asymmetry is
            the cost brake: prose can never dispatch the architect lane.
  pass.go   one pass over one epic: read, diff, record on the epic, and on
            drift create exactly ONE queued dispatch issue (Todo + label
            plan-drift + `ld:plan-drift` marker and JSON payload). R-25 never
            drafts a ticket body.

Receipts follow the R-25 design: a no-change pass returns a VISIBLE
success-noop carrying the epic comment ref (the comment itself opens with
"no change"); a pass that could not LOOK returns an EMPTY receipt which is
`missed`, never success-noop. A failed disposition returns an error so the
runner records `failed`, keeping the epic comment ref as evidence.

Child enumeration is modelled as `Children(ctx, epicID)` rather than a query
because the self-hosted proxy ignores `?parent=` (see the watch commit).

92.9% statement coverage; every test is fixture-driven and none opens a socket.
A second, read-only Plane observer with its OWN watermark (never the watch's:
one watermark answering two questions is why the arm exists separately). It
publishes one `ticket.done` event per issue that reached a terminal state group
AND has a parent — a parentless Done ticket has no epic whose plan could go
stale, so it produces nothing.

It mirrors the watch's discipline verbatim because that discipline is already
reviewed: order_by=-updated_at with a stop-below-the-watermark cutoff, an
overlap rescan window, (ticketID, revisionAt) dedup with retention pruning,
publish-before-save, a loud refusal on a truncated page, a page-budget error,
and the SAME cold-start refusal type (no policy is a decision, not a guess).
Every failure is logged and retried, never fatal; a cold-start refusal is
returned so the deployment crashloops visibly.

The default sink resolves the event against the routines registry. R-25 ships
paused (dep:routine-executor: the runner's event-trigger seam, the live
ExecutionPlane and an Executor are not wired), so the sink logs the boundary,
names the claimant and its paused reason, and says in as many words that NO RUN
WAS STARTED. An active claim with no executor is a WARN, not a silent success.

Fixture-driven throughout (watch.FixtureClient): first-pass emission for
completed and cancelled children, multi-page walking, transition-not-state
dedup, adopt-baseline and rescan, the page-budget refusal, and the
publish-before-save recovery after a mid-scan failure.
The window is MEASURED at runtime, never assumed: retention here is size-bound
(storage.tsdb.retention.size=30GiB) even though retention.time reads 15d, so the
realised window was 10.00d. A census that assumed 30d would report every
slow-cadence alert as never-firing, and a delete disposition built on that would
be wrong. census.json therefore records the window, both retention flags, the
evaluation interval, and queries[] — every PromQL string with its step and bounds,
so a reviewer can re-derive every number by hand.

Measured 2026-10-02 over 10.00d:
  repo-owned (the denominator)  63 rules, 27 page-eligible = 42.9%
  all live rules (annex)        210 rules, 66 page-eligible instances (64 names)
  fired and page-eligible       9 repo-owned
The generator FAILS CLOSED below a 7-day window (--min-window-days): a short
window manufactures false never-firing rules.

Page-eligibility is a walk of the LIVE Alertmanager route tree (parsed from
/api/v2/status config.original), never a hard-coded list; repo attribution comes
from the PrometheusRule manifests. Two traps are fixed and regression-tested:
summing series for %window (348% on a three-series rule) and classifying by
substring on the rule file path (the mount directory contains
'kube-prometheus-stack').

Findings the census surfaced, in notes/2026-10-02-live-probe-findings.md:
  - 6 repo-owned page-eligible rules are DEAD: their metrics have no series at all
  - the route tree names 5 alertnames of which 4 have no live rule; one of those
    (EtcdHealthCritical) was the #2 source of notifications in the rail, i.e.
    Alertmanager re-notifying stale state for a rule that is not deployed
  - ControlPlaneStaticPodsCrashLooping was notified on 2026-09-30 with no ALERTS
    samples anywhere in the window: recorded as an open anomaly, not papered over

Also: --record appends a sampled daily line per alertname (the honest path to the
ticket's literal 30-day census, true from ~2026-11-01), and --severity-from
manifests computes the PROJECTED page-eligible count for a dispositions change
that is not deployed yet, using the same route walk.
Before this change there were ZERO silences in git and 2 live in Alertmanager,
both created ad-hoc by 'pi-agent (incident-2026-10-01)' with no ticket and no
expiry review. A silence is a decision to stop telling a human about a condition;
a decision with no owner and no review date is indistinguishable from an outage
of the check. Both live silences are back-filled into silences.yaml, including
the finding that neither has a Plane ticket.

silence_reconcile.py compares the register with live Alertmanager and classifies:
unregistered-active/-expired, registered-expired, registered-absent, matcher-drift,
expiring-within-24h, over-max-active, ticket-missing, registered-closed.

FLAG-ONLY, by construction: the module contains no POST and no DELETE, asserted by
a unit test that walks its AST and requires every constructed HTTP request to be a
GET with no body (plus a grep in the alert-contract CI job). --fail-on-finding
changes only the exit code; it still writes nothing to Alertmanager. The ticket's
planted-silence criterion is met by a deterministic fixture test (an inert
LD45aPlantTest silence appears as unregistered-active) rather than by creating an
object in live Alertmanager.

Tier-2 interim home: systemd user units under deploy/ (LD4.4 has not landed, so
there is no routines registry). Those files are COPIES — the VM is authoritative —
and that is stated rather than pretended. Committing the daily outputs is
deliberately not automated (ALERT_CENSUS_AUTOPUSH is off by default).
Standing rule, enforced: every alert rule ships with a disposition contract —
ld-owner, ld-action, ld-frequency, ld-disposition (keep-page|keep-record),
ld-silence-policy, ld-review-after. A rule without a consumer does not merge.

Annotations, deliberately NOT labels: annotations do not participate in a series'
identity, so adding them creates no new ALERTS series, churns no fingerprints and
costs no cardinality. Labels would change the identity of every series — the
cheapest possible way to corrupt the census in the previous commit.

Two legs, because each alone has a hole:
  - diff-scoped refusal (ALERT-CONTRACT-001/002): rules added or modified by the
    PR must carry all six annotations with valid values
  - legacy ratchet (ALERT-CONTRACT-003/004): legacy-rules.txt lists the 32 rules
    that predate the contract; a listed rule that is MODIFIED without acquiring
    the contract is refused, and the file may only shrink (a new line is refused)

Stated limits, in the README rather than hidden: the gate can be bypassed by the
E-adj paths (Alertmanager route tree, defaultRules) which are protected-namespace
and controlled by review class; it refuses the ABSENCE of a contract, not a
dishonest one; and prod's 5 edge-probe rules carry the contract but are not yet
CI-gated (follow-up).
HOMELAB-1848: feat(monitoring): 15 demotions + ld-* contract on 24 rules
Some checks are pending
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
Alert Disposition Contract / alert-contract (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m18s
d760210dd4
Separate commit from the tooling so a revert of the paging change is one revert.

27 repo-owned rules were page-eligible (severity=critical reaches
pi-alert-receiver). 15 are demoted to severity=info with the ld-* contract, which
moves the repo-owned page fraction 42.9% -> 19.1% (27 -> 12 of 63), the ticket's
<20% target. Evidence per rule is in the ld-note annotation and in
lab-director/alerts/dispositions.yaml; every demotion is one of:

  (a) DEAD RULES (6) — the metric has no series anywhere in the TSDB, so the rule
      can never fire: VeleroBackupFailure, VeleroBackupMissing,
      LonghornBackupTargetMissing, LonghornVolumeReadOnly, PVEDiskFull,
      PVENodeUnreachable. Demotion is a no-op SAFETY step, not a paging
      reduction; deletion is proposed for the follow-up PR (demote first, delete
      after one clean cycle). Said plainly so the delta is not oversold.
  (b) CHRONIC STATE (3) — paging on a condition rather than an event, and
      duplicating a page-eligible chart rule: PersistentVolumeCritical (83.1% of
      the window at >90% PVC fill), NodeDiskFull (66 episodes around a flat 90%
      line), ClusterMemoryHeadroomCritical (fires below 2 GiB cluster-available
      while the measured minimum was 107 GiB — the threshold is ~54x below
      reality, with per-node usage peaking at 74% against NodeMemoryPressure's
      90% line).
  (c) FIRES CONSTANTLY, NEVER ACTED ON (5) — LonghornCSIAttacherLeaderLost (190
      episodes: the threshold is literally '> 0 restarts'), PodCrashLooping (99
      series, 44 notifications, for: 0m, duplicating the chart's damped
      KubePodCrashLooping), LonghornVolumeFaulted (47 volumes matched, consistent
      with detached/stale series), NodeMemoryOvercommit (55% observed against a
      200% line — a capacity signal, not an incident), LokiIngesterFailing
      (compares a raw counter to 0), TempoSpanPipelineStalled (an 'and' between
      differently-sampled counters: 22 pending episodes, 0 firing).

Deliberately NOT demoted, with reasons recorded in the annotations:
  - EdgeProbeDown (critical) — measured and found NOT to be a duplicate: it
    matches only the three self-hosted public services while the warning variant
    carries the other 25 edge hosts. Its flapping is a 'for:' problem, not a
    silencing problem.
  - ControlPlaneStaticPodsCrashLooping — a severity demotion CANNOT stop it
    paging: the Alertmanager route tree names it explicitly ahead of the
    severity=critical route.
  - CiliumEnvoyDown, NodeDown, PrometheusWALCorruption, LonghornDiskCritical,
    CNPGBackupStale — unique coverage, verified against the metric's observed
    range over the window (e.g. cilium-envoy up=1 and node-exporter up=1 for the
    whole window, Longhorn disk peaking at 70% against a 90% line).

No deletion disposition is proposed: the window is 10.0d, so a slow-cadence rule
would show zero evidence and a deletion would be window truncation, not proof.

Every annotated rule's expression was verified byte-identical (whitespace
normalised) to the running Prometheus revision, so the dispositions are authored
against what the cluster actually evaluates.
The trigger source, the disposition lane and the registry record:

  * `arms.boundary` (OFF by default) plus a `boundary:` block — interval,
    coldStartPolicy, pageSize/maxPages, overlap, emittedRetention. The
    cold-start policy has NO default and is REQUIRED when the arm is on: a
    scan that finds no watermark must be told whether the downtime was
    absorbed or lost, and the loader will not choose that for the operator
    (the watch's rule, restated for the second observer).
  * `templates.plan-drift` — the LD4.9 item kind, alongside LD4.1's two. It
    answers "which lane serves this item kind", not "how risky is this
    ticket", so it is a template key and not a class letter.
  * R-25 registers in the committed registry as `onEvent: ticket.done` +
    `scope: epic`, `cadencePeriod: week`, outputContract planeComment, status
    PAUSED with `pausedReason: dep:routine-executor` — the honest reason: the
    event-trigger seam through routines.Runner, a live ExecutionPlane and an
    Executor are not wired. `emitsWork` is deliberately unset (the entry is
    paused, so it creates nothing); when the executor lands, the drift dispatch
    issue must be declared there as an edge to whichever routine consumes
    `plan-drift` items — inventing an edge to R-12 (a calendar sweep) would be a
    fiction.
  * app.go wires the arm as a NON-FATAL runner and builds its OWN in-process
    watermark store (never the watch's). It reads the routines registry from
    ROUTINES_REGISTRY_PATH best-effort: a missing mount is a WARN and the sink
    logs `registry_loaded=false`, so "no mount" is never confused with "no
    routine wants this event".

Three sibling-facing test adjustments, all growth-tolerant so the other 4.x
registrants (R-24 next) do not have to touch them again: the registry census
count is now a floor plus a named R-25 shape assertion; the corpus-parity rule
now requires an entry outside R-01..R-22 to name its owning ticket; and
`routines -check`'s expected total is derived from the registry instead of
hardcoded.

`routines -check` is green (23 routines, 3 active, R-25 paused on
dep:routine-executor).
HOMELAB-1729: test(dispatcher): bound the boundary scan's steady-state cost
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 2m25s
968fe73348
Answers the stop condition with arithmetic rather than prose: the arm scans the
PROJECT, not the epics, so a per-epic watermark design is not needed. Steady
state is pages = 1 + ceil(overlap / span-per-page): the fixture (3 pages cold,
one hour between issues) shows 2 pages for one new boundary versus 3 for a cold
scan, and a cold start (or the `rescan` policy) is the only case that pays the
full page count. On the brief's measured project (1497 issues / 15 pages) that
is 15 requests cold and 1-2 in steady state, with no model call either way.
HOMELAB-1848: docs(alerts): dispositions.yaml reflects the tree's own severities
All checks were successful
CI Review / helm-validate (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Successful in 6s
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m11s
0e5f950acf
The first generation read severities from live Prometheus, which still says
'critical' for the 15 rules this branch demotes to info — the committed summary
would have contradicted the manifests it is derived from. Regenerated with
--severity-from manifests so it describes what the tree declares: 12 of 63
page-eligible, matching the projected-after triage run.
The heart of LD4.8: the seven scoring lines, the per-line/weakest-line floor
(never_below [ci_present, test_inventory], min_lines_passed 5 of 6 counted
lines, dev_env advisory v1), every threshold, the per-target override schema
(branch + advisory + neverBelow + a required reason), and the ruled target
scope. Data, not Go constants, so the floor changes in a reviewable diff.

Next: the definition loader (strict decode, unknown probe = refusal), the
mechanical probes, the scorer + report, the Forgejo/local-git signal adapters,
the R-24 registry entry, and the two-repo calibration.
T1: ledger.Run gains an additive optional `target` field (ld.routine.run/v1
stays), ValidateFor/TargetRequired enforce it for a routine that declares
targets, RunFilter gains Target so the per-target read is one call.

T2/T3: readiness_definition.go loads readiness.yaml strictly (unknown key,
unknown probe, unreasoned override, floor contradictions, credential value =>
refusal with a line number); readiness_probes.go implements the seven probes
with the SEND-437 mechanism in test_inventory (package attribution + build tags
+ -run matched against the file's own test function names, per class ratio);
readiness.go scores weakest-line and renders the canonical JSON + markdown;
readiness_workflows.go parses workflow triggers/steps and derives the gate
branch from the workflows' own on: blocks (the fleet's three-tier repos gate on
`live`, not on the default branch).
- readiness_git.go: WorktreeSignals (offline, rev-pinned via git) + RunsFromFile
  (a saved /actions/runs dump, so a calibration verdict is re-derivable).
- readiness_forgejo.go: read-only REST adapter (repo, tree, raw file, runs), one
  retry on 5xx/429, status-not-conclusion, token supplied by wiring and never
  logged.
- readiness_digest.go: the architect digest (below-floor first, narrowed floors
  printed) plus the window-two repair-ticket refs, off by default per the
  REPORT-ONLY ruling.
- cmd/readiness: the runnable mechanism (definition, targets, checkout or API,
  report dir, digest), exit 1 when a target is below floor.
- probe tests (fixtures only, no sockets): the SEND-437 shape reproduces as
  integration 2/42 (5%) with the untagged suite at 100%; build tags are honored;
  a -run slice cannot claim a file whose tests it does not select; the gate
  branch derives from the workflows' on: blocks; weakest-line floor; determinism.
- registry.yaml: R-24 agent-readiness-score becomes real (active, weekly, seven
  owner/name targets, outputContract metricRow+artifact, emitsWork false per the
  REPORT-ONLY first-window ruling, pinned to readiness.go). Reserved-ids comment
  updated (R-23, R-25..R-27 remain reserved); corpus-table.md untouched.
- registry_test.go: 23 routines (22 corpus + R-24). parity_test.go: the
  "registered but not in the corpus" check now derives the hosted 4.x set from
  testdata/registry-4x-shapes.yaml instead of listing it, so R-24 cannot claim an
  exemption the fixture does not grant.
- ci_green now names the newest gate-branch run when the scored revision has no
  run of its own (evidence + finding), so "no run" is actionable.
- testdata/readiness-calibration.md + the four reports and three runs dumps:
  the SEND-437-class state reproduces exactly (ci_green pass, test_inventory fail
  at integration 9/74 = 12%), and the brief's expected "clean" verdict for
  Homelab/infra-core does NOT reproduce - documented with the three
  independently checkable reasons and the merge-commit-ungated finding.
Registering R-24 makes the census 23 routines / 4 active, and the watchdog's
zero-row case now has four active routines. Both assertions are DERIVED from the
registry (Census().ActiveIDs) instead of hard-coded, so the next routine to earn
its mechanism does not fail these tests for the wrong reason.

go test -race ./... and go vet ./... green in lab-director/dispatcher.
Landing review blocker 2 (undeclared API fan-out): the adapter read one
GET /raw/{path} per test file, so cost was linear in the repo's test
count (207 files ~= 5 min against R-24's 900 s window).

- File() is now served from the revision tarball, materialized by ONE
  /archive/{rev}.tar.gz request; the raw endpoint survives only as a
  bounded fallback (archive disabled, gitlink).
- Runs are two bounded, filtered windows (the endpoint ignores limit
  without a filter): head_sha=<rev> and head_branch=<gate>&event=push.
  Both cached.
- MaxRequests hard stop with a loud ForgejoRequestBudgetError.
- BranchRunSource + probeContext.runs() merge the gate-branch window.
- cmd/readiness reports per-target wall time + GET count on stderr.
Landing review blocker 1: the PR body claimed httptest adapter tests for
the live Forgejo adapter and no test in the tree referenced ForgejoSignals
or WorktreeSignals. The claim is now true, and the tests assert the request
SHAPE the perf fix depends on:

- readiness_forgejo_test.go: scripted Forgejo fixture, tree+archive reads
  (one archive request for N files, zero raw reads), cost-constant-in-file-
  count, archive->raw fallback, filtered/bounded runs windows + caching,
  refusal of an unfiltered runs read, error paths (typed 404, 5xx retried
  once, decode, no base URL), credential redaction of error bodies, the
  request budget stop, RevFor, target, untar prefix strip + oversize refusal.
- readiness_git_test.go: real temp git checkout (revision pinning, tree,
  file, bad rev), Runs() requires a source, RunsFromFile dump decoding,
  BranchRunSource merge/dedupe + degraded-window finding.
The ticket's Verify line 1 needs a floor-passing repo to contrast the
SEND-437 scar against; the landing review found none in the ruled scope.

- Sendrast/sendrast-web @ aa500803 scores ready (6/6): ci.yml gates main,
  the scored sha has a green push run, the gate runs all 151 test files,
  4 docs files + a 238-line README, AGENTS.md at the root. Added to
  readiness.yaml targets and R-24 targets.paths (scored every window as
  the healthy control); the definition test now also asserts the
  definition's list cannot drift from the registry's authority.
- The ruling's named repo does not exist: Homelab/lab-director-contract
  404s, and ~/repos/lab-director-contract is a clone of aaron/lab-director,
  which scores 1/6 at both f2e16ae and current main (no gate, no README,
  no tests). Recorded with evidence; not forced, not added to the scope.
- readiness-calibration.md gains the clean target (§3), the 404 finding
  (§4), and the fan-out fix's before/after cost table (§6).
HOMELAB-1730: style(readiness): gofmt cmd/readiness after the cost-telemetry edit
Some checks are pending
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m11s
Plan in progress...
8597d0e945
First slice of LD4.2, per the orchestrator's assignment: prove or disprove the
brief's riskiest assumption (§5.3 / R-5) before anything is built on it.

Verdict: DISPROVED on both halves.
  * prod/manifests/agent-sandbox/ declares six SandboxTemplates (ld-sandbox,
    ld-build-test, ld-browser, ld-firmware, ld-meet-ai, ld-claude-code) and
    sandbox-init/roles.json declares two roles (light, builder-core). There is no
    roundtable class and no planner class.
  * The union of every class's egress allowlist is DNS, Forgejo, OmniRoute, the
    NATS bus and (light only) OTel. Outline appears nowhere in
    cilium-network-policies.yaml, and the dispatcher's own policy omits it too.

The unratified Gateway-VIP egress hole (HOMELAB-1786) is explicitly NOT a path
this slice may build on.

Consequence: the documented fallback applies. The meeting-record Plane item is the
record's durable home in v0; a roundtable class and its Outline egress are an
operator decision (new SandboxTemplate + role + CNP, all on AGENTS.md's dangerous
list), recorded in the PR body and the ticket's verification comment.
The routing half of LD4.2. A meeting has no ticket, so the meeting-record Plane
item IS the ticket: marker ld:meeting-record (+ label roundtable for humans),
routed by laneForIssue to a new template kind.

  * intake: MarkerMeetingRecord + LabelRoundtable, and MarkerLinePayload so
    director/ parses a marker payload the same way intake/ delimits one.
  * config: TemplateKindRoundtable (the third item-kind key) so the loader
    accepts it; the shipped binding is ld-sandbox, because there is no
    roundtable SandboxTemplate (see docs/ld4.2-roundtable-class-verdict.md).
  * laneForIssue: marker ld:meeting-record -> roundtable, checked before the
    LD4.1 markers. The MARKER routes, not the label - the same posture
    ReadUndigested takes; requiring the label too would let a missing label
    silently demote a meeting to ordinary ticket work.
  * config: arms.meetings (Bool, unset=OFF) + a meetings block
    (interval/state/pageSize/maxPages). meetings.state defaults to the watch's
    first dispatchableStates entry, so the arm's view of "open" and the
    watch's view of "dispatchable" cannot drift; the loader requires it when
    the arm is on.
The convening half of LD4.2: the UnwiredConvener seam (#569) is now wired.

director.PlaneConvener (director/plane_convener.go) implements Convener in the
shape the seam's own doc comment described - find-or-create the meeting-record
item, compile through the ordinary author, ONE spawn - and reports RunID as the
item's identifier (the durable handle) with SpawnCalls=1.

director/meeting_record.go is the item's grammar: marker line + ONE JSON line
carrying the meeting.completed event verbatim, plus the record's acceptance
criteria in the item's own words. One item per meeting: find-or-create means the
convening REUSES the item the ingest created, so a meeting never gets two ticket
surrogates.

cmd/dispatcher/meetings.go is the producer's consumer half. The trigger travels
Plane because there is nowhere else it can travel: the dispatcher has no ingress,
JetStream is not wired (direction/ledger.go:386), and the DM arm is a chat
trigger. So the ingest creates ONE meeting-record item in Todo and the arm sweeps
it, feeds director.MeetingHandler (unchanged), and stamps what it consumed.

Idempotency, stated honestly (in the code, the PR body and the ticket): the arm
dedupes by item UUID in memory and only convenes items in the configured state;
the durable stamp is the comment + the item's state. A dispatcher restart between
the convening and the item moving out of Todo would convene a second run - the
durable ledger is the JetStream follow-up.

Also: fixture.go gains Refs(); RoundtableRun.RunID's doc names the item
identifier as a legal value (the field no longer lies).

Evidence: go test -race ./... and go vet ./... green; the AC is in
cmd/dispatcher/roundtable_pipeline_test.go (one meeting-record item in, exactly
ONE spawn in the roundtable lane, keyed on the item; a gapped record opens
exactly one decision-queue item and spawns nothing; a second sweep convenes
nothing) and director/plane_convener_test.go. No test opens a socket.
The image half of S1. The image does not exist yet, so this is what makes it
buildable and publishable through the normal pipeline:

  * lab-director/whisper-asr/Dockerfile — multi-stage whisper.cpp v1.9.4 build
    (GGML_NATIVE=OFF: the CI runner and prod-wk-01 are different CPUs), with
    ggml-large-v3-turbo-q5_0 baked in and its sha256 VERIFIED at build time (the
    hash was read off the mgmt VM's own copy, so a drift fails the build rather
    than silently baking a different file). CPU only; no ffmpeg (the caller sends
    16 kHz mono WAV, which is the contract); curl is present so the smoke test and
    an operator's in-pod check need no second image.
  * .forgejo/workflows/whisper-asr-image-build.yaml — PR: build + smoke test (no
    credentials); live: publish <short-sha> + the 0.1.0/latest aliases and print
    the digest. The smoke test exercises the real contract on a generated WAV.
  * lab-director/whisper-asr/README.md — the contract, the consumers it copies
    (meeting-media.py, voice-whisper.json), and what is deliberately absent.

Also fixes a real defect this slice exposed: the FILE context author ignored a
resolved lane, so a roundtable dispatch under context.author=files (prod's
setting) would compile class "default" while the spawn arm was handed
"roundtable" — and spawn's preflight refuses a package whose class disagrees with
its spec. filesContextSource now implements PackageForLane, with the regression
test at cmd/dispatcher/roundtable_pipeline_test.go:TestTheFileAuthorHonoursTheRoundtableLane.
HOMELAB-1686: feat(meeting-ingest): the mgmt drop-directory ingest and the transcript writer
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m21s
38493bbe5e
LD4.2's producer: the operator-mediated half of the recording-ingest convention.
Proton Meet has no server-side recording API (E2E MLS), so the pipeline's input is
an exported MP4 an operator drops. This component runs on the mgmt VM as a systemd
USER unit — the pi-signal-bridge precedent (no npm deps, env file, loopback tunnel)
— because the orchestrator's ingest-v1 ruling is exactly that shape, and because no
workload in this cluster mounts the media NFS (the brief's in-cluster watcher would
have meant inventing a storage contract to satisfy an ingest).

  inbox/<recording> --ffmpeg 16kHz mono--> POST whisper-asr:8171/inference
     --> Outline transcript doc (Context Block + #t=HH:MM:SS cues)
     --> ONE meeting-record Plane item (marker ld:meeting-record, label roundtable,
         Todo) carrying the meeting.completed payload

The last step is the trigger the dispatcher's meetings arm sweeps.

Design points that are load-bearing rather than incidental:

* The wire is the SAME as meeting-media.py's: hand-assembled multipart, field
  "file" + response_format/temperature/language, 16 kHz mono pcm_s16le. A test
  asserts the exact bytes.
* The Plane payload is the Go marker grammar: marker line + ONE JSON line whose
  field names are director.MeetingRecord's. A Node-rendered item is committed to
  director/testdata/ and PARSED BY THE GO READER in a test, so a drift on either
  side breaks a test instead of a roundtable.
* MEETING_ATTENDEES is REQUIRED: events.MeetingCompleted.Gaps() treats an empty
  attendee list as a gap, so an ingest without the series' seats would produce a
  meeting that never convenes, and that failure would look like a broken roundtable
  rather than missing configuration. Diarization is deferred, so the seats are
  stated once by the operator, not guessed from the audio.
* Evidence is never deleted: sources are moved (inbox -> done/failed) by an atomic
  rename claim, and a failure leaves the file plus a .error.txt beside it.
* One writer per item class: this component creates meeting-record items only.
  Direction items are the roundtable's.

Also adds whisper-asr-tunnel.service (the loopback port-forward that reaches the
in-cluster ClusterIP without a new network surface).

Evidence: node --test -> 21 tests, 0 sockets opened; go test -race ./... and
go vet ./... green.
Merge pull request 'HOMELAB-1686: LD4.2 meeting pipeline — dispatcher convening, whisper-asr image, mgmt ingest' (#577) from plane/HOMELAB-1686-meetings into epic/LD4
Some checks failed
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 7s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Failing after 9s
Lint & Validate / yaml-lint (pull_request) Successful in 13s
CI Review / alert-receiver-test (pull_request) Successful in 23s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 1m33s
CI Review / dispatcher-test (pull_request) Successful in 2m26s
Lint & Validate / terraform-validate (pull_request) Successful in 3m9s
Build whisper-asr Image / build-and-push (pull_request) Failing after 7m19s
782b346526
HOMELAB-1850: feat(routines): the phase census — the split identity, P-ROUT-012 and the calibration line (R-29)
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 24s
CI Review / dispatcher-test (pull_request) Successful in 4m26s
4a6e192f9b
The check over what the emission side writes. `rollup.go` is untouched (its
sha256 pin is a promise an auditor re-derives, and LD4.4 owns it); this is a new
file that READS RollupResult:

* `SplitPhaseTotals` / `SplitPhases`: the per-sandbox census whose arithmetic is
  the ticket's verify line 2 — `Σ(build, review, verify) + untagged == total`
  exactly, integer tokens, no tolerance. `unphased` (the value the telemetry
  writes when nothing was declared) and any label outside the closed enum are
  UNTAGGED and NAMED, never a fourth bucket and never folded into one of the
  three. A blank-phase sample lands in untagged by construction (GroupSamples
  skips it deliberately).
* `ValidatePhaseSplit` / `ValidatePhaseSplitWithDeclarations`: the enforcement
  point. A non-summing split is an episode-class mismatch (a folding bug); a
  bucket that DECLARED a phase and still carries untagged spend is a dispatcher
  defect; untagged spend on an undeclared/legacy window is reported with the
  instrument's own coverage. Both cite P-ROUT-012.
* `CalibrationLine` / `PhaseCensusMarkdown` / `PhaseCensusDigest`: verify lines 3
  and 4. The line quotes THIS fleet's measured validation share with n, the
  window and the untagged share, and the vendor datum appears only as an
  external, single-mission comparison ("not reproduced at this sample size" /
  "one comparison, not corroboration"). An empty window is a visible no-op, never
  a published 0 %. The table gives untagged its own column: unknown is not
  healthy.
* `PhaseCensusExecutor`: the runner seam for R-29 (the R-06 ExecutorAdapter
  precedent) — names the series it emitted and refuses to claim success from a
  failed read.

Registry: R-29 `phase-census`, ACTIVE (its mechanism ships in the same build),
pinned to this file's sha256. It takes R-29, NOT the briefed R-27: the header has
reserved R-27 for 4.11 since LD4.4 and `testdata/registry-4x-shapes.yaml` already
carries R-27's maintainability-fitness shape (asserted by
TestSchemaAccommodatesHosted468Shapes), so a second R-27 would break the
registry's own "ids are stable forever" rule. R-28 is LD4.5b's on-call window;
R-29 is the next free id. The census floor in registry_test.go is now derived from
the corpus fixture instead of a hardcoded 24.

policy.go/policy.md: P-ROUT-012 (both files change together — the doc is parsed
back by TestPolicyBookMatchesDoc).

Tests: phase_test.go (TestC6*) covers the identity, the untagged cases, the
declared-phase defect, the mismatch episode, the calibration line, the rendered
census row and the no-op receipt. `go test -race ./...` + `go vet ./...` green.
Merge pull request 'HOMELAB-1850: feat(routines): phase census — split identity, P-ROUT-012, calibration line (R-29)' (#594) from plane/HOMELAB-1850-attribution into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Failing after 7s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 2m10s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m38s
Lint & Validate / terraform-validate (pull_request) Successful in 3m28s
Build whisper-asr Image / build-and-push (pull_request) Failing after 8m16s
2607c6e20c
Author
Owner

Companion PR note: #595 (LD4.5d's Tempo histogram_buckets + phase dimension) is reviewed CLEAN, minimal (exactly the two promised keys, no retention/query changes), and deliberately held for operator review (protected monitoring ns). Its base is epic/LD4, so its observable effect rides this PR's merge (prod pulls Tempo values from infra-core live). Operator review of #591 should include #595. 1593's premise independently reproduced during review: subagent span p99 = 16.38s.

Companion PR note: **#595** (LD4.5d's Tempo `histogram_buckets` + `phase` dimension) is reviewed CLEAN, minimal (exactly the two promised keys, no retention/query changes), and deliberately held for operator review (protected monitoring ns). Its base is `epic/LD4`, so its observable effect rides this PR's merge (prod pulls Tempo values from infra-core `live`). Operator review of #591 should include #595. 1593's premise independently reproduced during review: subagent span p99 = 16.38s.
The L7 self-improvement mechanism's payload and mechanism (contract §6):

- friction.Line — ld.friction/v1, closed class/surface/severity sets, derivable
  fingerprint, subject fleet.ledger.friction.<entrykind>. The NO-ARGUMENTS LAW
  is enforced in code: forbidden key families are refused at any depth and a
  where.ref that renders an invocation is refused (ReasonArgumentsPresent).
- friction.Transition — ld.proposal/v1, one record per TRANSITION, the five
  states as pure functions (Legal/Validate/DeriveState/Append). Append refuses
  self-acceptance, resurrection of a terminal pid, acceptance of a
  contract-class proposal by a non-operator, and applied without a merged PR.
- Kill arithmetic (L7-K0/K1/K2) as pure functions over ledger rows, with the
  L7-K2 fallback when unitValue is absent (LD4.4 §7.1 R6).
- friction.Bindings — the shadow-binding mechanism: pin selection sampled by a
  hash of the dispatch key, apply = sample 1.0, revert = restore the recorded
  sha256 pin, proven against the bytes that pin names (RevertAndProve).
- friction.Triage — the R-28 friction->Plane sink over the existing
  director.PlaneSink seam: group by fingerprint, dedup before any write, one
  open ticket per fingerprint, ticket budget, and a receipt row on
  fleet.ledger.friction.triage (log-only publisher default + JetStream stub).

Fixture-first: 30 tests over testdata/friction-lines.jsonl,
testdata/proposal-transitions.json and testdata/context-bindings.json; no
sockets. go vet + go test green.
R-28 is the L7 leg contract §6 names: it drains the friction ledger into deduped
Plane items (one per fingerprint; blocks or >=3 occurrences only; within a
per-run ticket budget) and nags the proposal ledger (proposals stuck
proposed/accepted past the SLA, shadows past their window with no verdict).

Registered PAUSED for dep:routine-executor, the same dependency R-22 and R-25
name: the mechanism ships in this ticket (dispatcher/friction/) but the executor
that schedules it does not, so the entry carries no scriptRef pin — an ACTIVE
script routine must pin its mechanism, a paused one must not pretend to.

Two deviations from the brief's proposed entry, both deliberate:
- no ticketBudgetPerRun field: the registry schema has no such key (loader is
  KnownFields(true)) and this ticket does not own the registry schema, so the
  bound lives in friction.DefaultTicketBudget and is named in the entry;
- no effects.writes: the Effects schema has no writes key; reads name
  jetstream:friction, jetstream:proposal and plane.

The registry header's RESERVED note records R-28's landing. No count assertion
is hard-coded anywhere, so nothing had to be made derived: TestRegistryMatchesCorpus
treats a post-R-23 id as visible growth when it names its owning ticket.

go test ./routines/ (parity, pins, corpus) and ./friction/ green.
The Go decoder now decodes, byte-for-byte, the exact ld.friction/v1 record the
capture surface's test asserts (pi-extensions/pi-friction/tests/types.test.ts,
"the wire shape (cross-implementation)"). The fixture is pinned in both repos on
purpose: the capture surface and the triage sink are independent implementations,
and this is what keeps one schema from becoming two.

It caught a divergence on the first run: Go omitted actor.sandboxEnv for a human
actor while the capture surface stamps `unbound`. Stamp() now fills `unbound` for
ANY actor with no sandbox and Validate requires it present, so a reader never has
to distinguish "no sandbox" from "the writer left it blank".

go vet + go test ./friction/ green.
HOMELAB-1690: fix(routines): re-id L7 triage R-28 → R-30 (R-28 is 4.5b's reservation)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 3m39s
71631d8b18
Merge pull request 'HOMELAB-1690: feat(friction): the L7 mechanism — ld.friction/v1, ld.proposal/v1 state machine, shadow bindings, R-28 triage sink' (#592) from plane/HOMELAB-1690-l7 into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
Alert Disposition Contract / alert-contract (pull_request) Failing after 7s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 13s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 3m15s
Lint & Validate / terraform-validate (pull_request) Successful in 3m27s
CI Review / dispatcher-test (pull_request) Successful in 3m50s
Build whisper-asr Image / build-and-push (pull_request) Failing after 8m39s
8d8cbd745b
An alert episode is claimed from Backlog and released back to Backlog. The
previous global ReleaseState (Todo) is the dispatcher's dispatchable state, so a
reaped episode ticket was spawned as builder work — the sharpest hazard in the
ticket. Class resolution is label-driven, unknown classes are refused at load
time, and the class-less path is byte-identical (15m lease, release to Todo).
The reaper releases the lease; R-03 (the reclaim driver, already active in the
registry) files the finding. Dedup is the point: a flapping claim appends one
observation to one ticket rather than minting a ticket per reap. A non-episode
reap files nothing, and a hygiene write failure does not fail the release.
The component: an HTTP receiver for the Alertmanager v4 payload behind the
existing rail, the episode ledger writer (fsync before 202), the ownership-aware
suppression read, the severity-adjusted digest window (one issue per window), and
the P1 immediate path. 202/400/401/413/503 semantics copied from
pi-alert-receiver, because Alertmanager's retry behaviour is defined by them.
- config.TemplateKindOncall + laneForIssue routes marker ld:alert-digest to it
- R-28 oncall-window registered (paused dep:routine-executor, like R-25); R-10's
  stale dep re-pointed at 4.5c's disposition enum (the ledger is 4.5b's)
- identity registry: on-call active with four surface bindings, no cluster cred
- compiler roles extended with on-call; R-03's in-process pin re-pinned
HOMELAB-1847: fix(registry): reconcile the active-today header with the paused registrations
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m57s
86316feac5
The rebase onto epic/LD4 unions R-29's section with this branch's R-28 entry and
LD4.6's R-30 (HOMELAB-1690, which re-id'd its triage sink from R-28). #594/#592
headers counted R-28 among 'Active today', but R-28 and R-30 both ship paused for
dep:routine-executor (the same dependency R-25 carries). The header now reads
(5) and names R-28 (LD4.5b) and R-30 (LD4.6) beside R-25 in the paused sentence.
Merge pull request 'HOMELAB-1847: LD4.5b — alert intake: episode ledger + the ONE on-call sandbox' (#596) from plane/HOMELAB-1847-oncall into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
Alert Disposition Contract / alert-contract (pull_request) Failing after 6s
CI Review / helm-validate (pull_request) Successful in 7s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m50s
CI Review / dispatcher-test (pull_request) Successful in 3m38s
Lint & Validate / terraform-validate (pull_request) Successful in 3m37s
Build whisper-asr Image / build-and-push (pull_request) Failing after 6m42s
7496e07fe9
OQ-3's activation: the roundtable is the role whose Plane user must own the
meeting-record comment, so the record is attributed to the role that produced it
rather than to the architect (contract §8).

- example-registry.yaml: roundtable binds all four canonical surfaces with
  tokenRefs (an ACTIVE role must bind every surface, registry.go), and joins
  activeRoles. Its plane ref points at kv/app-credentials — the same object the
  gateway's plane-role-writers ExternalSecret seeds (OQ-2(a)).
- example-registry.yaml: the architect's plane ref is reconciled to the SAME
  kv/app-credentials object (OQ-2(a): the only agent-seedable credential path per
  HOMELAB-1711's AppRole ruling). One path, so the gateway's per-role resolution
  and the mounted secret cannot disagree.
- model.go: ActiveRolesInV1() gains roundtable (appended last; the list order is
  pinned against the example's declaration order).
- registry_test.go / surfaces_test.go: the pinned counts move (9 roles, 39
  entries, 10 chat accounts) and the roundtable's refs/activity are asserted.

Minimal and localized on purpose: three sibling builders are adding routines
registry entries in parallel branches, so this hunk touches only the identity
artifact and its two pinned tests.
The one authenticated hop by which a sandbox's product reaches Plane, plus the
process that serves it.

- gateway/api.go: Submission{Verb, Comment, Drafts} — deliberately NO project,
  state, assignee or target field (the ticket is the nonce's, a draft's state is
  the Backlog constant the watch never publishes). Strict JSON decoding refuses
  an unknown field at the boundary, so no request can smuggle a target or a
  state. Verb allowlist + role charter: the roundtable comments, the architect
  drafts, every other role writes nothing. Size caps. Receipt shape.
- gateway/nonce.go: HMAC-SHA256 over role||ticket||lane||expiry, minted by the
  dispatcher (min(claim deadline, now+15m)), presented as
  "Authorization: Bearer <nonce>", verified statelessly in constant time. The
  token is <base64url(payload)>.<base64url(mac)> — the brief's shorthand writes
  the MAC alone, which a verifier cannot recompute the claims from; the file
  documents the deviation. Fingerprint-for-ledger, Bearer-only scheme.
- gateway/plane_writer.go: the gateway's own narrow Plane client (Comment,
  CreateItem which resolves the project's Backlog state by name or group; no
  transition, no edit, no read on the hot path). Separate from claims.APIWriter
  on purpose: the dispatcher owns state/leases with one key, the gateway owns
  product writes with per-role keys.
- gateway/server.go: the request path (nonce → target assertion → strict decode
  → shape → charter → replay ledger → per-role token → write → receipt). The
  replay ledger is a real single-use guarantee for ONE replica (reservation
  taken before the write, released on failure so an outage does not burn the
  nonce) and the documented TOCTOU beyond that (OQ-4). A path-form asserted
  ticket must equal the nonce's, so a nonce cannot be retargeted.
- cmd/ld-result-gateway: env config (LD_SUBMIT_HMAC_KEY, PLANE_*, the
  registry-derived per-role LD_PLANE_TOKEN_<ROLE>), --check/--version, graceful
  shutdown, /healthz + /readyz. The token map is built FROM the registry's
  tokenRefs, so adding a role's credential is a registry entry plus a Secret key.
- Dockerfile: the gateway binary rides in the dispatcher image (the same
  HOMELAB-1688 precedent as routines): one module, one reviewed digest.

Tests (no sockets): every Verify-line-1 refusal is a named fixture test asserting
ZERO Plane calls (no nonce, forged nonce, expired nonce, wrong-ticket assertion,
unknown field, unchartered role), plus attribution, Backlog-only creation, replay
409, 503-with-the-missing-ref and nonce-release on outage.
Design brief R2, landed BEFORE the CNP step so the class egress rule is never the
first thing that depends on an unshipped upload path.

- spawn.Spec.SubmitToken: the driver-minted dispatch nonce. Nil means no gateway
  is wired, and the arm writes nothing extra — a lane that stays inert beats a
  lane holding a half-configured credential.
- The arm writes .sandbox-init/submit-token (constant path, outside ContextDir:
  the package directory is content-addressed and pin-verified, so a nonce inside
  it would break the pin) and then chmod 600 through the sandbox's own shell, the
  contract's recipe for the sandbox-key file (the MCP upload tool cannot carry a
  mode). This is the arm's FIRST auxiliary upload: until now it wrote only
  package sections. The git token (contract §2.6.4's other .sandbox-init file) is
  still specified-but-unshipped, and the same helper is what will land it.
- checkSubmitToken refuses a malformed nonce at preflight, before any MCP call
  (whitespace, non-printable bytes, oversize) — ErrInvalidSubmitToken. The arm
  does not parse the nonce: it holds no key, and a driver that could interpret
  its own nonce could mint a different one.
- SpawnEvidence gains SubmitTokenPath and SubmitTokenDigest (sha256 of the
  delivered nonce, which is the gateway's ledger fingerprint). The token VALUE is
  never recorded, and the log one-liner carries neither the value nor its digest.

Tests: byte-exact upload at the contract path, exactly one write and it is
outside the package dir, the chmod argv, evidence path+digest and the absence of
the token from the log line; optional-token; the five malformed classes refused
with ZERO MCP calls; and a failed nonce upload aborting+deleting the sandbox
(stage=write-submit-token).
The result-return path: a lane now ends when its run's bundle lands, and Plane
gets written as the ROLE through the gateway.

- cmd/dispatcher/resultwatch.go: the resultBundleWatcher. It probes
  .sandbox-state/agent/result.json with the sandbox's own `test -f` through
  execute_command — a shape the contract verifies (a non-zero exit is DATA, not a
  tool error), unlike download_file (where absence and a broken transport look
  alike) and unlike the MCP file_exists tool, whose response shape is not verified
  against the pinned server. On presence it reads, strictly decodes into
  teardown.Result, and returns LaneOutcome{Result, Contract}: teardown is
  unchanged. On the sandbox TTL with nothing, the OLD ending stands —
  ReasonNoResultSource, a FAILED run. A bundle that declares its OWN contract is
  refused at the decoder boundary, and a gateway-wired lane whose bundle has no
  product-receipt (or a receipt from another ticket/role/lane) is refused.
- contractFromPackage: the ResultContract comes from the compiled package's
  ticket-and-acceptance-criteria section (OQ-7(ii)) — a fenced ld-result-contract
  block of name: sha256:<hex> entries. A malformed declaration is a refusal
  BEFORE the claim; no declaration is the honest empty contract.
- pipeline.go: the lane's dispatch nonce is minted after the claim (expiry =
  min(the claim's lease deadline, now+15m)) and handed to spawn; the watcher
  receives the lane's expectations (ticket UUID, role, whether a nonce was
  delivered, the ticket's contract) through the optional laneExpecter interface,
  so every existing watcher and fixture keeps working unchanged. An A-E class lane
  gets no nonce and keeps exactly the reach it had before.
- gateway/lanes.go: the v1 lane→role binding (roundtable→roundtable,
  direction→architect). The compiled package's static role is a section-ordering
  input, NOT the attribution authority; an unknown lane is inert.
- app.go / main.go: the default watcher becomes the bundle watcher; Options gains
  SubmitHMACKey (read from LD_SUBMIT_HMAC_KEY), so the whole product path is
  inert until the operator flips it.
- spawn/fixture.go: the fixture now MODELS `test -f <path>` from its own file map
  (a fixture that answers "present" for every path would make every watcher test
  a lie); a scripted ExecResult still wins.

Tests (Verify lines 2 and 3, mechanized through a REAL gateway over httptest, the
real mint path and the real verifier): the roundtable record lands and the lane
ends on the bundle with disposition=completed / evidence.verified=true and NO
no_result; the architect drafts land in Backlog; a lane that submits nothing
still ends reason=no_result + disposition=failed with the nonce delivered; a
self-declared contract is refused; a bundle without product-receipt is refused; a
class lane gets no nonce. Plus the extraction matrix and the malformed-contract
refusal before the claim.

Honest limit carried in the code: the nonce lives 15 minutes (min(claim deadline,
now+15m) as designed), so a run longer than that gets a 401 and its lane closes
ReasonNoResult — a loud, named failure. Re-minting on the heartbeat (or a
per-lane nonce TTL) is the follow-up; minting a longer-lived nonce here would
silently widen the boundary.
HOMELAB-1859: docs(gateway): the result gateway's README and the dispatcher table row
Some checks failed
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 8s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Failing after 4m53s
c174597919
The split R5 asks for, stated where a reader will find it: the dispatcher owns
state/leases with one shared key, the gateway owns product writes with per-role
keys, and the sandbox holds only a dispatch nonce. Plus the API in one screen, the
honest limits (single-replica replay guarantee, the 15-minute submission window,
body_sha256 bound but not yet independently re-derived) and the operator commands
(--check, the no-nonce 401 probe).
HOMELAB-1859: fix(spawn): read the fixture's file map under its lock (race found by -race)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 3m32s
c0a9abddd6
The `test -f` probe held a reference to the inner file map and indexed it after
unlocking. That looks harmless and is not: the sandbox (another goroutine in a
lane test) writes into the same inner map, and Go maps are not safe for
concurrent access even through two references. `go test -race` caught it on the
first run of the lane test — which is exactly the property the race detector is
enforced for in this module's CI (HOMELAB-1816).

Also merged origin/epic/LD4 (HOMELAB-1850's routines phase census, #594) so the
PR's diff against the epic base is only this ticket's changes.
HOMELAB-1859: merge epic/LD4 into the gateway branch — union the pinned identity totals
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 3m5s
7b276a7e3b
origin/epic/LD4 moved after this branch was cut (HOMELAB-1690's friction package,
#592), which also activated LD4.5b's `on-call` role. The merge conflicts in five
files, all of them the SAME additive edit on both sides, so each is resolved as a
union rather than by preferring one side:

- identity/example-registry.yaml + model.go: `activeRoles` and
  ActiveRolesInV1() now carry BOTH LD4.5b's `on-call` and this ticket's
  `roundtable` (8 active roles). The registry test compares the file's
  declaration order with the Go list, so both are ordered on-call then
  roundtable.
- identity/registry_test.go + surfaces_test.go: the pinned totals move to their
  true merged values — 10 roles described (8 active + scout + el-director),
  43 identity entries (9 full four-surface sets + sendrast + el-director's 3)
  and 11 chat accounts.
- Dockerfile: FOUR binaries now (dispatcher, routines, alertintake from
  HOMELAB-1847, ld-result-gateway from this ticket), each built and copied; the
  header comment and the EXPOSE line union both sides' reasons.

No product code was changed by the resolution: the gateway, the lane watcher,
the spawn nonce upload and the identity activation are untouched.

go vet ./... and go test -race ./... are green over the merged tree, and
`routines -check` accepts the merged registry (27 routines: 5 active, 22 paused).
Merge pull request 'HOMELAB-1859: LD4 slot 6 — sandbox class egress + result-return path (the result gateway)' (#597) from plane/HOMELAB-1859-gateway into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Failing after 7s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 3s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 22s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 1m3s
CI Review / dispatcher-test (pull_request) Successful in 2m26s
Lint & Validate / terraform-validate (pull_request) Successful in 2m56s
Build whisper-asr Image / build-and-push (pull_request) Failing after 6m47s
7366125b1d
New lab-director/dispatcher/fitness/: the deterministic, zero-LLM R-27
maintainability-fitness census (LD4.11) plus its reviewed data file.

- types/loader: window rules (weekly, activity floor), config-keyed T2
  rehearsal budget (dispatch OFF, worker tier, report-only), neverAGate
  citation, generated-file exclusions, probe catalogue; a strict loader
  refusing unknown/contradictory fields with a line number
- churn: one --numstat/--name-status git walk, spread median/p90/max,
  churn share, birth/revert rates, excluded share printed
- gitstate: shallow clones refused by default (the measured
  sendrast-web falsification); --allow-shallow downgrades to
  truncated-window and un-ranks
- coupling: declared territory-map section (pinned digest) -> co-change
  communities -> unpredicted; never directory depth, never primary
- flakiness: job re-run flips from /actions/tasks (per-test is not
  reachable at Forgejo 14.0.3), bounded paged live adapter, recorded
  dumps for offline re-derivation
- probe: ld.fitness.probe/v1 wire shape on the existing marker grammar,
  prediction precedence declared->derived->unpredicted, measurement
  refused without an evidence address, emission through
  director.PlaneSink; unmeasured label honest until the executor lands
- gate: AttemptGate/ReasonFitnessIsCareDial — the only promotion path,
  and it always refuses; ledgered 3/3 self-test line
- census/digest: ranking by the declared primary key, calibration pair
  vs the independent hygiene order (escalation, never a re-tune),
  change-friction slope with insufficient-series honesty, observer cost

Co-authored-by: pi fleet <fleet@aaron.reynoza.org>
- churn arithmetic over a hand-computed recorded dump
- glob matcher + the loader's refusal of a match-everything exclusion
- loader refusals: unknown field, non-weekly period, gate authority,
  unbudgeted live rehearsal, catalogue duplicate, credential value
- shallow detection on a real --depth 1 clone; refusal + downgrade
- coupling: declared map, co-change communities, unpredicted, exclusions
- flake grouping (same-sha flips), insufficient-history floors, the live
  adapter's page discipline (HOMELAB-1736) and its loud budget stop
- probe wire shape, marker grammar, prose-only-evidence refusal, AC drift
  delta, emission through director.PlaneSink
- never-a-gate: 9/9 (surface x metric) refusals, 3/3 ledgered self-test,
  digest gate-vocabulary assertion
- census end-to-end determinism (byte identity), shallow refusal, activity
  floor, recorded-recording ordering test
- cmd/fitness: the runnable in-process path (mirrors cmd/readiness)

Co-authored-by: pi fleet <fleet@aaron.reynoza.org>
The first live recording exposed a real bug: `--numstat` and
`--name-status` are both output-format flags and the LAST wins, so the
walk carried statuses and ZERO changed lines. The walk is now two bounded
git invocations merged by (sha, path), with a regression test
(TestParseGitLogRecordingMergesTheTwoWalks) and a two-section recording
format.

Also: the naive top-level-directory coupling proxy is now REPORTED beside
the territory measurement (never ranked) so any inversion is visible; the
loader refuses a `test` flake level (per-test is unreachable at Forgejo
14.0.3); derived coupling ranks whenever one stable community exists.

First weekly run (window 2026-W40, exit 0) — committed evidence under
fitness/testdata/calibration/:
- api-sendrast  @ 9cc2ee366c4e: 100 commits, spread 2/7/14, changed lines
  28811, generated-file excluded share 0.4716, coupling 0.54 (derived)
- sendrast-web @ bee874cee4cf: 100 commits, spread 2/8/64, coupling 0.55,
  flakes 1.0/100 (2 same-sha flip groups)
- calibration AGREES on the declared primary key (api 7 < web 8)
- slope: insufficient-series (window one, sample printed, no fake slope)
- never-a-gate self-test: 3/3 refused

sendrast-web's clone was shallow (341 commits) and was unshallowed once
to 509 commits; the run records both the fix and the refusal default.

Co-authored-by: pi fleet <fleet@aaron.reynoza.org>
HOMELAB-1731: feat(routines): register R-27 weekly/ACTIVE/script + the never-a-gate policy line
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m41s
0/0 projects policies checked successfully.
aa6d73f1f7
Registry (R-27 entry ONLY):
- weekly per the operator ruling of 2026-09-28 (contract Amendment 3),
  ACTIVE because the mechanism (fitness/*.go) ships in this build,
  kind: script (adjudicated 2026-10-03), pin on fitness/census.go (a NEW
  file, so no sha256-pinned sibling is touched)
- catchUpPolicy false (churn backfills, the CI-flake feed does not — a
  mixed series is worse than a gap); targets: the two ranked Sendrast
  repos; reviewBy 2026-12-31 (the calibration checkpoint)
- emitsWork false for window one, with the reason: probe tickets land in
  the `Backlog` state with the `fitness-probe` label and are never
  worked, so nothing they emit can re-trigger another routine yet
- reserved-ids header comment swept (R-23/R-26 remain), R-29's
  "why not R-27" note updated, active list now names R-27
- P-ROUT-013 added to policy.go AND policy.md (the parsed-back test holds
  the pair): a change-friction number is a care-dial input and is never
  rendered as a gate

Fixture + tests:
- testdata/registry-4x-shapes.yaml: R-27 quarterly/agent -> weekly/script
  with the ruling named in the comment
- TestSchemaAccommodatesHosted468Shapes now asserts the SHAPE property
  (coarse period + emitsWork + one edge), not the falsified quarterly
  value
- TestR27CadenceFollowsTheOperatorRuling (fixture AND registry: week, 7d
  period, 7d cron), TestR27CadenceAgreesBetweenFixtureAndRegistry,
  TestRegistryHoldsR27AsACareDialScript (pin, care-dial, reviewBy,
  targets), TestRegistryRefusesR27WithGateAuthority
- fitness: TestPolicyLineReferenceMatchesTheBook cross-checks the
  restated P-ROUT-013 id against the book

Co-authored-by: pi fleet <fleet@aaron.reynoza.org>
Merge pull request 'HOMELAB-1731: LD4.11 · maintainability fitness routine (weekly, Sendrast first)' (#598) from plane/HOMELAB-1731-fitness into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Failing after 10s
CI Review / helm-validate (pull_request) Successful in 10s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 7s
Lint & Validate / yaml-lint (pull_request) Successful in 15s
CI Review / alert-receiver-test (pull_request) Successful in 23s
Build whisper-asr Image / build-and-push (pull_request) Failing after 1m10s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m22s
CI Review / dispatcher-test (pull_request) Successful in 2m50s
Lint & Validate / terraform-validate (pull_request) Successful in 3m7s
43ae78e5e7
LD4.7's pure half: the signal schema, the gauge burn arithmetic over
probe_success, the priority ladder, the planner's ErrUncitedSignal seam, the
digest hash and CitesDigest.

- burn.go owns avg_over_time SEMANTICS as code (a 0/1 gauge is a MEAN, not a
  rate()), half-open windows, ErrNoSamples for an empty window (a coverage gap
  must never read as a quiet window), and budget_consumed_ratio.
- priority.go: Propose takes the citation as a PARAMETER and refuses one that is
  not measured, so there is no code path that yields a priority without
  evidence. Retiring surfaces are forced to P4 with the retire ticket as the
  citation; an edge-observed fault caps at P3.
- plan.go reuses 4.1's WindowKey/Plan shape and refuses the whole window on an
  unmeasured group (batch-wide, the 4.1 ruling).
- digest.go renders the item body with signal_digest_hash=..., cites.go decides
  whether a draft quotes a hash the ledger actually holds (Verify line 3).
- direction.Entry gains ONE additive omitempty DigestHash field (the transition
  4.1's KindDigest comment reserves for 4.7; the 4.8 Run.Target pattern).
- Policy as data: lab-director/signals/{slo-policy,priority-ladder}.yaml
  (target 0.99, retire surfaces flagged with SEND-546, fixture SLO flagged).
The I/O half: one range query per surface, the burn in Go, one direction item per
group above P4 into 4.1's direction sink, and the window row carrying the digest
hash.

- prom.go: the live Prometheus client (matrix-only; a vector/scalar answer to a
  range query is the wrong query and is refused), ProbeExpr so the citation's ref
  IS the query that ran, and an EMPTY result returned empty (ComputeBurn refuses
  it — a coverage gap is never a healthy window).
- logsource.go: the Loki coverage probe + the pure z-score anomaly planner, and
  ErrNoLogCoverage BY NAME when the only streams are the canary. No live
  application logs are shipped: the leg claims nothing about anomalies.
- plane.go: the sole writer for the application-signal class, idempotent on
  DedupKey by a marker read (the sink is the durable half of the dedup), labels
  and state resolved by name with a named refusal when either is missing.
- server.go: 4.1's arm contract (Now/After seams, sync-then-interval, every
  failure logged and skipped). The window guard runs BEFORE the queries: an
  already-open window issues no query at all. Counters for passes, queries,
  source failures, groups, items, recordals, refusals, no-coverage.
- The log-coverage gap and every refusal are LEDGER ROWS, so the absence of
  evidence is visible in production rather than inferred from a test.
- routines/registry.yaml: R-23 signals-from-production, ACTIVE, intervalSeconds
  300, go:in-process pinned on signalintake/plan.go (the pure half: the pin is
  worth a sha256 where the arithmetic and the refusals live), consumer
  architect, effects class A reads [prometheus, loki, plane], emitsWork with
  emits: [R-11]. The reserved-ids header is RECONCILED: R-23's event form was
  aspirational prose, no producer emits signal.production, and driving it off
  Alertmanager would route an application signal through the dispatch altitude
  (contract §1 Amendment 2 keeps it at proposal altitude).
- testdata/registry-4x-shapes.yaml: the R-23 row stays as the SCHEMA proof, with
  a comment saying it is superseded in TRIGGER, not in shape (registry_test's
  shape assertion still passes and is untouched).
- config: a signals block (URLs, policy paths, interval, lookback, logCoverage,
  lokiMatch, labels, state) + arms.signals (OFF by default: the arm needs one
  additive CNP egress rule, a dangerous-class change). Defaults and the example
  schema updated; two tests pin them.
- cmd/dispatcher: arms_signals.go builds the arm (Prom/Loki/Plane sink + the
  direction ledger), registered as a NON-FATAL runner so a Prometheus outage is a
  signals outage, not a dispatch outage.
Two corrections found while writing the prod half -- both are cases where the code
and the cluster would have disagreed:

- The fixtures Probe uses the EXISTING tcp_connect blackbox module (every
  http_edge_* module asserts TLS and would fail against an in-cluster plain-HTTP
  Service; adding an in-cluster HTTP module would be a write into the protected
  monitoring namespace). So the fixture surface id is the TCP target string
  "sendrast-test-canary.sendrast-test.svc.cluster.local:8080", not an http URL --
  otherwise the collector's query would select a series that does not exist and the
  fixture would be refused as a coverage gap. probe_success and the burn arithmetic
  are identical to production's.
- The policy documents are FLAT ConfigMap keys (/etc/lab-director/slo-policy.yaml,
  /etc/lab-director/priority-ladder.yaml), the identity-registry.yaml / policy.md
  precedent: a ConfigMap cannot carry a directory, so a signals/ subdirectory would
  have been a mount change rather than a key. Defaults, the example schema and the
  deployment's config all agree now.

Verified end to end against the DEPLOYMENT's own document (a throwaway check, not
committed): the prod ConfigMap's config.yaml loads under the strict decoder with
arms.signals on, and its embedded slo-policy.yaml + priority-ladder.yaml load
through this package's loaders including the ladder-vs-SLO band check.
CI run #1806 failed on dispatcher-test and the failure was real: the job mounts
ONLY lab-director/dispatcher into the golang container (`-v $PWD/lab-director/dispatcher:/src`),
so `../../signals/slo-policy.yaml` resolved to a path outside the mount and the
policy tests could not read the shipped documents. Reproduced locally by copying
just the dispatcher tree to an empty directory (the same shape as the mount):
the file is absent, the tests fail.

So the policy lives at lab-director/dispatcher/signals/{slo-policy,priority-ladder}.yaml
plus its README. Nothing about the design changes -- policy as data, read by R-23,
validated by the loader and pinned by tests -- but "adjacent to the dispatcher"
now means INSIDE the module, which is the only place the CI that must prove it
parses can see. A test that could not run in CI would have been a test that does
not exist.

Also aligns signalintake's own default policy paths with the deployment's flat
ConfigMap mount (/etc/lab-director/*.yaml), so the arm's defaults and the config
package's defaults are the same two paths.
HOMELAB-1728: fix(signals): emit the item's own group and write the marker description
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 8s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 3m8s
b8a7234d24
Merge pull request 'HOMELAB-1728: LD4.7 — signals-from-production intake (collector arm, SLO as data, burn in Go)' (#599) from plane/HOMELAB-1728-signals into epic/LD4
Some checks failed
Alert Disposition Contract / alert-contract (pull_request) Failing after 6s
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 4s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 13s
CI Review / alert-receiver-test (pull_request) Successful in 22s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m6s
CI Review / dispatcher-test (pull_request) Successful in 3m6s
Lint & Validate / terraform-validate (pull_request) Successful in 3m2s
Build whisper-asr Image / build-and-push (pull_request) Failing after 8m38s
a3d3b5a049
Leg B's evidence query (design brief §3.1): a firing requires the full
conjunction (message + role user + first block text + text opens with
<skill name=... location=...>), so the 25 assistant records and 8 toolResult
records that also contain `<skill ` are excluded STRUCTURALLY rather than by a
phrase list. A correction is the next operator turn (all-text, not a firing),
with the measured usage between the two lines as the episode's cost. Scope
discriminators: parentSession (a dispatch brief is not a correction), a
REQUIRED generation cutoff, and location routing to probe P-A1.
Probe P-A1 distinguishes a deleted referent (obsolescence, naming the deleting
commit from git log --diff-filter=D) from a token that never existed (a typo)
and from a vanished directory out of any repo (the live fleet-ops case). The
resolver is a seam: GitResolver for a real tree, MapResolver for fixtures.

Retirement drafts land the adjudicated decisions: candidateRev is the empty
blob (a deletion is a pin), the exposure is a ROSTER shadow (Binding gains
rosterExclude - decision D-3), and the proposal cites the probe AND the merge
(P-L7-011). Planning is ranked and budgeted, and what it holds back is printed
as deferred.

friction/shadow.go: a persisted REVERTED binding (sample: 0 + verdictAt) is now
loadable - the 4.6 handoff defect - while sample 0 with no verdict is still
refused.
P-A2 classifies the skill inventory into rosterless (retire), referenced-but-
unfired (review, not retirement: the honest confound) and body-duplicate pairs
(superseded, leg B's judgment). P-A3 grades a Context Block doc's claims against
history since its own last_verified and reports a no-baseline FAULT rather than
a silent pass. P-A4 generalises policy.md's rule+wiring shape: an unwired line
and a wiring row that cites a moved artefact are both rule drift.

The corpus is injected as SourceFile values so every probe is fixture-driven;
InventorySkills reports a missing root as a fault, because an empty scan and a
missing corpus must not look the same.
Bundle, not mount (adjudicated): probe candidates, episodes, the skill corpus
WITH bodies, the lore index and the budget render into one directory with a
manifest of digests; drafts come back as proposals.jsonl + episode verdicts,
and ReadProposalDrafts REFUSES any edge that is not proposed and any actor that
is not routine:R-26, so the observer can neither accept, apply nor revert.

The observer spec is zero-keys as a validated property: no keys, no service
account, no Plane/NATS/Outline egress, ephemeral, read-only, on an ACTIVE v1
role (architect - scout usage, not a new role).

The controller measures and RECOMMENDS. It refuses to widen on low signal and
CheckRateAuthority refuses an R-26-authored proposal against the registry file
that carries its own sampling.rate (self-rate-change is refused, per the
adjudication), with RateChangeTransition only authorable by agent:architect.

The digest refuses to render without the run row's cost line; the drain is
idempotent by byte digest and covers spool + proposals.jsonl + verdicts (the
4.6 handoff).
V1: a real deleted path in a real fixture repo -> one proposed retirement whose
baseRev is the HEAD blob pin, whose candidateRev is the empty blob, whose
evidence names probe:P-A1 and the deleting commit, and whose roster shadow
binding validates against the landed schema; a second identical run files
nothing and PRINTS the deferral.

V2: the episode fixture (firing + operator turn, with an assistant quote, a
toolResult and a custom_message as false fires) samples to one episode, the
zero-keys spec validates, and the observer's draft lands as one proposed
skill-class proposal quoting the correction and citing both lines.

V3: the digest's cost line equals the cost re-derived from the session by
RoutineRun/ReadSessionFile, and a run with no measured spend REFUSES to render.
R-26 (skill-lore-health-scan) is registered PAUSED, dep:routine-executor,
dep:LD4.10-observer-bundle: the mechanism ships in this ticket and the Executor
that would schedule it does not, so an active entry would be a ledger
obligation nothing can meet. runsAs: architect (the only active role that fits)
with scout-shaped usage, sampling.rate 0.25 as the CEILING, and
observerCostVisible: true as a receipt requirement.

P-L7-001..014 land in routines/policy.md AND in Book(), so the parity test
(TestPolicyBookMatchesDoc) holds the L7 block the way it holds P-ROUT-001..012;
the wiring table gains the L7 rows the drift probe reads. roles.json includes
the friction unit for both pi-running roles (4.6 handoff): without it the
observer's own friction surface writes nothing.
T4/T10/T11/T13/T14/T15 as asserted properties, plus the drain's idempotence and
the bundle's payload law. Integration against the real corpora: the live
fleet-ops deleted-path hit is re-derived (the design brief's 0.3 claim, now an
assertion), the firing census excludes the false fires on real records
(150-file slice of 924: 158 raw "<skill " occurrences vs 0 detector firings at
the 2026-09-20 cutoff; the full corpus yields 3), and the REAL
routines/policy.md parses with every P-L7 row wired.

The last of those found a real pre-existing hit: P-ROUT-006's wiring row cited
routines/attribution.go, which does not exist (attribution lives in
routines/sessions.go). Fixed in the same commit, because a known-false wiring
row is rule drift and the probe would report it forever.

Also: ReadSessionTurns gets a 64 MiB line buffer (one real transcript line is
over 8 MiB) and the census counts unreadable files instead of failing the whole
read.
The Go half of the 4.6 handoff defect the pi-extensions fix closes: the
`/friction` command stamps actor.kind human (id operator) with NO sandbox
binding, and the Go decode path must accept what the capture path writes.
Stamp derives `unbound` for exactly that case, so P-ROUT-006 keeps the work
visible and nothing about the human actor is special-cased.

It is also the fixture R-26's episode leg reads for its third correction source
(a human-actor ld.friction/v1 line on where.surface: skill), so the join key
between the operator's filing and the observer's proposal is asserted on the Go
side rather than assumed.

Cross-repo: Homelab/pi-extensions#63 (the TS one-liner + its vitest assertion).
The 4.6 handoff item, closed as a mechanism rather than a note.
`TeardownSpec.Drain` + `DrainPaths` read the sandbox's pending ledger files (the
friction spool, the observer's proposal drafts) through the SAME MCP client the
sandbox was spawned with, BEFORE `MCP.Delete`, and hand their BYTES to a sink.
Teardown gains no ledger dependency: it reads, the dispatcher appends.

Three properties, all asserted: every handed-over file is a report row carrying
the digest of the bytes teardown actually read (the artifact rows' rule);
a drain fault is RECORDED and non-fatal, because a cleanup that stops halfway
leaves an orphan and a lost spool line is recoverable from the session JSONL;
and the drain is opt-in, so every existing caller's MCP op sequence is
unchanged (the happy-path test still asserts Reads-then-Delete).

`skillhealth.TeardownDrain` adapts the dispatcher's writers to the sink and
keeps leg B's boundaries on the byte path: ParseProposalLines refuses any edge
but `proposed` and any actor but `routine:R-26`, the friction spool goes to
`fleet.ledger.friction.line`, the verdict file is held and counted, and a
retried drain dedups by byte digest.
The first real run reported 293 P-A1 candidates for one real hit. Five fixes,
each with the reasoning recorded in the code, took it to 21 candidates and 2
filed retirements:

* a skill's REPO-RELATIVE path and its WORKING TREE ROOT must come from the same
  pair (SkillDir gains RepoRoot): the first run joined a repo-relative path onto
  the skills directory and produced paths that existed nowhere;
* an absolute document path resolves its relative references against its OWN
  directory, not against a root joined twice;
* a repo-relative token may resolve against any KNOWN WORKING TREE (RootProvider),
  because the corpora live outside any one repo: `memory/fleet-model-routing.md`
  cited from a user-scoped skill is the HOMELAB repo's memory dir;
* placeholder paths (`path/to/file.ext`, `dir/source.json`, `YYYY-MM-DD_…`) are
  not references at all, and a bare relative path out of any repo is relative to
  a project the probe cannot name — counted as out of scope, never reported;
* a vanished ROOT-LEVEL directory (`/_helpers/…`, `/config/config.xml`) is a
  review candidate, not a retirement: the derivation cannot name what removed
  the referent, and P-L7-011 says an obsolescence claim that cannot is an
  opinion.

Also: PlanProposals now files ONE proposal per target, aggregating the target's
claims into the rationale and the friction refs — a file with nine deleted
referents is one retirement, not nine — and ranks by probe order then claim
count. P-A2 reports a FAULT instead of 24 retirements when no reference surface
was supplied (a rosterless verdict without a reference scan is not evidence),
and reads the roster document's own vocabulary when given one.
HOMELAB-1727: docs(registry): R-26 leaves the reserved list
All checks were successful
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Successful in 2m18s
a561d23b5b
Merge pull request 'HOMELAB-1727: LD4.10 — skill &amp; lore health scan (obsolescence probes + sampled observer)' (#600) from plane/HOMELAB-1727-skillhealth into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Failing after 6s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Build whisper-asr Image / build-and-push (pull_request) Failing after 1m6s
CI Review / dispatcher-test (pull_request) Successful in 2m54s
Build Pi Sandbox Image / build-and-push (pull_request) Failing after 2m53s
Lint & Validate / terraform-validate (pull_request) Successful in 3m1s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 3m4s
ea2bafebfb
HOMELAB-1850: fix(tempo): span-metrics histogram buckets + the phase dimension (OPERATOR REVIEW)
All checks were successful
0/0 projects applied successfully.
CI Review / helm-validate (pull_request) Successful in 13s
CI Review / receiver-test (pull_request) Successful in 10s
CI Review / alert-receiver-test (pull_request) Successful in 24s
CI Review / dispatcher-test (pull_request) Successful in 4m10s
ebdb5266bf
CLASS: E-adj / DANGEROUS. `monitoring` is a protected namespace (AGENTS.md), so
this commit must not auto-merge and is deliberately kept SEPARATE from the
routines/ and pi-extensions/ legs of HOMELAB-1850 so the class boundary is
visible on the board. Operator review required.

1. HOMELAB-1593 — histogram_buckets. The premise measured worse than stated:
   Tempo's default ladder tops out at 16.384s while subagent runs have a measured
   MEAN of ~1640s, so p99 was not coarse but CONSTANT (16.38s, one saturated
   bucket) and stall-outlier detection could not work, because the stall
   signature IS the top bucket. The ladder added here keeps the sub-second regime
   (tool spans run 2ms-64ms) and extends to 1h for agent spans — the ladder is
   GLOBAL to the metrics generator, not per span name, so dropping the small
   buckets would coarsen every `tool:*` span and both Pi dashboards.

2. HOMELAB-1850 — `phase` in `span_metrics.dimensions`, so the call/duration side
   of the Costs pane can be split by stage the way the token side already is.
   `phase` is a closed enum (build|review|verify|unphased; <= 4 values), so its
   cardinality cost is bounded by the existing dimension set.

Not taken here, deliberately: the `sandbox_env` dimension (LD4.4's key). It
multiplies with subagent.type x llm.model and the brief makes it a MEASURED
decision (R4) — enable it once the series count after this change is known.

Verified: `helm template grafana/tempo --version 1.24.2 -f <values>` renders both
keys at `metrics_generator.processor.span_metrics.{dimensions,histogram_buckets}`
(the chart passes `processor` through verbatim — chart 1.24.2 values lines 53-63),
i.e. the Tempo 2.9 span-metrics schema, so the keys are the chart's own and not a
guess.

Rollout: infra-core `epic/LD4` → `live` → prod `apps/tempo.yaml` ($values
reference, targetRevision: live) → ArgoCD sync. The metrics-generator restarts
with the change and the new ladder starts fresh (a step, not a backfill); the
default-ladder series age out of Prometheus' 10.03d retention.
Merge pull request 'HOMELAB-1850: fix(tempo): span-metrics histogram buckets + phase dimension — OPERATOR REVIEW (protected ns)' (#595) from plane/HOMELAB-1850-tempo into epic/LD4
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 4s
Alert Disposition Contract / alert-contract (pull_request) Failing after 7s
CI Review / helm-validate (pull_request) Successful in 9s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 14s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Build Pi Sandbox Image / build-and-push (pull_request) Failing after 1m54s
CI Review / dispatcher-test (pull_request) Successful in 2m42s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m54s
Lint & Validate / terraform-validate (pull_request) Successful in 3m20s
Build whisper-asr Image / build-and-push (pull_request) Failing after 7m47s
74029341e8
The whisper-asr smoke test failed as "/health on :8171 never answered - the
readiness probe would never pass" (run 1832), which named the wrong fault. The
container was already dead: `docker run -d --rm` printed an id, the process died
on exec, the auto-remove deleted the corpse, the loop's `docker exec` error went
to /dev/null, and the diagnostic `docker logs` answered "No such container".

Root cause, reproduced locally against whisper.cpp v1.9.4 with the Dockerfile's own
cmake flags: upstream configures BUILD_SHARED_LIBS=ON on Linux, so
build/bin/whisper-server comes out linked against libwhisper.so.1 + libggml*.so
with a RUNPATH into the build tree. The runtime stage copies ONE file, so the
binary dies with

  error while loading shared libraries: libwhisper.so.1: cannot open shared object
  file: No such file or directory   (exit 127)

before it ever binds :8171. Verified with a local build of both variants (shared:
6 NEEDED entries incl. libwhisper/libggml, exec 127 once the build tree is out of
RUNPATH; static: only libgomp/libstdc++/libm/libgcc_s/libc, binds in ~2s with the
real 574 MB model and returns an SRT from POST /inference for the smoke test's own
generated WAV).

- Dockerfile: -DBUILD_SHARED_LIBS=OFF, and a build-time assertion that refuses a
  binary needing libwhisper/libggml beside it. Runtime deps stay libgomp1 + the
  base image (libstdc++6 confirmed present in debian:bookworm-slim).
- Workflow: no `--rm` (so a crash keeps its log), an explicit liveness check that
  fails in <=1s with the real error, and a 180s ready wait for the honest slow
  case. The model digest is pinned in the workflow too, asserted against the
  Dockerfile ARG, and re-checked on the artifact inside the runtime image.
- README: the static-link requirement and its failure mode.
Run 1833 refused PR #591 (epic/LD4 -> live) with ALERT-CONTRACT-001 + 004 on
PrometheusStorageFilling in core/manifests/monitoring/rules/observability-alerts.yaml
- a rule the epic branch never touched. The gate read its base from the base ref's
TIP, so on a long-lived epic branch (90 commits behind live here) every rule live
had advanced since the fork point looked "modified by this change"; it reported the
epic's stale copy of a rule whose MERGE RESULT is live's own version (confirmed
with `git merge-tree`: the merge keeps live's improved expression).

"This change" is what `git diff base...HEAD` shows a reviewer, so the reference is
the merge base. On a branch forked from the base tip this is identical to before;
on a stale epic it judges the branch's own diff (24 changed rules, all annotated).
The ratchet is read at the same commit. A missing merge base (shallow clone,
unrelated histories) falls back to the tip and now says so in the header line.

No rule's severity or disposition is touched. New test builds a real repo with a
fork point, an epic-only added rule and a later base-side edit to a legacy rule:
the tip comparison still reproduces both findings, the gate as invoked passes and
reports `changed=1`. Only the pure-function tests existed before, which is why the
PR shape went unexercised; 50 tests pass.
HOMELAB-1727: fix(agent-sandbox): the render tests expect the friction include
Some checks failed
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
CI Review / receiver-test (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Successful in 8s
Lint & Validate / shellcheck (pull_request) Successful in 6s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
CI Review / alert-receiver-test (pull_request) Successful in 21s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 1m29s
CI Review / dispatcher-test (pull_request) Successful in 2m42s
Lint & Validate / terraform-validate (pull_request) Failing after 2m59s
Build Pi Sandbox Image / build-and-push (pull_request) Has been cancelled
Build whisper-asr Image / build-and-push (pull_request) Has been cancelled
f39221c093
Run 1831's Pi Sandbox build failed 2 of 60 sandbox-init tests: render.test.mjs
pinned `builder-core.include == []` and `light`'s missing units as `["plane"]`.
Both predate 15c22f5 (LD4.10), which deliberately added `friction` to both
pi-running roles - R-26's observer files its ld.friction/v1 line through it, and an
include the package did not ship is a warning, not a failure. The tests were the
stale side, not roles.json.

- "a role with no shipped extension": missing units are now `["plane","friction"]`,
  and both warnings are asserted (the package does not ship friction until
  HOMELAB-1798 compiles it).
- "roles.json is internally consistent": `builder-core.include` is `["friction"]`,
  plus an invariant that every role includes and never forbids `friction`, so the
  next edit to the mapping cannot silently drop it.

60/60 pass locally (run-tests.sh floor 60).
HOMELAB-1686: fix(ci): assert the static link with base-image tools, not readelf
Some checks failed
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
Lint & Validate / shellcheck (pull_request) Successful in 7s
Alert Disposition Contract / alert-contract (pull_request) Successful in 9s
Lint & Validate / yaml-lint (pull_request) Successful in 13s
CI Review / alert-receiver-test (pull_request) Successful in 23s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 2m31s
CI Review / dispatcher-test (pull_request) Successful in 3m0s
Build whisper-asr Image / build-and-push (pull_request) Failing after 3m48s
Build Pi Sandbox Image / build-and-push (pull_request) Successful in 4m36s
Lint & Validate / terraform-validate (pull_request) Failing after 4m42s
cfd8cd3db0
The runtime-stage precondition is "the build emitted no shared whisper/ggml
objects to leave behind", so assert exactly that with `find` (present in
debian:bookworm-slim, verified by exporting the base image's file list) instead of
`readelf`, which only exists once build-essential pulls in binutils — an assertion
that can fail because of the toolchain is worse than the fault it prevents.
Verified: fires on a shared build tree, passes on the static one.
HOMELAB-1686: fix(whisper-asr): set the model's mode in the download stage, not a second layer
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 6s
Alert Disposition Contract / alert-contract (pull_request) Successful in 7s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
Lint & Validate / shellcheck (pull_request) Successful in 6s
Lint & Validate / yaml-lint (pull_request) Successful in 12s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 2m32s
Lint & Validate / terraform-validate (pull_request) Failing after 2m45s
Build whisper-asr Image / build-and-push (pull_request) Successful in 2m54s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 4m8s
Build Pi Sandbox Image / build-and-push (pull_request) Successful in 4m33s
fb858d34b2
With the static build in place the smoke test passed for the first time (/health
answered 200 after 2s, POST /inference returned an SRT, the weights verified at the
pinned sha256 inside the runtime image) and the leg then failed where it had never
reached before: the 1 GB image-size gate, at 1.16 GB.

Root cause: `RUN chmod 0444 /models/...bin` after the COPY of a 574 MB file. A
metadata-only edit in a later layer still stores the file's content in that layer,
so the image carried the weights twice. 1241047892 bytes - 574041195 (one copy) =
667 MB, which is the base image + curl/ca-certificates/libgomp1 + the 3.4 MB static
binary + the model — the arithmetic identifies the duplicate exactly.

The mode now travels with the file: `chmod 0444` joins the download+verify RUN in
the model stage (a layer that is discarded anyway) and COPY preserves it, so the
runtime stage goes back to one copy and no metadata layer. The smoke test asserts
`stat -c %a` = 444 on the artifact, so the property is verified rather than assumed
(stat is coreutils, present in the base image).
HOMELAB-1686: fix(ci): give the whisper smoke container a per-run name
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 5s
Alert Disposition Contract / alert-contract (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 7s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 12s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 8s
CI Review / alert-receiver-test (pull_request) Successful in 21s
Build Pi Sandbox Image / build-and-push (pull_request) Successful in 2m36s
CI Review / dispatcher-test (pull_request) Successful in 2m48s
Lint & Validate / terraform-validate (pull_request) Failing after 4m1s
Build whisper-asr Image / build-and-push (pull_request) Successful in 4m56s
b74753c997
The smoke container no longer uses --rm (so a crash keeps its log), which means a
run killed mid-test can leave it behind: the EXIT trap cannot fire if the runner
process is stopped, and this runner's docker daemon is shared across jobs (:host
dind). A fixed name would then fail every later run with "container name
whisper-smoke is already in use" — a stale corpse that reads as a config error. Name
it per run (whisper-smoke-<short-sha>) instead; a leaked container is then inert.
HOMELAB-1713: ci: retrigger (terraform-validate provider-download flake)
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
Alert Disposition Contract / alert-contract (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 7s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 12s
Build Pi Sandbox Image / build-and-push (pull_request) Successful in 15s
CI Review / alert-receiver-test (pull_request) Successful in 21s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 1m34s
CI Review / dispatcher-test (pull_request) Successful in 2m43s
Lint & Validate / terraform-validate (pull_request) Failing after 4m8s
Build whisper-asr Image / build-and-push (pull_request) Has been cancelled
51b62d37bb
HOMELAB-1713: ci(lint): retry terraform init, keep stderr visible (runner provider-download flakiness)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 11s
Alert Disposition Contract / alert-contract (pull_request) Successful in 15s
CI Review / helm-validate (pull_request) Successful in 16s
Build Lab Director Dispatcher Image / preflight (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 8s
CI Review / alert-receiver-test (pull_request) Successful in 28s
Lint & Validate / yaml-lint (pull_request) Successful in 17s
Build Pi Sandbox Image / build-and-push (pull_request) Successful in 19s
Build Lab Director Dispatcher Image / build-and-push (pull_request) Successful in 13s
Build whisper-asr Image / build-and-push (pull_request) Successful in 58s
CI Review / dispatcher-test (pull_request) Successful in 3m37s
Lint & Validate / terraform-validate (pull_request) Successful in 4m4s
3514b82d4f
Author
Owner

Review — 3514b82

verdict: pass — integration review of the epic→live PR. Every constituent PR was independently reviewed by a fresh-context landing worker with an after-head verdict and merged CI-green (#567, #568, #569, #573, #575, #576, #577, #592, #594, #596, #597, #598, #599, #600 + pi-extensions #57/#60/#61/#63); the PR body inventories the stack, the OFF-by-default arm postures, and the activation sequence. Delta since the last constituent merge: three CI-leg fixes with diagnosed root causes (whisper static-link/exit-127, the alert-contract gate's merge-base bug, sandbox-init test pins), a terraform-init retry wrapper, and retrigger commits — all verified green on this head (CI run set on 3514b82: all jobs success). No blockers. Deployment risk posture: every new arm ships OFF; the rollout path is ArgoCD sync of the already-reviewed manifests.

## Review — 3514b82 **verdict: pass** — integration review of the epic→live PR. Every constituent PR was independently reviewed by a fresh-context landing worker with an after-head verdict and merged CI-green (#567, #568, #569, #573, #575, #576, #577, #592, #594, #596, #597, #598, #599, #600 + pi-extensions #57/#60/#61/#63); the PR body inventories the stack, the OFF-by-default arm postures, and the activation sequence. Delta since the last constituent merge: three CI-leg fixes with diagnosed root causes (whisper static-link/exit-127, the alert-contract gate's merge-base bug, sandbox-init test pins), a terraform-init retry wrapper, and retrigger commits — all verified green on this head (CI run set on 3514b82: all jobs success). No blockers. Deployment risk posture: every new arm ships OFF; the rollout path is ArgoCD sync of the already-reviewed manifests.
aaron merged commit 32773157ce into live 2026-10-03 18:18:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!591
No description provided.