The Stage-0 usability fix: tapir serve runs discovery for all users on an
interval (reusing the existing Runner.Loop, enumerate-users-then-withUser),
auto-summarize defaults ON for Future-B users so the list fills itself, and the
ADR-014 process-wide per-egress-IP rate gate is confirmed/finished in the same
slice because in-process + auto + multi-user makes it load-bearing. Records the
single-replica constraint as load-bearing, and a fallback (auto-summarize OFF
until the gate exists) so the dangerous combination never ships half-built.
Records the deliberate keep-or-reverse decision on the Dex-write invite flow.
Decision: KEEP. Deciding fact: not all intended Future-B users will use Google
accounts, so Google OIDC alone can't onboard them — the invite flow is
load-bearing, not redundant. Trust-surface cost accepted deliberately, explicitly
NOT as a precedent for widening further, and explicitly NOT by adding delete RBAC
to fix the orphan gap. Open items reframed as tracked follow-ups (verify RBAC
against the real manifest; accept orphan for Future B, revisit before Future C).
Added the reversal to rejected-alternatives.
Reconciliation finding: the v0.6.0 report's "migration 008 videos.rate_limited_at"
was a mislabel. Both transcript_status and rate_limited_at shipped in migration
007; the sequence legitimately skips 008, nothing was lost, and the runner's
column reads are sound. Updated the ADR-014 implementation note to state this
(was flagged as an unresolved discrepancy). The item-2 (shared per-egress-IP rate
gate vs per-video backoff) flag stays open — still unconfirmed.
Reconciliation pass after parallel agent sessions shipped v0.6.0/v0.7.0.
ADR-017 documents the v0.7.0 invite flow, which gave Tapir scoped create+get on
passwords.dex.coreos.com in the auth namespace — Tapir now WRITES to the shared
identity provider. This shipped with no ADR; recorded retroactively with the
principle-reversal named (partially supersedes ADR-002/013), the security analysis
(bounded RBAC, but a real larger trust surface), and the open gaps (orphaned Dex
accounts on delete; plaintext invite tokens). Cross-referenced in ADR-002 and
ADR-013 status lines so a future reader isn't misled.
ADR-014 annotated: 429 handling shipped in v0.6.0 but decision item 2 (shared
per-egress-IP rate gate) appears realised as per-VIDEO backoff, not a process-wide
IP gate — flagged not-confirmed-done. Also flags the missing migration 008 /
rate_limited_at discrepancy (v0.6.0 report cited 008; tree jumps 007->009).
No code changed in this commit — audit trail only.
Small tapir slice to make the gate measurable as written: append-only
login_events (RLS, per-user-per-day throttle) + a union query over reads
(login_events) and acts (summary_actions) for distinct-active-weeks. Carries the
honesty caveats (unprompted not measurable; data accrues from deploy; week-bucket
noise at low N) and the delete-cascade footgun (no FK, needs explicit delete +
test) from the prior delete work. Out of scope: analytics, prompt-tracking,
dashboards.
Reframes "unprompted" from an enforced criterion to a named measurement
limitation: organic-vs-prompted returns aren't distinguishable from any data
Tapir holds, so in practice all returns are counted and the result read with that
caveat (a nudged return is a weaker signal). Adds a "how it's measured" note
pointing at summary_actions (acts) + a new append-only login-events table
(read-returns), which accrue from deploy onward. Honest about the gap rather than
silently dropping the word.
Records the gate change: Stage 0 now passes when either the maintainer or an
onboarded friend returns unprompted in >=2 separate weeks. Behavioural (return
usage), not feedback-based, to resist politeness bias. Includes an honest
self-scrutiny note that this is a guardrail edit made while the original gate was
unmet — examined on that basis and proceeding because it broadens who supplies the
signal without softening the kind of signal required. Adds feedback-based-gate to
rejected alternatives.
Replaces the original "useful to me, specifically" gate with "me OR a friend
returns unprompted in >=2 separate weeks" — friendly-user signal counts, but the
test stays behavioural (return usage) not feedback-based, to resist politeness
bias. Folds the old Stage 1 ("a trusted user returns") into the new Stage 0 (they
were near-identical), renumbers hardening to Stage 1, and updates the drift
signals (the gate can be softened by mistaking polite feedback for evidence;
multi-user shipping ahead of the gate was a recorded exception per ADR-012, not a
precedent). Rationale recorded in ADR-016.
Records the infra#88 spike decision: per-user OAuth tokens are runtime app-state,
not config, so they live envelope-encrypted in PG18 under RLS (key from 1P via the
existing read-only SA) rather than in the vault. Infra creds stay ESO/1Password —
two mechanisms because they're two different things. Includes the falsification
conditions (frequent rotation; estate audit policy; key-rotation cost) so the
choice is earned not assumed. Adds the two rejected candidates (vault-write SA;
Supabase) to the rejected-alternatives table. Full reasoning in the #88 decision
doc; build + reboot-validation in #89.
Workstream A: public /welcome landing page (bubbletea aesthetic, one Dex login
flow, logged-in shortcuts) — with the oidc.go facts verified against main, incl.
the two corrections that only surface from reading the code (logout must redirect
to /welcome not /auth/login; bare-/ vs deep-link redirect split).
Workstream B: reconcile the guardrail docs against deployed reality (v0.4.0) —
auth.go comments, data-model isolation status + migrations 002-006 schema,
architecture web surface, use-case scenarios for the Stage-1 features, ADR
ordering, and a requirements-vs-shipped deviation check. Structured as two
parallel workstreams so the doc audit isn't done cursorily alongside the build.
ADR-013 records the deliberate choice that account deletion is Tapir-side only
(cascade + secret purge), leaving the shared Dex identity intact — clean
re-registration, but a noted GDPR-shaped gap if Future C ever arrives.
ADR-014 specifies timedtext 429 handling: Retry-After-aware backoff, a single
per-egress-IP rate gate shared by the batch and click paths, and honest in-flight
UX (summarizing / queued-waiting / no-transcript) so a rate-limited fetch never
presents as a stuck spinner or error. Whisper stays deferred pending measurement
of the sustainable rate, which this work finally makes measurable.
Lists the egress the build assumes (Go module proxy, toolchain download,
raw.githubusercontent for golangci-lint, github for non-proxied modules) and the
runtime assumes (LiteLLM gateway, brain-mcp, YouTube/Vimeo APIs, per-user BYO-AI
hosts only on opt-in), so a locked-down koala act_runner or dev env knows what to
allow or which GOPROXY to set. Notes the claude.ai sandbox allowlist is separate
and unrelated.
Tells an agent to wire skills via `task skills` (gitignored symlinks, never
committed) and which skills matter for Tapir; documents the scaffolded-and-RED
state with the first build task spelled out; flags the unverified setup items
(Go version, brain-mcp URL, secret-ref naming, model alias) to resolve against
the live cluster.
Excludes build artifacts and the .claude/skills symlink (wired by the skills
installer, never committed — matches the mathias/skills convention) and local
env files.
check -> build -> mirror, self-hosted runner, buildah to localhost:5000, k3s
smoke test. Follows the gitea-ci skill template and its act_runner gotchas
(secrets inlined in run:, no heredocs). check will be RED until the engine is
implemented (acceptance suite). Deploy job omitted until k3s manifests exist in
infra. GH_DEPLOY_KEY secret must be set before mirror succeeds.
Defines `task check` (fmt-check + vet + lint + test) — the gate ADR-009 and CI
both invoke. Also `task skills` to wire the engineering skills library via the
canonical installer (symlinks, gitignored). lint no-ops locally when
golangci-lint is absent; CI installs it.
Minimal main that identifies the binary (gives the CI smoke test something to
grep). HTTP server, watcher, adapter wiring, and config come with the build.
Executable translation of docs/use-cases/summarize_new_video.feature: captioned
video is summarized and delivered to the store sink; no-transcript video is
skipped with reason "no transcript" and no delivery. Drives the engine through
fake adapters (no live YouTube/brain). Intentionally RED until the engine is
implemented — this is the swarm's target.
Engine wires the ports and exposes ProcessNewVideo, the core use case. Returns
ErrNotImplemented on purpose so the acceptance suite fails RED — implementing it
to make those tests pass is the first build task. Depends only on ports + domain
(dependencies point inward).
The hexagonal interfaces the engine depends on. Keeps the engine provider- and
sink-agnostic: YouTube/Vimeo implement VideoSource, the AI router implements
Summarizer, store/brain implement Sink, ESO implements SecretStore. This is what
makes standalone-vs-homelab a wiring choice (ADR-003).
Pure domain types matching docs/data-model.md: User, Subscription, Video,
Transcript, Summary, plus Provider and TranscriptSource enums. Stdlib-only,
no outward dependencies — the innermost layer. Video carries user_id per the
per-user-isolation decision (no global dedup).
Go module root for Tapir. Go 1.23 — confirm against the koala act_runner
toolchain; bump to match the estate (ingestion uses 1.26.1) if the runner has it.
Adds a consolidated table of approaches considered and deliberately not taken
(Python, Supabase, living in the monolith, shared-lib lift, filesystem brain
package, reusing inbound oauth, global dedup table, Whisper-in-core, SaaS-now,
swarm-delegating the spike), each mapped to the ADR that settles it. Prevents a
later session from re-proposing settled rejections as fresh ideas.
Operational context for independent agent sessions: orientation order, TBD +
conventional-commit workflow, the three "looks reusable but isn't" traps (llm is
copied not imported, brain sink is HTTP not filesystem, OAuth is fresh), the
settled decisions not to reopen, the Clean Architecture/BDD stance, and a
provenance trail back to the S5 spike and the llm source. Distills the operating
knowledge surfaced during the 2026-06-02 planning+grill session.
Points anyone (or any agent) landing cold at the vision, decisions, architecture,
data model, and BDD feature specs; states the Clean Architecture / TDD-BDD / TBD
approach and the homelab conventions reused. Notes the repo is pre-code and the
docs are the version-controlled design intent.
Gherkin spec for connecting YouTube/Vimeo accounts and configuring optional
per-provider BYO AI credentials: connections sync subscriptions, tokens/keys are
stored only as secret references (never in the clear), revocation stops watching
but preserves history. Encodes the secrets-by-reference and data-isolation
guardrails (ADR-002, ADR-006, data-model).
Gherkin spec for the routing guardrail: local produces the summary by default;
on local failure, fall back only to a user-configured BYO provider; with no BYO,
queue for retry and never send content to a third-party model. Encodes the
local-first/user-owned principle (VISION) as executable behavior.
Gherkin spec for the core use case: captioned video is summarized and delivered;
no-transcript video is recorded as skipped; unsubscribed channels are ignored;
already-summarized videos are not reprocessed. These scenarios seed the use-case
test suite (Clean Architecture core tested through fake adapters).
Per-user-isolated entities (no global cross-tenant video table per the S5/grill
correction), secrets stored by reference only (ESO/1Password, never the token),
brain delivery modelled as a sink_delivery row rather than brain-specific tables.
fallback_used recorded per summary as the Stage 0 quality signal. Future C dedup
and sharding explicitly out of scope.
Context and container diagrams, sequence diagrams for the core summarize-new-video
use case and the local-first/BYO AI routing fallback, and the Clean Architecture
layering. Ports & adapters keep the engine provider- and sink-agnostic, making
standalone-vs-homelab a wiring choice (ADR-003), not two codebases.
Records the decisions from the S5 spike and the Full Grill as append-only ADRs:
Go not Python; no Supabase; standalone-first with brain as one sink; copy the
llm package; brain sink via HTTP brain-mcp; fresh outbound OAuth; captions-first
with STT deferred; Future C deferred behind the Stage 0 gate; trunk-based dev.
The top-level guardrail for Tapir: the problem, the product, the principles
(local-first, standalone-first, attention-as-scarce-resource, data isolation),
who it's for (now / Future B / deferred Future C), and a staged, falsifiable
definition of success with Stage 0 ("useful to me") as the gate.