Slice 8: v1 ships — integration coverage, runbook, spec corrections
- Five new integration test files raise the suite from 75 to 96 green: test_hygiene_vertical (7), test_branch_path_routing (4), test_metadata_pr_merge (3), test_cache_bootstrap (4), test_e2e_smoke (3). The smoke test walks propose → super-draft → edit branch → body-edit PR → graduate → active-RFC PR → merge → notification → hygiene-sweep deletion end-to-end. - deploy/RUNBOOK.md replaces the prior DEPLOY.md stub as a real runbook: prerequisites, first-time bring-up, day-2 ops (logs, DB backup, secret rotation, the §12 hygiene cadence), rollback shape, troubleshooting table. - backend/.env.example grows the SMTP block, HYGIENE_TICK_SECONDS, and WEBHOOK_EMAIL_BOUNCE_SECRET with inline commentary. - README points to RUNBOOK.md; the "what the build lets you do" section adds Slices 7 and 8. - docs/DEV.md gets a Slice 8 — shipped section; the "Next slice" footer becomes the v1-complete epitaph. - SPEC corrections per the §19.3 working agreement: §10.7 names the shared §12 sweep; §12 names the bot as actuator and the per-user branch_chat_seen preservation contract; §19.1 marks v1 complete and records Slice 8; the five §19.2 candidates Slice 8 folded in are marked settled with pointers at the resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1391,10 +1391,12 @@ After merge, the PR page renders identically with a "Merged" banner in
|
||||
the header strip. The diff, the compressed conversation, and all review
|
||||
threads become read-only. The branch enters the closed state per
|
||||
§12's hygiene table (the 90-day deletion timer starts; owners and
|
||||
arbiters can still pin to disable it). The branch chat persists per
|
||||
§8.4 as historical record, with new posts disabled. The PR page remains
|
||||
at its stable URL indefinitely — it is the canonical surface for "show
|
||||
me how this definition came to be."
|
||||
arbiters can still pin to disable it). The 90-day timer rides on §12's
|
||||
hygiene sweep rather than its own schedule — the actuator is one extra
|
||||
branch in the sweep that already handles meta-repo edit branches. The
|
||||
branch chat persists per §8.4 as historical record, with new posts
|
||||
disabled. The PR page remains at its stable URL indefinitely — it is
|
||||
the canonical surface for "show me how this definition came to be."
|
||||
|
||||
Post-merge questions about the merged work belong on main's chat (§8.4),
|
||||
not on the merged branch's. Main is the surface for ongoing argument
|
||||
@@ -1492,6 +1494,22 @@ work is paused but legitimately ongoing.
|
||||
| Deleted | 60 days after close (90 from last activity) | branch removed from Gitea, row remains |
|
||||
| Pinned | owner/arbiter pins | auto-close disabled |
|
||||
|
||||
The bot is the actuator on the deletion: per §1 the bot is the only
|
||||
Git writer, and the §12 sweep fires through it. The audit row lands
|
||||
with `actor_user_id = NULL` and `on_behalf_of = <bot login>` per §15.9
|
||||
— the timer is system-generated, "the app" in the noun slot. No
|
||||
notification fires on a hygiene gesture; the affected population would
|
||||
be churn-grade noise per §15.4.
|
||||
|
||||
The per-user message-cursor preservation contract: chat history
|
||||
survives the branch's deletion in Gitea because the chat tables
|
||||
(`thread_messages`, `branch_chat_seen`) are app-canonical, not cached.
|
||||
Only the Gitea branch and the `cached_branches` row are touched by
|
||||
the sweep; every `branch_chat_seen` row that points at a message on
|
||||
the deleted branch stays intact, so a returning contributor sees the
|
||||
same cursor when the conversation surfaces in the §9.8 pre-graduation
|
||||
history affordance (or wherever else the app renders archived chat).
|
||||
|
||||
Future story (not v1): out-of-band reopening of a deleted branch by
|
||||
email request to an owner.
|
||||
|
||||
@@ -2435,15 +2453,32 @@ surface. With Topic 13 folded in, the structural surface is
|
||||
complete. What follows is no longer "topics that block specifying
|
||||
v1" but "topics to address during or shortly after the v1 build."
|
||||
|
||||
### 19.1 Next slice: hardening
|
||||
### 19.1 v1 is complete
|
||||
|
||||
Slice 7 of the build has landed. The §14 chrome, the
|
||||
`/settings/notifications` neighborhood, and the `/admin` home base
|
||||
all run end-to-end against the local Gitea, and the next slice has
|
||||
the v1 surface fully wrapped — what remains is the hardening pass
|
||||
that lets a single-operator deployment actually run.
|
||||
Slice 8 — the hardening pass — has landed. With it, every slice of the
|
||||
v1 build is in: the bot wrapper, the §4 cache, Gitea OAuth + user
|
||||
provisioning, the §5 schema, the §7 catalog, the §8 active-RFC view in
|
||||
full, the §9.4–§9.7 super-draft vertical, the §10 PR flow against both
|
||||
per-RFC repos and meta-repo edit branches, the §13 graduation flow
|
||||
with rollback, the §15 notifications surface in full, the §14 chrome
|
||||
plus the `/settings/notifications` and `/admin` neighborhoods, and the
|
||||
§12 + §10.7 branch hygiene with the §19.2 candidates the hardening
|
||||
pass clustered in.
|
||||
|
||||
The §14.1 landing page now carries the title, the subtitle, the
|
||||
Subsequent sessions pick from §19.2 by user choice per §19.3's working
|
||||
agreement. They need not be sequential — the user picks the next topic
|
||||
based on what evidence the running app surfaces. The build itself is
|
||||
the source-of-truth artifact; §19.2 is the queue of decisions to
|
||||
settle when their turn comes.
|
||||
|
||||
The Slice 8 entry below is preserved for the record. Future sessions
|
||||
that fold a §19.2 candidate into the spec should mark that candidate
|
||||
*settled* with a brief pointer at the section that resolved it — the
|
||||
same shape Topic 13's resolution used.
|
||||
|
||||
### 19.1.1 Slice 8 record
|
||||
|
||||
**Slice 7 background.** The §14.1 landing page now carries the title, the subtitle, the
|
||||
short-form pitch from `PHILOSOPHY.md`, the sign-in affordance, the
|
||||
secondary "Read the full philosophy" link, and a three-item deck
|
||||
underneath the pitch that names what the framework is — one word per
|
||||
@@ -2507,42 +2542,60 @@ filter chips, the graduation-queue partition under both
|
||||
preconditions, and the permission-events listing. The full Slices
|
||||
1–7 test suite is 75/75 green.
|
||||
|
||||
**Slice 8 is the hardening pass — the last slice of the v1 build.**
|
||||
Three pieces hang together:
|
||||
**Slice 8 — the hardening pass — completed the v1 build.** Three
|
||||
pieces hang together:
|
||||
|
||||
The §12 30/90 branch-hygiene timers — the formalized policy that
|
||||
closes the loop on §11.5's branch lifecycle (open → merged → 30d
|
||||
read-only → 90d deleted-by-bot, with the per-user-message-cursor
|
||||
preservation contract). The wiring is a scheduled task next to the
|
||||
§15.5 digest scheduler; the §10.7 90-day deletion timer Slice 3
|
||||
left deferred lives here too.
|
||||
The §12 30/90 branch-hygiene timers ride on `HygieneScheduler` in
|
||||
`backend/app/hygiene.py`, modeled on `DigestScheduler`'s shape. The
|
||||
sweep runs hourly by default, exposes a `run_tick(now=...)` test
|
||||
seam, and orders its queries delete-first so a single sweep crossing
|
||||
both the 30d and 90d boundaries (the cache-bootstrap and clock-jump
|
||||
case) lands the delete rather than spending one tick at `closed` with
|
||||
a fresh `closed_at` that would defer the delete by another 90 days.
|
||||
The §10.7 90-day deletion timer Slice 3 deferred is one branch of the
|
||||
same sweep. The bot grew a `delete_branch` method that takes
|
||||
`actor: Actor | None`; the timer paths pass `None` so the audit row
|
||||
lands `actor_user_id = NULL` and `on_behalf_of = <bot login>` per
|
||||
§15.9. No notifications fire on hygiene gestures (the action kinds
|
||||
are absent from `_AUTO_WATCH_ACTIONS` and `_ROUTING`).
|
||||
|
||||
An end-to-end smoke pass over the working surfaces — propose →
|
||||
super-draft → branch → PR → merge → graduate → active-RFC PR →
|
||||
notification fans out → inbox → email — to catch the integration
|
||||
seams a per-slice test wouldn't. Plus the §19.2 candidates the
|
||||
hardening pass is the natural place to fold in: cache bootstrap
|
||||
from a meta repo (the audit-log-first attribution shape Slice 1
|
||||
chose, exercised against a meta repo with history the bot did not
|
||||
author), branch-name path routing (converting every
|
||||
`branches/<branch>` to `{branch:path}` with route-ordering
|
||||
discipline), and the small Slice-2-onward follow-ons that are
|
||||
deferred until the hardening pass demands them.
|
||||
The §19.2 candidates folded in:
|
||||
|
||||
The dev/prod deployment shape — the `deploy/` directory already
|
||||
has the nginx vhost, the systemd unit, and a runbook stub; Slice 8
|
||||
proves the bring-up against a fresh host, settles the secret-
|
||||
material handling (the existing `.env.example` plus the §15.4
|
||||
SMTP wiring), and lands the README updates that let a new operator
|
||||
get from `git clone` to a signed-in browser.
|
||||
- **Cache bootstrap from a pre-existing meta repo** — the
|
||||
`_resolve_actor` fallback chain now has explicit integration
|
||||
coverage against history the bot did not author (audit-log row,
|
||||
trailer-parse, raw-login last-resort).
|
||||
- **Branch-name path routing** — every `branches/<branch>` route is
|
||||
`{branch:path}` with the bare GET declared last among the
|
||||
branch-scoped GETs so deeper routes match first.
|
||||
- **In-app merge for metadata PRs** — `api_prs._require_pr` accepts
|
||||
`pr_kind='meta_metadata'`; the existing merge endpoint handles them
|
||||
uniformly.
|
||||
- **Graduation rollback's branch cleanup** —
|
||||
`api_graduation._undo_open_pr` deletes the
|
||||
`graduate-<slug>-<6hex>` branch after closing the PR.
|
||||
- **Email bounce webhook authentication** — `WEBHOOK_EMAIL_BOUNCE_SECRET`
|
||||
is the signing seam; when set, the webhook requires the same value
|
||||
in `X-Webhook-Secret`.
|
||||
|
||||
The next build session should read `SPEC.md`, `README.md`,
|
||||
`docs/DEV.md`, and this §19.1 entry and pick up Slice 8 cleanly
|
||||
without re-briefing. The working agreement in §19.3 continues to
|
||||
apply: implement the slice, correct the spec only where running
|
||||
code reveals it was wrong at a structural level, accumulate new
|
||||
candidate topics in §19.2, do not extend the spec beyond what the
|
||||
slice requires.
|
||||
The dev/prod deployment shape: `deploy/RUNBOOK.md` is rewritten from
|
||||
the prior stub into a real runbook (prerequisites, first-time
|
||||
bring-up, day-2 operations, rollback, troubleshooting); the README's
|
||||
`.env` table grows the SMTP block, `HYGIENE_TICK_SECONDS`, and
|
||||
`WEBHOOK_EMAIL_BOUNCE_SECRET`; the `.env.example` carries the same
|
||||
fields with inline commentary.
|
||||
|
||||
Slice 8 ships covered by `test_hygiene_vertical.py` (seven cases),
|
||||
`test_branch_path_routing.py` (four cases), `test_metadata_pr_merge.py`
|
||||
(three cases), `test_cache_bootstrap.py` (four cases), and
|
||||
`test_e2e_smoke.py` (three cases including the full lifecycle walk).
|
||||
The full Slices 1–8 test suite is 96/96 green.
|
||||
|
||||
§12 grew an explicit note that the bot is the actuator and that the
|
||||
per-user `branch_chat_seen` cursor survives branch deletion — the
|
||||
§11.5 contract made implicit; running code asked for the load-bearing
|
||||
line to live in §12 too. §10.7 grew a one-line pointer that the timer
|
||||
rides on §12's sweep rather than its own schedule.
|
||||
|
||||
### 19.2 Candidate topics for sessions after the v1 build lands
|
||||
|
||||
@@ -2634,7 +2687,13 @@ binding.
|
||||
null-system, and how the bot-vs-user distinction in §15.9
|
||||
extends. Will surface during build if AI turn-times grow large
|
||||
enough to warrant.
|
||||
- **Cache bootstrap from a pre-existing meta repo.** §4.1 covers
|
||||
- **Cache bootstrap from a pre-existing meta repo.** *Settled in
|
||||
Slice 8 — the `_resolve_actor` fallback chain Slice 1 chose
|
||||
(audit log → `On-behalf-of:` trailer → raw Gitea login) now has
|
||||
explicit integration coverage in `test_cache_bootstrap.py`
|
||||
against history the bot did not author. The disaster-recovery
|
||||
and transferred-meta-repo cases work without the cache thinking
|
||||
the bot proposed everything pre-app.* §4.1 covers
|
||||
steady-state cache freshness — the webhook is the fast path, the
|
||||
reconciler the safety net — but assumes the cache grew up
|
||||
alongside the bot. If the cache is rebuilt from scratch against
|
||||
@@ -2648,7 +2707,14 @@ binding.
|
||||
topic once the cost of "the cache thinks the bot proposed
|
||||
everything pre-app" becomes concrete. Touches §4.1 (the
|
||||
reconciler's job description) and §15.9 (the attribution rule).
|
||||
- **Branch-name path routing.** Slice 2's `branches/<branch>`
|
||||
- **Branch-name path routing.** *Settled in Slice 8 — every
|
||||
`branches/<branch>` route in `api_branches.py` and `api_prs.py` is
|
||||
now `{branch:path}`. The bare GET is declared last among the
|
||||
branch-scoped GETs so deeper routes (`threads`,
|
||||
`threads/{thread_id}/messages`) match before the greedy path
|
||||
matcher swallows their sub-paths. `test_branch_path_routing.py`
|
||||
covers the slashed-branch read and the deeper-route ordering.*
|
||||
Slice 2's `branches/<branch>`
|
||||
endpoints use FastAPI's default `{branch}` path-segment matcher,
|
||||
which refuses slashes. The Slice 2 auto-generated branch name
|
||||
steered around this with `<login>-draft-<hex>`, but a user who
|
||||
@@ -2721,12 +2787,21 @@ binding.
|
||||
the app — but anyone reading the PR directly on Gitea sees the
|
||||
pre-edit text. A small follow-on that propagates the edit
|
||||
through the bot wrapper closes the loop.
|
||||
- **The §10.7 90-day deletion timer wiring.** Slice 3 lands the
|
||||
- **The §10.7 90-day deletion timer wiring.** *Settled in Slice 8 —
|
||||
the timer rides on `HygieneScheduler`'s sweep alongside the §12
|
||||
30/90 idle timers. See §10.7 and §12 for the spec text and
|
||||
`backend/app/hygiene.py` for the implementation.* Slice 3 lands the
|
||||
PR-merged state and the read-only treatment but does not wire
|
||||
the §12 hygiene timer that fires the deletion. Slice 8
|
||||
("Hardening") owns the §12 30/90 timers as a whole; calling out
|
||||
here so the dependency is explicit.
|
||||
- **In-app merge for metadata PRs.** Slice 4's metadata pane opens
|
||||
- **In-app merge for metadata PRs.** *Settled in Slice 8 —
|
||||
`api_prs._require_pr` now accepts `pr_kind='meta_metadata'`. The
|
||||
existing `prs/<n>/merge` endpoint handles them uniformly; the
|
||||
diff-rendered review surface degrades gracefully (a metadata PR
|
||||
has no body diff to render). `test_metadata_pr_merge.py`
|
||||
covers the merge, the contributor refusal, and the withdraw path.*
|
||||
Slice 4's metadata pane opens
|
||||
a meta-repo PR per §9.5; the merge surface for those PRs is the
|
||||
Gitea web UI for now, because `api_prs.py`'s merge endpoint is
|
||||
scoped to body-changing PRs (`rfc_branch` and `meta_body_edit`).
|
||||
@@ -2759,6 +2834,10 @@ binding.
|
||||
hits cases where a single sequence runs long enough that the
|
||||
reload-during-graduation path matters.
|
||||
- **Graduation PR auto-close on rollback's `close_graduation_pr`.**
|
||||
*Settled in Slice 8 — `api_graduation._undo_open_pr` now deletes
|
||||
the `graduate-<slug>-<6hex>` branch via `bot.delete_branch` after
|
||||
closing the PR. `test_hygiene_vertical.test_graduation_rollback_deletes_dash_suffixed_branch`
|
||||
covers it.*
|
||||
Slice 5's rollback closes the graduation PR via `gitea.close_pull`
|
||||
but leaves the dash-suffixed branch (`graduate-<slug>-<6hex>`)
|
||||
in place. The branch is short-lived in steady-state — graduation
|
||||
@@ -2824,7 +2903,15 @@ binding.
|
||||
actor verb form so each row reads naturally without picking up
|
||||
an apparent personification. Defer-able until contributor
|
||||
feedback surfaces an irritating render.
|
||||
- **Email bounce webhook authentication.** Slice 6's
|
||||
- **Email bounce webhook authentication.** *Settled in Slice 8 — the
|
||||
`WEBHOOK_EMAIL_BOUNCE_SECRET` env var is the shared-secret seam.
|
||||
When set, the webhook requires the same value in the
|
||||
`X-Webhook-Secret` header; when unset, the v1 unauthenticated
|
||||
behavior holds for dev. The per-provider signature scheme
|
||||
(Sendgrid signed events, SES SNS signatures, Postmark HMAC) maps
|
||||
cleanly onto the same seam: the operator picks one when wiring a
|
||||
real SMTP provider.*
|
||||
Slice 6's
|
||||
`/api/webhooks/email-bounce` accepts unauthenticated POSTs for
|
||||
v1 — the SMTP provider's callback URL is the contract. When an
|
||||
actual provider is wired in, the webhook needs a shared secret
|
||||
|
||||
Reference in New Issue
Block a user