diff --git a/.agents/skills/html-plan/SKILL.md b/.agents/skills/html-plan/SKILL.md new file mode 100644 index 0000000..fbd3b66 --- /dev/null +++ b/.agents/skills/html-plan/SKILL.md @@ -0,0 +1,15 @@ +--- +name: html-plan +description: Create a self-contained HTML plan that is pragmatic, simple, and visually organized. Use when the user wants a plan page in the effective HTML style, wants the writing kept close to what they gave you, or wants the grammar cleaned up without turning it into a whole bigger thing. +disable-model-invocation: true +--- + +# HTML Plan + +Review the files throughout `references/html-effectiveness/`. + +After reviewing them, create an HTML file for the plan in a similar style. + +Keep it pragmatic and simple. + +Always include dark mode: hand-rolled CSS variables on `:root` / `html.dark`, a small theme toggle button, `localStorage` persistence, and an apply-before-paint script in `` (default to `prefers-color-scheme`). diff --git a/.agents/skills/html-plan/agents/openai.yaml b/.agents/skills/html-plan/agents/openai.yaml new file mode 100644 index 0000000..6a17c9d --- /dev/null +++ b/.agents/skills/html-plan/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "HTML Plan" + short_description: "Create pragmatic visual HTML plans" + default_prompt: "Use $html-plan to create a pragmatic HTML plan." + +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/html-plan/references/html-effectiveness/01-exploration-code-approaches.html b/.agents/skills/html-plan/references/html-effectiveness/01-exploration-code-approaches.html new file mode 100644 index 0000000..fc0c648 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/01-exploration-code-approaches.html @@ -0,0 +1,454 @@ + + + + + + + Debounced search — three approaches + + + +
+ +
+
Exploration · Acme web client
+

Three ways to implement debounced search

+
+ Prompt + Show me three different ways to implement debounced search for the task + filter input in our React codebase, with tradeoffs for each. +
+
+ + + +
+ + +
+
+

01Inline useEffect + setTimeout

+

Debounce logic lives directly inside the component that owns the input.

+
+ +
export function TaskSearch() {
+  const [draft, setDraft] = useState('');
+  const [query, setQuery] = useState('');
+
+  useEffect(() => {
+    const id = setTimeout(() => setQuery(draft), 300);
+    return () => clearTimeout(id);
+  }, [draft]);
+
+  const { data } = useTasks({ search: query });
+
+  return (
+    <input
+      value={draft}
+      onChange={(e) => setDraft(e.target.value)}
+      placeholder="Filter tasks…"
+    />
+  );
+}
+ +
+
+
Pro
+
Con
+
+
+
Zero new abstractions to learn
+
Logic duplicated everywhere search exists
+
+
+
Easy to step through in devtools
+
Two pieces of state for one conceptual value
+
+
+
No dependency or bundle change
+
Delay constant is buried in component body
+
+
+ +
+ Bundle impact: +0 kb + Testability: medium + Reuse: low + SSR safe: yes +
+
+ + +
+
+

02Custom useDebounce hook

+

Extract the timer into a shared hook under src/hooks/.

+
+ +
// src/hooks/useDebounce.ts
+export function useDebounce<T>(value: T, ms = 300): T {
+  const [debounced, setDebounced] = useState(value);
+  useEffect(() => {
+    const id = setTimeout(() => setDebounced(value), ms);
+    return () => clearTimeout(id);
+  }, [value, ms]);
+  return debounced;
+}
+
+// TaskSearch.tsx
+const [draft, setDraft] = useState('');
+const query = useDebounce(draft, 300);
+const { data } = useTasks({ search: query });
+ +
+
+
Pro
+
Con
+
+
+
Single import reused across filter, command bar, board search
+
One more file to maintain and document
+
+
+
Trivial to unit test with fake timers
+
Generic T hides intent slightly
+
+
+
Delay is a visible, tunable argument
+
Still re-renders on every keystroke
+
+
+ +
+ Bundle impact: +0.2 kb + Testability: high + Reuse: high + SSR safe: yes +
+
+ + +
+
+

03Tiny external library

+

Adopt use-debounce for both values and callbacks.

+
+ +
import { useDebouncedCallback }
+  from 'use-debounce';
+
+export function TaskSearch() {
+  const [query, setQuery] = useState('');
+
+  const onChange = useDebouncedCallback(
+    (next: string) => setQuery(next),
+    300,
+    { leading: false, maxWait: 1000 },
+  );
+
+  const { data } = useTasks({ search: query });
+
+  return (
+    <input
+      defaultValue=""
+      onChange={(e) => onChange(e.target.value)}
+    />
+  );
+}
+ +
+
+
Pro
+
Con
+
+
+
leading / trailing / maxWait handled for us
+
New runtime dependency to audit and update
+
+
+
Callback form skips intermediate re-renders
+
Uncontrolled input diverges from Acme form patterns
+
+
+
Well-tested edge cases (unmount, flush, cancel)
+
~1.4 kb gzipped for something we could own
+
+
+ +
+ Bundle impact: +1.4 kb + Testability: high + Reuse: high + SSR safe: yes +
+
+ +
+ + + + + +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/02-exploration-visual-designs.html b/.agents/skills/html-plan/references/html-effectiveness/02-exploration-visual-designs.html new file mode 100644 index 0000000..817fcc3 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/02-exploration-visual-designs.html @@ -0,0 +1,516 @@ + + + + + + + Empty state — four visual directions + + + + +
+
Acme · design exploration
+
+ Background: +
+ + +
+
+
+ +
+ +
+
Exploration · Empty states
+

Four visual directions for the “no tasks yet” state

+
+ Prompt + Explore four visual directions for our empty-state component. Render each + live so we can compare tone, density, and how well they hold up on light + and dark surfaces. +
+
+ +
+ + +
+ A — Minimal +
+
+

No tasks yet

+

When you create a task it will show up here.

+ New task +
+
+

+ Pure typography, single quiet action. Reads as calm and confident; + assumes the surrounding UI already carries enough personality. +

+
+ + +
+ B — Illustrated +
+
+ +

Start your first list

+

Group related work and watch progress roll up automatically.

+ Create a task +
+
+

+ A small geometric spot illustration anchors the eye and explains the + object model (lists contain tasks) without a wall of copy. +

+
+ + +
+ C — Playful +
+
+
+
+
+
+
+
+

Nothing on your plate

+

Enjoy the quiet, or add something to get moving.

+
+
+

+ A gently bobbing stack adds life to an otherwise static screen. Motion + is subtle enough to loop indefinitely without drawing complaints. +

+
+ + +
+ D — Instructional +
+
+

Set up this project

+
    +
  1. + Create your first task + Give it a name and an owner. +
  2. +
  3. + Add a due date + Acme will surface it on the timeline. +
  4. +
  5. + Invite a teammate + Shared projects stay in sync automatically. +
  6. +
+
+
+

+ Treats the empty state as onboarding. Higher density, but every line is + actionable — best when the user is new to the product, not just the view. +

+
+ +
+
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/03-code-review-pr.html b/.agents/skills/html-plan/references/html-effectiveness/03-code-review-pr.html new file mode 100644 index 0000000..bfb344b --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/03-code-review-pr.html @@ -0,0 +1,639 @@ + + + + + + +PR #247 — Review Summary + + + +
+ +
+
acme/web · Pull Request #247
+

Add optimistic updates to task list mutations

+
+
+
MO
+
+
Mira Okafor
+
opened 2 days ago
+
+
+
+ mo/optimistic-tasks main +
+
+ +142 / −38 + 6 files changed +
+
+
+ +
+

What this PR does

+ +
+ +
+

Risk map

+ +
+ safe + worth a look + needs attention +
+
+ +
+

Files

+ + +
+
+
+
src/hooks/useOptimisticTasks.ts
+
+
+ needs attention + +58 −0 +
+
+
+
@@ -0,0 +1,58 @@
+
1+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
2+import { updateTask, TaskPatch } from '../api/tasks';
+
3+import type { Task } from '../types/task';
+
4+
+
5+export function useOptimisticTasks(boardId: string) {
+
6+ const qc = useQueryClient();
+
7+ const key = ['tasks', boardId];
+
8+
+
9+ return useMutation({
+
10+ mutationFn: (patch: TaskPatch) => updateTask(patch),
+
11+ onMutate: async (patch) => {
+
12+ const prev = qc.getQueryData<Task[]>(key);
+
13+ qc.setQueryData<Task[]>(key, (old = []) =>
+
14+ old.map(t => t.id === patch.id ? { ...t, ...patch } : t)
+
15+ );
+
16+ return { prev };
+
17+ },
+
18+ onError: (_e, _p, ctx) => qc.setQueryData(key, ctx?.prev),
+
19+ });
+
20+}
+
+
+
+
line 11
+

