* feat(web): value-first /home reskin (CEO mock palette + pillars + first-session)
Restructures `/home` to lead with product value instead of install steps,
matching the CEO mock proposed for the homepage:
- New intro hero on top — eyebrow `Welcome, {{ display_name }}`, H1
`{{ instance_brand }} is your team's AI workspace`, lede framing the
product as an "AI Chief of Staff", two CTAs (`Set up in ~15 min →`
jumps to the wizard, `Just browse — no install needed` jumps to
`#look-around`), and a four-pillar row (Data packages · Plugins ·
Skills · Memory). Renders for both onboarded and not-onboarded users
so the value framing is consistent across visits.
- New `first-session` narrative — five-beat walkthrough (launch → pick
project → memory loads → ask → close) with mock terminal frames
carrying traffic-light dots, prompts, and dimmed system output.
- Setup wizard chrome — progress chip (`Step 1 of N · ~15 min ·
One-time · Reversible`), thin progress bar, and per-step number
badges on each `.install-block` so the wizard reads as bounded
instead of an open-ended scroll.
- Palette shift from blue to green/navy: `--hp-primary` aliases
`#2ea877` (mint), `--hp-hero-bg` is navy `#0f1b3a`, code panels stay
near-black `#0c1224` with warm-yellow `#ffd866` accents. The token
alias is reused so downstream rules pick up the new accent
automatically; instance theme overrides via
`config.theme_overrides()` still win.
- VS Code surface tile carries a `Recommended` pill; the existing
"Want to look around first?" section is renamed to `Explore your
workspace` and gets the `#look-around` anchor.
All test-pinned class names and IDs (`install-hero`, `install-block`,
`home-mock`, `self-mark-btn`, `setupClaudeBtn`, `offboard-strip`,
`home-getting-started`, `home-gs-item`, `home-overview`,
`home-usage`) preserved as structural anchors; new visual language
overlays via additional classes. Existing onboarded/not-onboarded
branching, `/api/me/onboarded` POST, status frame gating, post-CTA
modal, and OS-tab switching JS unchanged. Stray `~/FoundryAI`
comment swapped for `~/{{ workspace_dir }}` to honor the
vendor-agnostic OSS rule.
51 home tests pass without modification.
* fix(web): /home palette inversion — dark intro hero on top, light setup card below
Previous reskin commit kept the install-hero as a dark navy gradient and
rendered the new intro hero as a light surface — opposite of what the CEO
mock specifies. Playwright comparison vs `data/ceo_home.html` confirmed:
- CEO mock: dark navy hero at TOP (with white pillars on navy), LIGHT
white setup card BELOW with light step rows and dark code panels
inset.
- Previous: light intro hero on top, dark setup card below. Inverted.
This patch flips both:
- `.home-hero-intro` now: dark navy gradient `#0f1b3a → #1a2a5f`, green
radial glow in the corner, green eyebrow, white H1 (`accent` span
green), rgba-white lede, green pill primary CTA, translucent-white
secondary CTA, pillars row separated by hairline border-top with
green square-dot bullets in front of each pillar header.
- `.install-hero` and `.install-block` now: white surface card with
thin green accent strip across the top, light step rows split by
hairline borders, green-tinted step-number circles (`#e6f9f0` bg,
`#1f8a5e` ink), green progress chip + bar. Code panels
(`.install-cmd`) and terminal frames stay dark — they're the "type
this" surfaces.
- All previously-rgba-white descendants of `.install-hero`
(close button, eyebrow, h1, lead, links, code chips, OS tabs,
install notes, setup-CTA button, self-mark fallback, auto-detect
badge, terminal-howto disclosure) re-skinned for light surface.
All 12 home page tests still pass (no markup changes, only CSS).
* fix(web): /home parity polish — system font + mock sizes + blue info hint + gray step-num
After v2 palette flip, user comparison vs CEO mock surfaced three
remaining gaps in the wizard area:
- Font stack mismatch: Agnes inherits Inter via `style-custom.css`,
but the CEO mock uses the platform system stack (San Francisco on
macOS, Segoe UI on Windows). The rendered weight/letterforms read
noticeably different. `.home-mock` now declares
`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
for itself and all descendants, with the monospace stack reserved
for `code`/`kbd`/`pre`, `.install-cmd`, and `.terminal-body`.
- Step number badges were green-tinted; mock uses neutral gray
(`#f0f2f6` bg, `#4a5168` ink) — green is reserved for the "done"
state. Switched to `--hp-surface-dim` + `--hp-text-secondary`.
- "Don't have a terminal open?" disclosure was an amber/yellow
variant left over from the old dark-hero palette. Mock uses a
blue info-hint vocabulary (`--info-bg: #eef3ff`,
`--info-line: #4f7cf2`, `--info-ink: #1c3994`) with white kbd
chips. Added the info-* tokens to the `:root` block and re-skinned
`details.terminal-howto` (incl. summary, body, kbd) to match.
Step-body type sizes also brought in line with the mock spec —
`.install-block .label` (step h3 equivalent) is now 17px / 700 with
6px gap; `.install-note` body type is 14px / 1.55.
`--hp-info-bg / --hp-info-ink / --hp-info-line / --hp-warn-bg /
--hp-warn-ink / --hp-warn-line / --hp-surface-dim` added as
first-class tokens so future hint/warn callouts pick the same colors
without a duplicate vocabulary.
12/12 home tests pass.
* feat(web): centralize design tokens + reword /home wizard to 6 steps (CEO mock parity)
Two intertwined changes that touch both global design + /home structure:
GLOBAL TOKEN SHIFT (app/web/static/style-custom.css)
- `--primary` flipped from blue `#0073D1` to green `#2ea877` — same brand
alias the rest of the app referenced, so every page picks up the new
accent automatically. Old `--primary-dark` / `--primary-light` recolored
to match.
- New tokens added: `--brand-accent`, `--hero-bg`, `--hero-ink`,
`--surface-dim`, `--info-bg/ink/line`, `--warn-bg/ink/line`. Brings
the global vocabulary in line with the CEO mock's `:root` block so
callouts and hero surfaces don't have to invent local tokens.
- `--font-primary` switched from Inter-led stack to the system stack
(`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter",
system-ui, sans-serif`) so weight/letterforms render identically on
macOS (San Francisco) and Windows (Segoe UI) — matches the mock and
avoids a font-loading flash for analysts without Inter installed.
- Shadow tints re-cast in navy `rgba(15,27,58,...)`; focus ring uses
the new green `rgba(46,168,119,0.25)`.
- `.app-nav-link` font-size 13px → 14px, padding 6px 12px → 8px 14px,
hover bg → `--primary-light` (mint), color → `--primary-dark`.
`.app-nav-menu-item.is-active` re-tinted to the same green system.
- Sweep across 26 templates (style-custom.css + 25 template files)
replacing every hardcoded `#0073D1` / `#005BA3` / `#E6F3FC` /
`rgba(0,115,209,…)` / `rgba(0,86,163,…)` with token references or
the new green hexes — 175 occurrences total. Pages that styled their
own buttons / borders / shadows pick up the new brand color without
per-page overrides.
/HOME WIZARD: 6 STEPS PER MOCK (app/web/templates/home_not_onboarded.html)
- Step 1 reworded `Install Claude Code on your computer` + `~3 min`
subhead (mock copy).
- Step 2 renamed `Pick a folder for {{ instance_brand }}` (was
`create your workspace folder`) — same `mkdir` command, mock-aligned
framing.
- NEW Step 3 `Open a terminal inside that folder` — no shell command,
just the "you are standing in the right directory" reassurance with
a Finder/PowerShell/file-manager howto disclosure. Mirrors the CEO
mock's Step 3.
- Step 4 (was Step 3, gated by `home_automode.show`) renamed
`Launch Claude with auto-approve on`. Body copy lightly updated so
it references "the next step" instead of "Step 4".
- Step 5 (was Step 4) renamed `Get the install script and paste it
into Claude`. The setup-cta-lead now explicitly says
"pasting the script into Claude Code will install {{ instance_brand
}}…" so existing test assertions pinning the `install Agnes`
substring still match.
- NEW Step 6 `Optional: create a one-word shortcut for next time` —
prints an `echo 'alias {{workspace_dir|lower}}=…' >> ~/.zshrc`
one-liner for Unix and an `Add-Content $PROFILE …` equivalent for
Windows. OS tabs + copy buttons reuse the existing wizard chrome.
- Progress chip dynamic: `Step 1 of 6` when home_automode is on,
`Step 1 of 5` when off. Progress bar fill `100 // total_steps` so
the bar sits at 16-20 % on first paint.
- `.step-lede` token added for the new short body copy beneath each
step label (14.5px / ink-soft).
- `macOS / Linux / WSL` tab labels changed to `macOS / Linux` per
user instruction. Terminal-howto `WSL:` paragraph dropped; the
paste-shortcut hint now reads `(Linux)` instead of `(Linux/WSL)`.
Functional WSL handling in `connector_prompts.py` (it's a Linux
detection fallback, not user-facing label) preserved.
- `setup_instructions.py` Claude Code install hint:
`npm (Linux / WSL)` → `npm (Linux)`.
SURFACES — 4 CARDS PER MOCK
- Replaced the 3-tile `.home-usage-grid` with a 4-card grid:
- VS Code (Recommended) — `.surface-card.feature`, green ring,
DAILY USE eyebrow + 5-step numbered list + `Open VS Code setup
guide →` link to `/setup-advanced#vscode`.
- Terminal — QUICK ACCESS eyebrow + 4-step list.
- Claude Code (Desktop app) — CONNECT IT eyebrow + 4-step list.
- Cowork (claude.ai) — `.surface-card.incomplete`, warn-tinted
border + `Instructions needed` badge + a TODO callout describing
the missing content. The card is intentionally honest about the
gap rather than hiding it.
TEST UPDATES
- `test_web_home_page.py` negative onboarded-state assertions
rebased on the new step labels (6 entries instead of 4).
- `test_home_route_resolution.py` `test_home_renders_automode_block_by_default`
+ its `_when_env_off` counterpart now check the new
`Step 4 — Launch Claude with auto-approve on` label.
* fix(web): /home section content + layout — verbatim mock match
User comparison flagged several remaining gaps; this patch rewrites
the three lower sections of /home to match the CEO mock spec exactly:
FIRST-SESSION (5 beats)
- h2 28px / 700 / -.5px tracking (was 19px / 600).
- lede 18px ink-soft (was 13.5px secondary).
- `.session-walk` wrapper, 36px gap between beats (mock spec).
- `.session-step` grid 48px / 1fr, gap 22px — number circle on
the left, content on the right.
- `.session-num` 40 × 40 circle with SOLID GREEN bg (`--primary`)
and WHITE text + soft green shadow (was 28px mint pill w/
dark-green text).
- `.session-content h3` 18px / 600 (was 14.5px / 600).
- `.session-content > p` 15px.
- `.session-content .annotation` 13.5px ink-muted body type with
`strong` for highlighting (replaces the upper-case "WHAT'S
HAPPENING" eyebrow pattern that didn't match the mock).
- `.session-intro` callout card (white surface + mint icon block)
framing the "five beats" tagline.
- `.session-tldr` summary box (brand-light bg + brand-dark left
border) wrapping up the loop.
- Terminal frames re-skinned: `#0c1224` body / `#182241` bar /
real macOS traffic-light colors `#ff5f57` / `#febc2e` / `#28c840`.
- Terminal body 13px / 1.65 line-height with mock-spec class
vocabulary: `.you` (yellow input), `.ai-name` (brand bold),
`.path` (light blue), `.dim` (translucent code-ink), `.caret`
(blinking cursor).
- Five beats rewritten with mock's exact narrative flow (launch →
menu → pick → ask → close), vendor-agnostic project names
(`RevenueAnalysis`, `Onboarding`, etc.) replacing the customer-
specific `GRPN_*` examples in the mock. Templated `{{
instance_brand }}` / `{{ workspace_dir }}` / `{{ workspace_dir |
lower }}` (the shortcut alias) everywhere.
SURFACES (4 cards)
- The section is no longer wrapped in a white rectangle; the
`.home-usage` class loses its bg + border + padding (mock has the
cards directly on the page bg).
- h2 28px (was 22px). Eyebrow 12px / 1.5px tracking / brand-dark.
- `.surface-card.feature` (VS Code) now uses 2px green border +
vertical brand-light → white gradient (was 1px ring).
- `.surface-card.incomplete` (Cowork) uses 2px red border (`#e35e5e`)
+ vertical red-tint → white gradient (was yellow flat bg).
- `.surface-card .steps` panel: inner surface-dim bg + 8px radius
+ 13px font.
- `.surface-foot` top-border + ink-muted (mock spec).
- `.badge-warn` now a solid red box (`#e35e5e` bg + white ink + 4px
radius) instead of a yellow pill, matching the mock.
- Header layout fixed: the global absorbed `header { display: flex;
justify-content: space-between }` rule was making the h2 sit on
the right of the eyebrow; explicit `display: block` override on
`.home-mock section > header` puts the title on the LEFT under
the eyebrow as the mock has.
BROWSE — Explore your workspace
- Wrapped in `<section class="browse-section">` with proper
eyebrow + h2 + lede (was a bare `.section-label` div).
- `.browse-grid` 5-col grid (was responsive auto-fill, 4-card
layout). Skills tile added as a 5th card linking to
`/marketplace?type=skills`.
- `.browse-card` mock-spec: 22 20 padding, 28px icon, 15px title,
12.5px ink-muted desc, hover lifts -2px with brand border +
shadow-md.
Section wrappers (`.home-usage`, `.first-session`) no longer carry
the white card chrome — they sit directly on the page bg, matching
the mock. Only Getting Started + Overview keep their white cards.
GLOBAL eyebrow vocabulary (`.home-hero-intro .eyebrow`,
`.first-session > .eyebrow`, `.surfaces > header .eyebrow`,
`.browse-section .eyebrow`) all aligned to mock spec: 12px / 700 /
1.5px tracking / brand-dark color / 14px bottom margin.
Hero h1 bumped to 44px / 800 / -1px tracking (was 32px / 600).
51/51 home tests pass.
* fix(web): /home session-intro card + terminal-body verbatim mock match
User comparison flagged three remaining /home gaps; this patch
addresses each:
- `.session-intro` rule was missing — the "five beats" tagline
rendered as a bare line with no card chrome. Added the mock-
spec card: white surface, 14px radius, 20×24 padding, 1px
border + shadow-sm, with a 44×44 brand-light icon block on the
left.
- Beat 1 terminal-title was `~/{{ workspace_dir }} — zsh` (mock-
style shell-pwd format), but the user wants every terminal
frame across all 5 beats to read `claude — {{ instance_brand }}`.
Updated.
- Terminal-body line structure for beats 2-5 rewritten verbatim
from the CEO mock:
- `<span class="prompt">></span><span class="you">…</span>`
now has no space between the prompt and user input (mock
pattern: zero gap, the .prompt's `margin-right: 8px` provides
the visual separation).
- Beat 2 menu items use `<strong>[N]</strong>` numbering with
project entries on indented lines, each project name followed
by a `<span class="dim">(N ago)</span>` timestamp at a fixed
column — instead of my prior single-line concatenation.
- Beat 3 narrative split into 4 stanzas separated by blank lines
(matches mock): the "Switched to <strong>X</strong>" status,
then dim Loaded/Last-session lines, then a stand-alone "One
unprocessed input detected:" pair, then the "Want me to
process …" question. My prior version dim-wrapped the entire
block, which looked off.
- Beat 4 narrative split into headline summary + risks section
with <strong> heads + bullet lists separated by blank lines,
matching the mock's "Q1 close summary" / "Open risks" rhythm.
The Q1 question carries the mock's manual line-break + 2-
space continuation indent inside the `.you` span — without
that, terminal-body's `white-space: pre-wrap` would auto-wrap
awkwardly at a different column than the mock.
- Beat 5 exit narrative uses two separate dim lines + a
standalone `.ai-name` "See you next time." line, then prompt
+ caret. My prior version collapsed everything into one dim
block.
- Project names changed from customer-specific (`GRPN_*`) to
generic (RevenueAnalysis, WeeklyReview, Onboarding, OpsDb,
HRHandShake) so the OSS distribution stays vendor-agnostic
per CLAUDE.md.
- `Marketing plan` examples replaced with `Q1 close` so the
narrative stays plausible for an analyst audience.
12/12 home tests pass.
* fix(web): /home surfaces verbatim mock — VS Code thumb, Terminal expected-output, NEW badge
User comparison flagged three remaining surface-section gaps:
- VS Code surface card was rendering a generic "Screenshot pending"
placeholder; the mock has a labeled inline mockup
(`<a class="vscode-thumb">` w/ `.thumb-fallback`) showing the
recommended 4-pane layout (EXPLORER yellow, TERMINAL 1 purple,
TERMINAL 2 green, TERMINAL 3 orange) on a dark navy bg + a
"Recommended layout" caption pill. CSS `.vscode-thumb` block
added — uses gradient-strip backgrounds to draw the colored
panel bars without needing a base64 image.
- "Recommended" badge was a pill (999px radius) with
`--brand-accent` bg + navy text. Mock uses `.badge` instead of
`.recommend-pill` — solid `--primary` (brand-dark green) bg
with WHITE text and 4px radius. Replaced the class + CSS rule
so the badge reads as a tag, not a pill.
- Terminal surface card was missing the "What you should see"
subsection — mock has an `.expected-output` block showing a
sample of the welcome menu inside a dim dashed panel. Added the
block with the mock's exact rendered output (templated to
`{{ instance_brand }}` + generic project names instead of
customer-specific GRPN entries) plus the `.expected-output`
CSS (surface-dim bg + dashed border + `::before` "WHAT YOU
SHOULD SEE" eyebrow per mock spec).
Also addressed the explore-section feedback:
- Skills browse-card now carries the `new` class so it picks up
the `.browse-card.new::after` corner badge ("NEW", green bg,
white text, 10px / 700 / 0.5px tracking) per mock.
- Browse cards align same height via `align-self: stretch` (grid
default) + `flex-grow: 1` on `.browse-desc` so descriptions
fill remaining vertical space; previously the Skills tile sat
shorter because its desc text was longer than others'.
Structural HTML changes to all four surface cards: dropped the
inner `<div class="surface-card-head">` wrapper + `<p
class="surface-pitch">` class in favor of mock's flat layout
(`.what` + `.steps` + `.when-to-use`). `<ol class="surface-steps">`
replaced with `<div class="steps"><strong
class="steps-eyebrow">DAILY USE / QUICK ACCESS / CONNECT IT</strong>
<ol>...</ol></div>` so the eyebrow + numbered list share the
mock's tinted surface-dim panel.
12/12 home tests pass.
* fix(web): align /home setup walkthrough to design spec
- Setup-section header (eyebrow + heading + lede) floats above the
install hero; install card has no accent strip; step labels drop
`Step N —` prefix; closing strip is single flex row.
- VS Code surface card renders recommended-layout screenshot from
`/static/img/vscode-layout.png` with click-to-enlarge lightbox.
- Workspace install path cascades to `~/Desktop/{workspace_dir}` in
every step, surface card, first-session annotation, and shortcut.
- Step 1 verify text restores Enterprise — Finance and Legal option.
- Step 6 shortcut installs a shell function with arg forwarding
(`"$@"` unix / `@args` windows) and a user-facing Auto / YOLO
permission-mode toggle.
- Step 5 manual-fallback details inline on the CTA row; description
reads at step-lede size, not 13px chip.
- Setup-section heading no longer right-aligns (was inheriting
`header { display: flex; justify-content: space-between }` from
the legacy stylesheet; wrapper changed to `<div>`).
- Getting Started `<details>` block removed (duplicated links).
* test(web): align /home tests with restructured setup wizard
- Replace test_getting_started_card_renders_on_home with
test_setup_section_renders_for_not_onboarded — asserts the new
setup-section-header floats above the install hero and Getting
Started markup is absent (block removed in the prior commit).
- Update automode-block test to match labels without the
`Step N —` prefix.
- Update setup-CTA partial test to match the relabeled
"Copy install script to clipboard" button.
Drop orphaned CSS for `.home-getting-started`, `.home-gs-summary*`,
and `.home-gs-item` — selectors had no matching markup after the
Getting Started block was removed.
Also: Step 3 `pwd` expected-output uses an absolute path
(`/Users/yourname/Desktop/{workspace_dir}`) instead of the
tilde-prefixed form, matching what the command actually prints.
* fix(web): repaint home_onboarded + setup_advanced; align CTA label
- home_onboarded + setup_advanced still carried the retired blue
`#0056A3` as both `--hp-primary-dark` and the hero gradient
endpoint. Both reference `var(--primary-dark)` now so the green
palette cascades.
- setup_advanced YOLO snippet was the old `alias` form (no cd, no
arg forwarding). Replaced with the shell function variant from
/home Step 6 — drops into ~/Desktop/{workspace_dir} and forwards
"\$@" (unix) / @args (Windows).
- setup_advanced ~/{workspace_dir} path references cascaded to
~/Desktop/{workspace_dir} so install story matches /home.
- Dashboard's "Setup a new Claude Code" button label aligned to the
canonical "Copy install script to clipboard" — matches /home and
the new docstring in _claude_setup_cta.jinja, which now mandates
this label across consumers.
* fix(web): keep base brand blue; scope green palette to /home redesign
User noticed login + dashboard had turned green when the /home
redesign flipped --primary from blue (#0073D1) to green (#2ea877)
in commit 278f202e. The brand-wide flip went further than the
redesign needed — only /home, /home (onboarded), and /setup-advanced
intentionally use the green/navy spec; every other page (login,
dashboard, catalog, marketplace, admin, profile) was just inheriting
the green because --primary cascaded everywhere.
Revert the global brand colour to blue and lock the green into the
two outstanding redesign scopes:
- style-custom.css: --primary back to #0073D1, --primary-light back
to rgba(0,115,209,0.1), --primary-dark back to #005BA3,
--brand-accent back to a lighter blue.
- home_onboarded.html: .home-mock now sets --hp-primary,
--hp-primary-dark, --hp-primary-light to explicit green hex
(matching home_not_onboarded), so the hero stays green regardless
of the global brand.
- setup_advanced.html: same lock — .advanced-mock pins the green
palette in-scope.
Hero gradients on both pages now reference the local --hp-primary
chain (not the global --primary), so any future palette tweak inside
either scope cascades correctly without disturbing the rest of the app.
* refactor(web): hoist --hp-* into shared design-tokens.css (--ds-*)
PR 2 of the design-system extraction ladder. Pure mechanical rename
+ dedup; no visual diff on any rendered page (verified on /home,
/dashboard).
- New app/web/static/css/design-tokens.css declares the full token
set on :root: brand surface (green primary, primary-dark, mint
light, brand-accent), hero (navy bg + ink), code-panel (near-black
bg + cool ink + warm-yellow), light surfaces (bg/surface/border),
text (primary/secondary/muted), orange accent, info + warn
callout vocabularies, navy-tinted elevation shadows, system font
stack + mono.
- base.html loads it alongside style-custom.css so the tokens are
globally available.
- Rename --hp-* -> --ds-* in home_not_onboarded (313 refs),
home_onboarded (15), setup_advanced (39). 367 token references
pointed at one of three local blocks; now all point at the
global :root.
- Drop the three local token blocks. Each scope class
(.home-mock / .advanced-mock) only keeps its base ink + font-size
+ line-height rules.
The legacy --primary family stays canonical for the blue base
brand — login, dashboard, catalog, marketplace, admin still read
blue. The design system is opt-in via the scope class.
* refactor(web): extract shared components.css; migrate /home markup
PR 3 of the design-system extraction ladder. First batch of
reusable components lifted out of home_not_onboarded.html into a
new shared stylesheet; markup migrated to consume them.
- New app/web/static/css/components.css with five components, all
reusable on any page that loads design-tokens.css:
.callout-rec — amber lightbulb recommendation box
.callout-hint — blue info hint box
.code-output — "WHAT YOU SHOULD SEE" terminal output block
.lightbox — full-bleed image enlarge overlay
.setup-section-header — wizard header (eyebrow + h2 + lede)
- base.html loads components.css after design-tokens.css.
- home_not_onboarded.html markup renamed:
class="rec" -> class="callout-rec"
class="hint" -> class="callout-hint"
class="expected-output" -> class="code-output"
- Local CSS rules removed from home_not_onboarded.html for each of
the extracted components — ~150 lines down to 5-line "extracted to
components.css" comments. The bespoke wizard-specific styles
(.install-cmd, .os-tabs, .mode-tabs, .terminal-frame) stay
template-local for now since they only have one consumer.
Visual regression check: /home install hero renders the amber rec
callout, blue hint callout, dashed code-output block, green section
header, and click-to-enlarge VS Code thumb identically to the
pre-extraction render. 43 home tests pass.
* fix(web): unify page-headers — activity-center full-width, marketplace shares box
- /activity-center audit-log hero rendered as half-width because the
_page_hero include was inside <header class="obs-topbar">, a flex
row that pinned the time-range + auto-refresh controls next to it.
The hero is now a sibling rendered before the <header>, so it
spans the full container width like every other admin page; the
controls keep their flex row underneath.
- Marketplace hero unified with .page-header--hero. Markup is now
<section class="page-header page-header--hero mp-hero"> so the
shared box drives padding/radius/gradient/max-width/shadow; the
.mp-hero override block only carries the right-anchored cover
image and the rules for the search row + scope checkboxes (which
the canonical hero doesn't have). Inner text uses the canonical
.page-header__eyebrow / __title / __subtitle classes.
- .page-header--hero shadow tint now follows the brand blue
(rgba(0, 115, 209, 0.2)) instead of the leftover green from the
prior palette flip; same depth highlight everywhere the gradient
is blue.
* fix(web): unify remaining page heroes — admin, profile, install, store, stack
Sweep across pages that carried bespoke gradient hero markup so
every page-hero shares the canonical `.page-header--hero`
dimensions (padding 28/32/24, border-radius 14, max-width
var(--width-app), navy-tinted shadow, gradient with --primary →
--primary-dark). Inner text uses the .page-header__eyebrow /
__title / __subtitle classes so typography matches across the app.
- admin_tables: migrated to _page_hero.html include.
- admin_tokens: kept .tokens-hero wrapper for the counts-chip row
but added the canonical class on the same element; stripped
duplicate gradient + padding + typography rules.
- install: same pattern (kept hero-meta pill row).
- profile: migrated to _page_hero.html include.
- store_upload: kept .upload-hero wrapper for the .meta chip row;
composite class with the canonical hero.
- setup_advanced: .advanced-mock .ad-hero now matches canonical
dimensions; green palette retained via --ds-primary/dark.
- stack_card.css: .stack-hero (catalog + corporate-memory search
hero) uses canonical gradient + padding + max-width.
The detail-page heroes (marketplace_plugin_detail,
marketplace_item_detail, catalog_*_detail, store_edit,
admin_group_detail, admin_store_submission_detail) stay bespoke
for now — they're rich detail headers with photos, badges, install
actions; converting them would lose contract context. Same applies
to dashboard.html env-setup-cta (it's a CTA card, not a page hero).
* fix(web): canonicalise .container — single page shell every page inherits
Previously each admin page set its own `.container:has(.<page>)
{max-width: none}` + `.<page>-page {max-width: 1400px}` override,
and per-page hero markup either nested inside flex toolbars (which
pinned the hero next to filter controls and squeezed it half-width)
or self-constrained with a different max-width than the page. /home,
/dashboard, /marketplace, and /admin/* ended up at different widths
with different nav-to-hero gaps.
- style-custom.css `.container` now carries the canonical 1280px
max-width + `16px 32px 48px` padding so every page inherits the
same nav-to-hero gap and side gutters. `.container > main` is
margin/padding 0 so the container is the sole owner of gutters.
- `.page-header--hero` drops its self-constraining max-width and
auto-centering margin — the container provides the width, so the
hero sits flush with the table/toolbar below it.
- `.stack-hero` (catalog + corporate-memory) and `.advanced-mock
.ad-hero` (/setup-advanced) follow the same pattern: container
owns the width.
- Per-page max-width overrides stripped from admin_users,
admin_access, admin_groups, admin_marketplaces, admin_welcome,
admin_workspace_prompt.
- _page_hero include extracted from inside flex toolbars on
admin_users, admin_access, admin_groups, admin_marketplaces,
admin_server_config, admin_welcome, admin_workspace_prompt,
admin_sessions, admin_session_detail, admin_usage,
activity_center. The toolbar (`.users-toolbar`, `.gp-toolbar`,
etc.) keeps only the filter + action controls; hero renders
before it as a sibling.
- _page_chrome.html trimmed to just the page-background tint for
the redesign scopes; the duplicate `.container` rules it carried
are now redundant.
Verified: /home, /admin/marketplaces, /admin/users all render
container width 1280px with hero top at 88px (16px below the
72px-tall sticky nav). Same spacing as /home design spec.
* fix(web): admin_tables + admin_corporate_memory inherit canonical .container
Both pages were overriding `{% block layout %}` from base.html,
which bypasses the canonical `.container` wrapper. Result: hero
span the full viewport (1596px on a wide screen) while the inner
content sat at a narrower max-width — hero and content didn't
align, and the nav-to-hero gap differed from every other admin
page.
Switched both templates to `{% block content %}` so they render
inside the canonical `.container` from base.html — same path as
admin_groups, admin_users, admin_marketplaces, etc.
- admin_tables: dropped local `.page-title { max-width: 1600px }`
+ `.content { max-width: 1600px }` overrides (kept typography +
inner gutter rules) and the mobile padding overrides that paired
with them. Container now owns the gutters.
- admin_corporate_memory: only the block keyword needed changing;
the template already had a clean inner structure (no max-width
override on `.container-memory`).
Verified on /admin/tables and /admin/corporate-memory:
- .container width 1280, padding 16/32/48
- Hero top 88 (nav 72 + container padding-top 16)
- Hero + content both 1216px wide, both at left 190 — perfect
alignment with /admin/groups.
* fix(web): drop .page-shell padding override + admin_tables stale :root
Two regressions discovered after the canonical-container unification:
1. `.container:has(.page-shell)` still set `padding: 28px 32px 48px`
while the canonical `.container` had moved to `16px 32px 48px`.
Every page-shell consumer (/admin/sessions, /admin/sessions/<id>,
/admin/usage, /marketplace, /dashboard, marketplace detail pages,
/me/activity, /store/*, /admin/store-submissions) was rendering
with a 28px nav-to-hero gap while /admin/users + /admin/groups
rendered with 16px. Same width, mismatched vertical rhythm.
The opt-in rule is now a no-op marker: canonical container
already provides 1280px + 16/32/48 + main margin/padding 0.
2. admin_tables.html had a stale `<style>` block that re-declared
`:root { --primary: var(--primary); ... }`. The self-referential
token resolved to empty, collapsing the page-header hero's
`linear-gradient(135deg, var(--primary), var(--primary-dark))`
to no background — the hero appeared as a pale ghost without
colour. The entire shadow `:root` block was a stale copy of the
design tokens that style-custom.css already provides. Dropped
it; tokens now resolve from the global `:root`.
After both fixes /admin/sessions, /admin/tables, and every other
page-shell consumer match /admin/groups exactly: container 1280px,
container padding-top 16px, hero at top 88px / left 190px / width
1216px.
* fix(web): drop /admin/tokens .tokens-page width + padding override
`.tokens-page` carried its own `max-width: 1280px; margin: 0 auto;
padding: 28px 8px 48px` block — the canonical `.container` already
provides width + 16/32/48 padding, so the nested wrapper was
adding 28px on top of the container's 16px (= 44px nav-to-hero
gap, vs 16px on every other admin page) and shrinking the hero
sideways by 8px on each side (1200px vs the canonical 1216px).
After: container owns the layout; `.tokens-page` is just a
font-family scope. /admin/tokens hero now sits at top 88, left 190,
width 1216 — same numbers as /admin/groups / /admin/users.
* fix(web): hero links readable on blue; /admin/access Groups link href
- New `.page-header--hero a` rule in style-custom.css forces any
anchor inside a gradient hero to render white + underlined so
links stay readable on the blue background. Previously links
inherited the global `var(--primary)` blue, which disappeared
on top of the matching blue gradient. No per-page class needed —
drop a plain `<a>` in any hero subtitle and it just works.
- /admin/access hero subtitle was Jinja-passing the inline link
with HTML-entity-encoded quotes (`href="..."`). The
entities decoded to literal `"` characters inside the rendered
href, producing `/admin/%22/admin/groups%22` — a 404. Switched
the `set` to a block-set (`{% set page_hero_subtitle %}...{% endset %}`)
so the inline `<a href="/admin/groups">Groups</a>` survives
unescaped through `_page_hero.html`. Also stripped the now-redundant
inline `style="color:#fff;text-decoration:underline;"` — the new
shared rule handles it.
* fix(web): /dashboard top padding matches every other page
`.main` on /dashboard had `padding: 28px 32px 48px` while every
other page now uses `16px 32px 48px` via the canonical
`.container`. Dashboard bypasses `.container` (overrides
base.html's `layout` block to render a full-width `<main>`
directly), so the padding lives on `.main` itself — bumped the
top to 16px to match.
After: first child top = 88, left = 190, width = 1216 — same
numbers as /admin/groups / /admin/users / /admin/marketplaces.
* fix(web): green eyebrow + white title on .page-header--hero (matches /home)
`.page-header--hero .page-header__eyebrow` was faint white
(rgba(255,255,255,0.75)) — readable but unbranded against the blue
gradient. Changed to `var(--ds-brand-accent)` (mint green #54d3a0)
so every page hero pairs a green eyebrow with white title +
subtitle, echoing /home's setup-section header (green eyebrow,
dark heading combo). One CSS rule applies everywhere — no
per-page styling needed.
Also bumped the eyebrow to font-weight 700 / letter-spacing 1.2px
so the green stands out cleanly against the gradient.
* fix(web): page-header--hero + stack-hero use /home navy gradient
`.page-header--hero` and `.stack-hero` were on the brand-blue
gradient (`var(--primary)` → `var(--primary-dark)`) while
/home's hero (`.home-hero-intro`) sits on the deeper navy
gradient (`#0f1b3a` → `#1a2a5f`). Every other page-hero now
uses that same navy gradient so /home, /marketplace, /catalog,
/corporate-memory, /admin/*, /profile, /install, /dashboard,
/setup-advanced share one brand surface. Shadow tint adjusted
to the navy depth (rgba(15, 27, 58, 0.22)).
Brand blue stays the link/CTA colour everywhere else; only the
hero box itself is navy.
* fix(web): primary buttons green; marketplace tabs navy translucent
Two parity tweaks pulling the rest of the app toward /home's
visual language.
- `.btn-primary` (both rules in style-custom.css) now uses
`var(--ds-primary)` / `var(--ds-primary-dark)` green fill,
matching the "Copy install script to clipboard" button on
/home. Brand-blue `--primary` still drives link colour and the
accent surface; only the filled button background flipped to
green. Every page with a `.btn-primary` (admin "+Add user",
"+Add marketplace", catalog, marketplace actions, dashboard,
modals) now reads as the same "do it" affordance.
- `.mp-tabs` (Curated Marketplace / Flea Market / My Stack tab
group) now sits on the navy `--ds-hero-bg` with translucent
white pills (rgba(255,255,255,0.10) inactive, 0.18 active) —
same translucent-white-on-navy treatment as the "Just browse —
no install needed" pill on /home. Icons render as soft white;
per-tab colour-coding dropped in favour of the unified surface.
* fix(web): catalog/memory tabs + empty-state CTA + admin action buttons
Bring /catalog and /memory in line with /home + /marketplace:
- `.stack-tabs` (Browse / My Stack / Recipes on /catalog,
Browse / My Stack on /memory) now uses the navy `--ds-hero-bg`
container with translucent-white-on-navy pills, mirroring the
`.mp-tabs` treatment and /home's "Just browse — no install
needed" CTA pill. Per-tab icon colour-coding dropped — icons
render as soft white on the navy fill.
- `.stack-tabs-row__actions .btn` (right-slot "+New Recipe",
"+New Data Package" admin CTAs) now uses green primary fill
(`--ds-primary`), matching `.btn-primary` and /home's
"Copy install script to clipboard" button.
- `.stack-empty .cta a` (empty-state action button — the
"Open /admin/tables →" CTA on /catalog and equivalent on
/memory) flipped from blue `--primary` to green `--ds-primary`
so the colour aligns with every other primary button in the app.
* fix(web): marketplace Search button green (--ds-primary) matching other CTAs
* fix(web): unify Search button + admin-action button across browse pages
- Added Search button (`<button class="stack-hero__search-btn">`)
to /catalog and /memory heroes — same green pill as /marketplace.
Wired to the existing live-filter pipeline (button click runs
`applyFilters()` and refocuses the input). All three browse pages
now wear the identical search bar UI.
- `.stack-hero__search-btn` shares `--ds-primary` fill with
`.mp-hero .search-btn`.
- `.mp-actions .btn` ("Submit a skill or plugin" CTA on /marketplace)
flipped from the legacy blue-outline to the same green primary
fill + dimensions (`display: inline-flex; line-height: 1;
padding: 9px 16px; gap: 6px`) as `.stack-tabs-row__actions .btn`
on /catalog and /memory. All three right-slot action buttons
render at identical height now.
- `.stack-tabs-row__actions .btn` got `inline-flex` + `line-height: 1`
+ `gap: 6px` so a `<button class="btn">` and a `<a class="btn">`
both render at exactly 33px high — the embedded
`.admin-only-hint` chip no longer pushes one variant taller
than the other.
* fix(web): marketplace guide CTAs green (fastpath + primary); drop flea purple
* fix(web): dashboard CTA hero on navy; readable <code> chips in hero
- `.env-setup-cta` on /dashboard ("Set up a new Claude Code"
card) flipped from the brand-blue gradient + green-tinted shadow
to the canonical navy gradient (`--ds-hero-bg` → `#1a2a5f`) with
navy-tinted shadow + 14px radius + 28/32/24 padding, matching
`.page-header--hero` and /home's `.home-hero-intro`. Dashboard's
top CTA now sits on the same brand surface as every other hero.
- Added `.page-header--hero code` rule — translucent white pill +
warm-yellow ink (#ffd866) so `<code>` chips embedded in hero
subtitles read as code samples against the navy gradient. The
global `code` rule sets `color: var(--text-primary)` (dark),
which turned in-hero chips into invisible dark-on-white-on-navy
ghosts (e.g. the `-by-dev` suffix on /store/new).
- /store/new's `.page-header__subtitle code` dropped its inline
style override — the shared rule handles it now.
* feat(web): two-theme switching via data-theme + admin toggle
Introduces a theme system that flips the entire UI palette between
"navy" (current design, default) and "blue" (pre-redesign palette)
via a single `<html data-theme="...">` attribute. Page markup, class
names, and component styles don't change — only the `--ds-*` token
values flip.
Backend
- New `app/instance_config.py::get_instance_theme()` resolves the
active theme from `AGNES_INSTANCE_THEME` env > `instance.theme`
in instance.yaml > default "navy". Unrecognised values clamp to
"navy" so a typo doesn't break the page.
- `app/web/router.py::_build_context` injects `instance_theme`
alongside `instance_brand` etc. so every template inherits it.
- `app/web/templates/base.html` renders
`<html lang="en" data-theme="{{ instance_theme | default('navy') }}">`.
CSS
- `app/web/static/css/design-tokens.css` adds two new tokens to
the default `:root` set: `--ds-hero-shadow` (drop-shadow tint
on hero boxes) and `--ds-hero-eyebrow` (eyebrow accent colour).
Plus a `:root[data-theme="blue"]` override block that flips
seven tokens: `--ds-primary`, `--ds-primary-dark`,
`--ds-primary-light`, `--ds-brand-accent`, `--ds-hero-bg`,
`--ds-hero-bg-deep`, `--ds-hero-shadow`, `--ds-hero-eyebrow`.
The blue theme aliases the brand surface tokens back to the
legacy `--primary` family.
- `.page-header--hero`, `.stack-hero`, `.env-setup-cta`,
`.home-mock .home-hero-intro` now reference the new
`--ds-hero-shadow` and `--ds-hero-bg-deep` tokens instead of
hard-coding `rgba(15, 27, 58, 0.22)` and `#1a2a5f` — gradient +
shadow now flip with the theme.
- `.page-header--hero .page-header__eyebrow` uses
`var(--ds-hero-eyebrow)` so the eyebrow goes mint-green on
navy and translucent-white on blue (mint on blue reads poorly).
Admin
- `app/api/admin.py::_KNOWN_FIELDS["instance"]` now registers a
`theme` field of kind `select` with options `["navy", "blue"]`
and a `hint` explaining the trade-off. The existing
/admin/server-config UI auto-renders a select for this — no
template changes needed.
Defaults
- Default value is "navy" so existing instances see no visual
change. Admins flip to "blue" via /admin/server-config to
restore the pre-redesign look.
Restart note: uvicorn must reload to pick up the Python changes
(new getter, new template-context key, new known-field). CSS
changes hot-reload via browser refresh.
* fix(web): blue theme — home hero eyebrow + CTA contrast
`.home-hero-intro .eyebrow` and `.btn-intro-primary` referenced
`--ds-brand-accent` directly, which on the blue theme resolves to
the lighter brand-accent blue (#4F9DEB). Result: light-blue eyebrow
on the blue gradient ("WELCOME, ADMIN" barely readable) and a
light-blue button with darker-blue text ("Set up in ~15 min")
that all sat in the same hue range.
Introduces three new theme-aware tokens:
- `--ds-hero-eyebrow` already existed; blue theme bumped opacity
to 0.92 so the eyebrow reads as full white.
- `--ds-hero-cta-bg` + `--ds-hero-cta-fg` + `--ds-hero-cta-bg-hover`
flip the primary hero CTA: mint-green on navy (default), white-
on-blue under `data-theme="blue"`.
`.home-hero-intro .eyebrow` now uses `--ds-hero-eyebrow` (mint on
navy / white on blue) and `.btn-intro-primary` uses the CTA token
trio.
Recommended palette on blue theme:
- Eyebrow: white at 92% opacity (clear on the blue gradient).
- Primary CTA pill: white background, brand-blue dark text
(`--primary-dark` = #005BA3) for AAA-level contrast.
- Secondary CTA: translucent white pill (unchanged).
* fix(web): blue theme — callout-hint info bg/border/ink re-tinted to brand blue (was indigo, clashed with brand-blue hero)
* feat(home): Getting Started + Overview + Usage modes sections
Three new content cards rendered between the install-hero and the
existing connector tiles on /home. Order: Getting Started → Overview
→ Usage modes → connectors.
- Getting Started — dismissible card with two clickable rows linking
to /setup (install flow) and /setup-advanced (deeper reference).
Subsumes the legacy `.advanced-pointer` row that sat above the news
section. Per-device dismiss via a generic localStorage handler:
`.home-card-close[data-dismiss-key="..."]` inside a <section> wires
itself up at page load — drop in any future dismissible card without
per-card JS.
- Overview — operator-owned HTML body sourced from the new
`instance.overview` yaml field (env override
`AGNES_INSTANCE_OVERVIEW`). HTML in, HTML out via the same `| safe`
filter as news_intro. Empty default hides the section entirely,
keeping the OSS vendor-neutral; operators paste their product
framing / privacy posture into instance.yaml. New helper
`get_instance_overview()` in app/instance_config.py mirrors
`get_instance_logo_svg()`.
- Usage modes — three OSS-shipped tiles (Terminal / VS Code / Claude
Desktop · claude.ai) explaining each surface and linking to the
matching /setup-advanced anchors. Closes the gap for users
wondering "where do I actually run this".
Supporting changes:
- setup_advanced.html gains a new `#claude-app` section between
#vscode and #workspace, anchored by the Usage modes Claude Desktop
tile. Covers the marketplace registration paths and when to prefer
the terminal. Added to the table of contents.
- Three new tests in test_web_home_page.py pin the Getting Started
card markup, the Overview-on-when-yaml-set path, and the
Overview-off-by-default path. All 13 tests in the file pass.
Operator follow-up (separate infra PR — NOT this PR): paste the
Foundry-specific Overview body into instance.yaml's
`instance.overview` field. OSS ships with an empty default.
* fix(home): Overview is operator-owned content — drop dismiss button
Earlier iteration added a close X to the Overview section to match
the Getting Started card's dismiss UX. Wrong call: Overview is
operator-authored reference content (privacy posture, telemetry
policy, project framing) and a per-device localStorage hide means
returning users who want to re-read the policy can't recover it
without clearing storage.
Reverts the close button + the data-dismiss-key on the Overview
section. Test inverted to assert the dismiss key is absent (defends
against a future drive-by adding it back). Getting Started still
dismisses — that's procedural getting-started content users
legitimately stop needing once they've finished setup. Overview is
always reachable; whole section is still opt-in at the operator
level via the empty-yaml default.
* fix(home): Terminal usage-mode tile is informational (no click-through)
The setup hero above /home's Usage modes already walks the user
through the Claude Code CLI install — the Terminal tile click-through
to /setup just round-trips back to content the user already scrolled
past. Switch Terminal to a non-anchor <div> and scope the hover
affordance to a.home-usage-item so VS Code + Claude Desktop tiles
keep their click-through (those legitimately deep-link into
/setup-advanced anchors).
* fix(home): point Usage modes guidance at ~/{workspace}/Projects/ subfolder
The bundled plugin scopes the session-analysis loop and the
central-catalog sync to ~/<workspace>/Projects/, not the workspace
root itself — that convention already appears in the install hero's
Step 4 manual-fallback note ('Don't create ~/<workspace>/Projects/
manually — the bundled plugin offers to set it up after install').
Usage modes' footer guidance now matches: 'create every project
under ~/<workspace>/Projects/'. Also calls out that the
session-analysis loop is scoped to that root so users understand
why working outside the workspace dir is invisible to the platform.
* feat(brand): inline operator SVG logo + drop header subtitle (release 0.54.6)
Three header tweaks, one PR:
1. _app_header.html drops the small uppercase subtitle line below the
brand. instance.subtitle still flows into the CLAUDE.md preamble +
init welcome template ("Operated by …"); only the web header chrome
loses it.
2. get_instance_logo_svg() in app/instance_config.py reads
instance.logo_svg (yaml) / AGNES_INSTANCE_LOGO_SVG (env). The
yaml field was already documented in instance.yaml.example and the
template already supported inline <svg> via {{ config.LOGO_SVG |
safe }}, but router.py:344 hard-coded LOGO_SVG = "" — the middle
wire was missing. Now operators can paste a lockup directly into
their instance.yaml under instance.logo_svg: | and have it render
in the header. Resolution mirrors get_instance_brand (env > yaml >
""). instance.name remains independent: drives browser <title>
tags + page h1s + CLAUDE.md heading; the SVG is the web-header
visual only.
3. .app-header-logo svg gains max-height: 40px; width: auto; so any
operator's lockup scales via its viewBox to fit the 72px header
without per-asset width/height edits. Pairs with #2 — without the
clamp, raw artwork (e.g. a 1600x430 lockup) overflows the chrome.
Release-cut included per the same-PR rule (Unreleased contained only
these bullets after rebase onto 0.54.5).
* revert: keep app-header-subtitle span — out of scope for this PR
Initial commit dropped the subtitle line on the assumption that
the user wanted both the secondary header line AND the future-SVG
brand cleaned up. The actual ask was narrower: drop the hostname
suffix that renders inside instance.name ("Foundry AI (hostname)"),
which is a startup.sh concern, not a template one. Restore the
subtitle span and the CHANGELOG bullet that announced its removal.
PR scope narrows to LOGO_SVG wiring + CSS clamp only.
* fix(header): hide subtitle span when instance.subtitle is empty
Pre-fix the template fell back to the literal string 'Data Analyst
Portal' when INSTANCE_SUBTITLE was unset, so operators who left the
field empty saw a stray hardcoded label below their brand. Switched
to a Jinja {% if %} guard around the whole <span class="app-header-
subtitle"> so an empty subtitle produces no element at all — clean
header chrome instead of placeholder leak.
* feat(home): hide install-hero once onboarded + X close button
- Wrap the entire install-hero in `{% if not onboarded %}` so once
`users.onboarded=true` (auto-flipped by `agnes init` POSTing
/api/me/onboarded, or by the new X / existing fallback button) the
blue hero disappears entirely. Pre-PR the onboarded branch reused
the same shell with a "Welcome back" header + "Steps 1–4 done" badge
+ minimize toggle, which visually outweighed the actual nav hub.
- Add a circular × close button (top-right of the hero, rendered only
when not-onboarded). Click → window.confirm() asking the user to
acknowledge onboarding → POST /api/me/onboarded → reload. The
confirm string intentionally avoids the literal phrase
"Mark me as offboarded" because cli/commands/onboarded.py::status
scans /home's rendered HTML for that exact marker as a fallback for
the api/me/profile check.
- Lift the offboard escape hatch out of the hero into a discrete
`.offboard-strip` rendered below, gated `{% if onboarded %}`. Lets
the analyst flip back to the install view after wiping their
workspace folder.
- Centralize the /api/me/onboarded POST into a `postOnboarded()` JS
helper reused by the hero X, the existing "Mark me as onboarded"
fallback button, and the new offboard button.
Tests updated to match the new behavior:
- `test_home_onboarded_user_sees_nav_hub` — asserts the hero is gone
and the offboard strip is the only setup-flow remnant.
- `test_minimize_toggle_no_longer_rendered` (renamed) — asserts the
minimize toggle is absent in both states (was previously rendered
inside the now-hidden onboarded branch of the hero).
- `test_home_no_auto_transition_after_post_until_reload` — checks
offboard-strip presence post-flip instead of the removed
"Welcome back" hero copy.
* fix(home): X-close button used invalid source enum, hit 422
The X button's data-target-source was 'self_acknowledged_x' to give
audit_log a separate marker for X-vs-button-driven flips. But
app/api/me.py:38's OnboardedRequest pins source to a Literal of
['agnes_init', 'self_acknowledged', 'self_unmark'] — pydantic
returned 422 on every X click.
Confusing side effect: both buttons share self-mark-status as the
status element, so the failed X click rendered 'Failed (422)' next
to the still-functional 'Mark me as onboarded' button. Looked like
the button itself broke.
Fix: drop the _x suffix. Both surfaces now POST source='self_acknowledged'.
Distinction in audit_log is not load-bearing — the source field
captures user intent ('I'm onboarded'), not the specific UI affordance.
Three bundled improvements:
- #244 — new `agnes diagnose` check compares SessionStart events
(~/.claude/projects/<encoded>/*.jsonl) against agnes-push uploaded
log entries inside a 7-day window. Surfaces a warning when the gap
exceeds 3, hinting at silently-broken capture-session — previously
detectable only weeks after the fact.
- Dependabot — bumps transitive urllib3 from 1.26.20 to 2.7.0 to close
5 advisories (4 high, 1 medium). kbcstorage 0.9.5 still pins
urllib3<2.0.0 upstream; overridden via [tool.uv] override-dependencies
since the SDK works fine against 2.x in practice (Client + Tables
both flow through requests, which supports both lines).
- #252 — fix flaky test_scratch_dir_cleaned_up_after_failed_extraction
by redirecting tempfile.tempdir to a per-test tmp_path. Pre-#252 the
test scanned the shared system tmp dir and a sibling store test in
another pytest-xdist worker could trip the assertion mid-window.
Closes#244. Closes#252.
* Capture session paths via SessionStart hook + lock parallel pushes
Replace the encoding-based scan of ~/.claude/projects/<encoded-cwd>/ with
a queue file populated by a new `agnes capture-session` SessionStart hook.
The hook reads the documented `transcript_path` field from Claude Code's
hook stdin JSON, sidestepping the cwd-to-folder encoding (which is an
internal implementation detail and varies by Claude Code version).
- New `agnes capture-session` subcommand appends transcript_path to
<workspace>/.claude/agnes-sessions.txt. Silent on all malformed input
so a hook chain failure doesn't clutter Claude Code startup.
- `agnes push` now consumes the queue: atomic snapshot rename guards
against hooks writing during the push window, successful uploads land
in agnes-sessions-uploaded.txt (TSV: timestamp + path), failed paths
are requeued.
- Cross-platform single-instance lock via the filelock package (fcntl
on POSIX, msvcrt on Windows). Concurrent SessionEnd hooks — common
when the user closes several sessions at once — silent-exit on the
losing side instead of all racing the upload.
- Recovery: pre-existing snapshot files from a crashed push are picked
up and processed before the live queue.
- The SessionStart `agnes push` self-heal entry is dropped — it became
redundant once the queue persists across runs (orphans from headless /
crashed sessions ship out on the next interactive SessionEnd push).
Existing workspaces auto-migrate via the marker-based replace logic.
- Legacy encoding scan stays available behind `--legacy-scan` for one-
off backfills of sessions predating the queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add /agnes-private + statusLine indicator for private sessions
Users handling sensitive data inside Claude Code can now opt a session
out of the Agnes upload pipeline, either proactively (right after session
start) or reactively (mid-session). The `/agnes-private` slash command
runs `agnes mark-private` deterministically via `!`-prefix direct bash —
no AI in the loop. A workspace-installed statusLine surfaces a
`🔒 agnes-private` indicator in Claude Code's status bar so the user
sees the state at a glance.
Authoritative source of "do not upload" is a separate file
`<workspace>/.claude/agnes-sessions-private.txt` (one session_id per
line). Both `capture-session` (queue writer) and `push` (queue reader)
consult the list. This makes the slash-command / SessionStart-hook race
impossible by construction: whichever runs first, the session is correctly
filtered out.
- `agnes mark-private` reads `CLAUDE_CODE_SESSION_ID` from env (set by
Claude Code in every bash subprocess it spawns — stable documented API)
and appends to the private list.
- `agnes statusline` reads the session JSON Claude Code pipes on stdin,
checks the private list, and emits the indicator or nothing. Optimized
for the high call frequency of statusLine renders.
- `capture-session` extracts session_id from hook stdin and skips queue
write when the ID is already on the private list (race protection).
- `push` filters snapshot entries by the private list and appends to a
per-workspace audit log `agnes-sessions-private-skipped.txt`.
- Queue format migrated from `<path>` to `<session_id>\t<path>`; legacy
one-column lines still parse (empty session_id, still upload, can't be
marked private retroactively — fine, they pre-date the feature).
- `install_claude_hooks` writes a workspace statusLine unless the user
already has a custom one (warn + preserve). Idempotent re-init.
- `install_claude_commands` ships `agnes-private.md` alongside
`update-agnes-plugins.md`. Per-template fallback so a missing template
doesn't get clobbered with the wrong content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix setup-prompt + CLAUDE.md marketplace copy + drop skills step
Three issues against the post-PR-#240 / post-PR-#237 state:
1. Setup prompt's marketplace block trailer (both has-stack and
empty-stack variants) claimed the SessionStart hook keeps the
marketplace clone in sync via `agnes refresh-marketplace --quiet`
on every session and that admin grants land automatically — both
false since PR #237 (0.47.x) moved the install/update path out of
the hook into the `/update-agnes-plugins` slash command. The hook
is `--check`-only: detects server-side changes, prompts the user
to run the slash command, which does the full reconcile
interactively with output visible in the transcript.
2. The empty-stack variant framed composition as "admin grants only",
missing the actual three-source served stack:
(admin RBAC ∩ /marketplace subscriptions)
∪ system-mandatory plugins (admin-pinned, auto-applied)
∪ Flea market installs (skills/agents bundled, plugins standalone)
Updated copy spells out all three sources so analysts know where
their stack picks live, and what the SessionStart hook actually
does on change detection.
3. CLAUDE.md template's "Agnes Marketplace" section conflated
eligibility (`resolve_allowed_plugins` — what's listed) with served
stack (`resolve_user_marketplace` — what actually reaches Claude
Code). The two are different: a user can be RBAC-eligible for a
plugin without having subscribed to it on /marketplace. Rewrote
the section to distinguish the eligibility set from the served
stack and to describe the `--check`-only hook accurately.
Plus: deleted the setup prompt's interactive Skills step (final step
before Confirm). The named-opinion question — "do you want me to
bulk-copy every skill into ~/.claude/skills/agnes/ or pull on-demand
via `agnes skills show <name>`?" — had no obvious right answer for
new users at the tail end of a wall of technical steps. On-demand
lookup is the one-size-fits-all default; `agnes skills list/show`
remain discoverable and the CLAUDE.md template references specific
skills inline (e.g. agnes-data-querying in the BigQuery section)
where they're relevant. Layout: Confirm shifts from step 9 to step 8.
Tests updated, full setup/marketplace/welcome surface green (115
passed). Remaining full-suite failures are pre-existing (BQ/Keboola
fixtures, Windows charmap collection error in test_v26_keboola_e2e)
— verified against a clean stash, unrelated to this diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix session-queue race + snapshot PID-reuse data loss
Two blocker fixes from the PR #242 review:
1. Concurrent SessionStart hooks could corrupt the queue file on
Windows. Python's `open(path, "a")` is not atomic there — the CRT
does not pass FILE_APPEND_DATA to CreateFile, so concurrent
appenders (user opening several Claude Code windows simultaneously)
could interleave bytes mid-line. The malformed lines then silently
fail the parser and the entries are dropped.
Fix: wrap append_to_queue, requeue_failed, and snapshot_queue in a
short-lived FileLock on a dedicated `agnes-queue.lock`. Separate
from `agnes-push.lock` so capture-session hooks don't block on the
push command. New test_append_concurrent_threads_no_corruption
reproduces the race with 4 threads x 50 appends.
2. Snapshot filenames embedded only the PID (`agnes-sessions.snapshot.
<PID>.txt`). After a crashed push left a snapshot on disk and the
OS recycled the PID for a new push, `os.rename` would atomically
overwrite the recovery snapshot — every entry in it lost, silently.
Fix: append a uuid8 hex tail (`agnes-sessions.snapshot.<PID>.
<uuid8>.txt`). find_recovery_snapshots already globs the prefix
so it picks up both old and new format. New
test_snapshot_filename_is_unique_per_call asserts two consecutive
snapshots under the same PID don't collide.
Targeted tests green (47/47 in session_queue/capture_session/cli_push).
Full suite failures unchanged from baseline (pre-existing BQ/Keboola
fixture issues per CLAUDE.md).
* Auto-refresh workspace hooks + bash-wrap all hook entries (Windows)
Fixes from PR #242 second review (ZdenekSrotyr):
1. `uv.lock` regenerated to include `filelock 3.29.0` (declared in
pyproject.toml but missing from the lock file — CI's
lockfile-consistency check would fail; `uv pip install` on a clean
cache would silently miss the dep).
2. `agnes self-upgrade` now auto-refreshes the workspace Claude Code
hooks via the new `cli.lib.hooks.maybe_refresh_claude_hooks`. Closes
the silent-stop migration gap: a v0.48 workspace would auto-upgrade
the CLI from its existing SessionStart self-upgrade entry but never
pick up the new `agnes capture-session` SessionStart hook, leaving
the queue empty and `agnes push` uploading nothing.
The refresh fires on both the "info is None" fast path (CLI already
current — catches the second SessionStart after a prior upgrade)
and the install-success path. Guarded by `workspace_has_agnes_hooks`
so it never writes `.claude/settings.json` into directories that
aren't Agnes workspaces (e.g. `agnes self-upgrade` invoked from
`~/`). Errors are surfaced on stderr but never flip the upgrade exit
code.
3. All Agnes-managed hooks are now wrapped in `bash -c "..."`. The
self-upgrade+pull chained SessionStart entry was the only one still
shipping unwrapped — Claude Code on Windows runs hook commands
directly without a shell, so the `;` chain + `2>/dev/null` +
`|| true` shell syntax silently no-op'd on native Windows installs
without Git Bash on PATH. Workspaces still on the old form
auto-upgrade via the refresh path above.
Tests: +12 in test_lib_hooks.py (guard semantics, v0.48→v0.49
migration end-to-end, third-party-hook preservation, bash-wrap
invariant). +5 in test_self_upgrade.py (refresh fires on info=None,
fires on install success, skipped on failure, skipped on --check-only,
refresh failure never flips exit code).
130 targeted tests green. The 2 pre-existing Windows path-separator
failures in `test_smoke_test_detects_version_mismatch[uv|pip]` are
unrelated (path mismatch `\fake\uv\bin\agnes` vs `/fake/uv/bin/agnes`
in test asserts, pre-PR baseline).
* CHANGELOG: document PR-242 main features
Closes ZdenekSrotyr #4: the [Unreleased] block was missing entries for
the PR's primary surface — only the post-merge fix bullets and the
unrelated setup-prompt copy change were captured. Adds:
- ### Added: 6 bullets covering the session capture queue + new
`agnes capture-session` subcommand, `/agnes-private` slash + `agnes
mark-private`, `agnes statusline` + statusLine wiring, `--legacy-scan`
opt-in fallback, single-instance push lock, and the new `filelock`
runtime dep.
- ### Changed: BREAKING bullet on the SessionStart / SessionEnd hook
wire format change (capture-session as first SessionStart entry,
push self-heal removed, SessionEnd push detached via nohup, all
entries bash-wrapped). Folds the prior standalone bash-wrap bullet
into this consolidated entry — Z's review flagged the layout shift
as BREAKING, and grouping the related sub-changes makes the
migration story readable in one place.
- Operator migration is auto-handled by `maybe_refresh_claude_hooks`
invoked from `agnes self-upgrade` (separate Changed entry below).
No `agnes init` re-run required. Pre-queue session jsonls on
upgrading workspaces still need a one-off `agnes push --legacy-scan`
— flagged in the BREAKING bullet.
No code change; doc only.
* Drop permanent 4xx uploads instead of requeueing forever
Closes ZdenekSrotyr #5. Previously the push retry path requeued any
non-200 response except the literal "file not found on disk", so 401
(token expired), 403 (RBAC denial), 413 (payload too large), 400
(server-side validation) cycled through every push run forever — the
queue grew without bound and each run re-bombarded the server with the
same deterministically-failing upload.
Now 4xx (except 408 Request Timeout + 429 Too Many Requests, which the
HTTP spec marks as transient) is dropped and audit-logged to
`<workspace>/.claude/agnes-sessions-failed.txt`:
<iso_ts>\t<session_id>\t<status>\t<transcript_path>
5xx and network errors continue to requeue — those reflect server /
transport state that can change between runs, so retry is the right
behavior.
The audit log piggybacks on the push single-instance lock
(agnes-push.lock) — push is the only writer to this file, same as the
existing `mark_uploaded` and `mark_private_skipped` paths, so no
separate filelock is needed.
`agnes push --json` surfaces a new `dropped_permanent` counter; non-
quiet stdout mentions the audit-log path so operators tailing the
output have a pointer to the forensic trail.
Tests: +7 in test_cli_push.py (401/400/403/413 → drop; 408/429 →
requeue; 500/502/503 → requeue; network exception → requeue;
--json `dropped_permanent` counter; stdout audit-log pointer). +1 in
test_session_queue.py (mark_failed_permanent TSV format).
127/129 targeted tests green. The 2 pre-existing Windows
path-separator failures in `test_smoke_test_detects_version_mismatch
[uv|pip]` are unrelated (path mismatch `\fake\uv\bin\agnes` vs
`/fake/uv/bin/agnes` in test asserts, pre-PR baseline).
* Catch OSError in push lock acquisition
Closes ZdenekSrotyr #8. `acquire_or_skip` in `cli/lib/push_lock.py`
previously caught only `filelock.Timeout`. Any `OSError` from
`FileLock.acquire` — read-only filesystem, permission denied on
`.claude/`, disk full, hardware I/O failure — propagated as an
unhandled traceback.
Two visible failure modes:
- SessionEnd hook: `|| true` in the wrapper swallowed the error, so
daily pushes silently never ran. Operator had no signal.
- Manual `agnes push`: ugly Python traceback dumped to the terminal
instead of a clean exit.
Now `OSError` is treated the same as `Timeout` — yield `None`, caller
returns cleanly with rc=0. The operator's environment in these
scenarios has bigger problems than missing session uploads, so we
swallow rather than retry-loop or surface a noisy warning.
Test: `test_push_silent_exit_when_filelock_raises_oserror` patches
the `FileLock` used inside `push_lock` to raise OSError on acquire,
verifies push exits 0 with no traceback and the queue is preserved
for the next attempt.
* Address remaining S2 items from PR-242 review
Four items from ZdenekSrotyr's S2 list:
S2.10 — `_install_statusline` truthy check (cli/lib/hooks.py): replace
`if existing:` with explicit `if existing is None or existing == "":`.
Documents and tests the behavior for both edge cases (explicit-null
and empty-string `statusLine`) — both treated as "not configured"
rather than "explicit user opt-out", so we install ours. Two new
tests in test_lib_hooks.py pin the contract.
S2.6 — onboarding docs for /agnes-private. New "Private sessions"
subsection in `config/claude_md_template.txt` (next to Data Sync)
covering the slash command, statusbar indicator, and audit-log
location. One-line tip in `app/web/setup_instructions.py` so the
feature is discoverable at onboarding.
S2.9 — e2e privacy test (tests/test_e2e_privacy.py). Wires
capture_session → mark_private → push against a recording fake
api_post and asserts zero session uploads for the marked one.
Three cases: mark-before-capture (queue write skipped),
mark-after-capture (push-side filter catches it + audit-logs),
control (unmarked sessions upload normally).
David #8 — `--legacy-scan` help text now documents the
private-list gap (legacy entries carry empty session_id, so
the filter is not consulted). The practical impact is bounded —
pre-queue sessions cannot have been marked private since the
private list is a queue-era feature — but the disclaimer in the
help text means an operator running a backfill is not surprised.
68 targeted tests green (3 new e2e + 2 new truthy edge tests +
existing). 2 pre-existing Windows path-separator failures in
test_smoke_test_detects_version_mismatch[uv|pip] unchanged.
Remaining S2 items (statusline mkdir push-back, capture-session
silent-fail follow-up) handled in PR comment + follow-up issue
respectively.
* Address remaining S2 follow-ups (David #8, S2.7, David #11)
Three items left over from Mina's bbf63472 batch — that commit
addressed S2.6/S2.9/S2.10 + documented David #8 in help text but
deferred the actual implementations of S2.7, David #11, and the real
David #8 fix to follow-ups. This commit closes them.
David #8 — `agnes push --legacy-scan` now consults the private list.
Claude Code names jsonls `<session-id>.jsonl`, so the file stem IS
the session id; the legacy-scan path can apply the same private filter
the queue path uses. Both the dry-run and live-upload code paths fixed.
Help text updated (no longer warns the filter is bypassed). Two new
tests in test_cli_push.py cover the upload-skip path + the dry-run
`would_skip_private` segregation.
S2.7 — `statusline`/`is_private` no longer mkdir-pollutes arbitrary
workdirs. Split `_claude_dir` into `_claude_dir_writable` (used only
from `add_private`) and `_claude_dir_readonly` (no mkdir). The
read-only public helpers (`private_list_path`, `read_all_private`,
`is_private`) compose the no-mkdir variant by default; `add_private`
opts in via `writable=True`. Added a process-local mtime-keyed cache
around `read_all_private` so in-process callers (push doing one stat
per upload candidate, future `agnes diagnose`) don't re-parse the
file on every check. Cache eviction on `add_private` so a sub-second
write+read sequence doesn't see stale data even on coarse-mtime
filesystems. Two new tests pin the no-mkdir contract + the
in-same-second add+read consistency.
David #11 — `agnes capture-session` writes a breadcrumb log on every
invocation. New `<workspace>/.claude/agnes-capture-session.log` TSV:
`<iso_ts>\t<outcome>\t<detail>` where outcome covers every silent-
exit path (`ok`, `private_skip`, `empty_stdin`, `bad_json`,
`not_object`, `no_transcript_path`, `stdin_read_error`,
`write_error`). Gives operators a signal to detect "hook fires but
queue stays empty" — without it, an upstream Claude Code stdin-
contract change is invisible because the hook always exits 0. Log
rolls at 256 KiB so it doesn't grow unbounded on long-lived
workspaces. Best-effort: a breadcrumb-write failure is itself
swallowed so the hook contract stays "exit 0 always". Skipped in
non-Agnes workdirs (no `.claude/` exists) so opening Claude Code
in `~/` doesn't pollute it. Five new tests in test_capture_session.py
cover the success / bad_json / no_transcript_path / private_skip /
no-pollute paths.
115 targeted tests green (test_cli_push, test_capture_session,
test_private_list, test_session_queue, test_e2e_privacy,
test_lib_hooks, test_statusline, test_mark_private).
---------
Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com>
* feat(home+news): state-aware /home + /news + admin-edited news section
Squash of the vr/home-page feature work for clean rebase onto main.
Original 18-commit history preserved in branch backup/vr-home-page-pre-rebase.
What's in this PR:
**State-aware /home page**
- New `/home` route with hero + auto-mode + connectors (Asana / GWS /
Atlassian) + lookarounds. Onboarded vs not-onboarded state-machine
branches a single template (`home_not_onboarded.html`); the install
steps, "Setup a new Claude Code" CTA (90-day PAT mint), and per-
connector setup prompts hide once `users.onboarded=TRUE`. A
completion badge replaces them.
- "Mark me as offboarded" button reverses the flag without an SQL UPDATE.
- `users.onboarded BOOLEAN` column added; default FALSE; flipped by the
CLI's `agnes init` post-success POST and the `/admin/users` API.
- Connector setup prompts pre-check whether the tool is already
installed/connected before re-running setup.
- GWS scope set widened to include Google Chat (`chat.spaces`,
`chat.messages`).
**Single template + design tokens**
- `dashboard.html` now extends `base.html` via the new
`{% block layout %}` opt-out (full-width pages skip the 800px
`.container`). Net: every page shares one shell.
- `style-custom.css` `:root` extended with `--space-{7,9,10,12}`,
`--radius-2xl`, `--shadow-{card,elevated}`, `--text-{muted,disabled}`,
`--focus-ring`, `--transition-*`, `--width-{narrow,app,wide}` so
inline page styles can migrate incrementally.
**Auth redirects honor AGNES_HOME_ROUTE**
- `safe_next_path` resolves the configured home route when no `default=`
is passed; OAuth callbacks, magic-link clicks, password form, and
LOCAL_DEV_MODE shortcuts now land on `/home` (or whatever the operator
picked) instead of always /dashboard.
**News section + /news permalink + /admin/news editor**
- Schema-bumped `news_template` table (single versioned entity, draft +
publish gate). `published BOOLEAN` distinguishes draft from public;
monotonically-increasing `version` per save; rows >30d pruned on
save except the currently-displayed published version.
- `/home` bottom-of-page renders the latest published intro with a
"Read more →" link to `/news` (which renders the full body).
- `/admin/news` editor with sandboxed live preview, versions table,
per-row Unpublish, Format-help cheatsheet.
- `agnes admin news show / draft / edit / publish / unpublish /
versions / export` (CLI). Talks to the live server via the
`/api/admin/news/*` endpoints (PAT-authed) — no direct DB access
so it coexists with a running uvicorn.
- **Optimistic-lock guard**: `agnes admin news publish --version N` and
PUT/PATCH endpoints accept `expected_version` and 409 with structured
`{error: "version_conflict", expected, actual, actual_by}` when a
concurrent admin replaced the draft. Edit refuses to overwrite a
draft authored by someone else without `--force` or
`--expect-version`.
- nh3 (Rust-backed ammonia) HTML sanitizer; iframe pre-pass strips
any iframe whose src is not on the YouTube/Vimeo/Loom allowlist;
javascript:/data: schemes blocked everywhere.
- Author CSS vocabulary: `.news-hero` (blue gradient hero block),
`.callout`/`.callout-{info,warn,success,danger}`,
`.video-embed`, `.news-section`, `.news-grid-{2,3}`, `.news-cta` —
all consolidated in `style-custom.css` under "News content
vocabulary (shared)" so /home perex, /news body, and /admin/news
preview share one source of styling.
- Code-inside-`<pre>` contrast fix (was unreadable amber-on-silver).
- `.news-content` table styling (border, header band, row-hover).
**`scripts/dev/run-local.sh`** — local uvicorn launcher. Pulls Google
OAuth client id/secret from GCP Secret Manager
(`AGNES_OAUTH_GCP_PROJECT`-driven, no vendor defaults), points
`AGNES_CLI_DIST_DIR` at `./dist` so the wheel endpoint resolves, and
`--dev` flips `LOCAL_DEV_MODE=1` + `AGNES_HOME_ROUTE=/home` for one-
command iteration. `LOCAL_DEV_MODE=1` also enables the FastAPI debug
toolbar.
**CLAUDE.md "Run tests before every push" section** codifies
`pytest tests/ -n auto -q` as non-negotiable before each push.
**Tests**: 51 + 14 + 8 = 73 new tests across news-template repo,
sanitizer, API, web, CLI; plus updated home/auth/template tests for
the new shared-shell architecture.
Origin docs (gitignored, customer-fork content):
docs/brainstorms/home-page-requirements.md,
docs/plans/2026-05-07-001-feat-home-page-plan.md.
* feat(cli): agnes onboarded {on,off,status} — self-scoped flag toggle
User-facing equivalent of the in-page "Mark me as (off)boarded" button
on /home. POSTs /api/me/onboarded with {onboarded, source}; --source
overrides the audit-log marker so flips made from the CLI vs the web
button vs agnes init automation stay distinguishable.
`status` reads via /api/me/profile (when present); falls back to a
quick body-marker scan of /home so the read path doesn't write an
audit_log row. PAT-authed via cli.client.api_post — same convention
as agnes admin news / agnes admin add-user etc.
Tests: 5 covering on/off/status round-trip, idempotency, and
audit-log source recording. Full suite holds at 12 pre-existing
failures (same set as before).
* ui(nav+home): primary nav reorg + green What's new band + /marketplace link fix
Primary nav (post-rebase audit + per-user feedback):
- Items: Home → Marketplace → Data Packages → Memory. Admin dropdown
for admins only. The "Dashboard" label was renamed Home — point still
resolves through `home_route` so customer instances on /dashboard
still land there.
- Activity Center moved into the Admin dropdown. Per-team adoption
analytics is admin-consumed in practice; the route still allows
any authed user for direct deep-links so existing /home tile +
bookmarks keep working.
- Memory link added (→ /corporate-memory) — was previously buried in
the /home "Look around" tiles.
- Setup local agent + My Stack dropped from main nav. Setup is the
/home install flow's home now; My Stack lives as a tab inside
/marketplace.
/home tweaks:
- Plugin marketplace tile now points at /marketplace (was /store —
legacy from before the marketplace rebrand landed in #230).
- "What's new" section header gets a green band (success-flavored
D1FAE5 background, A7F3D0 border, darker green title) so the
bottom-of-page news block visibly distinguishes from the blue
install-hero at the top. Header strip only — body stays white.
Test fix: test_home_route_resolution renamed `dashboard_link_uses_home_route`
→ `home_link_uses_home_route` and asserts `href="/home">Home` instead
of `href="/home">Dashboard` after the label change.
* fix(home): decouple Step 3 + Connect-tools collapse from server onboarded flag
The server-side `users.onboarded` flip happens through two paths:
1. Explicit user click on "Mark me as onboarded" or `agnes onboarded on`.
2. Implicit `agnes init` POST → /api/me/onboarded on success.
Path 2 produced a UX surprise: an analyst running `agnes init` mid-flow
reloaded /home and saw Step 3 (auto-mode) + Connect-your-tools auto-
collapse to summary bars. They were actively working through those
sections — the install POST never signalled "I'm done with the rest
of setup", just "Agnes itself is installed".
Decouple the section-collapse decision from the server flag:
- Step 1 + Step 2 install blocks: still hidden on `onboarded=TRUE`
(their completion is a hard server signal — Agnes IS installed).
- Step 3 + Connect-your-tools: render flat by default in BOTH states.
Wrapped in `<details class="setup-collapsible" open>` so the
browser's native disclosure handles per-section toggle without JS,
but the `<summary>` is CSS-hidden until the page-level
`data-setup-minimized="1"` attribute is set on `.home-mock`.
- New "Minimize setup view" toggle inside the blue install-hero,
rendered only when onboarded. Click flips the data-attr on
`.home-mock` AND removes the `open` attribute from each
`<details>`. State persists in `localStorage["agnes_home_setup_minimized"]`
so the choice survives reloads but is per-device.
- "Show full setup view" (the same button when minimized) re-opens
both `<details>` and clears localStorage.
When minimized, each `<details>` still has its own native expand/
collapse — click the gray summary bar to peek at one section without
toggling the page-level minimize off.
Tests:
- test_step3_and_connectors_render_flat_when_onboarded_by_default —
asserts `<details class="setup-collapsible" ... open>` for both
sections post-onboarding and the absence of any server-rendered
`data-setup-minimized` attribute on the `.home-mock` root.
- test_minimize_toggle_visible_only_when_onboarded — toggle button
rendered only when onboarded.
Full pytest holds at 12 pre-existing failures (same set).
* feat(observability): optional PostHog integration (errors, LLM traces, replay, flags)
Off by default. Activates when POSTHOG_API_KEY is set in env. Defaults
to PostHog Cloud EU; override host for US Cloud or self-hosted.
Coverage:
- FastAPI 500 handler captures unhandled exceptions
- src/orchestrator.py rebuild + rebuild_source failures
- services/scheduler/ HTTP-job failures
- cli/main.py uncaught CLI errors (Typer.Exit/SystemExit/KeyboardInterrupt
skipped; flushes before re-raise so short-lived CLI invocations don't
drop events)
- connectors/llm/anthropic_provider.py + openai_compat.py emit
$ai_generation events with provider, model, latency, token counts
(prompt/completion bodies stay off unless POSTHOG_LLM_PAYLOADS=1
because LLM prompts here routinely include customer SQL/data)
- Browser snippet injected into every text/html response by
PosthogInjectionMiddleware — registered inside the GZip layer so it
sees uncompressed HTML before compression. Many templates are
standalone (their own DOCTYPE) and never extend base.html, so a
per-template include would miss them.
- Frontend: $pageview, $pageleave, JS error capture via window.error
and unhandledrejection handlers, masked session replay
(maskAllInputs: true plus CSS-selector mask for known data surfaces),
feature flags (browser posthog.isFeatureEnabled + server-side
feature_enabled with fallback for older SDKs).
Identification mode operator-configurable: none / id / email / full.
Default email ships user.id + email but never name. CLI entry point
moves from cli.main:app to cli.main:main (Typer wrapper).
Files:
- src/observability/posthog_client.py — lazy singleton, no network
when disabled, single-process flush on shutdown
- src/observability/llm_tracing.py — trace_generation context manager
- app/middleware/posthog_inject.py — HTML rewrite middleware
- app/web/templates/_posthog.html — browser snippet template
- docs/observability.md — operator guide
- config/.env.template — documented POSTHOG_* knobs
- tests/test_posthog_disabled.py + tests/test_posthog_client.py +
tests/test_llm_tracing.py — 18 tests covering disabled state,
identify-mode payloads, $ai_generation shape, error variant.
CHANGELOG entry under [Unreleased] Added.
* feat(observability): tag every PostHog event with environment + release
Splits PostHog dashboards cleanly between localhost / dev / staging /
production without manual tagging on every capture call.
- POSTHOG_ENVIRONMENT explicit override; auto-resolves to "local" when
LOCAL_DEV_MODE=1, else RELEASE_CHANNEL, else AGNES_DEPLOYMENT_ENV,
else "unknown".
- AGNES_VERSION → RELEASE_CHANNEL fallback feeds the `release` property
for "is this error new in this release?" cohorting.
- Backend gets both via the PostHog SDK's super_properties constructor
arg (every captured event picks them up automatically).
- Browser snippet calls posthog.register({environment, release}) inside
the loaded callback so $pageview, $exception, autocapture, etc. all
carry the same labels.
- request.state.user now populated by auth dependencies so the snippet
can actually call posthog.identify(user_id, {email}) for logged-in
users (previously the user block always resolved to None because
nothing wrote to request.state.user).
4 new tests cover env resolution: explicit > LOCAL_DEV_MODE > channel
> unknown, plus super-properties forwarding into the SDK constructor.
* feat(observability): inline user attrs on every PostHog event + debug throw route
PostHog's UI shows person properties on the Person profile page, not
inline on each event — so a reviewer triaging an exception couldn't tell
which user hit the bug without clicking through. Fix it on both sides.
- Backend capture_exception merges user_id / user_email / user_name into
the event properties (gated by POSTHOG_IDENTIFY_PII: none/id/email/full).
Backed by a new _user_props_for_event helper on PosthogClient.
- Browser snippet registers user_id + user_email + user_name as super-
properties via posthog.register({...}) so every $exception, $pageview,
and custom event coming from posthog.captureException() carries them
inline. Mirrors the backend so cross-referencing client/server events
doesn't require a person-profile lookup.
- /api/debug/throw — debug-only endpoint gated by DEBUG=1 (404 in prod).
Runs Depends(get_current_user) first so request.state.user is set when
the unhandled-exception handler captures the event. Lets operators
exercise the full observability path end-to-end without hand-rolling
a TestClient script. Configurable via ?kind=ValueError&msg=...
7 new tests cover: backend user-attr merge across identify modes,
anonymous request fall-through, browser snippet super-prop emission for
logged-in / anonymous / id-only / full-name cases.
* fix(observability): address minasarustamyan PR #231 review
Two bugs caught in review.
1. PosthogInjectionMiddleware dropped Response.background on every
return path. BaseHTTPMiddleware materialises the body and asks
subclasses to return a fresh Response — three paths in dispatch()
omitted background=, silently cancelling any BackgroundTask /
BackgroundTasks the route attached (audit logging, async webhooks,
email sends) with no log line. Fix: route every return through a
_passthrough() helper that forwards background.
Also adds a _MAX_BUFFER_BYTES (4 MB) cap so a streamed-HTML response
can't balloon RSS during buffering. Bigger bodies short-circuit
through with a warning rather than being injected.
Regression tests in tests/test_posthog_inject_middleware.py exercise
four return paths (snippet present, render-fail, double-injection
guard, non-HTML passthrough) plus the streaming-guard short-circuit.
2. $ai_input / $ai_output_choices were emitted without truncation, so
POSTHOG_LLM_PAYLOADS=1 silently dropped events past PostHog's ~32 KB
per-event ingest limit — exactly the calls (large prompts with
schemas / sample rows / SQL) an operator would want to inspect.
Fix: clip both at POSTHOG_LLM_PAYLOAD_MAX_CHARS (default 30000) with
an explicit "…[truncated N chars]" marker so readers don't mistake
truncated captures for complete ones. Metadata (provider, model,
tokens, latency, error) flows regardless. Three new tests cover
default-cap clipping, env-override, and pass-through under the cap.
37 PostHog tests pass.
Removed kbcstorage from all dependency groups (optional + dev) so urllib3
is no longer pinned to <2.0. Legacy Keboola client is available via
manual install: pip install kbcstorage
kbcstorage pins urllib3<2.0.0 which blocks Dependabot security patches.
Moved to [project.optional-dependencies] keboola-legacy since the primary
extraction path uses the DuckDB Keboola extension, not kbcstorage.
Legacy fallback uses lazy import — app works without it installed.