Document the forge hot loop and correct stale deployment claims

57cd8e71d9c0 · AtlantisPleb · · parent 8b44c5a96df1

Document the forge hot loop and correct stale deployment claims

Adds the operator runbook for the automated direct-load deploy loop:
the verified state of each lane, why the public loop metrics read null
today, the push-promote-build-deploy chain as wired, the enablement
checklist from builder image to feature flag, a first-drill procedure,
and failure modes. Records what the general relup lane still needs so
documentation stops overstating its readiness.

Also adds the operator PostHog analytics doc and updates the historical
hot deployment plan and gap audit: the fa4b792 transactional hot load
proves the direct lane on production, while relups remain proof-only.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified docs/2026-08-21-hot-deploy-gap-audit.md
  • added docs/2026-08-21-operator-posthog-analytics.md
  • modified docs/beam-hot-deployment-plan.md
  • added docs/operations/forge-hot-loop.md

Diff

4 files changed, +252 -3

docs/2026-08-21-hot-deploy-gap-audit.md modified +19 -1

@@ -22,6 +22,24 @@ Meanwhile every production deploy rides the structural class: one immutable imag

22 22
23 23
Closing the gap is operator work, not engineering work: apply the staging Terraform, run Gates 12–15 against the real builder image, pin the builder digest into fleet metadata, flip the feature flags, then extend the runbooks with the consolidated decision table and drill lessons listed in section 5.
24 24
25
### Addendum: post-measurement production events (2026-08-21, later)
26
27
After this audit was measured, two claims moved:
28
29
1. **The direct-load transaction now works on production.** Commit `fa4b792`
30
   (a code-only router fix) was loaded across all three nodes through the real
31
   transaction protocol — `live` Forge target and deployment receipt recorded,
32
   boot convergence satisfied, process uptimes unbroken. The operator assembled
33
   and applied the artifact manually; no receipted automated deploy exists yet.
34
2. **"Nothing needs re-implementing" holds for the hot-load loop only.** The
35
   general relup path remains unconnected: `BuildArtifact` classifies only
36
   `direct_candidate` and `needs_rolling_replace` and can never emit a relup
37
   class, and `RelupDeployment` accepts only the fixed `0.1.0 → 0.2.0`
38
   proof transition while production runs `0.2.0`. Closing that lane is
39
   recorded as future work in
40
   [`docs/operations/forge-hot-loop.md`](operations/forge-hot-loop.md), which
41
   is also the enablement runbook for the automated loop.
42
25 43
---
26 44
27 45
## 1. What Sarah proved

@@ -92,7 +110,7 @@ Verified in this repository at `eda094c`:

92 110
| Build lane producing normalized-BEAM changed-set artifacts | `lib/openagents/forge/build_worker.ex`, `build_artifact.ex` | Hardened rewrite: JSON queue contract, digest-addressed artifacts, `:beam_lib.strip/1` stable hashing (the fix for Sarah's hash-instability defect), structural-reason classifier for mix.lock/config/assets/NIF paths |
93 111
| Isolated builder container, credential-free queue | `docs/operations/forge-build-lane.md`, `Dockerfile` forge-builder target, `ops/forge/build-worker.exs` | Stronger than Sarah's root sidecar: no compiler in the serving image, askpass-based credentials, mode-separated queue files |
94 112
| Transactional fleet deployment with rollback | `lib/openagents/forge/deployment.ex` and siblings; runbook `docs/operations/forge-transactional-deployment.md` | Stronger than Sarah's Loop v0: prepare/apply/verify/commit with expiring tokens, exact prior-object capture per node (fixes Sarah's revert-divergence flaw), membership rechecks between phases |
95
| Relup lane with appup + proof harness | `mix.exs` appup wiring, `rel/openagents.appup.exs`, `ops/relup-proof/`, runbook `docs/operations/release-deployment-fallbacks.md` | Ported; forward/reverse/interrupted-install proofs are gate stages |
113
| Relup lane with appup + proof harness | `mix.exs` appup wiring, `rel/openagents.appup.exs`, `ops/relup-proof/`, runbook `docs/operations/release-deployment-fallbacks.md` | Proof harness only: the classifier never emits a relup class and `RelupDeployment` admits only the fixed `0.1.0 → 0.2.0` proof transition, so the general lane is ported but not connected |
96 114
| Boot convergence | `lib/openagents/forge/boot_converge.ex`; flag `OPENAGENTS_FEATURE_BOOT_CONVERGENCE` | Ported; readiness-gated so divergent nodes do not serve |
97 115
| Three-node fleet infrastructure with state disks and builder wiring | `infra/staging/main.tf`, `infra/staging/templates/fleet-startup.sh.tftpl` | Terraform-complete, safety-tested; cloud apply never ran |
98 116
| Promotion targets, receipts, WAL-backed git service | `lib/openagents/forge/targets.ex`, `pushes.ex`, `git_http.ex` | Ported and partially live: GitHub imports and the public clone URL already run in production |
docs/2026-08-21-operator-posthog-analytics.md added +78

@@ -0,0 +1,78 @@

1
# Operator PostHog analytics
2
3
Date: 2026-08-21
4
5
Status: Implemented; activates when read credentials are configured
6
7
`/admin/analytics` gives operators the trailing-twenty-four-hour product
8
picture without opening PostHog. It exists because the numbers an operator
9
acts on should be one navigation away, rendered in this product's own
10
components, and gated behind the same operator boundary as every other admin
11
surface.
12
13
## How it works
14
15
The page pulls computed results from the PostHog REST API at request time.
16
`OpenAgents.PostHog` sends HogQL queries over a bearer-authenticated personal
17
API key and shapes the rows into plain maps. The surface adds no aggregation
18
of its own: what it renders is exactly what the PostHog app answers for the
19
same window, so there is no second authority that can drift from the source.
20
21
One pull runs four bounded questions:
22
23
1. **Activation funnel** — authorization starts, accounts created, returning
24
   sign-ins, first chat messages sent.
25
2. **Chat turns** — count, outcome breakdown (completed, failed, cancelled),
26
   average and longest turn duration.
27
3. **Event volume** — every event name with count and distinct people.
28
4. **Top pages** — the eight most-viewed URLs.
29
30
## Configuration
31
32
All settings are optional. Absent credentials disable the read path only;
33
event capture is configured separately and keeps working either way.
34
35
| Setting | Requirement |
36
| --- | --- |
37
| `OPENAGENTS_POSTHOG_PERSONAL_API_KEY` | A `phx_...` personal API key created for this integration |
38
| `OPENAGENTS_POSTHOG_PROJECT_ID` | The numeric PostHog project id |
39
| `OPENAGENTS_POSTHOG_APP_HOST` | Optional; defaults to `https://us.posthog.com`. This is the app/API host, not the event ingest host used for capture |
40
41
Create the key in PostHog user settings and grant only read scopes. Treat it
42
as analytics material: it never needs write access, and nothing on this page
43
requires more.
44
45
## States
46
47
The surface designs its non-happy states as first-class UI:
48
49
| State | Rendered as | Meaning |
50
| --- | --- | --- |
51
| Loading | Inline notice | Queries are in flight; no cached numbers are shown |
52
| Unconfigured | Warning notice naming the two missing settings | Credentials absent at boot; no requests are made |
53
| Unavailable | Danger notice with retry | PostHog did not answer; nothing stale is rendered |
54
| Loaded | Cards and tables | Fresh pull with a generated-at stamp |
55
56
Refresh re-runs all four questions and replaces the whole projection.
57
58
## Boundaries
59
60
- Operator-gated like `/admin`: the route requires the configured operator
61
  GitHub IDs and is classified `analytics:read` in the route authority
62
  inventory.
63
- Aggregates only. No conversation content, memory claims, or message text is
64
  reachable from this page; the queries select counts, durations, and URLs.
65
- A failed query fails the whole pull. Partial numbers presented side by side
66
  read as complete, so the page refuses to render them.
67
68
## Testing
69
70
`test/openagents/posthog_test.exs` covers shaping, authentication headers,
71
transport failures, and the disabled path against a stubbed transport.
72
`test/openagents_web/live/admin_analytics_live_test.exs` covers the access
73
gates and all three non-loaded states plus refresh.
74
75
## Related
76
77
- [PostHog integration runbook](2026-08-21-posthog-integration-runbook.md) —
78
  the instrumentation this surface reads
docs/beam-hot-deployment-plan.md modified +9 -2

@@ -2,8 +2,15 @@

2 2
3 3
Date: 2026-08-19
4 4
5
Status: Design and partial local implementation; staging proof incomplete;
6
production use prohibited
5
Status: Historical design record. Superseded in part by operations: direct
6
BEAM loading of allowlisted web-layer changes has since run on production
7
through the transactional lane (`fa4b792`, 2026-08-21). Relups remain
8
unimplemented as a general path and not production-approved. The current
9
operator authority is
10
[`docs/operations/forge-hot-loop.md`](operations/forge-hot-loop.md) with the
11
[fallbacks runbook](operations/release-deployment-fallbacks.md); the
12
[hot deploy gap audit](2026-08-21-hot-deploy-gap-audit.md) records what
13
changed after this plan was written.
7 14
8 15
## Outcome
9 16
docs/operations/forge-hot-loop.md added +146

@@ -0,0 +1,146 @@

1
# Forge hot loop runbook
2
3
Date: 2026-08-21
4
5
Status: Enablement runbook for the automated direct-load deploy loop
6
7
This is the operator procedure for the fast deployment lane: push to the owned
8
forge, promote, and watch a code-only change go live across the fleet in
9
seconds without an image build. It also records why the loop reads as
10
disconnected today (`forge.loop.last_ms: null`, mirror `off`) and exactly what
11
flips it on.
12
13
## Verified state of the lanes
14
15
| Path | State | Evidence |
16
| --- | --- | --- |
17
| Direct BEAM transaction on production | Works | `fa4b792` loaded across three nodes via the transaction protocol; `live` target and deployment receipt recorded; uptimes unbroken |
18
| Automated push → promote → build → hot-load loop | Not yet operating | No receipted automated deploy exists, so `/api/status` reports `loop.last_ms: null` and `push_to_live_ms: null` |
19
| General relup lane | Proof harness only | The classifier emits only `direct_candidate` or `needs_rolling_replace`; `RelupDeployment` admits only the fixed `0.1.0 → 0.2.0` proof transition while production already runs `0.2.0`. Relups are not production-approved (`release-deployment-fallbacks.md`) |
20
| Rolling image replacement | Works, default for structural changes | Current release tooling path |
21
22
Two consequences worth stating plainly:
23
24
- Code-only changes do **not** require an image roll once the loop is enabled;
25
  the whole web layer is allowlisted.
26
- The current `main` (PostHog analytics, `73ff250`) touches
27
  `config/config.exs` and `config/runtime.exs`, which the classifier must
28
  refuse as structural. That change rides rolling replacement correctly.
29
30
## Why the metrics read null
31
32
- `loop.last_ms` and the median come from deploy receipts whose result is
33
  `live` with an integer `push_to_live_ms`. The manual `fa4b792`
34
  application produced a receipt without timing, so the projection has no
35
  sample yet. The first automated loop deploy populates both.
36
- Mirror state `off` means no repository has a configured mirror URL
37
  (`OPENAGENTS_FORGE_MIRROR_URLS` defaults to empty). Mirroring feeds GitHub;
38
  it plays no part in the deploy loop.
39
40
## The chain, as wired
41
42
Every link below exists in code and runs in this order:
43
44
1. **Push**: `git push` to the forge git service lands the packfile in the WAL
45
   and writes a push receipt (`OpenAgents.Forge.Pushes`).
46
2. **Promote** (operator): the Promote control on `/admin/forge`, or
47
   `OpenAgents.Forge.Targets.promote/3`, broadcasts on `forge:target`.
48
3. **Build**: `OpenAgents.Forge.Builder` advances the target to `building`,
49
   writes a request to the build queue, and waits. The builder sidecar claims
50
   it, fetches the commit into its warm workspace, compiles incrementally,
51
   hashes every BEAM, diffs against the baseline manifest, classifies the
52
   changed set, and writes the response plus a digest-addressed artifact.
53
4. **Verify**: `Builder` re-verifies digest and manifest, advances the target
54
   to `built` with module list and classification, then broadcasts
55
   `forge:builds`.
56
5. **Deploy**: `OpenAgents.Forge.HotLoader` verifies again, refuses anything
57
   that is not a `direct_candidate` or carries off-allowlist modules, and hands
58
   the artifact to `OpenAgents.Forge.Deployment` for the transactional
59
   prepare → canary → fleet apply → verify → commit sequence.
60
6. **Receipt**: a `live` deployment writes the deploy receipt including
61
   `push_to_live_ms`, measured from the push receipt.
62
63
## Enablement checklist
64
65
Work top to bottom; each step gates the next.
66
67
1. **Publish the builder image.** Build the `forge-builder` Docker target from
68
   the same revision as the serving image and push it to Artifact Registry