BlockingonMutate doesn't call qc.cancelQueries(key) first. If a background refetch lands between the optimistic write and the server response, it will clobber the optimistic state and the UI will flicker back to the old value.

+
+
+
line 18
+

NitRollback restores the list but never surfaces the error. Consider wiring the existing pushToast here so users know the toggle didn't stick.

+
+
+
+ + +
+
+
+
src/components/TaskList.tsx
+
+
+ worth a look + +31 −24 +
+
+
+
@@ -42,14 +42,17 @@ export function TaskList({ boardId }: Props) {
+
42 const { data: tasks } = useTasks(boardId);
+
43- const [pending, setPending] = useState<string | null>(null);
+
44-
+
45- async function toggle(task: Task) {
+
46- setPending(task.id);
+
47- await updateTask({ id: task.id, done: !task.done });
+
48- await refetch();
+
49- setPending(null);
+
50- }
+
43+ const { mutate, isPending } = useOptimisticTasks(boardId);
+
44+
+
45+ const toggle = (task: Task) =>
+
46+ mutate({ id: task.id, done: !task.done });
+
47
+
48 return (
+
49 <ul className="tasks">
+
50- {tasks?.map(t => <TaskRow key={t.id} task={t} busy={pending === t.id} />)}
+
50+ {tasks?.map(t => <TaskRow key={t.id} task={t} onToggle={toggle} />)}
+
51 </ul>
+
+
+
+
line 43
+

NitisPending is destructured but never read. Either drop it or pass it to TaskRow so the checkbox can dim while the request is in flight.

+
+
+
+ + +
+
+
+
src/api/tasks.ts
+
+
+ worth a look + +19 −6 +
+
+
+
@@ -12,10 +12,15 @@ export type TaskPatch = Partial<Task> & { id: string };
+
12
+
13-export async function updateTask(patch: TaskPatch) {
+
14- return http.patch(`/tasks/${patch.id}`, patch);
+
13+export async function updateTask(
+
14+ patch: TaskPatch,
+
15+ key = crypto.randomUUID(),
+
16+) {
+
17+ return http.patch(`/tasks/${patch.id}`, patch, {
+
18+ headers: { 'Idempotency-Key': key },
+
19+ });
+
20 }
+
+
+
+
line 15
+

BlockingGenerating the idempotency key as a default parameter means retries from the mutation layer get a new key each time, which defeats the purpose. The key should be minted once in onMutate and threaded through.

+
+
+
+ + +
+ + src/components/Toast.tsx + + safe + +14 −2 + + +
Adds a variant="warning" style and exports pushToast. Purely additive, no behaviour change for existing call sites.
+
+ +
+ + src/types/task.ts + + safe + +6 −2 + + +
Widens Task.status to include "archived" and adds an optional updatedAt timestamp. Type-only change.
+
+ +
+ + src/components/__tests__/TaskList.test.tsx + + safe + +14 −4 + + +
Adds a test asserting the row updates synchronously after click, and one asserting rollback when the mocked request rejects. Both pass locally.
+
+
+ + + +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/04-code-understanding.html b/.agents/skills/html-plan/references/html-effectiveness/04-code-understanding.html new file mode 100644 index 0000000..e36505b --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/04-code-understanding.html @@ -0,0 +1,492 @@ + + + + + + +How authentication flows through acme/web + + + +
+ +
+
acme/web · architecture note
+

How authentication flows through the codebase

+

+ Acme uses cookie-based sessions: the browser never holds a bearer token directly. Every authenticated request hits /api/*, passes through a single verifyToken() middleware, and resolves to a Session row that downstream handlers read off req.ctx. The middleware is the only place that talks to the session store, which is the only place that talks to the sessions table — so there's exactly one trust boundary to reason about. +

+
+ +
+

Request path

+
+ + + + + + + + + + + Browser + acme.app + + + + /api/session + route handler + + + + verifyToken() + middleware/auth.ts + + + + + + SessionStore + lib/sessionStore.ts + + + + Postgres + sessions table + + + + + + + + + cookie + lookup + +
+ +

Callstack walkthrough

+ + +
+
1
+
+
src/app/providers/AuthProvider.tsx :22-48
+

On mount, the React provider issues a GET /api/session with credentials: 'include' so the fw_sid cookie rides along. The response either hydrates currentUser into context or leaves it null, which the router treats as "show the sign-in screen".

+
+ show source +
// src/app/providers/AuthProvider.tsx
+export function AuthProvider({ children }: Props) {
+  const [user, setUser] = useState<User | null>(null);
+
+  useEffect(() => {
+    fetch('/api/session', { credentials: 'include' })
+      .then(r => r.ok ? r.json() : null)
+      .then(setUser);
+  }, []);
+
+  return <AuthCtx.Provider value={{ user }}>{children}</AuthCtx.Provider>;
+}
+
+
+
+ + +
+
2
+
+
src/server/routes/session.ts :9-27
+

The route itself is thin: it just returns whatever req.ctx.session the middleware attached. If the middleware short-circuited with a 401, this handler never runs — so there's no auth logic duplicated here.

+
+ show source +
// src/server/routes/session.ts
+router.get('/session', verifyToken, (req, res) => {
+  const { session } = req.ctx;
+  res.json({
+    id:    session.userId,
+    email: session.email,
+    role:  session.role,
+    exp:   session.expiresAt,
+  });
+});
+
+
+
+ + +
+
3
+
+
src/middleware/auth.ts :14-31
+

This is the trust boundary. verifyToken reads the signed fw_sid cookie, asks SessionStore to resolve it, and either populates req.ctx.session or responds 401. Every protected route in the app is mounted behind this function, so changing its behaviour changes auth globally.

+
+ show source +
// src/middleware/auth.ts
+export async function verifyToken(req, res, next) {
+  const raw = req.signedCookies['fw_sid'];
+  if (!raw) return res.status(401).end();
+
+  const session = await SessionStore.get(raw);
+  if (!session || session.expiresAt < Date.now()) {
+    return res.status(401).end();
+  }
+
+  req.ctx = { session };
+  next();
+}
+
+
+
+ + +
+
4
+
+
src/lib/sessionStore.ts :8-52
+

SessionStore is a small read-through cache: it checks an in-process LRU first, then falls back to Postgres. Writes (create, revoke) always go straight to the DB and invalidate the cache entry so other workers don't serve a stale session.

+
+ show source +
// src/lib/sessionStore.ts
+const cache = new LRU<string, Session>({ max: 5000, ttl: 60_000 });
+
+export const SessionStore = {
+  async get(id: string) {
+    const hit = cache.get(id);
+    if (hit) return hit;
+    const row = await db.one(SELECT_SESSION, [id]);
+    if (row) cache.set(id, row);
+    return row ?? null;
+  },
+  /* create, revoke, touch ... */
+};
+
+
+
+ + +
+
5
+
+
db/migrations/004_sessions.sql :1-18
+

The sessions table is keyed on a random 32-byte id (the cookie value) with a covering index on user_id for "sign out everywhere". Expiry is enforced both here (expires_at) and again in the middleware as defence in depth.

+
+ show source +
-- db/migrations/004_sessions.sql
+create table sessions (
+  id          text primary key,
+  user_id     uuid not null references users(id),
+  created_at  timestamptz default now(),
+  expires_at  timestamptz not null,
+  ip          inet,
+  user_agent  text
+);
+create index sessions_user_id_idx on sessions(user_id);
+
+
+
+
+ + + +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/05-design-system.html b/.agents/skills/html-plan/references/html-effectiveness/05-design-system.html new file mode 100644 index 0000000..da5460e --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/05-design-system.html @@ -0,0 +1,630 @@ + + + + + + +Acme — Design System Reference + + + +
+ +
+

Acme design system

+

+ Generated from src/styles/tokens.ts and src/components/ — use as a portable reference when prompting. +

+
+ + +
+

Color

+
+ +
+
Primary
+
+
+
+ #D97757 + --clay +
+
+
+ #141413 + --slate +
+
+
+ #FAF9F5 + --ivory +
+
+
+ #E3DACC + --oat +
+
+
+ +
+
Neutral
+
+
+
+ #FFFFFF + --white +
+
+
+ #F0EEE6 + --gray-100 +
+
+
+ #D1CFC5 + --gray-300 +
+
+
+ #87867F + --gray-500 +
+
+
+ #3D3D3A + --gray-700 +
+
+
+ +
+
Semantic
+
+
+
+ #788C5D + --success +
+
+
+ #C78E3F + --warning +
+
+
+ #B04A4A + --danger +
+
+
+ #5C7CA3 + --info +
+
+
+
+ + +
+

Typography

+
+ +
+
+
Plan the week ahead
+
+ Display + 48 / 1.1 / 500 +
+
+
+
Plan the week ahead
+
+ Heading 1 + 32 / 1.2 / 500 +
+
+
+
Plan the week ahead
+
+ Heading 2 + 24 / 1.3 / 500 +
+
+
+
Review milestones, assign owners, and surface blockers before they cascade.
+
+ Body + 16 / 1.55 / 430 +
+
+
+
Review milestones, assign owners, and surface blockers before they cascade.
+
+ Small + 14 / 1.5 / 430 +
+
+
+
UPDATED 2 HOURS AGO
+
+ Caption + 12 / 1.4 / 500 +
+
+
+
+ + +
+

Spacing

+
+ +
+
+
+
4--sp-1
+
+
+
+
8--sp-2
+
+
+
+
12--sp-3
+
+
+
+
16--sp-4
+
+
+
+
24--sp-5
+
+
+
+
32--sp-6
+
+
+
+
48--sp-7
+
+
+
+
64--sp-8
+
+
+
+ + +
+

Radius & Elevation

+
+ +
+
+
4px--r-xs
+
+
+
8px--r-sm
+
+
+
12px--r-md
+
+
+
20px--r-lg
+
+
+ +
+
+
--shadow-sm0 1px 2px / 6%
+
+
+
--shadow-md0 4px 10px / 8%
+
+
+
--shadow-lg0 12px 28px / 12%
+
+
+
+ + +
+

Core components

+
+ +
+
<Button />
+
+ + + + +
+
+ +
+
<Input />
+
+ + +
+
+ +
+
<Checkbox />
+
+ + +
+
+ +
+
<Badge />
+
+ Draft + In review + Done + Overdue +
+
+
+ +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/06-component-variants.html b/.agents/skills/html-plan/references/html-effectiveness/06-component-variants.html new file mode 100644 index 0000000..75debcf --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/06-component-variants.html @@ -0,0 +1,606 @@ + + + + + + +Acme — Card Variant Matrix + + + +
+ +
+

Card variant matrix

+

Six structural treatments of the Acme <Card /> component. Adjust density and emphasis with the controls, hover a variant to see its prop combo.

+
+ + +
+
+ Padding + + 20px +
+ +
+ Border +
+ + + +
+
+ +
+ +
+
+ + +
+ + +
+ A · Flat +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: dense lists on tinted backgrounds

+
+ + +
+ B · Outlined +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: default content cards on ivory

+
+ + +
+ C · Elevated +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: draggable items, popovers

+
+ + +
+ D · Accent stripe +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: pinned or priority items

+
+ + +
+ E · Inset +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: nested cards inside white panels

+
+ + +
+ F · Horizontal +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: compact row lists, sidebars

+
+ +
+ + +
+
JSX — hover a variant above
+
// hover a card to preview its props
+
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/07-prototype-animation.html b/.agents/skills/html-plan/references/html-effectiveness/07-prototype-animation.html new file mode 100644 index 0000000..b63929e --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/07-prototype-animation.html @@ -0,0 +1,456 @@ + + + + + + +Acme — Task completed micro-interaction + + + +
+ +
+
Acme / prototype / micro-interaction
+

Task completed

+

+ A single click should feel like a tiny win. Circle fills, check draws, + label strikes, a small burst, then the row quietly steps back. Click + the row to play; click again to reset. +

+
+ +
+ +
+
+
+ +
+ Send weekly digest + Fri +
+
+
+
+
+
+
+
click to toggle
+
+ + + +
+ +
+

Keyframes

+
+
fill0ms
+
check80ms
+
strike120ms
+
confetti200ms
+
collapse600ms
+
+
+ +
+

Copy-paste CSS

+
/* circle: clay flash, settle to olive with spring overshoot */
+.task.done .check {
+  animation: settle 380ms cubic-bezier(.34,1.56,.64,1) forwards;
+}
+@keyframes settle {
+  0%   { transform: scale(.8);  background: #D97757; }
+  55%  { transform: scale(1.18); }
+  100% { transform: scale(1);   background: #788C5D; }
+}
+
+/* checkmark draws via stroke-dashoffset, 80ms delay */
+.check path        { stroke-dasharray: 20; stroke-dashoffset: 20;
+                     transition: stroke-dashoffset 220ms var(--ease) 80ms; }
+.task.done path    { stroke-dashoffset: 0; }
+
+/* strikethrough grows left → right */
+.label::after      { width: 0; transition: width 240ms var(--ease) 120ms; }
+.task.done .label::after { width: 100%; }
+
+/* row steps back after the celebration */
+.task.done         { max-height: 44px; opacity: .6;
+                     transition-delay: 600ms; }
+
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/08-prototype-interaction.html b/.agents/skills/html-plan/references/html-effectiveness/08-prototype-interaction.html new file mode 100644 index 0000000..fb8fde0 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/08-prototype-interaction.html @@ -0,0 +1,397 @@ + + + + + + +Acme — Sidebar drag-to-reorder + + + +
+ +
+
Acme / prototype / interaction
+

Sidebar drag-to-reorder

+

+ Throwaway HTML so we can feel the reorder before porting it to + React. Native dragstart / dragover / drop, + ~40 lines of JS, no libraries. Grab a row by the dots and move it. +

+
+ +
+ +
+ +

Order persists in the DOM only — refresh to reset.

+
+ +
+
+

What you're feeling

+

Design decisions baked into this prototype, so you can push back on them.

+
    +
  • + Drop indicator snaps to the nearest gap, not the raw cursor Y. + It only moves when you cross a row's midpoint — feels more decisive, + less jittery. +
  • +
  • + Dragged row stays in place at 35% opacity with a 2° tilt. + Keeping the ghost in the list preserves your sense of where you + started; the tilt reads as "lifted." +
  • +
  • + Grip dots are the affordance, but the whole row is draggable. + Dots darken on hover to teach the gesture without forcing a + tiny hit target. +
  • +
  • + No auto-scroll, no drop animation. Left out on purpose so the + core feel is easy to judge — say the word and we add them next. +
  • +
+
+ +
+

Open questions

+
    +
  1. Should Trash (and maybe Archive) be pinned to the bottom and excluded from reordering?
  2. +
  3. Do we want rows to slide to their new slot on drop, or is the instant snap acceptable?
  4. +
  5. Keyboard path: is Alt + Arrow to move the focused row enough for the first ship?
  6. +
+
+
+ +
+
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/09-slide-deck.html b/.agents/skills/html-plan/references/html-effectiveness/09-slide-deck.html new file mode 100644 index 0000000..61fd981 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/09-slide-deck.html @@ -0,0 +1,593 @@ + + + + + + +Platform Eng — Week of Mar 10 + + + + + +
+
+ +

Platform Eng
— Week of Mar 10

+

+ What shipped, what's moving, and one decision we need from the + room before the Acme 2.4 cut. +

+ +
+
+ + +
+
+
Shipped this week
+

Three things out the door

+ +
+
+ + +
+
+
In progress
+

Carrying into next week

+ +
+
+ + +
+
+
Metrics
+

Numbers we watch

+ +
+
+
API p95 latency
+
184ms
+
↓ 12% wk/wk
+
+
+
Background job error rate
+
0.21%
+
↓ 0.08pp
+
+
+ +
+ +
+ p95 latency, trailing 8 days — lower is better +
+
+
+
+ + +
+
+
Decision needed
+

One call to make

+ +
+

+ Do we ship recurring tasks behind a workspace flag in 2.4, or + hold one more week for the timezone fixes? +

+

+ Flagged rollout gets it to design partners Friday but means two + code paths for ~2 weeks. Holding keeps a single path but slips + the partner promise. +

+
+ +
+ A — Flag it, ship Friday + B — Hold for 2.5 +
+
+
+ + +
+
+
Next week
+

On deck

+ +
+ Questions → drop them in the platform channel or grab anyone after standup. +
+
+
+ + +
1 / 6
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/10-svg-illustrations.html b/.agents/skills/html-plan/references/html-effectiveness/10-svg-illustrations.html new file mode 100644 index 0000000..c408408 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/10-svg-illustrations.html @@ -0,0 +1,493 @@ + + + + + + +Background jobs — header illustrations + + + +
+ +
+

Background jobs — header illustrations

+

+ Three 720×320 hand-drawn SVGs for the Acme docs section on + background jobs. Flat fills, 1.5–2px strokes, palette-locked. Each + exports standalone via the button below it. +

+
720 × 320 · inline SVG · no external assets
+
+ + +
+
+ + + + + + + + + + + + queue + + + + + job 5 + + + job 4 + + + job 3 + + + job 2 + + + + job 1 + + + + + + + + worker + pool=4 + + + next to run + Jobs are pulled FIFO; the worker leases one at a time. + +
+
+
+
Queue
+
For "How jobs are picked up" — intro page header.
+
+ +
+
+ + +
+
+ + + + + + + + + + t = 0 + time → + + + + + + + try 1 + + + + + + try 2 + + + + + + try 3 + + + + + try 4 + + + + +1s + + + +2s + + + +4s + + + Each failure waits twice as long before re-queuing; jitter not pictured. + +
+
+
+
Retry with backoff
+
For "Handling failures" — retry policy header.
+
+ +
+
+ + +
+
+ + + + + + + + + + + + + enqueue + batch() + + + + + + + + + + + shard 0 + + + shard 1 + + + shard 2 + + + shard 3 + + + + + + + + + + + merge + await all + + + fan-out + fan-in + Parent job spawns N children, then blocks on a completion barrier. + +
+
+
+
Fan-out / fan-in
+
For "Batch and parallel work" — fan-out pattern header.
+
+ +
+
+ + +
+

Palette & rules

+ +
+
+
+
+
ivory
+
#FAF9F5
+
+
+
+
+
+
slate
+
#141413
+
+
+
+
+
+
clay
+
#D97757
+
+
+
+
+
+
olive
+
#788C5D
+
+
+
+
+
+
oat
+
#E3DACC
+
+
+
+
+
+
gray-150
+
#F0EEE6
+
+
+
+
+
+
gray-300
+
#D1CFC5
+
+
+
+
+
+
gray-500
+
#87867F
+
+
+
+ + +
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/11-status-report.html b/.agents/skills/html-plan/references/html-effectiveness/11-status-report.html new file mode 100644 index 0000000..7f5ed34 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/11-status-report.html @@ -0,0 +1,529 @@ + + + + + + +Acme — Engineering Status — Week 11 + + + +
+ +
+
+

Engineering Status — Week 11

+ auto-generated +
+
+ Mar 10 – Mar 16, 2025  ·  + acme/app @ main +
+
+ + +
+
+
+
14
+
PRs merged
+
+3 vs wk10
+
+
+
6
+
Deploys
+
±0
+
+
+
1
+
Incidents
+
SEV-2 · 47m
+
+
+
3
+
Flaky tests fixed
+
suite now 99.1%
+
+
+
+ + +
+

Highlights

+
+ +
+ + +
+

Shipped

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PRTitleAuthorRisk
#4871Bulk edit toolbar: selection model + keyboard shortcutsMira OkaforMed
#4874Batch auth lookup for /v2/sync hot pathDevon ParkMed
#4878Fix race in attachment uploader retry loopSam ReyesLow
#4879Migrate reminder scheduler to idempotent job keysPriya AnandHigh
#4882Board view: collapse empty swimlanes by defaultMira OkaforLow
#4885Quarantine 3 flaky webhook integration testsJules TanLow
#4888Connection pool limits configurable per worker tierDevon ParkMed
#4891Dark mode pass on settings & billing panelsNoor HalabiLow
+
+ + +
+

Velocity

+
+
+ + + + + + + + + 0 + 1 + 2 + 3 + + + + + + + + + + + + + + + + + + + 2 + 3 + 1 + 4 + 2 + 1 + 1 + + + Mon + Tue + Wed + Thu + Fri + Sat + Sun + +
+ PRs merged per day. Thursday spike is the bulk-edit feature train (4 PRs landed together). +
+
+
+ + +
+

Carryover

+
+
+
+ In review +
+ Workspace export to CSV — waiting on pagination review. + · Sam Reyes +
+
+
+ Blocked +
+ SSO group mapping — blocked on staging IdP credentials from IT. + · Priya Anand +
+
+
+ Slipped +
+ Mobile push reliability dashboard — deprioritized for incident follow-up. + · Devon Park +
+
+
+
+ + + +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/12-incident-report.html b/.agents/skills/html-plan/references/html-effectiveness/12-incident-report.html new file mode 100644 index 0000000..ff06f1d --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/12-incident-report.html @@ -0,0 +1,597 @@ + + + + + + +INC-2025-0412 — Elevated 502s on task sync + + + + + + +
+ +
+
INC-2025-0412
+

Elevated 502s on task sync

+
+ SEV-2 + Resolved + Duration 47 min + Detected Apr 12 · 14:07 + Owner Devon Park +
+
+ +
+
TL;DR
+

+ A config rollout lowered the database connection-pool limit on the + sync-worker tier from 64 to 8, exhausting connections under + normal afternoon load. The sync API returned 502s for roughly 21% of + requests over 47 minutes. We mitigated by reverting the config and + cycling the worker fleet; no data was lost. +

+
+ + +
+

Timeline

+
+
+ +
+ + 14:02 +
+ Config change cfg-9a12 promoted to + production via the standard rollout pipeline. +
+
+ +
+ + 14:06 +
+ Impact starts. Sync workers begin queueing on pool + checkout; p95 latency climbs past 4s and the load balancer starts + returning 502s. +
+
+ +
+ + 14:07 +
+ Alert fires: sync_5xx_rate > 2% for 60s. + On-call (Devon) acknowledges. +
+
+ +
+ + 14:18 +
+ Initial hypothesis is a bad deploy of the API service; last two + application deploys are rolled back with no effect. +
+
+ +
+ + 14:31 +
+ Mira joins and notices pool-wait saturation in the worker dashboard. + Investigation pivots to infra config rather than app code. +
+
+ +
+ + 14:44 +
+ Mitigated. cfg-9a12 + reverted; worker fleet cycled. 5xx rate drops below 0.2% within + three minutes. +
+
+ +
+ + 14:49 +
+ Monitors green for 5 minutes. Incident declared resolved; status + page updated. +
+
+ +
+
+ + +
+

Root cause

+
+

+ PR #4888 made connection-pool limits + configurable per worker tier. The default for the new + sync-worker key was meant to inherit the + global value (64) but was hard-coded to 8 during a local test and + committed. The config linter only validates type, not magnitude, so the + change passed CI. +

+

+ Because config rollouts and code deploys go through separate pipelines, + the on-call's first instinct — rolling back the most recent + application deploys — had no effect and cost roughly 13 minutes of + diagnosis time. +

+ +
+ infra/config/workers.yaml +
pool:
+
global_max_connections: 64
+
tiers:
+
- sync-worker: { max_connections: 64 }
+
+ sync-worker: { max_connections: 8 } # debug value, do not ship
+
webhook-worker: { max_connections: 32 }
+
+
+ + +
+

Impact

+
+ + + + + + + + + + + + + + + + + + + + + +
Requests failed (502)~41,200
Peak error rate21.4%
Users affected~2,300 workspaces
Data lossNone — clients retried
SLA breachNo (within monthly budget)
+
+ + +
+

Action items

+
+
+ +
+ + DP + Revert cfg-9a12 and restore pool limit to 64 + Apr 12 +
+ +
+ + MO + Add config-linter range check for max_connections (warn < 32) + Apr 18 +
+ +
+ + SR + Surface “recent config rollouts” alongside deploys in the on-call dashboard + Apr 25 +
+ +
+ + PA + Canary config changes to one worker AZ for 10 min before fleet-wide promote + May 02 +
+ +
+
+ + + +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/13-flowchart-diagram.html b/.agents/skills/html-plan/references/html-effectiveness/13-flowchart-diagram.html new file mode 100644 index 0000000..8967f6e --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/13-flowchart-diagram.html @@ -0,0 +1,396 @@ + + + + + + +Deploy pipeline — annotated flowchart + + + +
+ +
+
Illustrations & Diagrams · Flowchart
+

What happens when you git push

+

+ The deploy pipeline for acme/web, drawn from .github/workflows/ and the + Argo manifests. Click any step to see what runs, how long it usually takes, and where it can short-circuit. +

+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + pass + + fail → status + + + + healthy + + canary fails + + + + + + + git push main + + + + + CI · lint + typecheck + ~2 min · ci.yml + + + + + Unit + integration tests + ~6 min · 3 shards + + + + + pass? + + + + + Post failure status + slack #deploys + + + + + Build + push image + ghcr.io/acme/web + + + + + Argo canary 5% + 10 min soak + + + + + SLO ok? + + + + + Promote 25 → 50 → 100% + ~8 min + + + + + Auto-rollback + revert image tag + + + + + Smoke tests in prod + playwright · 90s + + + + + ✅ Deploy complete + + +
+ +
+ process step + decision + terminal success + failure path +
+
+ + + +
+
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/14-research-feature-explainer.html b/.agents/skills/html-plan/references/html-effectiveness/14-research-feature-explainer.html new file mode 100644 index 0000000..aea9deb --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/14-research-feature-explainer.html @@ -0,0 +1,382 @@ + + + + + + +How rate limiting works in acme/api + + + +
+ + + + + +
+
+
Research & Learning · feature summary
+

How rate limiting works in acme/api

+
+ TL;DR — Every request passes through rateLimit() middleware, which resolves the + caller to a bucket key, fetches a token-bucket from Redis, and either consumes one token or + returns 429. Limits are declared per-route in config/limits.yaml; routes + without an entry inherit the default tier (100 req/min per API key). +
+
+ +

The request path, step by step

+

Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about + 0.4 ms p50 to every request.

+ +
+ 1 · Identify the caller middleware/ratelimit.ts:21 +
+

The middleware first reduces the request to a bucketKey: API key if an + Authorization header is present, otherwise the client IP (via the + x-forwarded-for chain, trusting only our own LB). Anonymous IP traffic gets a much + lower default tier.

+
+
+ +
+ 2 · Look up the bucket lib/tokenBucket.ts:9 +
+

The route name plus bucket key map to a Redis hash (rl:{route}:{key}) holding + tokens and updatedAt. If the key is missing it's created lazily at full + capacity — there's no warm-up.

+
+
+ +
+ 3 · Refill and consume lib/tokenBucket.ts:31 +
+

Refill is computed from elapsed time (rate × Δt, capped at burst), then + one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent + requests can't double-spend.

+
+
+ +
+ 4 · Reject when empty middleware/ratelimit.ts:48 +
+

If the script returns tokens < 0 the middleware short-circuits with + 429 Too Many Requests and sets Retry-After to the seconds until one token + refills. Successful responses always carry X-RateLimit-Remaining.

+
+
+ +

Configuring a limit on your route

+

You don't touch the middleware. Add an entry to config/limits.yaml keyed by route name, + and (optionally) tag the route so the middleware can find it.

+ +
+
+ + + +
+
# config/limits.yaml
+default:
+  rate: 100/min
+  burst: 120
+
+search.query:
+  rate: 20/min
+  burst: 40
+  key: api_key        # or: ip
+
// routes/search.ts
+router.post(
+  "/search",
+  rateLimit("search.query"),
+  handler,
+);
+
HTTP/1.1 429 Too Many Requests
+Retry-After: 17
+X-RateLimit-Limit: 20
+X-RateLimit-Remaining: 0
+
+{ "error": "rate_limited", "retry_after": 17 }
+
+ +
+ +
If you only need the default tier, you don't need a YAML entry at all — just wrap the handler in + rateLimit() with no argument. The route name is inferred from the path.
+
+ +

Gotchas worth knowing

+ + +

FAQ

+
+
How do I exempt internal traffic?
+
Set x-acme-internal: 1 from the caller; the middleware checks it against the + mTLS peer name and skips the bucket entirely.
+ +
Where do I see who's getting limited?
+
Every 429 emits a ratelimit.rejected metric tagged with route and key + type. There's a Grafana panel under API → Health.
+ +
Can a single user have a higher limit?
+
Yes — add their API key under overrides: in the YAML. Overrides are reloaded without + a deploy.
+
+
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/15-research-concept-explainer.html b/.agents/skills/html-plan/references/html-effectiveness/15-research-concept-explainer.html new file mode 100644 index 0000000..192869d --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/15-research-concept-explainer.html @@ -0,0 +1,369 @@ + + + + + + +Consistent hashing — an interactive explainer + + + +
+ +
+
Research & Learning · concept explainer
+

Consistent hashing, in one ring

+

+ You have K keys spread across N cache servers. A server dies, or you add one. How many + keys have to move? With naive hash(key) mod N the answer is "almost all of them." Consistent + hashing gets it down to roughly K / N. Here's why. +

+ +

The trick: hash onto a circle, not a line

+

+ Map both nodes and keys onto the same + ring (the hash output space, wrapped around). A key belongs to + the first node found by walking clockwise from the key's position. When a node leaves, only the keys in + its arc reassign — to the next node round — and everything else + stays put. +

+ +
+
+ +
+
+ + + 4 +
+
+ + + 32 +
+
+ + + +
+
+ 4 nodes · 32 keys · moved on last change +
+
+
+
+

+ Colored arcs show ownership. Removing a node hands its arc to its clockwise neighbor; every dot outside + that arc keeps its color. That's the whole idea. +

+ +

Versus mod N

+ + + + + + + + +
hash mod Nconsistent hashing
Keys moved when N→N+1~ (N−1)/N of all keys~ 1/(N+1)
Hot-spot riskeven by constructionuneven — fix with virtual nodes
Lookup costO(1)O(log N) (binary search on ring)
Used byarray sharding, simple LBDynamo, Cassandra, Memcached clients, Envoy
+ +

Where you'll meet it

+

+ Any time you're spreading state across a pool that changes size: cache fleets, partitioned queues, object + storage, request routing with sticky sessions. The + virtual-node variant (each physical node owns many small arcs + instead of one big one) is what production systems actually run, because it smooths out load and makes + rebalancing even gentler. +

+
+ + + + +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/16-implementation-plan.html b/.agents/skills/html-plan/references/html-effectiveness/16-implementation-plan.html new file mode 100644 index 0000000..483a826 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/16-implementation-plan.html @@ -0,0 +1,703 @@ + + + + + + + Implementation plan — Comment threads on task cards + + + +
+ +
+
Implementation plan · Acme web client
+

Comment threads on task cards

+
+ Prompt + Create a thorough implementation plan for adding threaded comments to + task cards. Include mockups, the data flow from client to persistence, + the key code I'll need to write, and a risk table. Make it easy to + skim on a phone — I'm going to pass this to the implementer as-is. +
+
+ + + +
+
Effort
~2 weeks
+
Surfaces touched
3 packages
+
New tables
2
+
Feature flag
task_comments_v1
+
+ + + +
+
01

Milestones

+

Ship in four slices, each independently reviewable and each behind the flag. Nothing is user-visible until slice 4.

+ +
+
+
Week 1 · Mon–Tue
+
+
+

Schema & API contract

+

New comments and comment_reads tables, migrations, and the tRPC router stubs. No UI. Contract reviewed before anything else lands.

+
packages/dbpackages/apimigration 0042
+
+
+ +
+
Week 1 · Wed–Fri
+
+
+

Thread component & composer

+

Static <CommentThread> rendered from fixtures. Optimistic insert on submit, rollback on failure, one level of nesting only.

+
apps/webstorybook
+
+
+ +
+
Week 2 · Mon–Wed
+
+
+

Realtime fan-out & unread state

+

Subscribe the open card to its comment channel. Track per-user read cursors so the sidebar can show an unread count without a second query.

+
packages/realtimeapps/web
+
+
+ +
+
Week 2 · Thu–Fri
+
+
+

Notifications, flag ramp, docs

+

Mention detection → notification row, email digest fallback, ramp task_comments_v1 to internal, then 10% → 100% over three days.

+
packages/notifygrowthbook
+
+
+
+
+ + + +
+
02

Data flow

+

Optimistic write path on the left, fan-out on the right. The read cursor update is fire-and-forget — we never block the thread render on it.

+ +
+ + + + + + + + + + + + + + <CommentComposer> + apps/web + + + React Query cache + optimistic insert + + + comments.create + tRPC · packages/api + + + comments table + postgres · packages/db + + + realtime channel + task:{id}:comments + + + Other viewers + subscribed cards + + + notify worker + @mentions → queue + + + + + + + + + + + + + + + + + + submit (id=temp) + mutate + INSERT + read cursor + enqueue + broadcast row + live append + reconcile temp id → real id + + +

Solid = request/response path. Dashed clay = realtime fan-out. The composer never waits on the dashed path.

+
+
+ + + +
+
03

Mockups

+

Not pixel-final — just enough that the reviewer and I agree on nesting depth, composer placement, and what the sidebar digest looks like.

+ +
+
+
A · Thread inside an open task card
+
+
+
Ship onboarding empty-state rewrite
+
BIR-1142 · Assigned to Priya · Due Fri
+
+
+
+
JM
+
+
Jonah M. 2h ago
+
Should the illustration swap when the workspace already has one project? Feels odd to show the "start here" art twice.
+ +
+
+
+
PS
+
+
Priya S. 40m ago
+
Good catch — I'll gate it on projects.count > 0 and fall back to the minimal variant.
+
+
+
+
Add a comment…
+
Post
+
+
+
+
+
+ +
+
B · Sidebar unread digest
+
+
+
+
JM
+
Jonah commented on BIR-1142 — "Should the illustration swap when…"
+
+
+
AK
+
Aiko mentioned you on BIR-1098 — "@priya can you confirm the copy here?"
+
+
+
RW
+
Rowan replied on BIR-0971 — "Merged, thanks for the quick turnaround."
+
+
+
+
+
+
+ + + +
+
04

Key code

+

The two pieces most likely to be done wrong: the migration (soft deletes, read cursors) and the optimistic mutation (temp-id reconciliation).

+ +
+
+
packages/db/migrations/0042_comments.sql
+
create table comments (
+  id          uuid primary key default gen_random_uuid(),
+  task_id     uuid not null references tasks(id),
+  parent_id   uuid references comments(id),   -- one level only,
+                                                -- enforced in API
+  author_id   uuid not null references users(id),
+  body        text not null,
+  created_at  timestamptz not null default now(),
+  deleted_at  timestamptz                       -- soft delete
+);
+
+create table comment_reads (
+  task_id    uuid not null references tasks(id),
+  user_id    uuid not null references users(id),
+  read_up_to timestamptz not null,
+  primary key (task_id, user_id)
+);
+
+create index comments_task_created
+  on comments (task_id, created_at);
+
+ +
+
apps/web/hooks/useAddComment.ts
+
export function useAddComment(taskId: string) {
+  const qc = useQueryClient();
+  return trpc.comments.create.useMutation({
+    onMutate: async (input) => {
+      const temp = { ...input, id: `temp-${nanoid()}`,
+                     createdAt: new Date(), pending: true };
+      qc.setQueryData(key(taskId), (prev) =>
+        [...(prev ?? []), temp]);
+      return { tempId: temp.id };
+    },
+    onSuccess: (row, _v, ctx) => {
+      // reconcile temp id → real id so the
+      // realtime append doesn't duplicate it
+      qc.setQueryData(key(taskId), (prev) =>
+        prev.map((c) => c.id === ctx.tempId ? row : c));
+    },
+    onError: (_e, _v, ctx) => {
+      qc.setQueryData(key(taskId), (prev) =>
+        prev.filter((c) => c.id !== ctx.tempId));
+    },
+  });
+}
+
+
+
+ + + +
+
05

Risks & mitigations

+
+
+
Risk
+
Sev
+
Mitigation
+
+
+
Realtime duplicate: socket append races with the HTTP response and the temp-id reconcile.
+
HIGH
+
Dedupe on server-assigned id in the cache updater; socket payload carries the real id, temp rows are filtered on reconcile.
+
+
+
Unread counts go stale when a user reads the thread on another device.
+
MED
+
Broadcast comment_reads upserts on the same channel; client treats its own cursor as max(local, remote).
+
+
+
Mention detection false-positives on pasted markdown (@media, @2x).
+
LOW
+
Resolve mentions against workspace members only, at write time, and store the resolved user ids — never re-parse on read.
+
+
+
+ + + +
+
06

Open questions

+
+
+
Do we allow editing, or only delete-and-repost?
+
Editing needs an edited_at column and an "edited" affordance. Delete-and-repost is simpler but loses the reply anchor. Leaning toward delete-only for v1.
+
Decide with · design, before slice 2
+
+
+
Email digest cadence when a user has the app closed
+
Immediate-per-mention will be noisy. Proposal: batch on a 15-minute window, collapse to one email per task, and respect quiet hours from the existing settings table.
+
Decide with · platform, before slice 4
+
+
+
+ +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/17-pr-writeup.html b/.agents/skills/html-plan/references/html-effectiveness/17-pr-writeup.html new file mode 100644 index 0000000..34c60d0 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/17-pr-writeup.html @@ -0,0 +1,596 @@ + + + + + + + PR #312 — Move notification delivery onto a queue + + + +
+ +
+
Pull request · Acme
+

#312 — Move notification delivery onto a queue

+
+ 9 files + +418 / −190 + branch notify-queuemain + author @priya +
+
+ Prompt + Write up PR #312 for my reviewers. Explain the motivation, walk them + through the change file by file with the why for each, show + before/after behavior, and tell them exactly where to focus. They + haven't touched the notification code in six months. +
+
+ +
+
+ +
+
TL;DR
+

Notification sends were happening inline in the request path. Under load they added 200–800 ms to mutation latency and silently dropped emails when the SMTP pool was exhausted. This PR moves delivery onto the existing pg-boss queue so the API returns immediately and failed sends retry with backoff.

+
+ + + +
+

Why

+

When we added @mentions in comments last quarter, every mention started triggering up to three sends (in-app, email, Slack) inside the same transaction that saved the comment. That was fine at launch. It is not fine now that a single task update can fan out to forty watchers.

+ +
+
+
Before
+
    +
  • Sends run inline in the mutation handler
  • +
  • SMTP timeout = 500 error for the comment
  • +
  • No retries — a dropped email is gone
  • +
  • p99 on comments.create: 1.4 s
  • +
+
+
+
After
+
    +
  • Handler enqueues one job per recipient, returns
  • +
  • Worker retries 3× with exponential backoff
  • +
  • Dead-letter table for inspection after exhaustion
  • +
  • p99 on comments.create: 180 ms (staging)
  • +
+
+
+
+ + + +
+

File-by-file

+

Ordered for reading, not alphabetically. Start at the worker — it's the new thing — then the enqueue call site, then the plumbing.

+ +
+ +
+ + + packages/notify/src/worker.ts + new + +126 + +
+

The heart of the PR. A pg-boss subscriber that pulls notify.deliver jobs, resolves the user's channel preferences, and calls the right adapter. Retries are configured per-channel — email gets three attempts, Slack gets one because its API is already idempotent on our side.

+
boss.work('notify.deliver', { batchSize: 20 }, async (jobs) => {
+  for (const job of jobs) {
+    const { userId, event, channel } = job.data;
+    const prefs = await getPrefs(userId);
+    if (!prefs[channel]) return;          // user muted this channel
+
+    try {
+      await adapters[channel].send(userId, event);
+    } catch (err) {
+      if (job.retryCount >= MAX_RETRY[channel]) {
+        await deadLetter(job, err);         // don't throw — ack & park
+        return;
+      }
+      throw err;                            // pg-boss reschedules
+    }
+  }
+});
+
+
+ +
+ + + packages/api/src/routers/comments.ts + mod + +14 −62 + +
+

Where the win shows up. The mutation used to call sendEmail, sendSlack, and createInApp directly. Now it inserts the comment, computes recipients, and enqueues. The try/catch soup is gone.

+
  const comment = await db.comments.insert(input);
+  const recipients = await resolveWatchers(input.taskId, input.mentions);
+
+  for (const r of recipients) {
+    await sendEmail(r, comment);       // blocked the response
+    await sendSlack(r, comment);
+  }
+  await boss.insert(recipients.flatMap((r) =>
+    CHANNELS.map((ch) => ({
+      name: 'notify.deliver',
+      data: { userId: r.id, channel: ch, event: toEvent(comment) },
+      singletonKey: `${comment.id}:${r.id}:${ch}`,  // idempotent
+    }))));
+  return comment;
+
+
+ +
+ + + packages/db/migrations/0051_dead_letter.sql + new + +22 + +
+

Table for jobs that exhaust their retries. Deliberately not auto-pruned — we want to look at these weekly until we trust the new path. Has the full job payload and the last error string.

+
+
+ +
+ + + packages/notify/src/adapters/{email,slack,inapp}.ts + mod + +88 −74 + +
+

Mostly moves. Each adapter now implements a shared Adapter interface and throws a typed RetryableError or PermanentError so the worker knows whether to retry. The email adapter also drops its internal retry loop — the queue owns retries now, double-retrying was how we got duplicate emails in April.

+
+
+ +
+ + + apps/worker/src/index.ts, infra/fly.toml + mod + +31 −4 + +
+

Registers the new subscriber in the existing worker process and bumps its concurrency from 5 → 20. No new deploy unit.

+
+
+ +
+ + + packages/notify/src/__tests__/worker.test.ts + new + +137 + +
+

Covers the retry boundary, the dead-letter path, channel muting, and the singleton key dedupe. Uses a real pg-boss against the test database — we got burned last quarter when mocked queue tests passed but prod ordering broke.

+
+
+ +
+
+ + + +
+

Where to focus your review

+
+
+
1
+
+
The retry / dead-letter boundary
+
worker.ts:31–44. I catch, check retryCount, and either park or rethrow. If this logic is wrong we either retry forever or drop messages — the two failure modes this PR exists to fix.
+
+
+
+
2
+
+
The singleton key
+
comments.ts:28. ${commentId}:${userId}:${channel} should make re-enqueues idempotent if the API handler retries. Sanity-check that this can't collide across tasks.
+
+
+
+
3
+
+
What I deliberately did not do
+
No per-user digest batching, no delivery receipts, no priority lanes. All of those layer on top of this cleanly; bundling them would make this unreviewable.
+
+
+
+
+ + + +
+

Test plan

+
+
Unit: retry → dead-letter path, channel mute, singleton dedupe
packages/notify — 14 cases, real pg-boss on test db
+
Integration: create comment with 3 watchers, assert 9 jobs enqueued and drained
+
Staging load: 500 rps on comments.create for 10 min, p99 = 180 ms
was 1.4 s before — dashboard linked in the PR description
+
Manual: kill SMTP mid-burst, confirm jobs land in dead-letter and nothing 500s
will do during the 10% ramp
+
+
+ + + +
+

Rollout

+

Behind notify_queue_v2. The old inline path stays in the codebase, dead but dormant, for one release in case we need to flip back.

+
+
Day 0
internal
Acme team only. Watch dead-letter table + worker error rate.
+
Day 2
10%
Random sample. Alert if dead-letter rate > 0.5% of sends.
+
Day 4
100%
Ramp fully, delete the inline path in a follow-up PR next week.
+
+
+ +
+ + +
+ +
+ + diff --git a/.agents/skills/html-plan/references/html-effectiveness/18-editor-triage-board.html b/.agents/skills/html-plan/references/html-effectiveness/18-editor-triage-board.html new file mode 100644 index 0000000..ae4dbb0 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/18-editor-triage-board.html @@ -0,0 +1,574 @@ + + + + + + +Acme — Cycle 14 triage + + + +
+ +
+
Acme / editor / triage
+

Cycle 14 triage

+

+ Twenty-four open Linear tickets, pre-sorted into a best guess. Drag them + across Now / Next / Later / Cut until the cut feels right, then copy the + result back into the planning doc as markdown. +

+
drag tickets between columns  ·  click a tag to filter
+
+ +
+
+
+ + + +
+ +
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/19-editor-feature-flags.html b/.agents/skills/html-plan/references/html-effectiveness/19-editor-feature-flags.html new file mode 100644 index 0000000..15be394 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/19-editor-feature-flags.html @@ -0,0 +1,664 @@ + + + + + + +Acme — flags.production.json + + + +
+ +
+
Acme / editor / feature-flags
+

flags.production.json

+

A form-based editor for the production feature-flag config. + Toggle flags, fix dependency warnings as they surface, then copy out + only the lines that changed.

+
+ +
+ + +
+ + +
+ +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/20-editor-prompt-tuner.html b/.agents/skills/html-plan/references/html-effectiveness/20-editor-prompt-tuner.html new file mode 100644 index 0000000..7a27d03 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/20-editor-prompt-tuner.html @@ -0,0 +1,723 @@ + + + + + + +Acme — Support reply prompt tuner + + + +
+ +
+
Acme / editor / prompt-tuner
+

Support reply draft prompt

+

Edit the system prompt on the left and watch three sample tickets re-render the filled template on the right, live as you type. When it reads well across all three moods, copy the template out.

+
+ +
+ + +
+
slots use {{double_brace}} syntax
+
+ +
+ + +
+
+ Template +
+ 0 chars · ~0 tokens +
+
+
+
Available slots
+
+
+
+ + +
+
Live preview · 3 sample tickets
+
+
+ +
+ + + +
+ + + + diff --git a/.agents/skills/html-plan/references/html-effectiveness/CODE_OF_CONDUCT.md b/.agents/skills/html-plan/references/html-effectiveness/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..91bac7d --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/CODE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainers at **opensource@anthropic.com**. All +complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +[homepage]: https://www.contributor-covenant.org diff --git a/.agents/skills/webapp-testing/LICENSE.txt b/.agents/skills/html-plan/references/html-effectiveness/LICENSE similarity index 99% rename from .agents/skills/webapp-testing/LICENSE.txt rename to .agents/skills/html-plan/references/html-effectiveness/LICENSE index 7a4a3ea..02fe842 100644 --- a/.agents/skills/webapp-testing/LICENSE.txt +++ b/.agents/skills/html-plan/references/html-effectiveness/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -187,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 Anthropic PBC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/.agents/skills/html-plan/references/html-effectiveness/README.md b/.agents/skills/html-plan/references/html-effectiveness/README.md new file mode 100644 index 0000000..aace658 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/README.md @@ -0,0 +1,41 @@ +# The unreasonable effectiveness of HTML — examples + +> **Sample code. Not maintained and not accepting contributions.** + +A gallery of standalone HTML examples that accompany the blog post on using HTML +as a flexible output format. Each file is a self-contained `.html` page (no build +step, no dependencies) demonstrating a different use case — from code review and +design systems to slide decks, status reports, and small interactive editors. + +Open [`index.html`](index.html) for the full, categorized index, or open any +numbered file directly in a browser. + +## Contents + +| Category | Examples | +|---|---| +| Exploration | code approaches, visual designs | +| Code | review, understanding, design systems, component variants | +| Prototyping | animation, interaction | +| Communication | slide deck, status report, incident report, PR write-up | +| Diagrams & research | flowchart, feature/concept explainers | +| Custom editing UIs | triage board, feature flags, prompt tuner | + +## Running + +There is nothing to install or build. Clone the repo and open `index.html` (or +any individual file) in a web browser. + +## A note on sample data + +All product names, data, and scenarios in these examples are fictional and used +only for illustration. The placeholder brand "Acme" and any figures shown are +not real. + +## Security + +See [SECURITY.md](SECURITY.md) for how to report a vulnerability. + +## License + +Released under the [Apache License 2.0](LICENSE). diff --git a/.agents/skills/html-plan/references/html-effectiveness/SECURITY.md b/.agents/skills/html-plan/references/html-effectiveness/SECURITY.md new file mode 100644 index 0000000..70e008f --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Reporting a vulnerability + +If you believe you have found a security vulnerability in this repository, +please report it responsibly. + +- **Do not** open a public issue for security problems. +- Email **security@anthropic.com** with a description of the issue and steps to + reproduce it. + +We will acknowledge your report, investigate, and keep you informed of the +resolution. + +## Scope + +This repository contains static, self-contained HTML example files with no +server-side components, no build pipeline, and no third-party runtime +dependencies. The most likely relevant reports are issues such as unsafe inline +script behavior or content that could mislead a reader. General questions about +the accompanying blog post are not security issues — please use normal issues +for those. diff --git a/.agents/skills/html-plan/references/html-effectiveness/index.html b/.agents/skills/html-plan/references/html-effectiveness/index.html new file mode 100644 index 0000000..4b06272 --- /dev/null +++ b/.agents/skills/html-plan/references/html-effectiveness/index.html @@ -0,0 +1,823 @@ + + + + + + +The unreasonable effectiveness of HTML — examples + + + +
+ +
+
+
+
Companion to the blog post
+

The unreasonable effectiveness of HTML

+

+ Twenty self-contained .html files an agent produced instead of a wall of markdown. + Each one trades a document you'd skim for one you'd actually read — open any of them directly in + a browser. Grouped by the kind of work they replace. +

+ +
+ +
+
+ + +
+
01

Exploration & Planning

3 demos
+

+ When you're not sure what you want yet. Ask the agent to fan out across several directions and lay + them next to each other so you can point at one — instead of reading three sequential walls of text + and trying to hold them all in your head. And once you've picked, turn the pick into a plan the + implementer can actually read. +

+ +
+ + +
+
02

Code Review & Understanding

3 demos
+

+ Diffs and call-graphs are spatial information; markdown flattens them. Let the agent render the + change as an annotated diff, draw the module as boxes and arrows, or write the PR description your + reviewers actually want — so the shape of the code is visible at a glance. +

+ +
+ + +
+
03

Design

2 demos
+

+ HTML is the medium your design system ships in, so it's the natural format for talking about + it. Tokens become swatches, components become contact sheets, and the artifact can be fed straight + back into the next prompt. +

+ +
+ + +
+
04

Prototyping

2 demos
+

+ Motion and interaction can't be described, only felt. A throwaway page with the real easing curve or + the real click-through tells you in five seconds what a paragraph of prose never could. +

+ +
+ + +
+
05

Illustrations & Diagrams

2 demos
+

+ Inline SVG gives the agent a real pen. Ask for the figures for a post or a flowchart of a process and + get vector art you can tweak by hand or paste straight into the final document. +

+ +
+ + +
+
06

Decks

1 demo
+

+ A handful of <section> tags and twenty lines of JS is a slide deck. Point the agent at a + Slack thread or a design doc and get something you can arrow-key through in a meeting — no Keynote, + no export step. +

+ +
+ + +
+
07

Research & Learning

2 demos
+

+ An explainer with collapsible sections, tabbed code samples and a glossary in the margin reads very + differently from the same words dumped linearly. The agent can build the scaffolding that makes a new + topic navigable. +

+ +
+ + +
+
08

Reports

2 demos
+

+ Recurring documents — status updates, post-mortems — benefit most from a bit of structure and color. + A small chart and a colored timeline turn something people skim into something they actually read. +

+ +
+ + +
+
09

Custom Editing Interfaces

3 demos
+

+ Sometimes it's hard to describe what you want in a text box. Ask for a throwaway editor for the exact + thing you're working on — and always end with an export button that turns whatever you did in the UI + back into something you can paste into the agent or commit. You stay in the loop; the loop gets tighter. +

+ +
+ + + +
+ + diff --git a/.agents/skills/webapp-testing/SKILL.md b/.agents/skills/webapp-testing/SKILL.md deleted file mode 100644 index 4726215..0000000 --- a/.agents/skills/webapp-testing/SKILL.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: webapp-testing -description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. -license: Complete terms in LICENSE.txt ---- - -# Web Application Testing - -To test local web applications, write native Python Playwright scripts. - -**Helper Scripts Available**: -- `scripts/with_server.py` - Manages server lifecycle (supports multiple servers) - -**Always run scripts with `--help` first** to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window. - -## Decision Tree: Choosing Your Approach - -``` -User task → Is it static HTML? - ├─ Yes → Read HTML file directly to identify selectors - │ ├─ Success → Write Playwright script using selectors - │ └─ Fails/Incomplete → Treat as dynamic (below) - │ - └─ No (dynamic webapp) → Is the server already running? - ├─ No → Run: python scripts/with_server.py --help - │ Then use the helper + write simplified Playwright script - │ - └─ Yes → Reconnaissance-then-action: - 1. Navigate and wait for networkidle - 2. Take screenshot or inspect DOM - 3. Identify selectors from rendered state - 4. Execute actions with discovered selectors -``` - -## Example: Using with_server.py - -To start a server, run `--help` first, then use the helper: - -**Single server:** -```bash -python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py -``` - -**Multiple servers (e.g., backend + frontend):** -```bash -python scripts/with_server.py \ - --server "cd backend && python server.py" --port 3000 \ - --server "cd frontend && npm run dev" --port 5173 \ - -- python your_automation.py -``` - -To create an automation script, include only Playwright logic (servers are managed automatically): -```python -from playwright.sync_api import sync_playwright - -with sync_playwright() as p: - browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode - page = browser.new_page() - page.goto('http://localhost:5173') # Server already running and ready - page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute - # ... your automation logic - browser.close() -``` - -## Reconnaissance-Then-Action Pattern - -1. **Inspect rendered DOM**: - ```python - page.screenshot(path='/tmp/inspect.png', full_page=True) - content = page.content() - page.locator('button').all() - ``` - -2. **Identify selectors** from inspection results - -3. **Execute actions** using discovered selectors - -## Common Pitfall - -❌ **Don't** inspect the DOM before waiting for `networkidle` on dynamic apps -✅ **Do** wait for `page.wait_for_load_state('networkidle')` before inspection - -## Best Practices - -- **Use bundled scripts as black boxes** - To accomplish a task, consider whether one of the scripts available in `scripts/` can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use `--help` to see usage, then invoke directly. -- Use `sync_playwright()` for synchronous scripts -- Always close the browser when done -- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs -- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()` - -## Reference Files - -- **examples/** - Examples showing common patterns: - - `element_discovery.py` - Discovering buttons, links, and inputs on a page - - `static_html_automation.py` - Using file:// URLs for local HTML - - `console_logging.py` - Capturing console logs during automation \ No newline at end of file diff --git a/.agents/skills/webapp-testing/examples/console_logging.py b/.agents/skills/webapp-testing/examples/console_logging.py deleted file mode 100644 index 9329b5e..0000000 --- a/.agents/skills/webapp-testing/examples/console_logging.py +++ /dev/null @@ -1,35 +0,0 @@ -from playwright.sync_api import sync_playwright - -# Example: Capturing console logs during browser automation - -url = 'http://localhost:5173' # Replace with your URL - -console_logs = [] - -with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page(viewport={'width': 1920, 'height': 1080}) - - # Set up console log capture - def handle_console_message(msg): - console_logs.append(f"[{msg.type}] {msg.text}") - print(f"Console: [{msg.type}] {msg.text}") - - page.on("console", handle_console_message) - - # Navigate to page - page.goto(url) - page.wait_for_load_state('networkidle') - - # Interact with the page (triggers console logs) - page.click('text=Dashboard') - page.wait_for_timeout(1000) - - browser.close() - -# Save console logs to file -with open('/mnt/user-data/outputs/console.log', 'w') as f: - f.write('\n'.join(console_logs)) - -print(f"\nCaptured {len(console_logs)} console messages") -print(f"Logs saved to: /mnt/user-data/outputs/console.log") \ No newline at end of file diff --git a/.agents/skills/webapp-testing/examples/element_discovery.py b/.agents/skills/webapp-testing/examples/element_discovery.py deleted file mode 100644 index 917ba72..0000000 --- a/.agents/skills/webapp-testing/examples/element_discovery.py +++ /dev/null @@ -1,40 +0,0 @@ -from playwright.sync_api import sync_playwright - -# Example: Discovering buttons and other elements on a page - -with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page() - - # Navigate to page and wait for it to fully load - page.goto('http://localhost:5173') - page.wait_for_load_state('networkidle') - - # Discover all buttons on the page - buttons = page.locator('button').all() - print(f"Found {len(buttons)} buttons:") - for i, button in enumerate(buttons): - text = button.inner_text() if button.is_visible() else "[hidden]" - print(f" [{i}] {text}") - - # Discover links - links = page.locator('a[href]').all() - print(f"\nFound {len(links)} links:") - for link in links[:5]: # Show first 5 - text = link.inner_text().strip() - href = link.get_attribute('href') - print(f" - {text} -> {href}") - - # Discover input fields - inputs = page.locator('input, textarea, select').all() - print(f"\nFound {len(inputs)} input fields:") - for input_elem in inputs: - name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]" - input_type = input_elem.get_attribute('type') or 'text' - print(f" - {name} ({input_type})") - - # Take screenshot for visual reference - page.screenshot(path='/tmp/page_discovery.png', full_page=True) - print("\nScreenshot saved to /tmp/page_discovery.png") - - browser.close() \ No newline at end of file diff --git a/.agents/skills/webapp-testing/examples/static_html_automation.py b/.agents/skills/webapp-testing/examples/static_html_automation.py deleted file mode 100644 index 90bbedc..0000000 --- a/.agents/skills/webapp-testing/examples/static_html_automation.py +++ /dev/null @@ -1,33 +0,0 @@ -from playwright.sync_api import sync_playwright -import os - -# Example: Automating interaction with static HTML files using file:// URLs - -html_file_path = os.path.abspath('path/to/your/file.html') -file_url = f'file://{html_file_path}' - -with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page(viewport={'width': 1920, 'height': 1080}) - - # Navigate to local HTML file - page.goto(file_url) - - # Take screenshot - page.screenshot(path='/mnt/user-data/outputs/static_page.png', full_page=True) - - # Interact with elements - page.click('text=Click Me') - page.fill('#name', 'John Doe') - page.fill('#email', 'john@example.com') - - # Submit form - page.click('button[type="submit"]') - page.wait_for_timeout(500) - - # Take final screenshot - page.screenshot(path='/mnt/user-data/outputs/after_submit.png', full_page=True) - - browser.close() - -print("Static HTML automation completed!") \ No newline at end of file diff --git a/.agents/skills/webapp-testing/scripts/with_server.py b/.agents/skills/webapp-testing/scripts/with_server.py deleted file mode 100755 index 431f2eb..0000000 --- a/.agents/skills/webapp-testing/scripts/with_server.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -""" -Start one or more servers, wait for them to be ready, run a command, then clean up. - -Usage: - # Single server - python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py - python scripts/with_server.py --server "npm start" --port 3000 -- python test.py - - # Multiple servers - python scripts/with_server.py \ - --server "cd backend && python server.py" --port 3000 \ - --server "cd frontend && npm run dev" --port 5173 \ - -- python test.py -""" - -import subprocess -import socket -import time -import sys -import argparse - -def is_server_ready(port, timeout=30): - """Wait for server to be ready by polling the port.""" - start_time = time.time() - while time.time() - start_time < timeout: - try: - with socket.create_connection(('localhost', port), timeout=1): - return True - except (socket.error, ConnectionRefusedError): - time.sleep(0.5) - return False - - -def main(): - parser = argparse.ArgumentParser(description='Run command with one or more servers') - parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)') - parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)') - parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)') - parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready') - - args = parser.parse_args() - - # Remove the '--' separator if present - if args.command and args.command[0] == '--': - args.command = args.command[1:] - - if not args.command: - print("Error: No command specified to run") - sys.exit(1) - - # Parse server configurations - if len(args.servers) != len(args.ports): - print("Error: Number of --server and --port arguments must match") - sys.exit(1) - - servers = [] - for cmd, port in zip(args.servers, args.ports): - servers.append({'cmd': cmd, 'port': port}) - - server_processes = [] - - try: - # Start all servers - for i, server in enumerate(servers): - print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}") - - # Use shell=True to support commands with cd and && - process = subprocess.Popen( - server['cmd'], - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - server_processes.append(process) - - # Wait for this server to be ready - print(f"Waiting for server on port {server['port']}...") - if not is_server_ready(server['port'], timeout=args.timeout): - raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s") - - print(f"Server ready on port {server['port']}") - - print(f"\nAll {len(servers)} server(s) ready") - - # Run the command - print(f"Running: {' '.join(args.command)}\n") - result = subprocess.run(args.command) - sys.exit(result.returncode) - - finally: - # Clean up all servers - print(f"\nStopping {len(server_processes)} server(s)...") - for i, process in enumerate(server_processes): - try: - process.terminate() - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - process.wait() - print(f"Server {i+1} stopped") - print("All servers stopped") - - -if __name__ == '__main__': - main() \ No newline at end of file