Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
907cba7
Workspace-run patches: seed overwrite, bootstrap NoOpValidator, cross…
christse Jul 17, 2026
877260b
Declare lodash as explicit boxel-ui dependency (CS-12187)
christse Jul 17, 2026
38fc6b2
Add on-demand skill tools and HTML screenshot tool to the factory agent
christse Jul 17, 2026
e9dc7d5
Add --v2 lean design-first mode to factory:go
christse Jul 17, 2026
e3f304b
V2 skill/prompt: Spec must populate catalog-facing title + description
christse Jul 17, 2026
2902016
Add RunLog live blog to --v2 runs; single-page mockups; no curl-polling
christse Jul 17, 2026
dc50089
Add --fork-context: prime once per brief, fork every implementation turn
christse Jul 17, 2026
133a8f3
Redesign RunLog to Boxel Workspace design language — alive under LIVE…
christse Jul 17, 2026
6305cde
RunLog: full activity coverage, show-me chips, throbber, live elapsed…
christse Jul 17, 2026
1a03291
Fix RunLog image field type: FileDef, not CardDef
christse Jul 17, 2026
f60ebc7
RunLog cold-start: intro/legend signage, top-pinned feed, warmup hint…
christse Jul 17, 2026
8916060
RunLog: raw re-write instance after sync to heal atomic FieldDef strip
christse Jul 17, 2026
bacab73
V2 live blog: stream tool events + agent post_update to the run log
christse Jul 17, 2026
6105e68
Ship moments carry live card links: stream handler tracks instance wr…
christse Jul 17, 2026
3ca6e7d
Validation entries link their step card (atom chip); v2 bootstrap sto…
christse Jul 17, 2026
3669071
Heal run-log after EVERY workspace sync (postSyncHeal hook)
christse Jul 17, 2026
b7ffcb0
Per-turn model/effort budget policy, orchestrator-owned
christse Jul 17, 2026
278e535
Cache-family heuristic: default fix budget tunes effort, not model; o…
christse Jul 17, 2026
d3dcc0b
Phase-split model mapping: DESIGN turn (flagship) + BUILD turn (cheap…
christse Jul 17, 2026
8d01aed
v2 skill: host tool import path rule + drag-drop items fallback rule
christse Jul 17, 2026
0c7e81a
v2 skill: searchResultsComponent wire-query contract rule
christse Jul 17, 2026
04fa079
v3: RunMonitor — orchestrator-authored run-log entries
christse Jul 17, 2026
54c7de5
v3: control/product realm split (--control-realm)
christse Jul 17, 2026
c8576cd
v3: host-import manifest gate (contract-drift elimination)
christse Jul 17, 2026
8713484
v3: render gate + acceptance walkthrough turn; v3 defaults
christse Jul 17, 2026
aab5219
v3: runtime-surface docs shipped as on-demand factory skills
christse Jul 17, 2026
2bc45fc
v3: GitHub-link briefs — port-analysis issue ahead of bootstrap
christse Jul 17, 2026
d5fd4bf
v3: --repo-url flag; port analysis goes INTO the code
christse Jul 17, 2026
729566a
v3: bootstrap prompt — repo-url stands in lieu of a wiki brief
christse Jul 17, 2026
cf3c875
Factory v2/v3: resume-safe restarts, session reuse, bug-fix path, run…
christse Jul 18, 2026
aafe8ae
docs: factory-v2-experiment branch overview (vs main)
christse Jul 18, 2026
e796601
docs: expand on-demand skill discovery/loading section
christse Jul 18, 2026
4c95090
Merge remote-tracking branch 'origin/main' into cs-12246-factory-v2-e…
jurgenwerk Jul 20, 2026
87d85fc
Update factory-entrypoint tests for the repo-url/control-realm arg su…
jurgenwerk Jul 20, 2026
092c276
Apply prettier formatting across software-factory
jurgenwerk Jul 20, 2026
da9bec4
Align seed e2e spec with resume-guard semantics
jurgenwerk Jul 20, 2026
580b9f2
Fix Spec realm routing and run-log render assertion
jurgenwerk Jul 20, 2026
49087c7
Drop the boxel-ui lodash declaration from this branch
jurgenwerk Jul 20, 2026
2a9bfd4
Add run-trace span telemetry across the factory loop
jurgenwerk Jul 21, 2026
3071755
Trace in-turn tool time, SDK lifecycle events, and full token usage
jurgenwerk Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 138 additions & 28 deletions .agents/skills/boxel-development/references/dev-fitted-formats.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,152 @@
## Fitted Format Essentials
# Fitted Format — Container-Query Standard

**Four sub-formats strategy:**
Distilled from the boxel-workspaces `container-query-fitted-layout.md`
standard (the full 1000-line guide with worked examples). Everything here
is CSS-only — no JS modifiers, no ResizeObserver.

- **Badge** (≤150px width, <170px height) - Exportable graphics
- **Strip** (>150px width, <170px height) - Dropdown/chooser panels
- **Tile** (<400px width, ≥170px height) - Grid viewing
- **Card** (≥400px width, ≥170px height) - Full layout
## The contract: the parent owns the cell size

**Container query skeleton:**
A fitted card never imposes its own size. The host wraps every fitted
template in `.field-component-card.fitted-format` with
`width: 100%; height: 100%; overflow: hidden; container-type: size;
container-name: fitted-card`.

**Query the host's `fitted-card` container. NEVER create your own
container on the root.** (An older version of this reference prescribed a
local `container-type: size` wrapper — that is WRONG and superseded: a
container cannot be styled by its own queries, so grid-template/padding
switches on your root silently stop working.)

The template root is a single `.fit` grid that fills the host wrapper:

```css
.fitted-container {
container-type: size;
width: 100%;
.fit {
width: 100%; /* the ONE sanctioned sizing declaration on the root */
height: 100%;
display: grid;
}

/* Hide all by default */
.badge,
.strip,
.tile,
.card {
display: none;
padding: clamp(0.25rem, 2%, 0.5rem);
@container fitted-card (max-height: 80px) {
.fit {
grid-template-rows: auto;
} /* queries against the HOST container style your root fine */
}
```

Keep OFF the root: fixed/min/max dimensions, `border`, `border-radius`,
`box-shadow`, `container-type`, `container-name` — the host owns all of
them. Background/foreground pairing from the theme is fine
(`background-color: var(--card); color: var(--card-foreground)`).

## Prefer `<FittedCard>` for standard compositions

For the standard composition — image/placeholder + eyebrow + title +
subtitle + meta + footer + badges — use the `FittedCard` component from
`@cardstack/boxel-ui/components` instead of hand-rolling. It implements
this whole standard internally (host-container queries, aspect-ratio
layout switching) and exposes `--fc-*` custom properties for tuning:

```gts
import { FittedCard } from '@cardstack/boxel-ui/components';

<FittedCard @imageUrl={{@model.imageUrl}} @imageAlt={{@model.title}}>
<:placeholder><BookOpen width='24' height='24' /></:placeholder>
<:eyebrow>Non-fiction</:eyebrow>
<:title><@fields.cardTitle /></:title>
<:subtitle><@fields.cardDescription /></:subtitle>
<:meta><span>150 mins</span></:meta>
<:footer><span>320 pages</span><span>2024</span></:footer>
</FittedCard>
```

No-media card types: omit `@imageUrl`, `:image`, AND `:placeholder` — the
image column disappears and content switches to its no-image layout.
Tune with `--fc-*` variables and `@container fitted-card (...)` overrides
from your scoped CSS; don't fork the layout.

**Hand-roll only special templates** (a dark terminal ticker, a ticket
stub, a boarding pass with an SVG flight path) — then follow the rest of
this reference.

## Size classification — plan content per quantum BEFORE writing CSS

Height quanta (what the host uses each size for):

| Quantum | Range | Visible regions | Host usage |
| ------- | --------- | ---------------------------------- | ----------------------- |
| h40 | ≤50px | head only | badges, inline mentions |
| h65 | 50–80px | head + meta | chooser dropdowns |
| h105 | 80–130px | head + body + meta | strips, search results |
| h170 | 130–200px | head + body + [tags] + meta | tiles |
| h275 | 200–320px | hero + head + body + [tags] + meta | large tiles, cards |
| h445 | >320px | all, spacious | expanded cards |

/* Activate by size - NO GAPS! */
@container (max-width: 150px) and (max-height: 169px) {
.badge {
display: flex;
Width classes:

| Class | Range | Behavior |
| ------ | --------- | -------------------------------------------------------------- |
| narrow | ≤170px | hide tags, clamp meta to 1 line, hide subhead at small heights |
| medium | 170–260px | hide tags, clamp meta to 1 line |
| wide | >260px | show tags; horizontal thumbnails at h40/h65/h105 |

Tags hide below 260px because wrapping pills consume unpredictable
height. Write the **content matrix first** — for each quantum, decide
which fields appear — then implement it as `@container fitted-card`
rules. A fitted view that only looks right at one size is a defect.

```css
@container fitted-card (max-height: 50px) {
/* h40: head only */
}
@container fitted-card (min-height: 50.1px) and (max-height: 80px) {
/* h65 */
}
@container fitted-card (min-height: 80.1px) and (max-height: 130px) {
/* h105 */
}
@container fitted-card (min-height: 130.1px) and (max-height: 200px) {
/* h170 */
}
@container fitted-card (min-height: 200.1px) {
/* h275+: hero appears */
}
@container fitted-card (max-width: 260px) {
.r-tags {
display: none;
}
}
```

**Content priority:**
## Overflow discipline (every region, no exceptions)

- Body/content rows: `minmax(0, 1fr)` — never `auto` (auto rows grow
with content and blow the cell).
- `overflow: hidden` on every region.
- Text clamps: `display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: N; overflow: hidden`.
- `min-height: 0` on any nested flex/grid child that must shrink.

## Container-query units caveat

`cqw`/`cqh`/`cqmin` only resolve against an actual container. Inside a
fitted template they resolve against the host's `fitted-card` container —
fine. On other surfaces (isolated/embedded) they silently fall back to
the VIEWPORT unless you establish a container on that surface with
`container-type: inline-size` first.

## Parent-side note

When a parent embeds fitted children (`<@fields.x @format='fitted' />`),
the parent sizes the cell (e.g. a grid of `160px × 180px` tiles) and may
need `@displayContainer={{false}}` to suppress double chrome — see
`dev-delegated-rendering.md`.

## Media fields (cardinal rule, applies to ALL formats)

1. Title/Name
2. Image
3. Short ID
4. Key info
5. Status badges
An image on a card is `@field image = linksTo(() => ImageDef)` (or
FileDef) pointing at a real realm file — write the binary into the
workspace and reference it. NEVER design a field that stores
`data:`/base64/blob strings in JSON attributes, not even "just a
placeholder" — inline media bytes in instance JSON are a hard rule
violation that corrupts diffs, bloats the index, and breaks the file's
identity. No-media-yet instances leave the link empty and the template
renders a placeholder block/icon.
96 changes: 96 additions & 0 deletions .agents/skills/boxel-workspace-cardinal-rules/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Boxel cardinal rules — silent-failure traps

Rules discovered the hard way in a downstream Boxel workspace: each one passes lint and
often passes indexing too, then breaks silently — corrupting the realm's index,
crashing at render, or dropping data with no error. Check every card/field you write
against this list before finishing an issue.

## 1. DateField vs DateTimeField value format

`DateField` values are `YYYY-MM-DD` (no `T`). `DateTimeField` values are full ISO
datetimes (`2026-07-16T14:30:00.000Z`, with `T`). Putting a datetime string in a
`DateField` (or vice versa) passes lint and indexes fine, then **crashes at render**
with `RangeError: Invalid time value` when a user actually opens the card. Naming
convention to follow when picking the field type: a `*At` suffix (`createdAt`,
`publishedAt`) means `DateTimeField`; a `*Date`/`*On` suffix or bare `dob` means
`DateField`.

## 2. Never put an external URL in `relationships.<field>.links.self`

If a `linksTo`/`linksToMany` field's JSON `links.self` points at a URL the indexer
can't parse as a card (an external website, an image CDN URL, anything not a card
resource), **the failed parse poisons the JSONB write and rolls back the WHOLE
REALM's indexing transaction** — every other file in the same push silently fails to
index too, with no error pointing at the actual bad file. For an external image/URL,
use the pair pattern instead: `linksTo(ImageDef)` (or a similar file/media field) +
`contains(UrlField)` as two separate fields, never one relationship pointing straight
at an external URL.

## 3. `linksToMany` JSON uses indexed top-level keys, never an array

Correct:

```json
"relationships": {
"items.0": { "links": { "self": "../foo" } },
"items.1": { "links": { "self": "../bar" } }
}
```

Wrong (rejected outright — "instance ... is not a card resource document"):

```json
"relationships": {
"items": { "links": { "self": ["../foo", "../bar"] } }
}
```

## 4. Never inline media or binary bytes in card JSON

No `data:` URIs, `blob:` URIs, base64, or raw media bytes in any JSON string field or
attribute. Store media as a realm file linked via `linksTo(FileDef)` (or a FileDef
subtype: `ImageDef`, `CsvFileDef`, etc.) instead — never embed the bytes directly in
the instance JSON.

## 5. Every query needs a realm scope, and don't start it before the realm is known

A card-owned query with a missing or empty `realm` argument silently falls back to
searching **every realm the server can see**, not just the current one. Always scope
queries to the current card's own realm, and don't kick off the query before that
realm URL is actually resolved. Cap general-purpose result sets (~100) rather than
pulling unbounded result sets.

## 6. Query-filter shape: `type` selects instances, `on` only scopes predicates

To select every instance of a type, filter on `{ type: <ref> }` directly — never wrap
it in `{ on: <ref> }` alone (`on` only scopes _other_ predicates like `eq`/`contains`;
a bare `{ on: ref }` with nothing else matches nothing). Build refs with a `codeRef()`
helper, not a manually-constructed object.

## 7. Don't push more than ~30 files through one atomic batch to a fresh realm

Large atomic pushes (30+ files via a single bulk-write endpoint) can report success
while silently dropping some files' indexing jobs. For bulk kit/asset installs, push
in smaller batches and verify each batch's expected file count actually shows up in a
realm search before pushing the next batch.

## 8. NEVER curl / HTTP-GET a `https://cardstack.com/base/*` module URL to inspect a base card

Base card module URLs (`https://cardstack.com/base/theme`, `.../base/card-api`,
`.../base/cards/structured-theme`, etc.) are **loader-resolved module references, not
fetchable HTTP resources.** `cardstack.com` is a marketing site — a direct GET or a
realm op (`_mtimes`, `boxel file read`) against `cardstack.com/base/...` returns a
generic Webflow **404 HTML page** (`data-wf-domain=... %%PUBLISH_URL_REPLACEMENT%%`),
NOT the card. Do not keep retrying it — that page will never become the schema. To
learn a base card's fields/shape, use the **`get_card_schema` tool** (it resolves
through the realm server), or read an existing instance of that card already in the
target realm. Same rule for any published `*.boxel.site` / `*.boxel.build` URL: those
are Webflow-published sites, not realms — never point realm operations at them.

Also: many base cards are **default exports**, so the schema ref is `name: "default"`,
NOT the class name. The base **Theme** card is the default export of
`https://cardstack.com/base/theme` (the module is `export default Theme`) — query it as
module `https://cardstack.com/base/theme`, name `default` (querying `#Theme` fails).
`StructuredTheme` is likewise the default export of `base/structured-theme`. When a
`get_card_schema` call fails with "named export is a CardDef", retry with `name:
"default"` before assuming the card is unreachable — do NOT fall back to curling the URL.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: boxel-delegated-render-control
description: Controlling child-card renders from the parent — fitted vs embedded format choice, the host-injected CardContainer chrome, and the plural-field/atom/stagger/divider traps. Use when embedding cards via <@fields.X @format='...' /> or when an embedded/fitted child looks wrong (empty boxes, collapsed grids, double borders, invisible atoms).
---

# Delegated Render Control

When a parent card renders a child via `<@fields.X @format='...' />`, the
host wraps the child in a `CardContainer` with chrome you didn't write
(rounded corners, 1px halo, light background, `overflow: hidden`). Two
decisions dominate the outcome: **which format** you pick, and **who owns
the chrome**. The full mechanics — exact injected DOM/CSS, override layers,
recipes, and the child-side contract — live in
`references/delegated-render-control.md`.

## Format choice = who owns the cell size (decide BEFORE styling)

| Format | Who controls the box size? | Use when |
| ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `embedded` | **The child.** Width fluid, height = the card's natural content. | Vertical lists, feeds, roster rows, variable-height items. |
| `fitted` | **The parent.** Child fills the box you give it (`width/height: 100%`, `container-type: size`). | Uniform tile grids (calendar cells, portraits, badge strips) where you deliberately set the cell size. |

**The single most common rendering bug:** picking `fitted` for a list of
variable-height cards — short content leaves a big empty box below each row.
The fix is the format choice, upstream of any CSS. Decision rule: _did you
set the cell size?_ Yes → `fitted` (+ `min-height`/`aspect-ratio` on the
cell). No → `embedded`.

## Who owns the chrome — three override layers (pick the lowest that works)

1. **Theme cascade** — if the linked card has `cardInfo.theme`, the theme's
`--background`, `--foreground`, `--border`, `--radius` flow into the
wrapper. Cleanest, but only when the child instance actually has a theme.
2. **`:deep()` from the parent's `<style scoped>`** — the workhorse.
Target `:deep(.boxel-card-container)` for radius/background,
`:deep(.boxel-card-container--boundaries)` for the 1px halo.
3. **`@displayContainer={{false}}`** — kills the chrome entirely
(`display: contents`); pair with a parent-owned wrapper element.

## The high-frequency traps

1. **Plural-field wrapper.** Rendering a plural field with one tag
(`<@fields.items @format='fitted' />`) inserts wrapper divs between your
grid and the cards — the grid sees ONE child and collapses to a single
column. Fix: `:deep(> .plural-field) { display: contents; }` plus
`:deep(.linksToMany-itemContainer), :deep(.containsMany-item) { display:
contents; }`. Targeting only `.containsMany-field` is the most common
bug — `linksToMany` ships `.linksToMany-field`, which never matches.
2. **Atoms on dark backgrounds disappear.** The atom chip's own near-white
background + halo win over your `color: inherit`. Fix: either
`@displayContainer={{false}}` (plain inline text), or keep the chip and
recolor via `:deep(.field-component-card.atom-format) { background:
transparent; box-shadow: none; color: var(--accent); }`.
3. **Stagger through `display: contents`.** `:nth-child` on the cards
themselves always resolves to 1 (each is the only child of its wrapper).
Set `--stagger-d` on `:deep(.linksToMany-itemContainer:nth-child(N))` /
`:deep(.containsMany-item:nth-child(N))` and read
`animation-delay: var(--stagger-d)` on `.field-component-card` — custom
properties inherit through `display: contents`.
4. **Divider strategy — parent draws OR child halo, never both.** Either
the parent draws dividers and kills the child halo
(`:deep(.boxel-card-container--boundaries) { box-shadow: none; }`), or
the halo IS the boundary and the parent adds no borders. Leaving both
yields the "drop shadow fighting a thin border" double-rule. When
switching strategies, DELETE the stale rule — same-selector CSS resolves
by source order.

## Don't override

`width/height: 100%` on `.fitted-format`, or `container-type` /
`container-name` on embedded/fitted — the child's container queries depend
on them. Style chrome (radius, background, border, shadow, padding), not
layout primitives.

## The child-side contract

The child draws ONLY inside the box; host or parent draws the box. No
`border-radius`, `border`, `box-shadow`, `overflow`, or size properties on
any format's outermost element — see the reference for the per-format table
and self-check.
Loading