docs: refine agent skill guidance
This commit is contained in:
@@ -18,37 +18,21 @@ Only update the `[Unreleased]` bullets. Never add a new release header.
|
||||
|
||||
## Gather Context First
|
||||
|
||||
Read recent release sections for style before drafting:
|
||||
|
||||
```bash
|
||||
head -140 CHANGELOG.md
|
||||
```
|
||||
|
||||
Collect git context (base tag, commit count, changed files):
|
||||
Read recent release sections for style. Determine the latest tag (or initial commit fallback), then inspect every commit and changed path through `HEAD`:
|
||||
|
||||
```bash
|
||||
BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
|
||||
echo "Base: $BASE"
|
||||
echo "Commits since base: $(git rev-list --count "$BASE"..HEAD)"
|
||||
echo "Diff stats: $(git diff --shortstat "$BASE"..HEAD)"
|
||||
echo "=== Top 30 commits ==="; git log --oneline -30 "$BASE"..HEAD
|
||||
echo "=== Changed files ==="; git diff --stat "$BASE"..HEAD
|
||||
git log --oneline "$BASE"..HEAD
|
||||
git diff --stat "$BASE"..HEAD
|
||||
```
|
||||
|
||||
Inspect all commits after the base up to `HEAD`. Use the changed files/code paths to decide which platform each change touches.
|
||||
Context gathering is complete when each user-visible change has evidence, platform reach, and contributor identity where available.
|
||||
|
||||
## Squashed PR Merges
|
||||
|
||||
A squashed merge commit often collapses a whole PR into a single terse subject line that omits valuable detail. When a commit looks like a squashed PR merge (subject ending in `(#123)`, or a `Merge pull request #123` commit), inspect the PR itself — its title and description usually carry the real user-facing context.
|
||||
|
||||
```bash
|
||||
# Find PR-linked commits since base
|
||||
BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
|
||||
git log --oneline "$BASE"..HEAD | grep -oE '#[0-9]+'
|
||||
|
||||
# Read a PR's title, body, and author (requires gh)
|
||||
gh pr view <number> --json number,title,body,author,mergedAt
|
||||
```
|
||||
Use `gh pr view <number> --json number,title,body,author,mergedAt` for PR evidence.
|
||||
|
||||
- Prefer the PR description over the squashed commit subject when the description explains the user-visible change more accurately.
|
||||
- Do not copy PR descriptions verbatim; distill them into the changelog style below.
|
||||
@@ -90,7 +74,7 @@ gh pr view <number> --json number,title,body,author,mergedAt
|
||||
- Find usernames from commit authors (GitHub username, not email) or PR metadata when available.
|
||||
- Skip credit when the contributor is `btriapitsyn` (repo owner).
|
||||
|
||||
## Quality Checks Before Editing
|
||||
## Completion Criteria
|
||||
|
||||
- For every bullet: "Could a user point to this in the UI or behavior?" If not, rewrite or drop it.
|
||||
- For every VS Code bullet: verify the change applies to the extension, not just shared web UI or server code.
|
||||
@@ -99,9 +83,11 @@ gh pr view <number> --json number,title,body,author,mergedAt
|
||||
- Do not bundle unrelated changes to reduce bullet count. Prefer omitting minor internal fixes over vague catch-all sentences.
|
||||
- Mention mostly-internal refactors only when there is a concrete user-visible fix; otherwise add no bullet.
|
||||
|
||||
The lists are complete when every bullet is supported by inspected evidence, points to user-observable behavior, is ranked by impact, appears only in changelogs whose runtime receives it, and credits eligible contributors.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Gather repo style and git context (commands above).
|
||||
1. Gather repo style and complete git/PR context.
|
||||
2. Propose the new `[Unreleased]` bullet list for the main `CHANGELOG.md`.
|
||||
3. Propose the VS Code-specific `[Unreleased]` list for `packages/vscode/CHANGELOG.md`.
|
||||
4. Edit both files to update their respective `[Unreleased]` sections.
|
||||
|
||||
@@ -17,36 +17,19 @@ Use this skill for terminal CLI work only (for example `packages/web/bin/*`).
|
||||
|
||||
Do not use this skill for web UI or VS Code webview styling work.
|
||||
|
||||
## Mandatory Rules
|
||||
## Mode Contract
|
||||
|
||||
1. **Validation first**
|
||||
- Safety and correctness checks must run in all modes.
|
||||
- Prompts may help collect input, but cannot be the only guard.
|
||||
Run safety and correctness validation before presentation in every mode. Prompts collect missing input; they never enforce policy alone.
|
||||
|
||||
2. **Mode parity is required**
|
||||
- Behavior must be equivalent in:
|
||||
- Interactive TTY
|
||||
- Non-interactive shells
|
||||
- `--quiet`
|
||||
- `--json`
|
||||
- Fully pre-specified flags
|
||||
- Invalid operations must fail deterministically with non-zero exit code.
|
||||
| Mode | Prompt | Output | Failure |
|
||||
|---|---|---|---|
|
||||
| Interactive TTY | Allowed when input is missing | Framed human output | Concise human error, non-zero exit |
|
||||
| Fully specified flags | None required | Human output | Same policy and exit semantics |
|
||||
| Non-TTY/piped | Never | Deterministic script-safe output | Non-zero without hanging |
|
||||
| `--quiet` | Never | Essential result only | Concise error, non-zero exit |
|
||||
| `--json` | Never | JSON only, including warnings/errors | JSON failure payload, non-zero exit |
|
||||
|
||||
3. **Prompt guard contract**
|
||||
- Only prompt when all are true:
|
||||
- stdout is TTY
|
||||
- not `--quiet`
|
||||
- not `--json`
|
||||
- not automated/non-interactive context
|
||||
|
||||
4. **Output contract**
|
||||
- `--json`: machine-readable output only.
|
||||
- `--quiet`: suppress non-essential output only.
|
||||
- Neither mode weakens policy enforcement.
|
||||
|
||||
5. **Cancellation contract**
|
||||
- Handle prompt cancellation with `isCancel` + `cancel(...)`.
|
||||
- Handle SIGINT cleanly and use consistent exit semantics.
|
||||
Handle prompt cancellation with `isCancel` + `cancel(...)` and SIGINT with consistent exit semantics.
|
||||
|
||||
## Clack Primitive Standard
|
||||
|
||||
@@ -140,9 +123,9 @@ Quiet output should still be complete enough for scripts and quick human scannin
|
||||
- Prefer this style over boxed notes for routine follow-up actions.
|
||||
- Reserve `note`/boxed callouts for rare, high-context guidance where a long paragraph is truly necessary.
|
||||
|
||||
## Parity Verification Matrix
|
||||
## Completion Criteria
|
||||
|
||||
For each command/subcommand, manually verify:
|
||||
Every command/subcommand must have a tested answer for:
|
||||
|
||||
1. default interactive TTY output
|
||||
2. `--quiet` output (minimal but informative)
|
||||
@@ -154,13 +137,7 @@ For each command/subcommand, manually verify:
|
||||
|
||||
Load `references/snippets.md` when implementing prompt guards, non-interactive fallback, spinner lifecycle, or JSON/human output branching.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
1. Add or update core validators first.
|
||||
2. Ensure validators execute in all modes.
|
||||
3. Add interactive Clack UX only as enhancement.
|
||||
4. Verify parity between interactive and non-interactive flows.
|
||||
5. Ensure script-safe deterministic failure behavior.
|
||||
Implementation is complete when validators run before every mode branch, interactive Clack UX is only an enhancement, and all five cases above produce deterministic output and exit behavior.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ description: Use when changing Electron main/preload code, desktop IPC, native w
|
||||
|
||||
# Desktop Shell
|
||||
|
||||
## Read First
|
||||
## Required Context
|
||||
|
||||
Read `packages/electron/README.md` and nearby `packages/electron` code before editing.
|
||||
Read `packages/electron/README.md` and nearby `packages/electron` code before editing. Context gathering is complete when each changed behavior is assigned to main, preload, renderer/shared UI, or web/runtime ownership.
|
||||
|
||||
Load `ui-api-decoupling` when a native change adds or alters a renderer-facing capability, `RuntimeAPIs`, runtime auth/URL behavior, or shared bridge contract. This skill owns the Electron privilege boundary; `ui-api-decoupling` owns the shared UI/runtime contract.
|
||||
|
||||
## Runtime Boundary
|
||||
|
||||
- Electron boots `@openchamber/web` in the same Node process and loads the UI over loopback. Do not introduce a sidecar server process.
|
||||
- Keep OpenCode feature backends and shared domain logic in web/server or runtime APIs.
|
||||
- Keep Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle.
|
||||
- Shared renderer-facing contracts belong in `packages/ui`; shared server behavior belongs in `packages/web`.
|
||||
- Keep renderer contracts and domain logic in `packages/ui`, server behavior in `packages/web`, and Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle.
|
||||
- Electron is the desktop release target.
|
||||
|
||||
## IPC And Security
|
||||
@@ -47,4 +47,4 @@ Non-user-visible child processes must never flash a console window.
|
||||
|
||||
## Validation
|
||||
|
||||
Run the Electron package type-check/lint commands from `package.json` and focused tests. For startup, preload, routing, or packaging changes, test both HMR development and bundled UI mode. For Windows process work, inspect the complete process tree and verify no console flash; a successful command alone is insufficient.
|
||||
Run focused Electron tests and package checks. For startup, preload, routing, or packaging changes, completion requires both HMR development and bundled UI validation. For Windows process work, completion requires inspection of the complete process tree with no console flash; command success alone is insufficient.
|
||||
|
||||
@@ -79,7 +79,7 @@ const onDragEnd = (e: DragEndEvent) => {
|
||||
|
||||
IDs must be **stable per item** (derive from the item's identity, e.g. `type:name`), never the array index — index ids break tracking after the first move.
|
||||
|
||||
## Minimal working pattern (wrapping, variable width, desktop + touch)
|
||||
## Minimal Wiring
|
||||
|
||||
```tsx
|
||||
import { DndContext, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
|
||||
@@ -97,41 +97,21 @@ const Item: React.FC<{ id: string; label: string; onClick: () => void }> = ({ id
|
||||
);
|
||||
};
|
||||
|
||||
const Row: React.FC<{ items: Item[]; onReorder: (next: Item[]) => void }> = ({ items, onReorder }) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }),
|
||||
);
|
||||
const onDragEnd = (e: DragEndEvent) => {
|
||||
const { active, over } = e;
|
||||
if (!over || active.id === over.id) return;
|
||||
const from = items.findIndex(i => i.id === active.id);
|
||||
const to = items.findIndex(i => i.id === over.id);
|
||||
onReorder(arrayMove(items, from, to));
|
||||
};
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<SortableContext items={items.map(i => i.id)} strategy={rectSortingStrategy}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map(i => <Item key={i.id} id={i.id} label={i.label} onClick={i.onClick} />)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
// Configure sensors per Rule 3, reorder onDragEnd per Rule 5, and choose the
|
||||
// SortableContext strategy from Rule 2. This item wiring preserves item width.
|
||||
```
|
||||
|
||||
A clickable element can be draggable at the same time: keep `onClick` on the button and the activation constraint (distance/delay) lets a plain click/tap through.
|
||||
|
||||
## Pitfalls we already hit (don't repeat)
|
||||
## Symptom Index
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Dragged item **stretches** to the target slot width | `CSS.Transform.toString` applies scaleX/scaleY | Use `CSS.Translate.toString` (Rule 1) |
|
||||
| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | `horizontalListSortingStrategy` on a wrapping row | Use `rectSortingStrategy` (Rule 2) |
|
||||
| Dragged item **stretches** to the target slot width | Scale from `CSS.Transform.toString` | Rule 1 |
|
||||
| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | Single-row strategy on wrapping layout | Rule 2 |
|
||||
| **"Maximum update depth exceeded"** during drag + dragged element floats **offset from the cursor** | Live-reorder in `onDragOver` (empty strategy + `setState` each over) oscillates A↔B with variable sizes; the empty `DragOverlay` we paired with it was mispositioned | Don't reorder in `onDragOver`. Reorder once in `onDragEnd` (Rule 5). Only reach for live-reorder if you truly need physical row-reflow, and then guard against oscillation. |
|
||||
| Touch drag scrolls the page instead of dragging | Missing `touch-action: none` | Add `touch-none` (Rule 4) |
|
||||
| Touch: every finger move drags, or tap doesn't register | Single `PointerSensor` with distance | Split into MouseSensor + TouchSensor(delay) (Rule 3) |
|
||||
| Touch drag scrolls the page instead of dragging | Missing touch ownership | Rule 4 |
|
||||
| Touch: every finger move drags, or tap doesn't register | One sensor for mouse and touch | Rule 3 |
|
||||
|
||||
## If `rectSortingStrategy` still isn't crisp enough
|
||||
|
||||
@@ -142,3 +122,7 @@ Reordering variable-width chips across wrapped rows is a documented rough edge i
|
||||
- Variable-width wrapping chips: `packages/ui/src/components/chat/DraftPresetChips.tsx`
|
||||
- Single-row tab strip: `packages/ui/src/components/ui/sortable-tabs-strip.tsx`
|
||||
- Library: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` (already in `packages/ui/package.json`)
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
Verify every applicable rule on desktop and touch. Wrapping layouts must preserve item width, reflow across rows, allow taps and scrolling before long-press activation, and reorder exactly once on drag end with stable IDs.
|
||||
|
||||
@@ -9,8 +9,6 @@ description: Use when creating or modifying OpenChamber UI text, labels, buttons
|
||||
|
||||
User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components.
|
||||
|
||||
Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states.
|
||||
|
||||
## Translate everything immediately (no English placeholders)
|
||||
|
||||
Every key you add to a non-English dictionary MUST contain a real translation in that language — never the English source string as a stand-in. There is NO "leave it in English for now" convention in this project; if an agent told you there was, it was wrong. Copying the English value into `es.ts`/`fr.ts`/`ko.ts`/`pl.ts`/`pt-BR.ts`/`uk.ts`/`zh-CN.ts`/`zh-TW.ts` is a defect, not a deferral. The app ships every locale at once, so an untranslated key is a visible bug for those users.
|
||||
@@ -104,20 +102,11 @@ date
|
||||
: t('dialog.delete.description', { count })
|
||||
```
|
||||
|
||||
## What Counts As UI Text
|
||||
## Translation Boundary
|
||||
|
||||
- Button and menu labels
|
||||
- Settings labels and descriptions
|
||||
- Placeholder text
|
||||
- Tooltip content
|
||||
- Dialog titles/descriptions/actions
|
||||
- Toast title/description/action labels
|
||||
- Empty/error/loading states
|
||||
- `aria-label`, `title`, image `alt` text when user-facing
|
||||
Translate visible text, placeholders, tooltips, dialogs, toasts, empty/error/loading states, and user-facing `aria-label`, `title`, and `alt` text.
|
||||
|
||||
## Exceptions
|
||||
|
||||
Do not translate:
|
||||
Keep these literal:
|
||||
|
||||
- Product names: `OpenChamber`, `OpenCode`, `GitHub`
|
||||
- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API`
|
||||
@@ -125,10 +114,11 @@ Do not translate:
|
||||
- File paths, command names, environment variables
|
||||
- User/generated content
|
||||
|
||||
## Review Checklist
|
||||
## Completion Criteria
|
||||
|
||||
- No new hardcoded user-facing English in changed UI files.
|
||||
- Every new key exists in all dictionaries.
|
||||
- Every new key exists in all dictionaries with a real translation.
|
||||
- All translated values are resolved inside a reactive render/hook boundary.
|
||||
- No locale state added to broad/shared stores.
|
||||
- No full app remount for locale changes.
|
||||
- Locale switch preserves current UI state.
|
||||
|
||||
@@ -9,15 +9,11 @@ description: Use when implementing, fixing, refactoring, or otherwise modifying
|
||||
|
||||
Make the smallest complete change and validate at the narrowest level that covers the real risk.
|
||||
|
||||
Identify existing behavior covered by tests or callers; preserve it unless the requested change explicitly replaces it.
|
||||
|
||||
## Before Editing
|
||||
|
||||
1. Read the nearest `DOCUMENTATION.md` and package `README.md` when present.
|
||||
2. Inspect nearby implementation and tests before introducing a pattern.
|
||||
3. Load every additional project skill whose trigger matches the change.
|
||||
4. Classify the highest applicable change risk below.
|
||||
5. Identify affected consumers, runtimes, persisted data, and public exports.
|
||||
1. Inspect nearby implementation, callers, and tests before introducing a pattern.
|
||||
2. Classify every applicable change risk below.
|
||||
3. Identify every affected consumer, runtime, persisted format, and public export. This step is complete only when each risk has an owner and required validation.
|
||||
|
||||
When instructions materially conflict, stop and resolve the conflict instead of silently choosing one.
|
||||
|
||||
@@ -33,30 +29,23 @@ When instructions materially conflict, stop and resolve the conflict instead of
|
||||
|
||||
Apply every matching category. Do not escalate local work into workspace-wide ritual, and do not treat a type-only export as local merely because it emits no JavaScript.
|
||||
|
||||
## Mandatory Rules
|
||||
## Structural Discipline
|
||||
|
||||
- Identify existing behavior covered by tests or callers; preserve it unless explicitly replaced.
|
||||
- Do not add dependencies unless explicitly requested.
|
||||
- Do not add compatibility paths without a concrete persisted or external consumer.
|
||||
- Enforce security and correctness in core logic, not only UI controls or prompts.
|
||||
- Never add, persist, or log secrets, bearer tokens, pairing data, or sensitive user content.
|
||||
- Make data loss, partial failure, rollback, and fallback behavior explicit.
|
||||
- Update owning documentation when module ownership, contracts, or invariants change.
|
||||
- Complete the cumulative validation required by every applicable risk category.
|
||||
|
||||
## Engineering Preferences
|
||||
|
||||
- Prefer the smallest correct change; avoid drive-by refactors.
|
||||
- Keep orchestration entrypoints thin and move domain logic to focused modules.
|
||||
- Preserve behavior established by callers and tests unless the request replaces it. Keep the diff scoped to the complete requested behavior.
|
||||
- Make the normal use-case path read top to bottom in domain terms. Keep orchestration entrypoints thin and move mechanics or domain logic behind focused, intention-revealing boundaries.
|
||||
- Pull complexity downward only when a boundary hides meaningful mechanics, owns an invariant, isolates a proven integration, or captures stable repetition. Do not spread obvious code across pass-through layers.
|
||||
- Prefer explicit dependencies and dependency injection over hidden module coupling.
|
||||
- Follow local TypeScript types; avoid `any`, blind casts, and guessed payload shapes.
|
||||
- Prefer early returns and explicit branches over nested conditionals.
|
||||
- Reject invalid inputs and broken preconditions early so the valid path stays flat. Do not force a numeric happy-path/error-path ratio when correctness requires substantial failure handling.
|
||||
- Require evidence before adding retries, caches, compatibility paths, lifecycle machinery, or generalized race handling. Security, data-loss, destructive-operation, and concurrency invariants still require proactive design when the risk is inherent to the operation.
|
||||
- Make partial failure, rollback, cleanup, and user-visible outcomes explicit for destructive or multi-step work.
|
||||
|
||||
## Review Prompts
|
||||
|
||||
Before broadening a change, ask:
|
||||
|
||||
- Is the new abstraction reused or merely possible to reuse?
|
||||
- What concrete complexity, invariant, stable repetition, or boundary does each new helper, interface, layer, and file pay for?
|
||||
- Is the code in the package that owns the behavior?
|
||||
- Does the change alter shared UI contracts across web, desktop, VS Code, or mobile?
|
||||
- Does it change persisted data, IDs, routes, exports, generated files, or package entrypoints?
|
||||
@@ -75,8 +64,6 @@ Do not hide a required architectural migration behind a local heuristic. Do not
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
Use `package.json` scripts as the command source of truth.
|
||||
|
||||
| Change | Minimum validation |
|
||||
|---|---|
|
||||
| Executable source | Focused tests plus package-scoped type-check and lint |
|
||||
@@ -108,15 +95,4 @@ For type-only shared contracts, validate compile-time consumers. Add runtime ser
|
||||
- Run focused regression tests for the changed contract.
|
||||
- Preserve unrelated changes encountered in shared files.
|
||||
- Re-read the owning docs and update them when the implementation changed their truth.
|
||||
- Do not claim runtime, relay, performance, or platform correctness from type-check/lint alone.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
| Failure | Correction |
|
||||
|---|---|
|
||||
| Refactoring nearby code while fixing one bug | Keep the diff scoped unless the nearby change is required |
|
||||
| Adding a helper used once | Keep direct code until reuse or composability is real |
|
||||
| Swallowing an error for smoother UX | Preserve the failure signal and handle presentation separately |
|
||||
| Updating a bridge without all runtimes | Load the runtime/API skill and make parity explicit |
|
||||
| Running only broad checks | Add focused tests that exercise the changed behavior |
|
||||
| Running only focused checks after a shared-contract change | Add workspace-wide validation |
|
||||
- Perform a final simplification pass: remove speculative branches, shallow wrappers, stale compatibility, and names that do not clarify intent.
|
||||
|
||||
@@ -11,6 +11,8 @@ Optimize the amount and frequency of work before optimizing individual operation
|
||||
|
||||
**Core principle:** Make expensive work structurally unnecessary. A fast inner function still freezes the app when called millions of times on the main thread.
|
||||
|
||||
Load `sync-state-invariants` when an optimization changes state authority, reconciliation, optimistic data, event ordering, cache lifecycle, or destructive cleanup. This skill owns measured cost; `sync-state-invariants` owns state correctness.
|
||||
|
||||
## Start With A Performance Contract
|
||||
|
||||
Define before editing:
|
||||
@@ -27,6 +29,8 @@ Do not optimize against a toy fixture when the report provides production scale.
|
||||
|
||||
## Workflow
|
||||
|
||||
Complete the numbered workflow in order. An optimization is complete only when the exact measured scenario meets its budget and separate correctness checks preserve every applicable state, identity, layout, and lifecycle transition.
|
||||
|
||||
### 0. Trust The Measurement Before Trusting The Number
|
||||
|
||||
A measurement setup that is wrong produces clean, confident, wrong numbers, and
|
||||
@@ -65,6 +69,8 @@ validity checks ran.
|
||||
|
||||
Do not infer a bottleneck from code appearance when a trace or counter can identify it.
|
||||
|
||||
Treat every proposed optimization as a hypothesis. Memoization, caches, indexes, workers, scheduling, retries, and lifecycle machinery must address an observed cost or failure in the measured path; “could be slow” or “might race” is not evidence. Keep only the smallest mechanism that meets the contract, except where an inherent security, data-loss, destructive-operation, or concurrency invariant requires proactive protection.
|
||||
|
||||
**Never accept an "after" without a "before" on the identical scenario and
|
||||
build.** Measuring a fixed build against a remembered number, a different
|
||||
scenario, or a nearby baseline proves nothing: the mechanism you changed may
|
||||
@@ -193,6 +199,8 @@ Add a cache only when all are explicit:
|
||||
- runtime/project/user isolation where identities can collide;
|
||||
- proof that caching removes enough work to meet the budget.
|
||||
|
||||
Do not introduce a cache merely to make an abstraction reusable or prepare for future consumers. First prove repeated work in the real path; then place the cache with the narrowest owner and lifetime that can invalidate it correctly.
|
||||
|
||||
A cache inside an `O(consumers × entities × candidates)` loop is a mitigation, not automatically a complete fix.
|
||||
|
||||
## Repository Tooling
|
||||
@@ -276,24 +284,6 @@ Ship a bounded cache-only or local mitigation under deadline pressure only when:
|
||||
|
||||
If the interaction remains above budget, do not call the mitigation the completed performance fix.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "The helper is cheap" | Multiply it by events, entities, candidates, and consumers. |
|
||||
| "No component rerendered" | Selectors and equality comparisons may still burn CPU. |
|
||||
| "`useMemo` fixes it" | Memoization does not help when dependencies churn or consumers duplicate work. |
|
||||
| "The cache made it 10× faster" | Compare the result with the interaction budget, not only the baseline. |
|
||||
| "Projects are few" | Identify the dimension that is large and the dimensions multiplying it. |
|
||||
| "Move it to a worker" | Moving waste changes responsiveness, not total cost or data correctness. |
|
||||
| "Empty means nothing exists" | Empty after failure or partial loading is not authoritative absence. |
|
||||
| "We can optimize later" | Add a scale regression now or the multiplier will return. |
|
||||
| "The profile is clean" | Prove the instrument fired and the renderer was not throttled. A disabled instrument looks identical to a fast app. |
|
||||
| "It is much faster now" | Against which baseline, on which build, in which scenario? Re-run the unchanged build. |
|
||||
| "Most of the time is `(program)`" | The sampler cannot see native work. Read the timeline trace. |
|
||||
| "It does not reproduce here" | Compare your scale to the reporter's on the dimension the code keys on. |
|
||||
| "It cannot hurt to keep the change" | An unmeasured change is unvalidated complexity that hides the path from the next investigation. |
|
||||
|
||||
## Exit Checklist
|
||||
|
||||
- [ ] Measurement validity established: no throttling, instruments confirmed firing, workload comparable.
|
||||
|
||||
@@ -11,6 +11,8 @@ OpenChamber has a private relay: a client (mobile app, browser, another desktop)
|
||||
|
||||
Architecture overview: `packages/web/server/lib/relay/DOCUMENTATION.md`. Code: `packages/ui/src/lib/relay/` (client + shared, TS) and `packages/web/server/lib/relay/` (host, JS).
|
||||
|
||||
Load `ui-api-decoupling` when the change adds or alters a shared runtime API, URL/auth contract, bridge, proxy, or runtime-switch behavior. This skill owns relay mechanics; `ui-api-decoupling` owns the shared UI/runtime boundary.
|
||||
|
||||
**Why this skill exists:** relay bugs do not show up in normal testing. The event stream is SSE (which behaves differently from WebSockets), so a new WebSocket feature is often the *first* real WebSocket to cross the tunnel on mobile — and it fails there while working everywhere else. We have fixed the same class of bug across several iterations. The rules below are those lessons.
|
||||
|
||||
## The core mental model
|
||||
@@ -20,7 +22,7 @@ Architecture overview: `packages/web/server/lib/relay/DOCUMENTATION.md`. Code: `
|
||||
- HTTP and SSE authenticate with the client's **bearer token** (a header). They "just work" through the tunnel for any allowlisted `/api/*`, `/auth/*`, `/health` path.
|
||||
- **WebSockets cannot send headers.** They authenticate with a short-lived **URL-scoped token** (`oc_url_token`) that must be minted first and passed as a query parameter. This is the source of most relay WS bugs.
|
||||
|
||||
## Rules for adding or changing a WebSocket endpoint
|
||||
## WebSocket Endpoint Branch
|
||||
|
||||
Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requires ALL of these, or it breaks over the relay:
|
||||
|
||||
@@ -32,19 +34,19 @@ Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requir
|
||||
4. **Do not touch origin handling.** The server rejects WS upgrades whose `Origin` it does not trust. Over the tunnel the host dials loopback and presents the loopback origin (`http://127.0.0.1:<port>`), which the server trusts as same-origin — this already covers every allowlisted WS path. **Never reintroduce reliance on `window.location.origin`**: in the iOS WKWebView it is `"null"`/empty for the custom scheme, so forwarding it produces a 403.
|
||||
5. **Test over the relay, not just direct/desktop.** A new WS may be the first WebSocket the mobile client runs through the tunnel (events are SSE-locked on Capacitor). Passing on desktop or a direct connection proves nothing about the relay path.
|
||||
|
||||
## Rules for the tunnel/crypto/codec internals
|
||||
## Wire Format And Codec Branch
|
||||
|
||||
- **Two implementations must stay byte-compatible.** The E2EE and framing exist as TS (`packages/ui/src/lib/relay/{crypto,handshake,tunnel-codec}.ts`, normative) and a JS host mirror (`packages/web/server/lib/relay/{e2ee,tunnel-codec}.js`). Any wire-format, frame-type, handshake, or batching change must update **both** and keep `packages/web/server/lib/relay/cross-compat.test.js` green.
|
||||
- **Frame types live in `protocol.ts`** and must match across `protocol.ts`, `tunnel-codec.ts`, and `tunnel-codec.js`. Adding a frame type without mirroring it corrupts the stream on one side.
|
||||
- **Frame batching is capability-negotiated** in the handshake with a legacy fallback, so mixed client/host app versions still interoperate. Preserve the negotiation and the single-frame fallback; do not make batching unconditional.
|
||||
- **The encrypted-frame counter/IV is per-direction and strictly increasing.** One encrypted WS message = one encrypt call = one counter tick. Keep encrypt+send serialized per direction; do not reorder or parallelize it.
|
||||
|
||||
## Rules for the runtime transport layer
|
||||
## Runtime Transport Branch
|
||||
|
||||
- Relay mode routes through `runtime-switch` (activates the tunnel singleton), `runtime-fetch` (routes runtime requests through it), `runtime-url`/`runtime-socket` (tunnel-backed URLs/sockets), and `runtime-auth` (mints the URL token through the tunnel). When refactoring any of these, preserve the relay branch and the direct-URL/Electron-realtime-proxy branches — they must remain byte-identical in behavior for non-relay runtimes.
|
||||
- **The host dispatcher never injects credentials.** Tunneled requests carry the client's own token; the server authenticates them. Do not add host-side auth shortcuts, and do not trust loopback source address as authentication (relay traffic arrives at loopback but represents remote clients).
|
||||
|
||||
## Reconnect pacing
|
||||
## Reconnect Branch
|
||||
|
||||
For indefinite SSE/WebSocket reconnect loops:
|
||||
|
||||
@@ -56,17 +58,10 @@ For indefinite SSE/WebSocket reconnect loops:
|
||||
|
||||
Blind short retries on hidden, offline, unauthorized, or stale-path clients waste battery and flood server logs.
|
||||
|
||||
## Testing guidance (a stub that skips auth/origin hides the exact bugs)
|
||||
## Verification
|
||||
|
||||
- Exercise the real auth and origin gates. An end-to-end test whose stub server accepts any WS upgrade will pass while the real server rejects it — this is precisely how the origin-check bug shipped. When writing a relay integration test, mirror the real gates (`ensureSessionToken` via `oc_url_token`, `isRequestOriginAllowed`) or run against the real server pieces.
|
||||
- Run relay tests per file (`bun test <file>`); the suite has order sensitivity.
|
||||
- Validate both sides: `packages/ui` `type-check`/`lint`, and `node --check` on changed JS host files.
|
||||
|
||||
## Quick checklist before finishing relay-adjacent work
|
||||
|
||||
- [ ] New WS endpoint added to `ALLOWED_WS_PATHS` AND `isUrlAuthWebSocketPath`?
|
||||
- [ ] UI opens it via `openRuntimeWebSocket`, not `new WebSocket`?
|
||||
- [ ] URL token minted before the WS connects?
|
||||
- [ ] No new dependence on `window.location.origin`?
|
||||
- [ ] Wire/codec/handshake change mirrored in TS and JS, cross-compat test green?
|
||||
- [ ] Direct and relay paths both still work; verified over the relay on the transport that actually uses it?
|
||||
Completion requires every applicable branch above: WS path allowlists/auth/origin and real relay exercise; mirrored TS/JS wire changes with cross-compat coverage; preserved direct and relay runtime branches; or reconnect pacing under offline, hidden, permanent-failure, recovery, and abort conditions.
|
||||
|
||||
@@ -7,44 +7,34 @@ description: Use when working with the OpenChamber iOS Simulator app without ope
|
||||
|
||||
Use `serve-sim` to stream and control a booted Apple Simulator from the terminal. It captures the simulator framebuffer, serves a browser preview, and exposes CLI controls for taps, typing, gestures, hardware buttons, rotation, memory warnings, permissions, camera injection, and accessibility inspection.
|
||||
|
||||
## OpenChamber Defaults
|
||||
## Scripted Workflow
|
||||
|
||||
- Mobile package: `packages/mobile`
|
||||
- iOS bundle id: `com.openchamber.app`
|
||||
- Headless env wrapper: `packages/mobile/scripts/with-mobile-env.mjs`
|
||||
- iOS simulator helper: `packages/mobile/scripts/ios-sim.mjs`
|
||||
- Preferred scripts:
|
||||
- `bun run mobile:build:ios:simulator`
|
||||
- `bun run mobile:sim:run`
|
||||
- `bun run mobile:sim:serve`
|
||||
- `bun run mobile:sim:list`
|
||||
- `bun run mobile:sim:kill`
|
||||
- `bun run mobile:sim:dev` — foreground build + run + stream in one command (`--no-build` to skip the build); intended for the user, agents should prefer the discrete scripts above
|
||||
Run the discrete scripts from the repository root so each step has an observable completion boundary:
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Build the simulator app without opening Xcode:
|
||||
1. Build the simulator app:
|
||||
```sh
|
||||
bun run mobile:build:ios:simulator
|
||||
```
|
||||
|
||||
2. Boot a simulator if needed, install, and launch the app:
|
||||
2. Boot if needed, install, and launch:
|
||||
```sh
|
||||
bun run mobile:sim:run
|
||||
```
|
||||
|
||||
3. Start the browser stream in detached JSON mode:
|
||||
3. Start the detached browser stream:
|
||||
```sh
|
||||
bun run mobile:sim:serve
|
||||
```
|
||||
Surface the returned `url` to the user. It normally starts at `http://127.0.0.1:3100`; always use the `url` from the JSON output rather than assuming the port.
|
||||
Surface the returned JSON `url`; it is the only authoritative stream address.
|
||||
|
||||
4. Stop helpers when finished unless the user asks to keep them running:
|
||||
```sh
|
||||
bun run mobile:sim:kill
|
||||
```
|
||||
|
||||
## Direct CLI Controls
|
||||
Completion means the app launched, the returned stream URL was surfaced, requested interactions were verified, and helpers were stopped or intentionally left running.
|
||||
|
||||
## Manual Controls
|
||||
|
||||
- Tap normalized coordinates: `bunx serve-sim tap 0.5 0.5`
|
||||
- Type focused text: `bunx serve-sim type "hello"`
|
||||
@@ -64,9 +54,4 @@ Coordinates are normalized `0..1`, not pixels. Prefer `tap` for simple taps; do
|
||||
- Node 18+.
|
||||
- At least one simulator can be booted with `xcrun simctl`.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Do not open Xcode just to build/install/launch during agent work; use the scripts above.
|
||||
- Do not parse human output from `serve-sim`; use `-q` for JSON.
|
||||
- Do not leave helper streams running unintentionally.
|
||||
- Do not guess coordinates after accessibility lookup fails; report the missing target instead.
|
||||
Use the scripts above instead of opening Xcode for build/install/launch. Consume JSON output rather than parsing human output. If accessibility lookup cannot identify a target, report the missing target instead of guessing coordinates.
|
||||
|
||||
@@ -36,7 +36,7 @@ shape is genuinely missing.
|
||||
| Field rows, checkboxes, radios, chips, selects, inputs, numeric steppers, info hints | `references/controls.md` |
|
||||
| Adding/moving controls, pages, availability, anchors, or search entries | `references/search.md` |
|
||||
|
||||
Load every matching reference before editing.
|
||||
Load each reference whose task branch applies; reference loading is complete when layout, control, and search implications are each classified.
|
||||
|
||||
## Quick Primitive Selection
|
||||
|
||||
@@ -77,7 +77,7 @@ Every stable Settings control addition or move must consider search in the same
|
||||
|
||||
Dynamic entity rows normally are not indexed. Load `references/search.md` for exact rules.
|
||||
|
||||
## Review Checklist
|
||||
## Completion Criteria
|
||||
|
||||
- Built from shared primitives; no ad-hoc page/section/row markup.
|
||||
- Explanatory text hidden behind `info`; warnings/syntax/status still visible.
|
||||
|
||||
@@ -5,9 +5,9 @@ description: Use when changing session synchronization, bootstrap or reconnect s
|
||||
|
||||
# Sync State Invariants
|
||||
|
||||
## Read First
|
||||
## Required Context
|
||||
|
||||
Read `packages/ui/src/sync/DOCUMENTATION.md` and the nearest owning module documentation before editing.
|
||||
Read `packages/ui/src/sync/DOCUMENTATION.md` and the nearest owning module documentation before editing. Context gathering is complete when every changed state has an identified owner, authority, scope, and lifecycle.
|
||||
|
||||
## Sources Of Truth
|
||||
|
||||
@@ -22,6 +22,10 @@ Classify every input before deriving state:
|
||||
|
||||
Prefer deterministic authoritative records over heuristics. Derive live behavior from live channels, not historical anomalies.
|
||||
|
||||
Give each state and its invariants one owner. Callers request domain transitions from that owner; they do not inspect one field, mutate another collection, and repair status externally. Split ownership only when the states have genuinely independent lifecycles.
|
||||
|
||||
Represent mutually exclusive lifecycle states with discriminated unions or equally precise contracts. Avoid boolean/nullable field combinations that permit impossible states. Reject invalid transitions at the owning boundary so downstream reducers and effects receive trusted state.
|
||||
|
||||
## Failure Is Not Empty
|
||||
|
||||
Any authoritative loader whose result can replace, delete, or clear state must distinguish failure from successful empty data.
|
||||
@@ -53,6 +57,7 @@ Inferring destructive cleanup from disappearance between snapshots requires an e
|
||||
|
||||
## Event Reducers
|
||||
|
||||
- Make the valid transition path explicit and flat. Return early for irrelevant entities and semantic no-ops; assert or reject transitions that violate an established invariant.
|
||||
- Clone only fields the event mutates; preserve every unrelated reference.
|
||||
- Return no change for semantically identical events.
|
||||
- Gate scans behind cheap event/entity checks.
|
||||
@@ -76,6 +81,7 @@ For streaming-frequency work, also load `performance-engineering`.
|
||||
|
||||
## Optimistic Updates
|
||||
|
||||
- Keep optimistic promotion, reconciliation, and rollback behavior behind the store/module that owns both visible and shadow state; do not expose collections for callers to mutate independently.
|
||||
- Insert optimistic data into the visible store and a separate shadow tracker.
|
||||
- Use client-generated IDs accepted and echoed by the server to reconcile in place.
|
||||
- Remove optimistic data from both visible and shadow state on failure.
|
||||
@@ -133,7 +139,7 @@ When state exists in memory and one or more persistent stores, define an explici
|
||||
|
||||
## Verification
|
||||
|
||||
Cover the relevant lifecycle, not only static state:
|
||||
Cover every applicable lifecycle branch, not only static state. Verification is complete when failure cannot masquerade as empty success, stale or partial data cannot cause destructive replacement, and each transition remains with its owner:
|
||||
|
||||
- fresh bootstrap and successful empty result;
|
||||
- fetch failure preserving prior state;
|
||||
@@ -147,17 +153,3 @@ Cover the relevant lifecycle, not only static state:
|
||||
- identity-preserving moves/category changes and runtime/scope changes resetting cleanup baselines;
|
||||
- create, update, move, archive, and delete mutations surviving responses started before those mutations;
|
||||
- missing versus empty persistence, malformed payloads, out-of-order writes, hydration races, and lifecycle durability behavior.
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Fetch helper catches and returns `[]`.
|
||||
- Historical message/session data drives a live spinner.
|
||||
- One failed entity blocks or clears all entities.
|
||||
- Light polling overwrites fields it did not fetch.
|
||||
- Queue reads current model/agent at send time.
|
||||
- New session lookup assumes SSE already indexed it.
|
||||
- Optimistic data has no shadow entry or rollback.
|
||||
- Snapshot-difference cleanup treats its first startup snapshot as a disappearance event.
|
||||
- Eviction runs on the acquisition path, or a cache limit is raised in response to a request loop.
|
||||
- Missing or malformed persistence becomes authoritative empty state.
|
||||
- Debounced writes are canceled on owner/lifecycle change without completing against the captured owner or an explicit durability/data-loss contract.
|
||||
|
||||
@@ -10,7 +10,7 @@ description: Use when creating or modifying OpenChamber UI components, styling,
|
||||
- Use semantic OpenChamber theme tokens; never hardcode hex colors or generic Tailwind palette colors.
|
||||
- Use shared UI primitives before introducing feature-local controls.
|
||||
- Use the shared `Button`; do not create button wrappers such as `ButtonSmall` or `ButtonLarge`.
|
||||
- Every dropdown-style value-picker trigger (shows current value, opens a picker) takes its chrome from `dropdownTriggerVariants` in `packages/ui/src/components/ui/dropdown-trigger.ts` (sizes: `sm` dense h-6, `default` forms h-8; native `SelectTrigger` consumes it). Call sites add layout classes only (width/truncation) — never re-declare border/radius/bg/hover. Deliberately chrome-less pickers (chat composer, headers) are the only exception.
|
||||
- Every dropdown-style value-picker trigger takes its chrome from `dropdownTriggerVariants` in `packages/ui/src/components/ui/dropdown-trigger.ts`; call sites add layout classes only. Deliberately chrome-less pickers in composers or headers are the exception.
|
||||
- Use the sprite-based `Icon`; never import icons directly from `@remixicon/react`.
|
||||
- Apply hover tokens only to interactive elements.
|
||||
- Use status colors only for actual status/feedback.
|
||||
@@ -24,7 +24,7 @@ description: Use when creating or modifying OpenChamber UI components, styling,
|
||||
| Adding, converting, storing, or generating icons | `references/icons.md` |
|
||||
| Adding built-in or custom themes | `references/adding-themes.md` |
|
||||
|
||||
Load every matching reference before editing. Settings work must also load `settings-ui-patterns`; user-facing or accessible text must load `locale-ui-patterns`.
|
||||
Load every matching reference before editing. User-facing or accessible text must load `locale-ui-patterns`. Settings composition is owned by `settings-ui-patterns`, which declares `theme-system` as its one-way companion.
|
||||
|
||||
## Token Decision
|
||||
|
||||
@@ -73,30 +73,11 @@ Use `IconName` for icon values stored in arrays, objects, state, or config. `Ico
|
||||
|
||||
## Animation Contract
|
||||
|
||||
Animate only `transform` and `opacity`. The compositor drives those; every other
|
||||
property recalculates style on each frame for as long as the animation runs, and
|
||||
geometry properties add layout on top. Measured on this repository's fixture,
|
||||
identical at any element count from 1 to 32:
|
||||
Animate only `transform` and `opacity`. Use `transform: rotate(...)`, not the individual `rotate` property. Non-composited properties recalculate style continuously; geometry also triggers layout, and wrappers, `will-change`, `contain`, or stepped timing do not remove that cost. Animate only while conveying live information.
|
||||
|
||||
| Animated property | Style recalculations/sec | Layouts/sec |
|
||||
|---|---|---|
|
||||
| `transform`, `opacity`, `filter` | 0 | 0 |
|
||||
| `rotate` (the individual property) | 60 | 0 |
|
||||
| `background-position`, `border-color`, `box-shadow` | 60 | 0 |
|
||||
| `width` and other geometry | 60 | 60 |
|
||||
For any other technique, load `performance-engineering` and `scripts/perf/DOCUMENTATION.md`, measure it with `bun run profile:animation`, and add a fixture variant when needed. This skill owns animation styling; `performance-engineering` owns performance evidence.
|
||||
|
||||
- `rotate: 360deg` is not a cheap synonym for `transform: rotate(360deg)`.
|
||||
Prefer the `transform` form.
|
||||
- Cost applies for the entire time an animation runs, so an indicator tied to a
|
||||
long-running operation pays it continuously. An indicator that is not
|
||||
conveying anything should not be animating.
|
||||
- `will-change`, wrapper elements, `contain`, and `steps()` timing do not make a
|
||||
non-composited property cheap. Only changing the property does.
|
||||
- Verify with `bun run profile:animation` rather than reasoning about it; add a
|
||||
variant to `scripts/perf/animation-fixture.html` for a technique not covered.
|
||||
See `scripts/perf/DOCUMENTATION.md`.
|
||||
|
||||
## Verification
|
||||
## Completion Criteria
|
||||
|
||||
- Animations are limited to `transform` and `opacity`, or their cost was measured and accepted.
|
||||
- No hardcoded/palette colors were introduced.
|
||||
@@ -104,4 +85,4 @@ identical at any element count from 1 to 32:
|
||||
- Icons use `Icon`/`IconName`, and generated sprite changes are intentional.
|
||||
- Hover, selection, primary, and status semantics are distinct.
|
||||
- Light/dark/high-contrast and long-text states remain legible.
|
||||
- Relevant type-check, visual/runtime validation, and generated-asset checks ran.
|
||||
- Every applicable contract and loaded task reference was verified with relevant type-check, visual/runtime validation, and generated-asset checks.
|
||||
|
||||
@@ -11,6 +11,7 @@ description: Use when creating or modifying OpenChamber shared UI data access, O
|
||||
- OpenChamber-owned HTTP capabilities use `RuntimeAPIs` where runtime-specific behavior exists, otherwise explicit OpenChamber routes through `runtimeFetch`.
|
||||
- Browser/realtime consumers use shared runtime URL/socket helpers.
|
||||
- Shared UI never hardcodes localhost, ports, API origins, credentials, or one runtime's transport assumptions.
|
||||
- Treat runtime adapters as the imperative shell: they own transport, auth, serialization, and platform mechanics. Shared feature code receives trusted contracts and owns domain decisions.
|
||||
|
||||
## Classify First
|
||||
|
||||
@@ -45,6 +46,9 @@ Load every matching reference before editing.
|
||||
8. **Authoritative fetches must signal failure.** Do not convert failure into a valid empty value that callers use to clear state.
|
||||
9. **Keep privileges at the native/runtime boundary.** UI visibility and prompts are not authorization.
|
||||
10. **Confirm trust-boundary mutations.** Host imports, credential writes, privileged deep links, and runtime switching require explicit user intent.
|
||||
11. **Parse at the boundary.** Treat external, persisted, bridge, IPC, and network payloads as unknown until a schema, parser, or narrow constructor produces the trusted type consumed by shared code. Do not validate fields and then continue passing the raw payload.
|
||||
12. **Model the real contract.** Prefer precise result/state unions and required dependencies over loose strings, boolean combinations, optional callback bags, `any`, or repeated casts. Make unsupported runtime behavior and failure distinct from valid empty success.
|
||||
13. **Keep adapters deep and bridges thin.** Hide meaningful protocol or platform mechanics behind an intention-revealing runtime operation; do not add pass-through layers that only rename SDK, fetch, or bridge calls.
|
||||
|
||||
## HTTP Decision Rules
|
||||
|
||||
@@ -69,6 +73,8 @@ Plain `fetch` is reserved for intentional external origins that are not the acti
|
||||
|
||||
Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores, session memory, and caches. Key caches by runtime identity where IDs, paths, or URLs can collide. Reset or reconnect affected state through the established runtime-switch flow.
|
||||
|
||||
Re-parse values obtained after a switch at their owning boundary. A type established for one runtime response does not make cached raw data from another runtime trustworthy.
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
| Avoid | Use |
|
||||
@@ -80,6 +86,8 @@ Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores
|
||||
| Web-only shared route | Explicit VS Code/mobile decision |
|
||||
| Returning `[]` after authoritative fetch failure | Throw or distinct failure result |
|
||||
| Rebuilding SDK `Request` from URL only | Preserve original request body/headers/signal |
|
||||
| Component validates unknown JSON then passes it onward | Adapter parses once and returns a trusted contract |
|
||||
| Boolean/nullable combinations for exclusive outcomes | Discriminated result or state union |
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: writing-for-agents
|
||||
description: Writing documents for agents. Use when creating or editing skills or modifying AGENTS.md.
|
||||
author: Matt Pocock
|
||||
---
|
||||
|
||||
Reference for writing any document an agent consumes — a skill, an `AGENTS.md`, a doc reached by a pointer. The packaging differs; the writing does not: the same levers make each one predictable — the agent taking the same _process_ every run, not producing the same output.
|
||||
|
||||
## Context pointers
|
||||
|
||||
A **context pointer** is a reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. A skill's description is one; a line in `AGENTS.md` naming a doc is the same object. The pointer's _wording_, not its target, decides when the agent reaches the material — and how reliably. A must-have target behind a weakly worded pointer is a variance bug: sharpen the wording first, and inline the material only if sharpening fails.
|
||||
|
||||
A pointer does two jobs — state what the material is, and list the **branches** that should trigger reaching it (a branch is a distinct case the document handles, so different runs take different paths through it). Every word of an always-loaded pointer costs on every turn, so it earns even harder pruning than the body:
|
||||
|
||||
- **Front-load the leading word** — the pointer is where it does its triggering work.
|
||||
- **One trigger per branch.** Synonyms that rename a single branch are one branch written twice; collapse them and keep only genuinely distinct branches.
|
||||
- **Cut identity the body already carries.**
|
||||
|
||||
## The two loads
|
||||
|
||||
Every document and pointer you add spends one of two budgets:
|
||||
|
||||
- **Context load** — the cost of always-loaded material on the agent's window: an `AGENTS.md` line, a skill description, anything sitting in context every turn, spending tokens and attention whether or not it fires.
|
||||
- **Cognitive load** — the cost on the human: which documents exist and when to reach for each. The human is the index. Not a cost to minimise — it is the price of human agency; spend it where human judgement matters, remove it where it does not.
|
||||
|
||||
Material reached only through a pointer escapes context load at the price of the pointer's own line; material with no pointer at all rides entirely on cognitive load.
|
||||
|
||||
## Information hierarchy
|
||||
|
||||
A document is built from two content types — **steps** (the ordered actions the agent performs) and **reference** (definitions, rules, facts consulted on demand) — that mix freely: all steps (a recipe), all reference (a review's rules, this skill), or both. The core decision is where each piece sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
|
||||
|
||||
1. **In-file step** — the primary tier: what the agent does, in order.
|
||||
2. **In-file reference** — consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell.
|
||||
3. **Disclosed reference** — pushed out into a separate file, reached by a context pointer, loaded only when the pointer fires. Spans a sibling file in the same folder through fully external reference that lives anywhere and any document can point at.
|
||||
|
||||
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
|
||||
|
||||
**Progressive disclosure** is the move down the ladder — out of the main file and behind a pointer — so the top stays legible. Not primarily a token optimisation: it is how the hierarchy is protected. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. When a document has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one.
|
||||
|
||||
**Co-location** is the within-file companion: where the ladder decides _how far down_ a piece sits, co-location decides _what sits beside it_ once there. Keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it. The test: the document should read like documentation written for the agent — grouped material reads that way; scattered material does not. (Distinct from duplication: that repeats one meaning in two places; scattering fragments one meaning across many.)
|
||||
|
||||
**Sprawl** is the failure mode here: a document simply too long, even when every line is live and unique. Attention thins across the excess, and every extra line is one more to keep relevant. The cure is the ladder: disclose reference behind pointers, and split by branch or sequence so each path carries only what it needs.
|
||||
|
||||
## Steps and completion criteria
|
||||
|
||||
Every step ends on a **completion criterion** — the condition that tells the agent the work is done. Two properties make it a lever:
|
||||
|
||||
- **Clarity** — can the agent tell done from not-done? A vague bound ("understanding reached") invites **premature completion**: ending the step before it is genuinely done, attention slipping to _being done_. The visible steps still ahead — the **post-completion steps** — supply the pull; the criterion's clarity is the resistance. Defend in order: **sharpen the bound first** (local and cheap); only if it is irreducibly fuzzy _and_ you observe the rush, hide the later steps by splitting the sequence — and hiding only works across a real context boundary (a hand-off or a subagent dispatch; an inline call leaves the later steps in context and clears nothing).
|
||||
- **Demand** — how much it requires. "Every modified model accounted for" forces thorough work where "produce a change list" does not. Demand drives **legwork** — the digging the agent does within the work, latent in the wording rather than written as its own step — and it is not step-bound: "every rule applied" binds a body of flat reference just as "every step done" binds a sequence, which is how an all-reference document still carries an exhaustiveness bar.
|
||||
|
||||
The strongest criteria are both checkable and exhaustive.
|
||||
|
||||
## When to split
|
||||
|
||||
Splitting one document into two spends one of the two loads, so split only when the cut earns it:
|
||||
|
||||
- **By sequence** — split a run of steps where the post-completion steps tempt the agent to rush the one in front of it. Keeping them out of view drives more legwork on the current task. Beware the reverse: merging sequences exposes each step's later steps to what follows, inviting premature completion.
|
||||
|
||||
## Leading words
|
||||
|
||||
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the document (_lesson_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free; reach for an existing word first.
|
||||
|
||||
It anchors twice. In the body, _execution_: the agent reaches for the same behaviour every time the word appears, and inside flat reference it focuses attention on a class of thing to look for. In a pointer, _invocation_: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the material and reaches it more reliably.
|
||||
|
||||
Hunt for opportunities to refactor with leading words. A triad spelled out at three sites, a pointer spending a sentence to gesture at one idea — each is a passage begging to collapse into a single token:
|
||||
|
||||
- "fast, deterministic, low-overhead" → _tight_ (a _tight_ loop).
|
||||
- "a loop you believe in" → _red_ — a fuzzy gate becomes a binary observable state (the loop goes _red_ on the bug, or it doesn't).
|
||||
|
||||
You win twice: fewer tokens, and a sharper hook for the agent to hang its thinking on. Assume every document is carrying restatements that leading words retire — go find them.
|
||||
|
||||
**Negation** is the failure mode beside this lever: steering by prohibition drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; the negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Prompt the **positive** — state the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do.
|
||||
|
||||
## Pruning
|
||||
|
||||
- Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit. **Duplication** — the same meaning in more than one place — costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank. (The accidental inverse of a leading word, which repeats a token on purpose, never the meaning.)
|
||||
- For cross-document guidance, name one canonical owner. Other documents point to it and state only their local consequence; they do not restate the shared rule.
|
||||
- The **environment** is a source of truth too — `package.json` scripts, config files, the directory layout, `--help` output — and a document that restates it is a **cache**: a copy of a lookup, earning its load only when the lookup is expensive. Cache what the agent cannot find by looking: the unwritten convention, the reason behind a choice, the gotcha no config confesses. Leave the one-file, one-command lookups to the environment, where they cannot go stale.
|
||||
- Check every line for **relevance**: does it still bear on what the document does? A line loses relevance by never bearing on the task (mere exposition, or a branch that should be disclosed) or by going stale as the behaviour or world it describes changes. Shorter documents are easier to keep relevant. Without a pruning discipline the default fate is **sediment**: stale layers that settle because adding feels safe and removing feels risky, until you must core down through them to find what is still live.
|
||||
- Hunt **no-ops** sentence by sentence: an instruction the model already obeys by default pays load to say nothing. The test — does it change behaviour versus the default? — is model-relative, not reader-relative: two people disagreeing about a no-op disagree about the default, and settle it by running the document, not by debate. When a sentence fails, delete the whole sentence rather than trim words from it. The test also grades leading words: a word too weak to beat the default (_be thorough_ when the agent is already thorough-ish) is a no-op, and the fix is a stronger word (_relentless_), not a different technique.
|
||||
Reference in New Issue
Block a user