69
   (`ops/staging/publish-candidate.sh` already handles `openagents-builder`
70
   tags).
71
2. **Pin the sidecar into fleet metadata.** Set `openagents-builder-image`
72
   (digest-addressed) and `openagents-builder-digest` on each fleet instance.
73
   The startup script validates both against the registry path and launches
74
   the `openagents-builder` container with the shared workspace and artifacts
75
   volumes plus its own credential env file. An empty value means no builder
76
   runs, and every promotion then times out at `build_timeout`.
77
3. **Confirm executor settings on the web role.**
78
   `OPENAGENTS_FORGE_BUILD_EXECUTOR=sidecar`, with `forge_build_queue_dir`,
79
   `forge_build_dir`, and `forge_artifact_dir` on durable state disks. Runtime
80
   configuration validates these when the deploy lane is enabled.
81
4. **Enable the deploy lane flag** (`OPENAGENTS_FEATURE_FORGE_DEPLOY=true`).
82
   This starts `Builder`, `HotLoader`, `Janitor`, and `MirrorWatch` under
83
   `OpenAgents.Forge.Supervisor`. Boot convergence is already proven on this
84
   fleet.
85
5. **Allowlist**: no change needed. Baked configuration already admits the
86
   whole `OpenAgentsWeb.` layer plus `OpenAgents.Changelog`,
87
   `OpenAgents.Forge.Browse`, `OpenAgents.BuildInfo`, and the scratch prefix,
88
   with boot-time classification self-tests.
89
6. **Optional — turn the mirror on** by configuring a mirror URL for
90
   `openagents.com`. This only affects the public status projection and GitHub
91
   mirroring, never deploys.
92
93
## First automated drill
94
95
Run this once enablement completes; it is also the regression check after any
96
builder change.
97
98
1. Pick a commit that changes only allowlisted modules (a template or
99
   LiveView edit is ideal).
100
2. Push it to the forge remote:
101
   `git push openagents <sha>:main`. Confirm the push receipt on
102
   `/admin/forge`.
103
3. Promote the new SHA from `/admin/forge`.
104
4. Watch the target walk `promoted → building → built → deploying → live`.
105
5. Assert the deploy receipt shows `result: live`, a nonzero module count, and
106
   a populated `push_to_live_ms`. Then confirm `/api/status` now reports
107
   `forge.loop.last_ms`.
108
6. Restart one node and verify boot convergence restores the same revision
109
   before it serves.
110
111
Expect the first real build to take minutes: the loop's economics assume the
112
warm workspace that the first build creates. Subsequent web-layer diffs should
113
land in seconds. Receipts measure pipeline time from push ack to live, not
114
human reaction time.
115
116
## Failure modes
117
118
| Symptom | Meaning | Action |
119
| --- | --- | --- |
120
| Target stalls at `building`, fails `build_timeout` | No sidecar claimed the request | Check the builder container is running and the queue volume is shared |
121
| `needs_rolling_replace` with `structural_reasons` | Honest refusal: config, dependencies, assets, or release files changed | Ride the image-roll path; this is correct behavior |
122
| `needs_rolling_replace` with `off_allowlist:` reasons | The diff touched modules outside the allowlist | Widen deliberately in config, or route around the change |
123
| Artifact verification failure | Digest or manifest mismatch between builder and coordinator | Treat as a builder defect; inspect the retained build output |
124
| A node restarts mid-fleet-deploy | Membership recheck pauses phases | Boot convergence holds the node out until it converges |
125
126
A `reverted` outcome still warrants checking fleet convergence even though
127
this design captures each node's prior object code for exact rollback.
128
129
## What relups still need (out of scope here)
130
131
Getting hot loads running requires nothing below. Recording it ends the
132
documentation drift about the third lane:
133
134
1. The classifier needs a `needs_relup` class with version-boundary reasons;
135
   today it can never emit one.
136
2. `RelupDeployment` needs general from/to version admission driven by the
137
   packaged appup, replacing the fixed proof-transition pins.
138
3. The lane needs its own gate evidence and production approval per
139
   `docs/operations/release-deployment-fallbacks.md`.
140
141
## References
142
143
- [Hot deploy gap audit](../2026-08-21-hot-deploy-gap-audit.md)
144
- [Forge build lane](forge-build-lane.md)
145
- [Transactional deployment](forge-transactional-deployment.md)
146
- [Release deployment fallbacks](release-deployment-fallbacks.md)

This page updates live while a promote is in flight · changelog