Skip to content

fix(selfhost): align Docker + smoke CI with flat repo layout - #8

Merged
seungdo-keum merged 17 commits into
mainfrom
phase-4/selfhost-smoke-flat-repo
May 27, 2026
Merged

fix(selfhost): align Docker + smoke CI with flat repo layout#8
seungdo-keum merged 17 commits into
mainfrom
phase-4/selfhost-smoke-flat-repo

Conversation

@seungdo-keum

Copy link
Copy Markdown
Member

Why

The docker/ skeleton and selfhost-smoke.yml were drafted assuming an apps/web monorepo layout. The extracted repo is flat — the Next.js web app is at the root, and packages/mcp is the only workspace. That mismatch made the self-host smoke matrix structurally unrunnable (every docker compose / build path was wrong), which blocks DoD #3 — self-host 3-OS PASS evidence.

What

  • docker/Dockerfile.web — build at repo root (drop apps/web + nonexistent packages/core); npm install (repo ships no lockfile, so npm ci can't work); npx prisma generate; full-copy next start runner (robust vs. output: standalone tracing, which isn't enabled and is finicky with Prisma). Copies prisma.config.ts so runtime db push resolves schema + datasource.
  • docker/entrypoint.sh — schema path → root; migrate deploydb push (no prisma/migrations/ folder ships); POSIX while loop; no --accept-data-loss (a destructive diff fails loudly instead of dropping a self-hoster's data; fresh DBs push cleanly).
  • src/app/api/healthz/route.ts (new) — liveness probe: 200 iff server up + Postgres reachable. Distinct from /api/health (readiness — 503s when optional Stripe/Resend/OpenRouter are unconfigured, which is wrong for docker compose up --wait on a minimal install). Both the compose healthcheck and the CI probe already point at /api/healthz.
  • .github/workflows/selfhost-smoke.yml — flat-repo path filters (src/**, prisma/**, public/**, root configs); COMPOSE_FILE=docker/docker-compose.yml job-env so every docker compose step finds the file without -f.
  • .gitattributes — pin Dockerfile* to LF (Alpine build safety on Windows checkouts).

Test plan

  • selfhost-smoke matrix green on ubuntu-22.04
  • selfhost-smoke matrix green on macos-14
  • selfhost-smoke matrix green on macos-13
  • ci (type-check / lint / build / test / mcp-pack) green

Closes the structural blocker on #5.

🤖 Generated with Claude Code

seungdo-keum and others added 17 commits May 27, 2026 14:19
The docker/ skeleton and selfhost-smoke.yml were drafted assuming an
apps/web monorepo layout, but the extracted repo is flat (web app at the
root, packages/mcp the only workspace). That mismatch made the self-host
smoke matrix unrunnable. This fixes the whole path end-to-end:

- Dockerfile.web: build at repo root (no apps/web / packages/core), use
  `npm install` (repo ships no lockfile), `npx prisma generate`, and a
  full-copy `next start` runner (robust vs. standalone tracing). Copy
  prisma.config.ts so `db push` resolves schema + datasource at runtime.
- entrypoint.sh: schema path -> root; switch `migrate deploy` -> `db push`
  (no migrations/ folder ships); POSIX while-loop; no --accept-data-loss
  so a destructive diff fails loudly instead of dropping self-hoster data.
- New GET /api/healthz liveness probe (server up + Postgres reachable),
  distinct from /api/health readiness which 503s on optional integrations.
  Both the compose healthcheck and the CI probe already point at it.
- selfhost-smoke.yml: flat-repo path filters (src/prisma/public/configs),
  and COMPOSE_FILE=docker/docker-compose.yml so every `docker compose`
  step finds the file without -f.
- .gitattributes: pin Dockerfile* to LF (Alpine build safety).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
actions/setup-node@v4 with `cache: 'npm'` requires a lockfile to compute
its cache key. The repo intentionally ships no package-lock.json, so every
CI job was failing at the setup-node step before install even ran. Drop
the cache directive across all five jobs (type-check / lint / build / test
/ mcp-pack) so installs run via `npm install`.

Follow-up (not this PR): commit a package-lock.json to restore cacheable,
reproducible installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Prisma client uses the new prisma-client generator with a custom
output (src/generated/prisma), imported as @/generated/prisma/client.
The type-check and test jobs never generated it, so prisma.* resolved to
`any` — the root of the bulk of the strict-mode TS7006/TS2339 cascade.
The build job + Docker already run generate; add it to these two jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The gmail/outreach + google-sc modules import googleapis, but the dep was
dropped during the OSS package.json restructuring, causing 7 TS2307
"Cannot find module 'googleapis'" errors that cascade. Restore the exact
version the app uses (^144.0.0).

Note: this operator growth/outreach tooling (outreach crons, Search Console
SEO automation, Slack summaries) is surplus to the self-host product and is
tracked for removal as a follow-up; restoring the dep unblocks the build now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rift)

The last 2 of 149 type errors: stripe@^22 caret floats (no lockfile) and its
pinned-version literal type drifted to 2026-04-22.dahlia, rejecting our
deliberately-pinned 2026-03-25.dahlia. Cast to Stripe.LatestApiVersion to keep
the intended runtime value type-stable across SDK minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cast to Stripe.LatestApiVersion failed (TS2694 — not an exported member
in stripe@22). Omitting apiVersion entirely is always type-valid and uses
the Stripe account's default pinned version — the correct default for a
self-host deployment, and immune to the lockfile-free SDK caret drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Docker)

GitHub arm64 macOS runners lack nested virtualization, so Colima/docker
compose fails at VM start (exit 125) — macos-14 could never pass the compose
smoke. Split the matrix: docker-compose smoke runs on ubuntu-22.04 + macos-13
(full boot + DB + healthz), and a new native-build job proves Apple-Silicon
parity by running prisma generate + next build on arm64 macOS (exercises the
arch-sensitive Prisma engine / bcryptjs / lightningcss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsc includes tests/, so the e2e spec's hardcoded apiVersion literal was the
last type error (the bootstrap script mirrors it for consistency). Same fix
as the client: omit apiVersion to stay type-stable across stripe@22 minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The class extends the built-in Error; transpilation breaks `instanceof` for
genuine instances unless the prototype is reset. Add Object.setPrototypeOf
in the constructor — fixes the lone failing vitest assertion
(token-budget.test.ts > assertBudget — trial plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`next build` of this heavy app (pdfjs/exceljs/pptxgenjs/tiptap/react-pdf)
exceeded the default V8 old-space heap on the 7 GB ubuntu runner —
"Ineffective mark-compacts near heap limit". Set NODE_OPTIONS
--max-old-space-size=6144 in the CI build job and the Docker builder stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colima on the Intel macos-13 runner is too memory-starved (4 GB VM) to
build this app and is slow/flaky; arm64 macos-14 can't run Docker at all.
Drop Docker-on-macOS entirely: keep the full docker-compose smoke on
ubuntu-22.04 (the canonical Linux self-host target), and prove macOS
self-host parity (both Intel + Apple Silicon) via a native `next build`
matrix [macos-13, macos-14] with the same heap bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The real cause of the failing assertion: FREE_TRIAL.trialTotalTokens was
bumped 200K → 700K on 2026-05-15, but the test still fed 200K usage and
expected a trial_exceeded throw. 200K < 700K → no throw → expect.fail()
threw the AssertionError we saw. Reference FREE_TRIAL.trialTotalTokens
directly so the test tracks the constant and can't go stale again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…protocol/sdk

The Docker builder copied only the root package.json before npm install, so
the packages/mcp workspace deps were never installed. `next build`
type-checks the whole project (incl. packages/mcp, which imports
@modelcontextprotocol/sdk) and failed: "Cannot find module
'@modelcontextprotocol/sdk/server/stdio.js'". Copy packages/mcp/package.json
before install so npm (workspaces) resolves it. (CI Build + native macOS
passed because their npm install ran against the full checkout.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hip client

Two runtime issues that kept the web container from going healthy:
1. entrypoint ran `prisma db push --skip-generate`, but Prisma 7 removed that
   flag — it errored, printed help, and crashed the container (set -e).
   Use a plain `prisma db push`.
2. The Prisma client uses a custom output (src/generated/prisma, not
   node_modules/.prisma) that the runner stage never copied, so the server
   + db push couldn't resolve @/generated/prisma/client. Copy it explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…macos-13

Two things kept the ubuntu docker-compose smoke from going healthy / the run
from completing:
1. The schema has a vector(1024) column but docker-compose used
   postgres:16-alpine (no pgvector) and nothing created the extension, so
   `prisma db push` failed: ERROR: type "vector" does not exist. Switch to
   pgvector/pgvector:pg16 + a first-boot init that runs CREATE EXTENSION
   vector. This is a real self-host correctness fix, not just CI.
2. macos-13 native build sat queued 45+ min (GitHub Intel-mac runner
   unavailability) and wedged the whole workflow from ever completing. Drop
   it from the matrix; macos-14 (Apple Silicon) covers native build parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The container went "unhealthy" (compose --wait failed) even though the app
boots fine (Postgres reachable, db push synced, Next.js Ready). Decouple:
`docker compose up -d --build` (no --wait), and let the explicit probe step
gate — now printing the HTTP code + body each attempt so a 503 reveals the
actual healthz error instead of an opaque "unhealthy". Either goes green or
surfaces the precise runtime cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator decision: merge now, track the Docker HTTP-serving gap. The ubuntu
job now asserts the core self-host proof as fatal — image builds, container
boots, Postgres reachable, schema synced via prisma db push — and treats the
in-container HTTP route probe as non-blocking, because the hand-rolled
`next start` runtime doesn't serve built routes (404; needs output:standalone,
tracked as a follow-up issue). Makes selfhost-smoke green on what genuinely
works instead of red on the one packaging gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@seungdo-keum
seungdo-keum merged commit 02bfa76 into main May 27, 2026
7 of 8 checks passed
@seungdo-keum
seungdo-keum deleted the phase-4/selfhost-smoke-flat-repo branch May 27, 2026 08:17
seungdo-keum added a commit that referenced this pull request May 28, 2026
…, skill promotion

Three new autonomous agents that close the AI-Native Agent loop's
"proactive preparation + intelligent intervention" principles:

- P2-02 src/lib/agents/anomaly-detector.ts — scans each client's
  ClientContext + recent ConversationMessages for out-of-pattern
  signals (threshold breaks, contradicting facts, unusual cadence).
  Writes ApprovalItem of type "anomaly_alert" with severity
  high/medium/low → priority 80/50/25. Confidence < 0.65 outputs
  drop silently so the queue doesn't fill with noise. Pulls active
  pattern-learner rules into the system prompt so previously-
  dismissed pattern shapes get downweighted automatically.

- P2-03 src/lib/agents/comms-drafter.ts — once-daily (17:00 local),
  drafts client-facing reminder emails for each pending action item
  that needs a recipient nudge. Tone-aware via client preferences +
  pattern-learner. Each draft becomes an ApprovalItem of type
  "email_draft" with priority weighted by nudgeWithinDays so soonest-
  needed nudges sort to the top of the queue.

- P2-05 src/lib/skill-generator.ts — nightly batch that promotes
  AgentRule rows from "candidate pattern" to "promoted skill" once
  appliedCount ≥ 5, confidence ≥ 0.85, and recent verdictWeight
  average ≥ 0.85. Promoted rules surface in the system prompt with
  stronger language ("APPLY this template" vs "consider this
  pattern"). Demotion is just a flag flip — no row deletes if the
  operator loses confidence later. Implemented inside the existing
  AgentRule schema rather than introducing a new Skill table; the
  promoted flag lives in the action JSON.

Lovable-mark gate touched: #8 (background agent producing real value
— nightly briefing + anomaly detector + comms drafter now stack
multiple ApprovalItems per active client per day).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 28, 2026
…03) + self-paging wired (P1-07)

Closes lovable-mark gate #2 (memory loop) for the *whole product*,
not just daily-briefing. Replaces the flat `prisma.clientContext.
findMany({ take: 50 })` block scattered across runner/daily-briefing/
anomaly-detector/comms-drafter/chat with a single 5-tier composer
that aggregates:

  - **T0 ClientProfile** (250 tokens) — always-inline core memory
    a-la Letta. Identity + industry + relationship length +
    operator-set tone / formats / note. Built from Client.preferences.
  - **T1 RollingDigest** (500 tokens) — nightly-compacted 30-day
    rolling summary stored in ClientContext.category="digest".
    Falls back to top-3 pinned facts when the compactor hasn't run
    yet. Zep "Community subgraph" plays this role.
  - **T2 VectorHits** (600 tokens) — hybridSearchKnowledgeBase
    (0.4×trigram + 0.6×cosine over content_embedding) +
    loadActiveFacts (bitemporal ClientFact rows). Only fires when
    the caller passes `query`. Mem0g-style structured beliefs +
    paragraph hits for grounding.
  - **T3 EpisodicTimeline** (300 tokens) — last 5 AgentTask
    summaries + last 3 ApprovalItem decisions, newest-first. Zep
    "Episode subgraph" — the model finally sees "we already sent
    that reminder Tuesday" as a tier rather than reconstructing
    from titles.
  - **T4 FirmPatterns** (250 tokens) — promoted vs candidate
    AgentRule entries. Surfaces the operator's repeated decisions
    to ALL agents now, not just daily-briefing. Promoted rules get
    "APPLY this template" framing per Hermes/skill-promotion lit.

Composer (`src/lib/memory/loader.ts`) does proportional scaling
when budget < 2000, greedy fit in T0→T1→T4→T3→T2 order, emit in
stable T0→T1→T4→T3→T2 sequence for prompt-cache stability.
Returns full per-tier observability map.

Migrations:

  - daily-briefing.ts → 1800-token budget, no query (full scan).
  - anomaly-detector.ts → 1700-token budget, query="anomaly out-of-
    pattern unusual transaction threshold".
  - comms-drafter.ts → 1700-token budget, query="pending action
    items requiring outbound nudge reminder".
  - chat/route.ts → 1500-token budget (tighter — tool-use loops
    compound the system prompt across rounds), query=last user
    message. renderSystemPrompt drops the legacy raw-contexts
    rendering and embeds `memory.prompt` directly under "Quick
    reference".

P1-03 Digest Compactor: `src/lib/memory/digest-compactor.ts` +
nightly cron `/api/cron/digest-compactor`. Every active client
(any ConversationMessage / AgentTask / ApprovalItem in last 30d)
gets one 400-word past-tense plain-English digest written by
Haiku 4 (~$0.30 per night for an entire firm). Idempotent — prior
digest rows flip to `category: "digest_archive"` in the same tx.
SLACK_WEBHOOK_URL receives an error ping when ≥1 client digest
fails. CRON_SECRET-gated; `DIGEST_COMPACTOR_DISABLED=1` is the
emergency kill.

P1-07 Self-paging: `recall_archival` tool was already wired into
chat tools by sub-agent A. Confirmed runner-managed agents see
the same tool dispatch path so background agents can self-page
mid-run. Hard 800-token cap injected into next-turn user message
prevents runaway recall.

Operator probe: `GET /api/dev-test/memory-snapshot?clientId=...&
query=...&budget=...` — auth-gated, returns the rendered prompt
+ per-tier breakdown so an operator can see exactly what their
agents are loading.

Token economics: median chat turn dropped from ~3.5K input tokens
to ~1.7K (-52%) on seeded fixture client. With 200 active clients
× 12 chat turns/day × Anthropic Sonnet 4.5 input pricing, that's
~$1.50/day saved per active firm — or ~$45/month per firm of
useless context cost.

Validation:
- `npm run type-check` clean.
- `npm run lint` clean (only pre-existing warnings in unrelated
  files).
- `vitest src/lib/memory/loader.test.ts` 19/19 pass — token
  counter, T0 profile renderer, composer skip + budget bounds +
  preloadedClient short-circuit + missing-client fallback.
- New `tests/e2e/memory-tiering.spec.ts` covers prod auth-gated
  probe (skips when E2E_TEST_EMAIL unset).
- All 4 migrated agents + chat type-check + lint clean.

Research artifact: `.cycle/research/2026-04-28-memory-deep-dive.md`
documents the external pattern survey (Letta MemGPT, Zep/Graphiti,
Mem0/Mem0g, Cognee, MemKraft), brutal evaluation of the prior
flat-list path, and the design rationale for each tier choice.

Lovable-mark gates touched: #2 (memory loop closes — pattern
learner + temporal facts + vector + digest + episodic ALL
demonstrably influence every agent + chat output now), #8
(background agents producing real value — composer makes their
prompts coherent rather than wall-of-text).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 28, 2026
…efresh crons

Two new daily/weekly crons that close audit gap #8 (FactEdge writes
absent) and AEO research priority #6 (freshness refresh boosts
~2.5× citation rate).

1. **FactEdge inference** (`/api/cron/factedge-inference`, 04:00 UTC daily)
   - `inferEdgesForClient(clientId)` walks up to 30 active ClientFact
     rows per client, asks Claude (tool_use schema) to propose
     directed edges with one of 5 relations: causes / contradicts /
     refines / depends_on / follows.
   - Cost-bounded: skips clients with < 5 active facts. One Claude
     call per client. ~$0.50/day across ~10 firms × 5 clients.
   - Idempotent: writes via the existing
     `@@unique([fromFactId, toFactId, relation])` constraint; on
     conflict refreshes weight if higher. Re-running is safe.
   - Edge weight floor 0.55 — proposed edges below that drop. Match
     to the corpus's exact fact ids (no invented ids).
   - Slack agent_cron_summary / _warning emits aggregate counters
     at the end. Failure-per-client doesn't kill the cron.

2. **30-day freshness refresh** (`/api/cron/freshness-refresh`, 09:00 UTC Tuesday)
   - Picks up to 6 oldest blog posts whose `dateModified` is > 30
     days ago. Per post, asks Claude to spot ONE stale claim + propose
     a replacement (verbatim phrase to update + 1-3 sentence proposed
     update + rationale + confidence).
   - Persists proposals to `AuditLog` with `action="content_freshness_proposal"`.
     Operator reviews via the audit table or a future admin page.
     **No automated PR creation** — the operator must validate +
     manually apply, which keeps the cron Vercel-runtime-safe (no
     gh credentials required).
   - 14-day cooldown per slug so a previously-proposed post doesn't
     get a duplicate proposal next Tuesday.
   - Confidence < 0.6 → skip. Most posts on most weeks have nothing
     to refresh; empty answer is correct most of the time.
   - Cost: ~$0.12/week (6 posts × ~$0.02 each at Sonnet 4.5).

Safety guarantees:
- Both crons are READ-mostly. Only writes are FactEdge rows (existing
  schema + unique constraint) and AuditLog rows (existing model).
- Both use the existing CRON_SECRET / SEO_DEPLOY_SECRET / x-vercel-cron
  auth path that all other crons share.
- Both fail closed on per-item errors so one bad client / post
  doesn't cascade.
- vercel.json gets 2 new entries (Hobby-compliant: daily + weekly,
  not hourly).

233/233 vitest pass · type-check ✅. New crons additive — zero
behavior change to existing daily-briefing / anomaly-detector /
comms-drafter / digest-compactor flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 28, 2026
* fix(selfhost): align Docker + smoke CI with flat repo layout

The docker/ skeleton and selfhost-smoke.yml were drafted assuming an
apps/web monorepo layout, but the extracted repo is flat (web app at the
root, packages/mcp the only workspace). That mismatch made the self-host
smoke matrix unrunnable. This fixes the whole path end-to-end:

- Dockerfile.web: build at repo root (no apps/web / packages/core), use
  `npm install` (repo ships no lockfile), `npx prisma generate`, and a
  full-copy `next start` runner (robust vs. standalone tracing). Copy
  prisma.config.ts so `db push` resolves schema + datasource at runtime.
- entrypoint.sh: schema path -> root; switch `migrate deploy` -> `db push`
  (no migrations/ folder ships); POSIX while-loop; no --accept-data-loss
  so a destructive diff fails loudly instead of dropping self-hoster data.
- New GET /api/healthz liveness probe (server up + Postgres reachable),
  distinct from /api/health readiness which 503s on optional integrations.
  Both the compose healthcheck and the CI probe already point at it.
- selfhost-smoke.yml: flat-repo path filters (src/prisma/public/configs),
  and COMPOSE_FILE=docker/docker-compose.yml so every `docker compose`
  step finds the file without -f.
- .gitattributes: pin Dockerfile* to LF (Alpine build safety).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): drop npm cache from setup-node (repo ships no lockfile)

actions/setup-node@v4 with `cache: 'npm'` requires a lockfile to compute
its cache key. The repo intentionally ships no package-lock.json, so every
CI job was failing at the setup-node step before install even ran. Drop
the cache directive across all five jobs (type-check / lint / build / test
/ mcp-pack) so installs run via `npm install`.

Follow-up (not this PR): commit a package-lock.json to restore cacheable,
reproducible installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): run prisma generate before type-check and test

The Prisma client uses the new prisma-client generator with a custom
output (src/generated/prisma), imported as @/generated/prisma/client.
The type-check and test jobs never generated it, so prisma.* resolved to
`any` — the root of the bulk of the strict-mode TS7006/TS2339 cascade.
The build job + Docker already run generate; add it to these two jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): restore googleapis (dropped in extraction, still imported)

The gmail/outreach + google-sc modules import googleapis, but the dep was
dropped during the OSS package.json restructuring, causing 7 TS2307
"Cannot find module 'googleapis'" errors that cascade. Restore the exact
version the app uses (^144.0.0).

Note: this operator growth/outreach tooling (outreach crons, Search Console
SEO automation, Slack summaries) is surplus to the self-host product and is
tracked for removal as a follow-up; restoring the dep unblocks the build now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): cast apiVersion to LatestApiVersion (lockfile-free SDK drift)

The last 2 of 149 type errors: stripe@^22 caret floats (no lockfile) and its
pinned-version literal type drifted to 2026-04-22.dahlia, rejecting our
deliberately-pinned 2026-03-25.dahlia. Cast to Stripe.LatestApiVersion to keep
the intended runtime value type-stable across SDK minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion (Stripe.LatestApiVersion type not exported)

The cast to Stripe.LatestApiVersion failed (TS2694 — not an exported member
in stripe@22). Omitting apiVersion entirely is always type-valid and uses
the Stripe account's default pinned version — the correct default for a
self-host deployment, and immune to the lockfile-free SDK caret drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): native build job for macos-14 (Apple Silicon can't run Docker)

GitHub arm64 macOS runners lack nested virtualization, so Colima/docker
compose fails at VM start (exit 125) — macos-14 could never pass the compose
smoke. Split the matrix: docker-compose smoke runs on ubuntu-22.04 + macos-13
(full boot + DB + healthz), and a new native-build job proves Apple-Silicon
parity by running prisma generate + next build on arm64 macOS (exercises the
arch-sensitive Prisma engine / bcryptjs / lightningcss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion in e2e test + bootstrap script too

tsc includes tests/, so the e2e spec's hardcoded apiVersion literal was the
last type error (the bootstrap script mirrors it for consistency). Same fix
as the client: omit apiVersion to stay type-stable across stripe@22 minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): restore prototype chain on BudgetExceededError

The class extends the built-in Error; transpilation breaks `instanceof` for
genuine instances unless the prototype is reset. Add Object.setPrototypeOf
in the constructor — fixes the lone failing vitest assertion
(token-budget.test.ts > assertBudget — trial plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): raise V8 heap for next build (was OOMing on CI runner)

`next build` of this heavy app (pdfjs/exceljs/pptxgenjs/tiptap/react-pdf)
exceeded the default V8 old-space heap on the 7 GB ubuntu runner —
"Ineffective mark-compacts near heap limit". Set NODE_OPTIONS
--max-old-space-size=6144 in the CI build job and the Docker builder stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): ubuntu-only docker + native build matrix for both Macs

Colima on the Intel macos-13 runner is too memory-starved (4 GB VM) to
build this app and is slow/flaky; arm64 macos-14 can't run Docker at all.
Drop Docker-on-macOS entirely: keep the full docker-compose smoke on
ubuntu-22.04 (the canonical Linux self-host target), and prove macOS
self-host parity (both Intel + Apple Silicon) via a native `next build`
matrix [macos-13, macos-14] with the same heap bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): update stale trial-allowance test (200K → constant)

The real cause of the failing assertion: FREE_TRIAL.trialTotalTokens was
bumped 200K → 700K on 2026-05-15, but the test still fed 200K usage and
expected a trial_exceeded throw. 200K < 700K → no throw → expect.fail()
threw the AssertionError we saw. Reference FREE_TRIAL.trialTotalTokens
directly so the test tracks the constant and can't go stale again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): install workspace deps so next build finds @modelcontextprotocol/sdk

The Docker builder copied only the root package.json before npm install, so
the packages/mcp workspace deps were never installed. `next build`
type-checks the whole project (incl. packages/mcp, which imports
@modelcontextprotocol/sdk) and failed: "Cannot find module
'@modelcontextprotocol/sdk/server/stdio.js'". Copy packages/mcp/package.json
before install so npm (workspaces) resolves it. (CI Build + native macOS
passed because their npm install ran against the full checkout.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): runtime boot — drop Prisma 7-invalid --skip-generate + ship client

Two runtime issues that kept the web container from going healthy:
1. entrypoint ran `prisma db push --skip-generate`, but Prisma 7 removed that
   flag — it errored, printed help, and crashed the container (set -e).
   Use a plain `prisma db push`.
2. The Prisma client uses a custom output (src/generated/prisma, not
   node_modules/.prisma) that the runner stage never copied, so the server
   + db push couldn't resolve @/generated/prisma/client. Copy it explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(selfhost): pgvector Postgres image + create extension; drop dead macos-13

Two things kept the ubuntu docker-compose smoke from going healthy / the run
from completing:
1. The schema has a vector(1024) column but docker-compose used
   postgres:16-alpine (no pgvector) and nothing created the extension, so
   `prisma db push` failed: ERROR: type "vector" does not exist. Switch to
   pgvector/pgvector:pg16 + a first-boot init that runs CREATE EXTENSION
   vector. This is a real self-host correctness fix, not just CI.
2. macos-13 native build sat queued 45+ min (GitHub Intel-mac runner
   unavailability) and wedged the whole workflow from ever completing. Drop
   it from the matrix; macos-14 (Apple Silicon) covers native build parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): gate on explicit verbose healthz probe, not compose --wait

The container went "unhealthy" (compose --wait failed) even though the app
boots fine (Postgres reachable, db push synced, Next.js Ready). Decouple:
`docker compose up -d --build` (no --wait), and let the explicit probe step
gate — now printing the HTTP code + body each attempt so a 503 reveals the
actual healthz error instead of an opaque "unhealthy". Either goes green or
surfaces the precise runtime cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): assert build+boot+db-sync (fatal); HTTP serve = known-gap

Operator decision: merge now, track the Docker HTTP-serving gap. The ubuntu
job now asserts the core self-host proof as fatal — image builds, container
boots, Postgres reachable, schema synced via prisma db push — and treats the
in-container HTTP route probe as non-blocking, because the hand-rolled
`next start` runtime doesn't serve built routes (404; needs output:standalone,
tracked as a follow-up issue). Makes selfhost-smoke green on what genuinely
works instead of red on the one packaging gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 29, 2026
…, skill promotion

Three new autonomous agents that close the AI-Native Agent loop's
"proactive preparation + intelligent intervention" principles:

- P2-02 src/lib/agents/anomaly-detector.ts — scans each client's
  ClientContext + recent ConversationMessages for out-of-pattern
  signals (threshold breaks, contradicting facts, unusual cadence).
  Writes ApprovalItem of type "anomaly_alert" with severity
  high/medium/low → priority 80/50/25. Confidence < 0.65 outputs
  drop silently so the queue doesn't fill with noise. Pulls active
  pattern-learner rules into the system prompt so previously-
  dismissed pattern shapes get downweighted automatically.

- P2-03 src/lib/agents/comms-drafter.ts — once-daily (17:00 local),
  drafts client-facing reminder emails for each pending action item
  that needs a recipient nudge. Tone-aware via client preferences +
  pattern-learner. Each draft becomes an ApprovalItem of type
  "email_draft" with priority weighted by nudgeWithinDays so soonest-
  needed nudges sort to the top of the queue.

- P2-05 src/lib/skill-generator.ts — nightly batch that promotes
  AgentRule rows from "candidate pattern" to "promoted skill" once
  appliedCount ≥ 5, confidence ≥ 0.85, and recent verdictWeight
  average ≥ 0.85. Promoted rules surface in the system prompt with
  stronger language ("APPLY this template" vs "consider this
  pattern"). Demotion is just a flag flip — no row deletes if the
  operator loses confidence later. Implemented inside the existing
  AgentRule schema rather than introducing a new Skill table; the
  promoted flag lives in the action JSON.

Lovable-mark gate touched: #8 (background agent producing real value
— nightly briefing + anomaly detector + comms drafter now stack
multiple ApprovalItems per active client per day).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 29, 2026
…03) + self-paging wired (P1-07)

Closes lovable-mark gate #2 (memory loop) for the *whole product*,
not just daily-briefing. Replaces the flat `prisma.clientContext.
findMany({ take: 50 })` block scattered across runner/daily-briefing/
anomaly-detector/comms-drafter/chat with a single 5-tier composer
that aggregates:

  - **T0 ClientProfile** (250 tokens) — always-inline core memory
    a-la Letta. Identity + industry + relationship length +
    operator-set tone / formats / note. Built from Client.preferences.
  - **T1 RollingDigest** (500 tokens) — nightly-compacted 30-day
    rolling summary stored in ClientContext.category="digest".
    Falls back to top-3 pinned facts when the compactor hasn't run
    yet. Zep "Community subgraph" plays this role.
  - **T2 VectorHits** (600 tokens) — hybridSearchKnowledgeBase
    (0.4×trigram + 0.6×cosine over content_embedding) +
    loadActiveFacts (bitemporal ClientFact rows). Only fires when
    the caller passes `query`. Mem0g-style structured beliefs +
    paragraph hits for grounding.
  - **T3 EpisodicTimeline** (300 tokens) — last 5 AgentTask
    summaries + last 3 ApprovalItem decisions, newest-first. Zep
    "Episode subgraph" — the model finally sees "we already sent
    that reminder Tuesday" as a tier rather than reconstructing
    from titles.
  - **T4 FirmPatterns** (250 tokens) — promoted vs candidate
    AgentRule entries. Surfaces the operator's repeated decisions
    to ALL agents now, not just daily-briefing. Promoted rules get
    "APPLY this template" framing per Hermes/skill-promotion lit.

Composer (`src/lib/memory/loader.ts`) does proportional scaling
when budget < 2000, greedy fit in T0→T1→T4→T3→T2 order, emit in
stable T0→T1→T4→T3→T2 sequence for prompt-cache stability.
Returns full per-tier observability map.

Migrations:

  - daily-briefing.ts → 1800-token budget, no query (full scan).
  - anomaly-detector.ts → 1700-token budget, query="anomaly out-of-
    pattern unusual transaction threshold".
  - comms-drafter.ts → 1700-token budget, query="pending action
    items requiring outbound nudge reminder".
  - chat/route.ts → 1500-token budget (tighter — tool-use loops
    compound the system prompt across rounds), query=last user
    message. renderSystemPrompt drops the legacy raw-contexts
    rendering and embeds `memory.prompt` directly under "Quick
    reference".

P1-03 Digest Compactor: `src/lib/memory/digest-compactor.ts` +
nightly cron `/api/cron/digest-compactor`. Every active client
(any ConversationMessage / AgentTask / ApprovalItem in last 30d)
gets one 400-word past-tense plain-English digest written by
Haiku 4 (~$0.30 per night for an entire firm). Idempotent — prior
digest rows flip to `category: "digest_archive"` in the same tx.
SLACK_WEBHOOK_URL receives an error ping when ≥1 client digest
fails. CRON_SECRET-gated; `DIGEST_COMPACTOR_DISABLED=1` is the
emergency kill.

P1-07 Self-paging: `recall_archival` tool was already wired into
chat tools by sub-agent A. Confirmed runner-managed agents see
the same tool dispatch path so background agents can self-page
mid-run. Hard 800-token cap injected into next-turn user message
prevents runaway recall.

Operator probe: `GET /api/dev-test/memory-snapshot?clientId=...&
query=...&budget=...` — auth-gated, returns the rendered prompt
+ per-tier breakdown so an operator can see exactly what their
agents are loading.

Token economics: median chat turn dropped from ~3.5K input tokens
to ~1.7K (-52%) on seeded fixture client. With 200 active clients
× 12 chat turns/day × Anthropic Sonnet 4.5 input pricing, that's
~$1.50/day saved per active firm — or ~$45/month per firm of
useless context cost.

Validation:
- `npm run type-check` clean.
- `npm run lint` clean (only pre-existing warnings in unrelated
  files).
- `vitest src/lib/memory/loader.test.ts` 19/19 pass — token
  counter, T0 profile renderer, composer skip + budget bounds +
  preloadedClient short-circuit + missing-client fallback.
- New `tests/e2e/memory-tiering.spec.ts` covers prod auth-gated
  probe (skips when E2E_TEST_EMAIL unset).
- All 4 migrated agents + chat type-check + lint clean.

Research artifact: `.cycle/research/2026-04-28-memory-deep-dive.md`
documents the external pattern survey (Letta MemGPT, Zep/Graphiti,
Mem0/Mem0g, Cognee, MemKraft), brutal evaluation of the prior
flat-list path, and the design rationale for each tier choice.

Lovable-mark gates touched: #2 (memory loop closes — pattern
learner + temporal facts + vector + digest + episodic ALL
demonstrably influence every agent + chat output now), #8
(background agents producing real value — composer makes their
prompts coherent rather than wall-of-text).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 29, 2026
…efresh crons

Two new daily/weekly crons that close audit gap #8 (FactEdge writes
absent) and AEO research priority #6 (freshness refresh boosts
~2.5× citation rate).

1. **FactEdge inference** (`/api/cron/factedge-inference`, 04:00 UTC daily)
   - `inferEdgesForClient(clientId)` walks up to 30 active ClientFact
     rows per client, asks Claude (tool_use schema) to propose
     directed edges with one of 5 relations: causes / contradicts /
     refines / depends_on / follows.
   - Cost-bounded: skips clients with < 5 active facts. One Claude
     call per client. ~$0.50/day across ~10 firms × 5 clients.
   - Idempotent: writes via the existing
     `@@unique([fromFactId, toFactId, relation])` constraint; on
     conflict refreshes weight if higher. Re-running is safe.
   - Edge weight floor 0.55 — proposed edges below that drop. Match
     to the corpus's exact fact ids (no invented ids).
   - Slack agent_cron_summary / _warning emits aggregate counters
     at the end. Failure-per-client doesn't kill the cron.

2. **30-day freshness refresh** (`/api/cron/freshness-refresh`, 09:00 UTC Tuesday)
   - Picks up to 6 oldest blog posts whose `dateModified` is > 30
     days ago. Per post, asks Claude to spot ONE stale claim + propose
     a replacement (verbatim phrase to update + 1-3 sentence proposed
     update + rationale + confidence).
   - Persists proposals to `AuditLog` with `action="content_freshness_proposal"`.
     Operator reviews via the audit table or a future admin page.
     **No automated PR creation** — the operator must validate +
     manually apply, which keeps the cron Vercel-runtime-safe (no
     gh credentials required).
   - 14-day cooldown per slug so a previously-proposed post doesn't
     get a duplicate proposal next Tuesday.
   - Confidence < 0.6 → skip. Most posts on most weeks have nothing
     to refresh; empty answer is correct most of the time.
   - Cost: ~$0.12/week (6 posts × ~$0.02 each at Sonnet 4.5).

Safety guarantees:
- Both crons are READ-mostly. Only writes are FactEdge rows (existing
  schema + unique constraint) and AuditLog rows (existing model).
- Both use the existing CRON_SECRET / SEO_DEPLOY_SECRET / x-vercel-cron
  auth path that all other crons share.
- Both fail closed on per-item errors so one bad client / post
  doesn't cascade.
- vercel.json gets 2 new entries (Hobby-compliant: daily + weekly,
  not hourly).

233/233 vitest pass · type-check ✅. New crons additive — zero
behavior change to existing daily-briefing / anomaly-detector /
comms-drafter / digest-compactor flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
seungdo-keum added a commit that referenced this pull request May 29, 2026
* fix(selfhost): align Docker + smoke CI with flat repo layout

The docker/ skeleton and selfhost-smoke.yml were drafted assuming an
apps/web monorepo layout, but the extracted repo is flat (web app at the
root, packages/mcp the only workspace). That mismatch made the self-host
smoke matrix unrunnable. This fixes the whole path end-to-end:

- Dockerfile.web: build at repo root (no apps/web / packages/core), use
  `npm install` (repo ships no lockfile), `npx prisma generate`, and a
  full-copy `next start` runner (robust vs. standalone tracing). Copy
  prisma.config.ts so `db push` resolves schema + datasource at runtime.
- entrypoint.sh: schema path -> root; switch `migrate deploy` -> `db push`
  (no migrations/ folder ships); POSIX while-loop; no --accept-data-loss
  so a destructive diff fails loudly instead of dropping self-hoster data.
- New GET /api/healthz liveness probe (server up + Postgres reachable),
  distinct from /api/health readiness which 503s on optional integrations.
  Both the compose healthcheck and the CI probe already point at it.
- selfhost-smoke.yml: flat-repo path filters (src/prisma/public/configs),
  and COMPOSE_FILE=docker/docker-compose.yml so every `docker compose`
  step finds the file without -f.
- .gitattributes: pin Dockerfile* to LF (Alpine build safety).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): drop npm cache from setup-node (repo ships no lockfile)

actions/setup-node@v4 with `cache: 'npm'` requires a lockfile to compute
its cache key. The repo intentionally ships no package-lock.json, so every
CI job was failing at the setup-node step before install even ran. Drop
the cache directive across all five jobs (type-check / lint / build / test
/ mcp-pack) so installs run via `npm install`.

Follow-up (not this PR): commit a package-lock.json to restore cacheable,
reproducible installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): run prisma generate before type-check and test

The Prisma client uses the new prisma-client generator with a custom
output (src/generated/prisma), imported as @/generated/prisma/client.
The type-check and test jobs never generated it, so prisma.* resolved to
`any` — the root of the bulk of the strict-mode TS7006/TS2339 cascade.
The build job + Docker already run generate; add it to these two jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): restore googleapis (dropped in extraction, still imported)

The gmail/outreach + google-sc modules import googleapis, but the dep was
dropped during the OSS package.json restructuring, causing 7 TS2307
"Cannot find module 'googleapis'" errors that cascade. Restore the exact
version the app uses (^144.0.0).

Note: this operator growth/outreach tooling (outreach crons, Search Console
SEO automation, Slack summaries) is surplus to the self-host product and is
tracked for removal as a follow-up; restoring the dep unblocks the build now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): cast apiVersion to LatestApiVersion (lockfile-free SDK drift)

The last 2 of 149 type errors: stripe@^22 caret floats (no lockfile) and its
pinned-version literal type drifted to 2026-04-22.dahlia, rejecting our
deliberately-pinned 2026-03-25.dahlia. Cast to Stripe.LatestApiVersion to keep
the intended runtime value type-stable across SDK minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion (Stripe.LatestApiVersion type not exported)

The cast to Stripe.LatestApiVersion failed (TS2694 — not an exported member
in stripe@22). Omitting apiVersion entirely is always type-valid and uses
the Stripe account's default pinned version — the correct default for a
self-host deployment, and immune to the lockfile-free SDK caret drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): native build job for macos-14 (Apple Silicon can't run Docker)

GitHub arm64 macOS runners lack nested virtualization, so Colima/docker
compose fails at VM start (exit 125) — macos-14 could never pass the compose
smoke. Split the matrix: docker-compose smoke runs on ubuntu-22.04 + macos-13
(full boot + DB + healthz), and a new native-build job proves Apple-Silicon
parity by running prisma generate + next build on arm64 macOS (exercises the
arch-sensitive Prisma engine / bcryptjs / lightningcss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion in e2e test + bootstrap script too

tsc includes tests/, so the e2e spec's hardcoded apiVersion literal was the
last type error (the bootstrap script mirrors it for consistency). Same fix
as the client: omit apiVersion to stay type-stable across stripe@22 minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): restore prototype chain on BudgetExceededError

The class extends the built-in Error; transpilation breaks `instanceof` for
genuine instances unless the prototype is reset. Add Object.setPrototypeOf
in the constructor — fixes the lone failing vitest assertion
(token-budget.test.ts > assertBudget — trial plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): raise V8 heap for next build (was OOMing on CI runner)

`next build` of this heavy app (pdfjs/exceljs/pptxgenjs/tiptap/react-pdf)
exceeded the default V8 old-space heap on the 7 GB ubuntu runner —
"Ineffective mark-compacts near heap limit". Set NODE_OPTIONS
--max-old-space-size=6144 in the CI build job and the Docker builder stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): ubuntu-only docker + native build matrix for both Macs

Colima on the Intel macos-13 runner is too memory-starved (4 GB VM) to
build this app and is slow/flaky; arm64 macos-14 can't run Docker at all.
Drop Docker-on-macOS entirely: keep the full docker-compose smoke on
ubuntu-22.04 (the canonical Linux self-host target), and prove macOS
self-host parity (both Intel + Apple Silicon) via a native `next build`
matrix [macos-13, macos-14] with the same heap bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): update stale trial-allowance test (200K → constant)

The real cause of the failing assertion: FREE_TRIAL.trialTotalTokens was
bumped 200K → 700K on 2026-05-15, but the test still fed 200K usage and
expected a trial_exceeded throw. 200K < 700K → no throw → expect.fail()
threw the AssertionError we saw. Reference FREE_TRIAL.trialTotalTokens
directly so the test tracks the constant and can't go stale again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): install workspace deps so next build finds @modelcontextprotocol/sdk

The Docker builder copied only the root package.json before npm install, so
the packages/mcp workspace deps were never installed. `next build`
type-checks the whole project (incl. packages/mcp, which imports
@modelcontextprotocol/sdk) and failed: "Cannot find module
'@modelcontextprotocol/sdk/server/stdio.js'". Copy packages/mcp/package.json
before install so npm (workspaces) resolves it. (CI Build + native macOS
passed because their npm install ran against the full checkout.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): runtime boot — drop Prisma 7-invalid --skip-generate + ship client

Two runtime issues that kept the web container from going healthy:
1. entrypoint ran `prisma db push --skip-generate`, but Prisma 7 removed that
   flag — it errored, printed help, and crashed the container (set -e).
   Use a plain `prisma db push`.
2. The Prisma client uses a custom output (src/generated/prisma, not
   node_modules/.prisma) that the runner stage never copied, so the server
   + db push couldn't resolve @/generated/prisma/client. Copy it explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(selfhost): pgvector Postgres image + create extension; drop dead macos-13

Two things kept the ubuntu docker-compose smoke from going healthy / the run
from completing:
1. The schema has a vector(1024) column but docker-compose used
   postgres:16-alpine (no pgvector) and nothing created the extension, so
   `prisma db push` failed: ERROR: type "vector" does not exist. Switch to
   pgvector/pgvector:pg16 + a first-boot init that runs CREATE EXTENSION
   vector. This is a real self-host correctness fix, not just CI.
2. macos-13 native build sat queued 45+ min (GitHub Intel-mac runner
   unavailability) and wedged the whole workflow from ever completing. Drop
   it from the matrix; macos-14 (Apple Silicon) covers native build parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): gate on explicit verbose healthz probe, not compose --wait

The container went "unhealthy" (compose --wait failed) even though the app
boots fine (Postgres reachable, db push synced, Next.js Ready). Decouple:
`docker compose up -d --build` (no --wait), and let the explicit probe step
gate — now printing the HTTP code + body each attempt so a 503 reveals the
actual healthz error instead of an opaque "unhealthy". Either goes green or
surfaces the precise runtime cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): assert build+boot+db-sync (fatal); HTTP serve = known-gap

Operator decision: merge now, track the Docker HTTP-serving gap. The ubuntu
job now asserts the core self-host proof as fatal — image builds, container
boots, Postgres reachable, schema synced via prisma db push — and treats the
in-container HTTP route probe as non-blocking, because the hand-rolled
`next start` runtime doesn't serve built routes (404; needs output:standalone,
tracked as a follow-up issue). Makes selfhost-smoke green on what genuinely
works instead of red on the one packaging gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant