Sandbox Audit
Worker skills inventory for SerpBot operations.
This public documentation page renders the root SKILLS_INVENTORY.md file, including each discovered skill path, role, size, trigger status, and full readable source content.
Skills
73
Mandatory
11
Optional
2
Source
123KB
Skills Inventory
- Generated: 2026-07-01 21:14:51 UTC
- Source directory:
.agents/skills/ - Resolved source:
/opt/nanocorp/skills - Total skills found: 13
- Inventory policy: all readable
SKILL.mdfiles are included in full. No proprietary files were redacted because every discovered file was readable plain text. - Status convention:
Mandatorymeans always required for this worker workflow;Mandatory-on-triggermeans required when the described situation occurs;Optionalmeans reference guidance used when relevant.
Summary Table
| Skill | Path | Type | Role | Size | Status |
|---|---|---|---|---|---|
agent-browser | .agents/skills/agent-browser/SKILL.md | Directory skill (SKILL.md) | Browser automation CLI playbook | 26,370 bytes | Optional (use when browser automation is needed) |
browser-troubleshooting | .agents/skills/browser-troubleshooting/SKILL.md | Directory skill (SKILL.md) | Browser automation recovery and stop rules | 9,688 bytes | Mandatory-on-trigger (before repeated browser retries) |
ceo-task-hygiene | .agents/skills/ceo-task-hygiene/SKILL.md | Directory skill (SKILL.md) | CEO task scoping and retry hygiene | 8,701 bytes | Mandatory-on-trigger (before creating/splitting CEO tasks) |
frontend-design | .agents/skills/frontend-design/SKILL.md | Directory skill (SKILL.md) | Production-grade frontend design guidance | 4,408 bytes | Mandatory-on-trigger (when building frontend UI) |
git-push-safe | .agents/skills/git-push-safe/SKILL.md | Directory skill (SKILL.md) | Safe Git push/rebase conflict workflow | 8,949 bytes | Mandatory-on-trigger (when push/rebase conflicts occur) |
nanocorp-cli | .agents/skills/nanocorp-cli/SKILL.md | Directory skill (SKILL.md) | NanoCorp CLI command reference | 13,455 bytes | Optional (reference for NanoCorp CLI workflows) |
nextjs-bootstrap | .agents/skills/nextjs-bootstrap/SKILL.md | Directory skill (SKILL.md) | Next.js setup, install, and build decision tree | 7,481 bytes | Mandatory-on-trigger (when installing/building Next.js) |
polling-and-waits | .agents/skills/polling-and-waits/SKILL.md | Directory skill (SKILL.md) | Bounded polling/waiting patterns | 7,603 bytes | Mandatory-on-trigger (when waiting/polling jobs) |
stripe-products | .agents/skills/stripe-products/SKILL.md | Directory skill (SKILL.md) | Stripe product and checkout-link safety rules | 7,512 bytes | Mandatory-on-trigger (before Stripe product changes) |
stripe-webhook | .agents/skills/stripe-webhook/SKILL.md | Directory skill (SKILL.md) | NanoCorp Stripe webhook contract guidance | 10,484 bytes | Mandatory-on-trigger (before payment handling code) |
task-result-summary | .agents/skills/task-result-summary/SKILL.md | Directory skill (SKILL.md) | Required worker result summary format | 6,797 bytes | Mandatory (worker final result format) |
vercel-deploy-verify | .agents/skills/vercel-deploy-verify/SKILL.md | Directory skill (SKILL.md) | Single-attempt Vercel deployment verification | 6,431 bytes | Mandatory-on-trigger (after pushed frontend deploy) |
worker-stop-conditions | .agents/skills/worker-stop-conditions/SKILL.md | Directory skill (SKILL.md) | Hard retry limits and terminal-error rules | 8,288 bytes | Mandatory (worker retry/stop rules) |
Complete Skill Contents
agent-browser
- Path:
.agents/skills/agent-browser/SKILL.md - Resolved path:
/opt/nanocorp/skills/agent-browser/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Browser automation CLI playbook
- Size: 26,370 bytes
- Status: Optional (use when browser automation is needed)
- Frontmatter description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
---
name: agent-browser
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*)
---
# Browser Automation with agent-browser
The CLI uses Chrome/Chromium via CDP directly. Install via `npm i -g agent-browser`, `brew install agent-browser`, or `cargo install agent-browser`. Run `agent-browser install` to download Chrome. Run `agent-browser upgrade` to update to the latest version.
## Core Workflow
Every browser automation follows this pattern:
1. **Navigate**: `agent-browser open <url>`
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
3. **Interact**: Use refs to click, fill, select
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
```bash
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i # Check result
```
## Command Chaining
Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.
```bash
# Chain open + wait + snapshot in one call
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
# Chain multiple interactions
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3
# Navigate and capture
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
```
**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).
## Handling Authentication
When automating a site that requires login, choose the approach that fits:
**Option 1: Import auth from the user's browser (fastest for one-off tasks)**
```bash
# Connect to the user's running Chrome (they're already logged in)
agent-browser --auto-connect state save ./auth.json
# Use that auth state
agent-browser --state ./auth.json open https://app.example.com/dashboard
```
State files contain session tokens in plaintext -- add to `.gitignore` and delete when no longer needed. Set `AGENT_BROWSER_ENCRYPTION_KEY` for encryption at rest.
**Option 2: Persistent profile (simplest for recurring tasks)**
```bash
# First run: login manually or via automation
agent-browser --profile ~/.myapp open https://app.example.com/login
# ... fill credentials, submit ...
# All future runs: already authenticated
agent-browser --profile ~/.myapp open https://app.example.com/dashboard
```
**Option 3: Session name (auto-save/restore cookies + localStorage)**
```bash
agent-browser --session-name myapp open https://app.example.com/login
# ... login flow ...
agent-browser close # State auto-saved
# Next time: state auto-restored
agent-browser --session-name myapp open https://app.example.com/dashboard
```
**Option 4: Auth vault (credentials stored encrypted, login by name)**
```bash
echo "$PASSWORD" | agent-browser auth save myapp --url https://app.example.com/login --username user --password-stdin
agent-browser auth login myapp
```
**Option 5: State file (manual save/load)**
```bash
# After logging in:
agent-browser state save ./auth.json
# In a future session:
agent-browser state load ./auth.json
agent-browser open https://app.example.com/dashboard
```
See [references/authentication.md](references/authentication.md) for OAuth, 2FA, cookie-based auth, and token refresh patterns.
## Essential Commands
```bash
# Navigation
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser close # Close browser
# Snapshot
agent-browser snapshot -i # Interactive elements with refs (recommended)
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer)
agent-browser snapshot -s "#selector" # Scope to CSS selector
# Interaction (use @refs from snapshot)
agent-browser click @e1 # Click element
agent-browser click @e1 --new-tab # Click and open in new tab
agent-browser fill @e2 "text" # Clear and type text
agent-browser type @e2 "text" # Type without clearing
agent-browser select @e1 "option" # Select dropdown option
agent-browser check @e1 # Check checkbox
agent-browser press Enter # Press key
agent-browser keyboard type "text" # Type at current focus (no selector)
agent-browser keyboard inserttext "text" # Insert without key events
agent-browser scroll down 500 # Scroll page
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
# Get information
agent-browser get text @e1 # Get element text
agent-browser get url # Get current URL
agent-browser get title # Get page title
agent-browser get cdp-url # Get CDP WebSocket URL
# Wait
agent-browser wait @e1 # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/page" # Wait for URL pattern
agent-browser wait 2000 # Wait milliseconds
agent-browser wait --text "Welcome" # Wait for text to appear (substring match)
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
# Downloads
agent-browser download @e1 ./file.pdf # Click element to trigger download
agent-browser wait --download ./output.zip # Wait for any download to complete
agent-browser --download-path ./downloads open <url> # Set default download directory
# Network
agent-browser network requests # Inspect tracked requests
agent-browser network route "**/api/*" --abort # Block matching requests
agent-browser network har start # Start HAR recording
agent-browser network har stop ./capture.har # Stop and save HAR file
# Viewport & Device Emulation
agent-browser set viewport 1920 1080 # Set viewport size (default: 1280x720)
agent-browser set viewport 1920 1080 2 # 2x retina (same CSS size, higher res screenshots)
agent-browser set device "iPhone 14" # Emulate device (viewport + user agent)
# Capture
agent-browser screenshot # Screenshot to temp dir
agent-browser screenshot --full # Full page screenshot
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
agent-browser pdf output.pdf # Save as PDF
# Clipboard
agent-browser clipboard read # Read text from clipboard
agent-browser clipboard write "Hello, World!" # Write text to clipboard
agent-browser clipboard copy # Copy current selection
agent-browser clipboard paste # Paste from clipboard
# Diff (compare page states)
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
agent-browser diff screenshot --baseline before.png # Visual pixel diff
agent-browser diff url <url1> <url2> # Compare two pages
agent-browser diff url <url1> <url2> --wait-until networkidle # Custom wait strategy
agent-browser diff url <url1> <url2> --selector "#main" # Scope to element
```
## Batch Execution
Execute multiple commands in a single invocation by piping a JSON array of string arrays to `batch`. This avoids per-command process startup overhead when running multi-step workflows.
```bash
echo '[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["screenshot", "result.png"]
]' | agent-browser batch --json
# Stop on first error
agent-browser batch --bail < commands.json
```
Use `batch` when you have a known sequence of commands that don't depend on intermediate output. Use separate commands or `&&` chaining when you need to parse output between steps (e.g., snapshot to discover refs, then interact).
## Common Patterns
### Form Submission
```bash
agent-browser open https://example.com/signup
agent-browser snapshot -i
agent-browser fill @e1 "Jane Doe"
agent-browser fill @e2 "jane@example.com"
agent-browser select @e3 "California"
agent-browser check @e4
agent-browser click @e5
agent-browser wait --load networkidle
```
### Authentication with Auth Vault (Recommended)
```bash
# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY)
# Recommended: pipe password via stdin to avoid shell history exposure
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
# Login using saved profile (LLM never sees password)
agent-browser auth login github
# List/show/delete profiles
agent-browser auth list
agent-browser auth show github
agent-browser auth delete github
```
### Authentication with State Persistence
```bash
# Login once and save state
agent-browser open https://app.example.com/login
agent-browser snapshot -i
agent-browser fill @e1 "$USERNAME"
agent-browser fill @e2 "$PASSWORD"
agent-browser click @e3
agent-browser wait --url "**/dashboard"
agent-browser state save auth.json
# Reuse in future sessions
agent-browser state load auth.json
agent-browser open https://app.example.com/dashboard
```
### Session Persistence
```bash
# Auto-save/restore cookies and localStorage across browser restarts
agent-browser --session-name myapp open https://app.example.com/login
# ... login flow ...
agent-browser close # State auto-saved to ~/.agent-browser/sessions/
# Next time, state is auto-loaded
agent-browser --session-name myapp open https://app.example.com/dashboard
# Encrypt state at rest
export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)
agent-browser --session-name secure open https://app.example.com
# Manage saved states
agent-browser state list
agent-browser state show myapp-default.json
agent-browser state clear myapp
agent-browser state clean --older-than 7
```
### Working with Iframes
Iframe content is automatically inlined in snapshots. Refs inside iframes carry frame context, so you can interact with them directly.
```bash
agent-browser open https://example.com/checkout
agent-browser snapshot -i
# @e1 [heading] "Checkout"
# @e2 [Iframe] "payment-frame"
# @e3 [input] "Card number"
# @e4 [input] "Expiry"
# @e5 [button] "Pay"
# Interact directly — no frame switch needed
agent-browser fill @e3 "4111111111111111"
agent-browser fill @e4 "12/28"
agent-browser click @e5
# To scope a snapshot to one iframe:
agent-browser frame @e2
agent-browser snapshot -i # Only iframe content
agent-browser frame main # Return to main frame
```
### Data Extraction
```bash
agent-browser open https://example.com/products
agent-browser snapshot -i
agent-browser get text @e5 # Get specific element text
agent-browser get text body > page.txt # Get all page text
# JSON output for parsing
agent-browser snapshot -i --json
agent-browser get text @e1 --json
```
### Parallel Sessions
```bash
agent-browser --session site1 open https://site-a.com
agent-browser --session site2 open https://site-b.com
agent-browser --session site1 snapshot -i
agent-browser --session site2 snapshot -i
agent-browser session list
```
### Connect to Existing Chrome
```bash
# Auto-discover running Chrome with remote debugging enabled
agent-browser --auto-connect open https://example.com
agent-browser --auto-connect snapshot
# Or with explicit CDP port
agent-browser --cdp 9222 snapshot
```
Auto-connect discovers Chrome via `DevToolsActivePort`, common debugging ports (9222, 9229), and falls back to a direct WebSocket connection if HTTP-based CDP discovery fails.
### Color Scheme (Dark Mode)
```bash
# Persistent dark mode via flag (applies to all pages and new tabs)
agent-browser --color-scheme dark open https://example.com
# Or via environment variable
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com
# Or set during session (persists for subsequent commands)
agent-browser set media dark
```
### Viewport & Responsive Testing
```bash
# Set a custom viewport size (default is 1280x720)
agent-browser set viewport 1920 1080
agent-browser screenshot desktop.png
# Test mobile-width layout
agent-browser set viewport 375 812
agent-browser screenshot mobile.png
# Retina/HiDPI: same CSS layout at 2x pixel density
# Screenshots stay at logical viewport size, but content renders at higher DPI
agent-browser set viewport 1920 1080 2
agent-browser screenshot retina.png
# Device emulation (sets viewport + user agent in one step)
agent-browser set device "iPhone 14"
agent-browser screenshot device.png
```
The `scale` parameter (3rd argument) sets `window.devicePixelRatio` without changing CSS layout. Use it when testing retina rendering or capturing higher-resolution screenshots.
### Visual Browser (Debugging)
```bash
agent-browser --headed open https://example.com
agent-browser highlight @e1 # Highlight element
agent-browser inspect # Open Chrome DevTools for the active page
agent-browser record start demo.webm # Record session
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop trace.json # Stop and save profile (path optional)
```
Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode.
### Local Files (PDFs, HTML)
```bash
# Open local files with file:// URLs
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
agent-browser screenshot output.png
```
### iOS Simulator (Mobile Safari)
```bash
# List available iOS simulators
agent-browser device list
# Launch Safari on a specific device
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
# Same workflow as desktop - snapshot, interact, re-snapshot
agent-browser -p ios snapshot -i
agent-browser -p ios tap @e1 # Tap (alias for click)
agent-browser -p ios fill @e2 "text"
agent-browser -p ios swipe up # Mobile-specific gesture
# Take screenshot
agent-browser -p ios screenshot mobile.png
# Close session (shuts down simulator)
agent-browser -p ios close
```
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
## Security
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output.
### Content Boundaries (Recommended for AI Agents)
Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content:
```bash
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
agent-browser snapshot
# Output:
# --- AGENT_BROWSER_PAGE_CONTENT nonce=<hex> origin=https://example.com ---
# [accessibility tree]
# --- END_AGENT_BROWSER_PAGE_CONTENT nonce=<hex> ---
```
### Domain Allowlist
Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on:
```bash
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
agent-browser open https://example.com # OK
agent-browser open https://malicious.com # Blocked
```
### Action Policy
Use a policy file to gate destructive actions:
```bash
export AGENT_BROWSER_ACTION_POLICY=./policy.json
```
Example `policy.json`:
```json
{ "default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"] }
```
Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.
### Output Limits
Prevent context flooding from large pages:
```bash
export AGENT_BROWSER_MAX_OUTPUT=50000
```
## Diffing (Verifying Changes)
Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.
```bash
# Typical workflow: snapshot -> action -> diff
agent-browser snapshot -i # Take baseline snapshot
agent-browser click @e2 # Perform action
agent-browser diff snapshot # See what changed (auto-compares to last snapshot)
```
For visual regression testing or monitoring:
```bash
# Save a baseline screenshot, then compare later
agent-browser screenshot baseline.png
# ... time passes or changes are made ...
agent-browser diff screenshot --baseline baseline.png
# Compare staging vs production
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
```
`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.
## Timeouts and Slow Pages
The default timeout is 25 seconds. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:
```bash
# Wait for network activity to settle (best for slow pages)
agent-browser wait --load networkidle
# Wait for a specific element to appear
agent-browser wait "#content"
agent-browser wait @e1
# Wait for a specific URL pattern (useful after redirects)
agent-browser wait --url "**/dashboard"
# Wait for a JavaScript condition
agent-browser wait --fn "document.readyState === 'complete'"
# Wait a fixed duration (milliseconds) as a last resort
agent-browser wait 5000
```
When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait <selector>` or `wait @ref`.
## Session Management and Cleanup
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
```bash
# Each agent gets its own isolated session
agent-browser --session agent1 open site-a.com
agent-browser --session agent2 open site-b.com
# Check active sessions
agent-browser session list
```
Always close your browser session when done to avoid leaked processes:
```bash
agent-browser close # Close default session
agent-browser --session agent1 close # Close specific session
```
If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.
To auto-shutdown the daemon after a period of inactivity (useful for ephemeral/CI environments):
```bash
AGENT_BROWSER_IDLE_TIMEOUT_MS=60000 agent-browser open example.com
```
## Ref Lifecycle (Important)
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
- Clicking links or buttons that navigate
- Form submissions
- Dynamic content loading (dropdowns, modals)
```bash
agent-browser click @e5 # Navigates to new page
agent-browser snapshot -i # MUST re-snapshot
agent-browser click @e1 # Use new refs
```
## Annotated Screenshots (Vision Mode)
Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot.
```bash
agent-browser screenshot --annotate
# Output includes the image path and a legend:
# [1] @e1 button "Submit"
# [2] @e2 link "Home"
# [3] @e3 textbox "Email"
agent-browser click @e2 # Click using ref from annotated screenshot
```
Use annotated screenshots when:
- The page has unlabeled icon buttons or visual-only elements
- You need to verify visual layout or styling
- Canvas or chart elements are present (invisible to text snapshots)
- You need spatial reasoning about element positions
## Semantic Locators (Alternative to Refs)
When refs are unavailable or unreliable, use semantic locators:
```bash
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find role button click --name "Submit"
agent-browser find placeholder "Search" type "query"
agent-browser find testid "submit-btn" click
```
## JavaScript Evaluation (eval)
Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.
```bash
# Simple expressions work with regular quoting
agent-browser eval 'document.title'
agent-browser eval 'document.querySelectorAll("img").length'
# Complex JS: use --stdin with heredoc (RECOMMENDED)
agent-browser eval --stdin <<'EVALEOF'
JSON.stringify(
Array.from(document.querySelectorAll("img"))
.filter(i => !i.alt)
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
)
EVALEOF
# Alternative: base64 encoding (avoids all shell escaping issues)
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"
```
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
**Rules of thumb:**
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
- Programmatic/generated scripts -> use `eval -b` with base64
## Configuration File
Create `agent-browser.json` in the project root for persistent settings:
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data"
}
```
Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config <path>` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced.
## Deep-Dive Documentation
| Reference | When to Use |
| -------------------------------------------------------------------- | --------------------------------------------------------- |
| [references/commands.md](references/commands.md) | Full command reference with all options |
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
## Browser Engine Selection
Use `--engine` to choose a local browser engine. The default is `chrome`.
```bash
# Use Lightpanda (fast headless browser, requires separate install)
agent-browser --engine lightpanda open example.com
# Via environment variable
export AGENT_BROWSER_ENGINE=lightpanda
agent-browser open example.com
# With custom binary path
agent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com
```
Supported engines:
- `chrome` (default) -- Chrome/Chromium via CDP
- `lightpanda` -- Lightpanda headless browser via CDP (10x faster, 10x less memory than Chrome)
Lightpanda does not support `--extension`, `--profile`, `--state`, or `--allow-file-access`. Install Lightpanda from https://lightpanda.io/docs/open-source/installation.
## Ready-to-Use Templates
| Template | Description |
| ------------------------------------------------------------------------ | ----------------------------------- |
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
```bash
./templates/form-automation.sh https://example.com/form
./templates/authenticated-session.sh https://app.example.com/login
./templates/capture-workflow.sh https://example.com ./output
```
browser-troubleshooting
- Path:
.agents/skills/browser-troubleshooting/SKILL.md - Resolved path:
/opt/nanocorp/skills/browser-troubleshooting/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Browser automation recovery and stop rules
- Size: 9,688 bytes
- Status: Mandatory-on-trigger (before repeated browser retries)
- Frontmatter description: >- Recovery cookbook and stop rules for the
agent-browserCLI when it misbehaves. Use this BEFORE you make a third or fourth retry against the same URL or command. Triggers include "Chrome not found", "ERR_NAME_NOT_RESOLVED", "net::ERR_CERT_AUTHORITY_INVALID", a "TimeoutError" from locator.waitFor, a "locator.screenshot: Unexpected token" error, "Unknown command" for execute/clear/wait-for, "Target page, context or browser has been closed", agent-browser failing to install, agent-browser snapshot returning empty, or a loop ofagent-browser open <url>failing the same way more than twice. Pair withagent-browser(the how-to) — this skill is the diagnostic and stop-rules layer.
---
name: browser-troubleshooting
description: >-
Recovery cookbook and stop rules for the `agent-browser` CLI when it misbehaves.
Use this BEFORE you make a third or fourth retry against the same URL or command.
Triggers include "Chrome not found", "ERR_NAME_NOT_RESOLVED",
"net::ERR_CERT_AUTHORITY_INVALID", a "TimeoutError" from locator.waitFor,
a "locator.screenshot: Unexpected token" error, "Unknown command" for
execute/clear/wait-for, "Target page, context or browser has been closed",
agent-browser failing to install, agent-browser snapshot returning empty,
or a loop of `agent-browser open <url>` failing the same way more than twice.
Pair with `agent-browser` (the how-to) — this skill is the diagnostic and stop-rules layer.
---
# Browser Troubleshooting
The Modal image pre-installs `agent-browser` and a working Chromium. When `agent-browser` fails, there is almost always a specific root cause — and looping at it never fixes it. Diagnose the error class, apply ONE fix, then either continue or escalate.
## Hard stop rules — read first
These exist because long traces show the same loops over and over. Internalize them.
1. **One install attempt per task.** If `agent-browser install` fails or "Chrome not found" persists after one install, **STOP**. This is a platform issue — surface it in your task result. Do not retry, do not `apt install`, do not `brew install`, do not `npm i -g`.
2. **3-strike rule per URL.** Three consecutive failures on the same URL = stop trying that URL. If it's DNS or cert, the domain is wrong/dead. If it's a timeout, the page is genuinely slow — switch to a more permissive wait OR move on.
3. **5-strike rule per session.** If `agent-browser` has produced 5+ errors in this task, stop using it. Write what you got and move on.
4. **Never loop a screenshot.** A screenshot retry after a failure costs ~3s for nothing. If the page loaded, snapshot once; if it didn't, fix the load.
5. **No fallback installs.** Don't try Playwright, Puppeteer, `apt-get install chromium`, or curl-as-a-browser. The platform provides `agent-browser` only.
## Error → diagnosis → fix
Match against the **exact** error text. Don't pattern-match loosely — different errors have different fixes.
### "Chrome not found" / "Could not find Chrome"
```
Error: Could not find Chrome (ver. 131.0.6778.85). This can occur if either
1. you did not perform an installation before running the script (e.g. `npx puppeteer browsers install chrome`)
2. ...
```
Fix (ONCE only):
```bash
agent-browser install
agent-browser open https://example.com # try once to confirm
```
If `agent-browser install` itself fails OR the next call still says "Chrome not found", **STOP**. This is a platform problem. Write in your result: "agent-browser unavailable in this sandbox — needs platform fix." Move on to non-browser parts of the task.
### "ERR_NAME_NOT_RESOLVED" / "DNS_PROBE_FINISHED_NXDOMAIN"
```
✗ Navigation failed: net::ERR_NAME_NOT_RESOLVED
```
The domain doesn't exist or isn't resolvable. **Do not retry.** Common causes:
| Likely cause | Action |
|---|---|
| Typo in URL | Re-read your URL — `aceme.com` vs `acme.com` |
| Hallucinated domain (e.g. an imaginary partner site) | Stop. Don't keep guessing other domains. |
| Site truly dead | Skip this target, log it in your result |
| Missing `https://` prefix | `agent-browser open` accepts bare hostnames but be explicit: `https://acme.com` |
Three NXDOMAIN errors in a row on different guessed domains = your data source is wrong. Stop scraping and surface the data problem.
### "net::ERR_CERT_AUTHORITY_INVALID" / "ERR_SSL_PROTOCOL_ERROR"
```
✗ Navigation failed: net::ERR_CERT_AUTHORITY_INVALID
```
```bash
# Bypass for development / known self-signed sites (use carefully):
agent-browser --ignore-https-errors open https://example.com
```
If it's a production site, the cert is genuinely broken; you can still read it with `--ignore-https-errors`. **Never disable HTTPS verification for sites you'll submit forms to** (don't send credentials over an unvalidated cert).
### "TimeoutError" / "Timeout 25000ms exceeded"
```
TimeoutError: locator.waitFor: Timeout 25000ms exceeded.
```
The default timeout is 25 s. Two ways forward, in this order:
```bash
# 1. Wait for a specific signal instead of relying on default timeout
agent-browser open https://slow.example.com
agent-browser wait --load networkidle # wait for network to settle
# or
agent-browser wait "#main-content" # wait for a specific element
# or
agent-browser wait --url "**/dashboard" # wait for a navigation
# 2. If the page is genuinely slow, raise the timeout ONCE
AGENT_BROWSER_DEFAULT_TIMEOUT=60000 agent-browser open https://slow.example.com
```
Don't raise the timeout and ALSO `sleep N` — pick one. Default to `wait --load networkidle` for slow pages.
### "Unknown command: execute" / "Unknown command: clear" / "Unknown command: wait-for"
You guessed a command that doesn't exist. The real names:
| You typed | Use instead |
|---|---|
| `agent-browser execute "<js>"` | `agent-browser eval '<js>'` (or `eval --stdin <<EOF`) |
| `agent-browser clear @e1` | `agent-browser fill @e1 ""` |
| `agent-browser wait-for <selector>` | `agent-browser wait <selector>` |
| `agent-browser sleep 2000` | `agent-browser wait 2000` |
| `agent-browser goto <url>` | `agent-browser open <url>` (works as alias too) |
| `agent-browser screenshot <url>` | `agent-browser open <url> && agent-browser screenshot <path>` |
| `agent-browser scrape <url>` | `agent-browser open <url> && agent-browser get text body` |
The full command list is in the `agent-browser` skill. When you forget, `agent-browser -h` is fast.
### `screenshot` errors: "Unexpected token \"/\" parsing css selector"
```
agent-browser screenshot "https://example.com/foo" --path /tmp/p.png
# ✗ locator.screenshot: Unexpected token "/" parsing css selector
```
`screenshot` takes an **output path** as positional arg, not a URL. The browser must already be navigated.
```bash
# Wrong
agent-browser screenshot "https://example.com/foo" --path /tmp/p.png
# Right
agent-browser open "https://example.com/foo"
agent-browser wait --load networkidle
agent-browser screenshot /tmp/p.png
# Or full page:
agent-browser screenshot --full /tmp/p.png
# Or with element labels (great for clicking):
agent-browser screenshot --annotate /tmp/p.png
```
### Stale @refs after navigation
```
Error: Element ref @e3 is no longer attached to the DOM
```
Refs invalidate the moment the page changes. Re-snapshot **every time**:
```bash
agent-browser click @e2 # this navigates
# DO NOT reuse @e1, @e2, @e3...
agent-browser snapshot -i # get fresh refs
agent-browser click @e1 # new @e1 from the new page
```
### "Target page, context or browser has been closed"
The daemon crashed or the session was killed. Recovery:
```bash
agent-browser close 2>/dev/null # clean any leaked state
agent-browser open https://example.com # fresh start
```
If you're running concurrent work, ALWAYS use `--session <name>` so sessions don't collide:
```bash
agent-browser --session a open https://x.com
agent-browser --session b open https://y.com
```
### Empty / suspiciously short snapshot
If `agent-browser snapshot -i` returns an empty tree or only `<html>`:
1. Page may not be loaded — `agent-browser wait --load networkidle && agent-browser snapshot -i`.
2. Page may be a SPA that paints later — `agent-browser wait "main" && agent-browser snapshot -i`.
3. Page may be blocking on a captcha or auth — open a screenshot to see; if it's a captcha or login wall, this URL is unscrapable, move on.
```bash
agent-browser open <url>
agent-browser screenshot /tmp/diag.png # eyeball it
agent-browser snapshot -i # try again
```
## "Loop alarm" — am I stuck?
If you catch yourself running the same `agent-browser` command pattern 3+ times in a row, you ARE stuck. Apply this checklist before the 4th attempt:
1. **Is the URL the same as the last failure?** If yes, that URL is dead-to-you for this task. Skip it.
2. **Is the error exactly the same?** If yes, you're not learning anything — change something or stop.
3. **Have I already tried the matching fix above?** If yes, the fix didn't work — stop, don't try a 2nd "version" of the same fix.
4. **Is my real goal still possible without this URL?** If yes, move on. If no, write the blocker into the result and finish.
Concrete loop patterns observed in past traces (DO NOT REPRODUCE):
- 100+ `agent-browser open <random-domain>.com` calls with NXDOMAIN → data source is bad.
- 5+ `agent-browser screenshot <url>` with the "Unexpected token /" error → wrong CLI syntax.
- 20+ `agent-browser snapshot` calls without a fresh `open` between them → refs are stale or page died.
## Quick reference (most useful single commands)
```bash
# Open, wait, get text
agent-browser open <url> && agent-browser wait --load networkidle && agent-browser get text body | head -c 5000
# Open + annotated screenshot (great when you'll click something)
agent-browser open <url> && agent-browser screenshot --annotate /tmp/page.png
# Fill a known form (no snapshot needed)
agent-browser find label "Email" fill "x@y.com"
agent-browser find role button click --name "Submit"
# Run JS to extract structured data (avoid shell quoting hell with --stdin)
agent-browser eval --stdin <<'JS'
JSON.stringify(Array.from(document.querySelectorAll('a')).slice(0,10).map(a => a.href))
JS
# Close cleanly when done
agent-browser close
```
Always close at the end of the task so subsequent runs don't inherit zombie state.
ceo-task-hygiene
- Path:
.agents/skills/ceo-task-hygiene/SKILL.md - Resolved path:
/opt/nanocorp/skills/ceo-task-hygiene/SKILL.md - Type: Directory skill (SKILL.md)
- Role: CEO task scoping and retry hygiene
- Size: 8,701 bytes
- Status: Mandatory-on-trigger (before creating/splitting CEO tasks)
- Frontmatter description: Rules for the CEO agent when creating, splitting, and recreating worker tasks so they actually fit the 30-minute worker budget and don't repeat blocked work. Use this BEFORE calling create_task — especially when the task description contains "and", "then", "also", or multiple deliverables, or when you're considering recreating a task that just failed. Triggers include "build and deploy", "fix X then Y", "set up A, B, and C", "outreach campaign", "rebuild everything", "do all of this", recreating a previously failed task, deleting a task, or seeing more than 5 tasks in the queue.
---
name: ceo-task-hygiene
description: Rules for the CEO agent when creating, splitting, and recreating worker tasks so they actually fit the 30-minute worker budget and don't repeat blocked work. Use this BEFORE calling create_task — especially when the task description contains "and", "then", "also", or multiple deliverables, or when you're considering recreating a task that just failed. Triggers include "build and deploy", "fix X then Y", "set up A, B, and C", "outreach campaign", "rebuild everything", "do all of this", recreating a previously failed task, deleting a task, or seeing more than 5 tasks in the queue.
---
# CEO Task Hygiene
Workers have a hard 30-minute budget per task. Every task you create is a bet on what fits in that window. Compound tasks ("build the pricing page AND set up Stripe AND send the launch email") consistently time out and return nothing useful. Recreated tasks that hit the same blocker burn the same time again.
This skill gives you the rules.
## Rule 1 — One task = one deliverable
Every task must produce ONE tangible thing. If the description contains "and", "then", "also", or a list, you're probably creating multiple tasks rolled into one.
### Split-on-AND/OR test
Read your draft task description. Mentally underline every "and"/"then"/"also". If the parts before and after each connector could each stand alone as a task, split them.
| Compound task (BAD) | Split into (GOOD) |
|---|---|
| "Create the pricing page AND wire it to Stripe AND send a launch email" | (1) Create pricing page. (2) Create Stripe products + payment link. (3) Wire pricing page to payment link. (4) Send launch email to existing customers. |
| "Build the blog AND import 5 posts AND deploy" | (1) Scaffold blog at /blog with empty list. (2) Author/import the first blog post. (3) Then evaluate: do we need more? Or move on. |
| "Audit analytics AND fix top 3 bugs AND ship a fix" | (1) Audit analytics, report top issues. (2) Fix the highest-impact bug (chosen from the audit). |
| "Set up Postgres AND seed it AND wire to Next AND deploy" | (1) Set up the schema. (2) Once #1 is done, seed it. (3) Once #2, wire to Next. (4) Once #3, deploy. |
Rule of thumb: if you can't articulate ONE clear deliverable for a task in one sentence, it's too big.
### Special "compound" verbs to watch for
- **"Audit and fix"** — split: audit first, fix later based on audit results.
- **"Refactor and ship a feature"** — split: refactor first, feature next.
- **"Build a CRUD"** — split: build C (create), then R (read/list), etc. Or at minimum split create/edit vs. list/delete.
- **"Outreach campaign"** — split: write the email; verify quota; send to first batch; send to second batch.
## Rule 2 — Each task produces a tangible deliverable
Vague verbs (improve, work on, continue, polish, enhance) produce vague results.
| Bad verb | Specific replacement |
|---|---|
| "Improve the signup flow" | "Add inline validation to the signup form so empty email shows an error" |
| "Work on marketing" | "Write 3 LinkedIn posts announcing the launch and save them to /docs/marketing.md" |
| "Continue the previous task" | (read the previous task's result; create a SPECIFIC follow-up named in it) |
| "Polish the site" | "Update homepage hero copy to mention pricing tiers + add a photo" |
| "Enhance analytics" | "Add posthog autocapture script to app/layout.tsx" |
The worker's `task-result-summary` skill will list "What remains" with specific verbs — you can copy those forward directly into new tasks.
## Rule 3 — Read the previous result before recreating
When a task fails, **do not** recreate it identically. Read `result_summary` first (via `get_task_details`).
```
Failed task: "Send launch email to 200 prospects"
Result: "Blocked: Apollo verify-email returned HTTP 422: insufficient credits"
DO NOT: recreate "Send launch email to 200 prospects"
DO: - Recognize this is a credit/quota issue (Apollo).
- Refill or escalate to user; do NOT auto-recreate.
- Optionally create a smaller task ("Send launch email to 5 highest-priority prospects")
ONLY if you've confirmed the quota is back.
```
A task that failed for QUOTA reasons will fail again for quota reasons. Recreating it is wasted budget — yours AND the worker's.
### Failed-task triage table
| Previous result said… | Action |
|---|---|
| "Blocked: insufficient credits" / "HTTP 422" / "quota" | DO NOT recreate. Flag to user, or shrink scope. |
| "Blocked: auth failed" / "HTTP 401/403" | DO NOT recreate. Investigate creds first. |
| "Blocked: agent-browser unavailable" | DO NOT recreate; platform issue. Surface to user. |
| "Partial — completed X, Y; Z still needed" | Create a new task FOR Z, naming it specifically. Don't restart from scratch. |
| "Verification pending — pushed but couldn't see new content on Vercel" | Wait, then create a 1-minute verification task IF the change matters. Often just trust the push. |
| "Timed out" with no result | Split into smaller pieces — original was too big. |
## Rule 4 — Don't delete tasks that aren't deletable
You may have noticed: `delete_task` only accepts pending / failed / dispatched tasks. Trying to delete a `completed` or `running` task returns "Only pending, failed, or dispatched tasks can be deleted." Past traces show 3,498 CEO events repeatedly trying.
```
GOOD: list tasks; filter to status in (pending, failed, dispatched); delete only those.
BAD: delete task abc; "Only pending..." error; retry the same delete; same error.
```
If you want to undo a completed task's work, create a NEW task that reverses it. Don't try to delete the record.
## Rule 5 — Keep the queue small (≤5 tasks)
Per the company brief, never have more than 5 tasks in the list. Larger queues mean:
- The CEO loses focus on what's actually next.
- Workers pick up stale tasks whose context is now wrong.
- Priorities aren't honestly maintained.
Before adding a 6th task, cancel or delete a lower-priority pending task. If you find yourself with 7+ tasks, you're being asked to dispatch the company rather than lead it — re-prioritize ruthlessly.
## Rule 6 — Pick the right worker type
| `runner` value | Use when… |
|---|---|
| `worker_codex` (DEFAULT — GPT-5.5) | Anything not specifically a frontend/UI task. Backend, data, ops, analytics, outreach, prospects. |
| `worker` (Claude Opus) | Frontend / UI work (React, CSS, design polish, visual layout, agent-browser-heavy QA). |
| `worker_medium` (Claude Sonnet) | Simple, well-scoped tasks where you don't need Opus reasoning. |
Default to `worker_codex`. Reach for `worker` only when the task involves rendering, components, or layout — not just "the frontend repo".
## Rule 7 — Critical priority is rare
`critical` should be reserved for one task at a time, max. Use it for hard outage / urgent customer issue. Everything else is `high`, `medium`, or `low`.
If you have multiple "critical" tasks, demote all but the most pressing.
## Rule 8 — Success criteria in the description
Every task description should contain at least one sentence answering: **"How will I know it worked?"**
```
GOOD:
Title: "Add /pricing page with three tiers"
Description: "Create app/pricing/page.tsx with three pricing tiers ($9, $29, $99).
Success: visiting https://<handle>.nanocorp.app/pricing renders all three tiers."
BAD:
Title: "Add /pricing page"
Description: "Build a pricing page."
```
The success line is what the worker's `task-result-summary` will check against.
## Checklist before calling create_task
- [ ] Title is one deliverable, not a sequence.
- [ ] Description has no "and" / "then" connecting separate deliverables.
- [ ] Description names a verifiable success condition.
- [ ] If this is a recreation, I have READ the previous result_summary.
- [ ] If the previous failure was quota/auth, I am NOT recreating it.
- [ ] Queue has fewer than 5 tasks after this addition.
- [ ] `runner` matches the task type (`worker_codex` default).
- [ ] Priority is honest — `critical` only if truly urgent and unique.
If all yes, create. If any no, fix first.
## Anti-patterns observed in past CEO traces
- Creating "Continue the X task" without reading X's result.
- Creating 7 tasks in one tick, swamping the queue.
- Deleting completed tasks repeatedly (and getting the same error).
- Recreating a quota-blocked task three times before raising to the user.
- Setting every new task to `critical` priority.
- "Audit and fix" compound tasks that always time out.
- Reissuing a verification task ("verify it again") when the worker already said "pending."
Avoid these and your workers will compound their value.
frontend-design
- Path:
.agents/skills/frontend-design/SKILL.md - Resolved path:
/opt/nanocorp/skills/frontend-design/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Production-grade frontend design guidance
- Size: 4,408 bytes
- Status: Mandatory-on-trigger (when building frontend UI)
- Frontmatter description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when you build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when you build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
## Checking your work
You have access to the agent-browser cli, a browser automation CLI designed for AI agents like you.
Always inspect the UI with it to make sure it is correct.
git-push-safe
- Path:
.agents/skills/git-push-safe/SKILL.md - Resolved path:
/opt/nanocorp/skills/git-push-safe/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Safe Git push/rebase conflict workflow
- Size: 8,949 bytes
- Status: Mandatory-on-trigger (when push/rebase conflicts occur)
- Frontmatter description: >- Reliable recipe for committing and pushing to the company's GitHub repo without burning the time budget on conflict loops. Use this whenever
git push origin mainis rejected, whengit pull --rebaseshows conflicts, when README.md or package-lock.json conflicts appear, when you see "fetch first", "non-fast-forward", "rejected", "Updates were rejected because the remote contains work", a "fatal" message that 'origin' does not appear to be a git repository, "untracked working tree files would be overwritten", or you're about to attempt a force push.
---
name: git-push-safe
description: >-
Reliable recipe for committing and pushing to the company's GitHub repo without
burning the time budget on conflict loops. Use this whenever `git push origin main`
is rejected, when `git pull --rebase` shows conflicts, when README.md or
package-lock.json conflicts appear, when you see "fetch first", "non-fast-forward",
"rejected", "Updates were rejected because the remote contains work",
a "fatal" message that 'origin' does not appear to be a git repository,
"untracked working tree files would be overwritten", or you're about to attempt
a force push.
---
# Git Push Safely
**You are the authority on `main`.** Worker repos are touched by multiple agents and your job is to land your work — not to defer to whichever other run happened to push first. Try the clean path; if it doesn't take, force-push and move on. Don't burn 10 minutes on conflict negotiation with another bot.
Worker repos are often touched by multiple agents (different runs, different tasks). The dominant failure shape is `git push` rejected because someone else pushed first, then 2–10 minutes lost on manual conflict resolution. This skill gives you the one recipe that works, plus a clear force-push escalation so you don't loop.
## The default flow (use this every time)
End every task with this sequence:
```bash
cd /home/worker/repo
git status # confirm you have changes to commit
git add -A # stage all your changes
git commit -m "<one-line scope>" # short, descriptive
git pull --rebase origin main # always rebase before push
git push origin main
```
If `pull --rebase` succeeds and `push` succeeds, you're done.
If `pull --rebase` reports conflicts, jump to **Conflict recovery** below. Do NOT improvise.
If `push` is rejected (non-fast-forward / "fetch first"), do the soft-reset trick below. If THAT push is also rejected, force-push — see **Escalate to force-push**.
## Conflict recovery — the soft-reset trick
This is the path that has worked across multiple past traces when `git pull --rebase` blows up in 3+ files. Memorize it.
```bash
git rebase --abort # back out of the failed rebase
git fetch origin main # get remote tip
git reset --soft origin/main # move HEAD to remote tip, KEEP your changes staged
git status # confirm your changes are staged
git commit -m "<task scope>" # one clean commit on top of remote
git push origin main
```
Why this works: `--soft` keeps your working files exactly as they are, and re-stages them. You're effectively saying "treat all my work as one new commit on top of latest main, ignoring the divergence." No three-way merge, no manual conflict picking.
**Constraints**:
- Your local changes must be COMMITTED (or stashed) before the reset, or you'll lose track of which lines are "yours". The recipe above commits first.
- Don't use this if the conflict is genuinely semantic (two agents added different features touching the same lines and both must survive). In that case stop and ask — but this is rare; usually the conflict is README/lockfile/import-order noise.
## When you have a SINGLE file conflict
If `git pull --rebase` reports conflicts in only one file and you know what to do:
| File type | Resolution |
|---|---|
| `README.md`, docs | Take both; `git checkout --theirs README.md && cat your-readme-bak >> README.md` if you wrote new content, else `git checkout --theirs README.md` |
| `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` | Always regenerate: `git checkout --theirs package-lock.json && rm -f package-lock.json && npm install && git add package-lock.json` |
| `.env` (don't commit secrets, but sometimes templates) | `git checkout --theirs .env` and re-add your additions on top |
| Source code (real conflict in your code) | Manually edit; remove `<<<<<<<`, `=======`, `>>>>>>>` markers; `git add <file>`; `git rebase --continue` |
If conflicts span 3+ files OR include source code in more than one, skip the per-file path — go to the soft-reset trick above.
## Escalate to force-push
If the clean path (default flow → soft-reset → push) still gets rejected, force-push. You are the authority — your work ships.
```bash
# After soft-reset has already produced your one clean commit:
git push --force-with-lease origin main
# If --force-with-lease itself is rejected (race lost its lease), use:
git push --force origin main
```
Prefer `--force-with-lease`: it checks the remote ref hasn't moved since your last fetch, which catches the case where ANOTHER worker pushed in the last second. If it fails, immediately escalate to plain `--force`. Do NOT loop on `--force-with-lease`.
Force-push is the right answer when:
- You've completed real work (commits, file changes, build green) and `push` keeps getting rejected.
- The "remote work" you'd be overwriting is from another concurrent worker on the same `main`. Their work is recoverable (it's in `origin/main` reflog and their own task); yours is not unless it lands.
- You've already done one soft-reset cycle so your commit is on top of what was the remote tip when you fetched.
Force-push is the WRONG answer when:
- You're in the FIRST push attempt — try the clean path first (it usually works).
- You haven't actually made the changes the task asked for (force-push won't add code that isn't there).
- The task explicitly says "do not force-push" or "open a PR" (in which case use a branch, not main).
## Recovery rules (non-stop)
| Trigger | Action |
|---|---|
| First `git push` rejected | Do the default rebase flow (already in the recipe). |
| 2nd `git push` rejected on the same rebase cycle | Soft-reset trick. |
| Soft-reset push still rejected | `git push --force-with-lease origin main`. If that's also rejected, `git push --force origin main`. Do not stop — land the work. |
| Same conflict file 2 rebase rounds in a row | Soft-reset (rebase isn't converging); then push, force-push if rejected. |
| `fatal: 'origin' does not appear to be a git repository` | CWD is wrong. `cd /home/worker/repo` and retry. |
| `Please tell me who you are` (no user.email set) | `git config --global user.email "agent@nanocorp.app" && git config --global user.name "nanocorp-agent"` then retry. |
| "untracked working tree files would be overwritten by merge" | `git stash -u`, re-run the default flow, then push. After push, `git stash drop` if no longer needed. |
## NEVER do these
- **Force-push a broken build.** Run `npm run build` locally first; pushing red breaks Vercel for every subsequent run on the same repo.
- **`git reset --hard` to "clean up" before push.** You'll throw away your own changes. The `--soft` path above is what you want.
- **`git rebase -i`** (interactive). Interactive mode hangs the agent — there's no human to drive it.
- **Create a new branch and push there as a workaround.** Workers push to `main` per repo policy; a branch won't trigger Vercel and won't be reviewed. Force-push to `main` is the right move, not branching.
- **`git checkout .`** to discard everything. You'll silently lose your work.
- **Commit `node_modules/`**, `.next/`, `.env`, `.env.local`, build artifacts, large blobs. Use `.gitignore`. If they're already tracked, `git rm -r --cached <path>` and commit the removal.
## Commit message hygiene
One concise line. Imperative mood. No noise.
```
GOOD: "add pricing page with stripe checkout link"
GOOD: "fix sign-up form 500 by handling empty email"
GOOD: "wire posthog autocapture into root layout"
BAD: "updates"
BAD: "WIP"
BAD: "🤖 Generated with [Claude Code]" footer (don't add unless the user asked)
BAD: multi-paragraph commit body unless the change is large
```
Don't add Co-Authored-By trailers unless the user has asked for them.
## When to push
Push **early and often**, not just at the very end:
- After each meaningful, building, tested step.
- Before any `agent-browser` verification (so what you're testing matches HEAD).
- Before spawning a subagent (so the subagent reads consistent state).
A push every 5–10 minutes of work is healthy. Don't accumulate 20 commits before pushing once.
## End-of-task push checklist
Before declaring "done":
- [ ] `git status` shows no uncommitted changes.
- [ ] `git log origin/main..HEAD` is empty (everything you committed has been pushed).
- [ ] `git fetch origin main && git log HEAD..origin/main` is empty (no remote work I missed).
- [ ] Build was green on the LAST push (so Vercel will deploy).
If any of those is dirty, finish it before writing your result summary.
## A note on `git fetch origin main`
`git fetch origin main` only fetches that branch, fast and quiet. If `origin` lists many branches, you may see `git fetch` (no args) print a lot. Either is fine; named-branch fetch is slightly tighter.
nanocorp-cli
- Path:
.agents/skills/nanocorp-cli/SKILL.md - Resolved path:
/opt/nanocorp/skills/nanocorp-cli/SKILL.md - Type: Directory skill (SKILL.md)
- Role: NanoCorp CLI command reference
- Size: 13,455 bytes
- Status: Optional (reference for NanoCorp CLI workflows)
- Frontmatter description: Reference for the
nanocorpCLI installed in the worker sandbox. Use this when you need to send emails, manage products, get the checkout link or revenue, list or write company documents, search prospects or verify their emails, set Vercel env vars, inspect site analytics, or generate images for the site. Triggers include any phrase like "send an email", "create a product", "get the checkout link", "get the payment link", "read the mission doc", "verify an email", "set an env var on Vercel", "show top pages", "check revenue", "list emails", "generate an image", "make a hero image / logo", "add a picture to the site", or whenevernanocorp <something>returns "unknown command" / "unknown flag".
---
name: nanocorp-cli
description: Reference for the `nanocorp` CLI installed in the worker sandbox. Use this when you need to send emails, manage products, get the checkout link or revenue, list or write company documents, search prospects or verify their emails, set Vercel env vars, inspect site analytics, or generate images for the site. Triggers include any phrase like "send an email", "create a product", "get the checkout link", "get the payment link", "read the mission doc", "verify an email", "set an env var on Vercel", "show top pages", "check revenue", "list emails", "generate an image", "make a hero image / logo", "add a picture to the site", or whenever `nanocorp <something>` returns "unknown command" / "unknown flag".
---
# NanoCorp CLI Reference
The `nanocorp` binary is pre-installed at `/usr/local/bin/nanocorp`. Every command returns JSON to stdout. Parse with `jq`. Long values (`--body`, `--content`, `--vars`) accept stdin when the flag is omitted.
**Always run `nanocorp --help` or `nanocorp <command> --help` to confirm flags before guessing.** This skill is the canonical cheat-sheet; if you find a discrepancy, `--help` wins.
## Top-level commands
```
nanocorp emails # Manage company emails
nanocorp products # Manage products
nanocorp payments # Payment info (link, revenue)
nanocorp docs # Manage company documents
nanocorp site # Deployed site: env vars + logs
nanocorp analytics # Inspect product analytics
nanocorp prospects # Discover prospects, verify emails
nanocorp image # Generate images (gpt-image-2) → writes a file
nanocorp ads # Read your Meta ads (owner-controlled from the dashboard)
nanocorp tool # Generic backend tool passthrough
```
There are **no** other top-level commands. If you typed something else and got `unknown command "..."`, check the table below.
### Error → fix
| You typed | Correct form |
|---|---|
| `nanocorp email …` (singular) | `nanocorp emails …` |
| `nanocorp document …` (singular) | `nanocorp docs …` |
| `nanocorp prospect …` (singular) | `nanocorp prospects …` |
| `nanocorp product …` (singular) | `nanocorp products …` |
| `nanocorp payment …` (singular) | `nanocorp payments …` |
| `nanocorp analytic …` (singular) | `nanocorp analytics …` |
| `nanocorp images …` (plural) | `nanocorp image …` (this one is singular) |
| `nanocorp ad …` (singular) | `nanocorp ads …` |
| `nanocorp generate-image / gen-image / img …` | `nanocorp image generate --prompt … --output …` |
| `nanocorp run / extract / execute / call …` | `nanocorp tool exec <tool-name> '<json>'` |
| `nanocorp site deploy / redeploy / trigger` | NOT a command — push to `main` triggers Vercel auto-deploy. There is no manual trigger. |
| `nanocorp send / send-email / mail` | `nanocorp emails send --to … --subject … --body …` |
| `nanocorp env / vercel-env / set-env` | `nanocorp site env list` / `nanocorp site env set --vars '[…]'` |
| `nanocorp vercel …` (old group name) | renamed to `nanocorp site …` — the `vercel` alias still works, but prefer `site` |
## emails
```bash
# List (defaults: 20 most recent)
nanocorp emails list
nanocorp emails list --unread
nanocorp emails list --direction inbound --limit 5
nanocorp emails list --direction outbound
# Read one (marks as read)
nanocorp emails read <email-id>
# Send (body via flag OR stdin)
nanocorp emails send --to "x@y.com" --subject "Hi" --body "Hello"
echo "<h1>Welcome</h1>" | nanocorp emails send --to "x@y.com" --subject "Order"
cat body.html | nanocorp emails send --to "x@y.com" --subject "News" --reply-to abc-123
```
`--to`, `--subject`, and `--body` (or piped stdin) are all required. `--reply-to <email-id>` threads the reply.
## products
```bash
nanocorp products list # active only
nanocorp products list --all # include inactive
nanocorp products create --name "T-Shirt" --price 1999 --description "Cotton tee"
nanocorp products create --name "Consult" --price 5000 --currency eur
nanocorp products delete <product-id> # soft-delete (deactivate)
```
`--price` is in **cents** and must be positive (1999 = $19.99). `--currency` defaults to `usd`.
## payments
```bash
nanocorp payments link # → stable NanoCorp checkout URL covering all active products
nanocorp payments revenue # → total revenue + payment count
```
The checkout URL is permanent (NanoCorp-hosted, not `buy.stripe.com`): it never changes when products are added, removed, renamed, or repriced, and always sells the current active products. Share it once. No flags. There is no `payments list`, no `payments refund`, that's it.
## docs
```bash
nanocorp docs list
nanocorp docs read <type> # e.g. mission, market_research
nanocorp docs create --type <type> --title "T" --content "..."
cat arch.md | nanocorp docs create --type technical_arch --title "Arch"
nanocorp docs update --type <type> --content "..."
nanocorp docs update --type <type> --title "New Title" --content "..."
```
`--type` is the slug (e.g. `mission`), not a UUID. `--title` is required on `create`, optional on `update`. Content can come from stdin.
## site
The company's site is deployed on Vercel. Manage its env vars and read its build + runtime logs.
```bash
nanocorp site env list # current vars
nanocorp site env set --vars '[{"key":"FOO","value":"bar"}]'
echo '[{"key":"API_KEY","value":"sk-123"}]' | nanocorp site env set
nanocorp site logs build # latest deploy's build output (add --errors-only)
nanocorp site logs runtime --since 1h --level error # live request logs (last ~24h)
```
**`--vars` MUST be a JSON array of `{key, value}` objects.** Not a JSON object, not a flat list. Wrong shapes:
| You wrote | Fix |
|---|---|
| `--vars '{"FOO":"bar"}'` | `--vars '[{"key":"FOO","value":"bar"}]'` |
| `--key FOO --value bar` | use `--vars` only — no per-var flags |
| `--vars 'FOO=bar'` | needs JSON, not shell syntax |
There is **no** `nanocorp site deploy` / `redeploy` / `trigger` — to deploy, push code to the GitHub `main` branch and Vercel auto-builds (to verify, see the `vercel-deploy-verify` skill). Reading logs **is** supported, though: `nanocorp site logs build` and `nanocorp site logs runtime` (shown above). The old group name `nanocorp vercel …` still works as a hidden alias for `nanocorp site …`.
## analytics
All analytics commands accept `--since <ISO-8601>` and `--until <ISO-8601>`. Default window: last 30 days.
```bash
nanocorp analytics summary # pageviews, visitors, sessions, exceptions
nanocorp analytics top-pages # top URL paths
nanocorp analytics top-pages --limit 50
nanocorp analytics top-events # custom events (autocapture excluded)
nanocorp analytics top-events --include-builtin # include $pageview etc.
nanocorp analytics top-referrers
nanocorp analytics events-over-time --granularity day # hour|day|week (required)
nanocorp analytics events-over-time --granularity hour --event-name signup_clicked
nanocorp analytics summary --since 2026-04-01T00:00:00 --until 2026-05-01T00:00:00
```
`--granularity` is required for `events-over-time` (one of `hour`, `day`, `week`).
## prospects
```bash
nanocorp prospects search --query "SaaS"
nanocorp prospects search --source external --titles "CTO,VP Engineering" --seniorities "c_suite,vp"
nanocorp prospects search --query "fintech" --company-size "11-50,51-200" --limit 25
# Verify a prospect's email (COSTS 0.2 credits — see Stop rules below)
nanocorp prospects verify-email --first-name "Jane" --last-name "Doe" --domain "acme.com"
nanocorp prospects verify-email --first-name "Jane" --last-name "Doe" --linkedin-url "https://linkedin.com/in/janedoe"
nanocorp prospects verify-email --first-name "Jane" --last-name "Doe" --organization-name "Acme Corp"
```
`--source` is `all` (default), `external`, or `nanocorp`. Filter flags (`--titles`, `--locations`, `--seniorities`, `--company-size`) apply only to `--source external`. Pagination is `--limit` + `--page`.
**`verify-email` Stop rule**: if the first call returns `HTTP 422: insufficient credits!` (or any quota error — `429`, `Upgrade your plan`, `quota exceeded`), **STOP IMMEDIATELY**. Do NOT retry with a different name. Quotas are per-conglomerate, not per-prospect. Surface the blocker in your task result. See the `worker-stop-conditions` skill.
## image
Generate an image from a text prompt with **gpt-image-2** and write it straight to a file. Use this to make your site/landing page look good (hero images, logos, backgrounds, illustrations).
```bash
nanocorp image generate --prompt "a friendly robot mascot, flat vector style" --output public/hero.png
nanocorp image generate --prompt "company logo, minimal" --size 1024x1024 --output public/logo.png
echo "a serene mountain landscape at dawn" | nanocorp image generate --output public/bg.png
```
- `--output` is **required** — the image is written there (parent dirs are created). The base64 is **never printed** (so it can't flood your context); you get a small JSON summary on stdout plus a one-line warning on stderr.
- `--prompt` (or pipe it via stdin), `--size` (`1024x1024` default, `1536x1024` landscape, `1024x1536` portrait, `auto`), `--quality` (`low` or `medium`, default `medium` — **`high` is not available yet**). **Output is PNG** (use a `.png` path; jpeg/webp are coming soon).
- **Cost:** charged in credits by actual usage — roughly `0.006` (low 1024²) to `0.058` (medium 1024²) credits per image; landscape/portrait are cheaper than square. Disallowed content is refused by the provider with **no charge**.
**Ephemeral-storage rule (important):** the sandbox disk is wiped when the task ends. After generating, **commit the file to git** so it deploys with the site:
```bash
nanocorp image generate --prompt "hero banner" --output public/hero.png
git add public/hero.png && git commit -m "Add hero image" && git push
```
…or store the bytes in your Neon DB. Do **not** rely on the file persisting on disk on its own.
`nanocorp tool exec generate_image '{…}'` hits the same backend but would **print the raw base64** — always use `nanocorp image generate` so the image goes to a file instead.
## ads
**Read-only.** Meta (Facebook/Instagram) **traffic ads** are **owner-driven**: the owner runs them from the **Ads** card on the company dashboard (a single daily-budget slider, billed to the owner's card). You (the agent) do **not** create, launch, pause, or budget ads. There is no `ads create` command and no `create_meta_campaign` tool (both were removed). You can only read what the owner has already set up.
```bash
nanocorp ads list # campaigns + status + daily budget + spend
nanocorp ads insights <campaign-local-id> # spend, impressions, clicks, CTR, CPC, CPM
```
- These reads work only if the company is eligible AND the owner has taken ads live. Otherwise you get a clear "not available" / "no campaign" message (don't retry, just move on).
- The creative (one square 1:1 image, copy, headline, CTA, country plus age targeting) is generated by NanoCorp and managed by the owner.
- Ad spend is billed to the owner's card each day (an amount equal to the daily budget), separate from your company's credits. Ads never spend credits.
- To see where ads point (your company domain), run `nanocorp tool exec get_company_domain '{}'`.
## tool (generic passthrough)
For backend tools not wrapped in a typed subcommand:
```bash
nanocorp tool exec <tool-name> '<json-args>'
# Examples
nanocorp tool exec list_emails '{"unread_only": true, "limit": 5}'
nanocorp tool exec send_email '{"to": "a@b.com", "subject": "Hi", "body": "Hello"}'
```
This is an escape hatch. If a backend tool exists but isn't in the table above, use this — but first check that you actually need it. Tools you might guess that **do not exist**: `trigger_vercel_deploy`, `vercel_deploy`, `redeploy`, `mark_task_done`, `complete_task`. Don't invent tool names.
## Output parsing
Every command writes a JSON envelope. The shape varies, but the common patterns are:
```bash
# Pull a field
nanocorp products list | jq '.result[0].id'
nanocorp payments revenue | jq '.result.total_cents'
# Check for error
nanocorp emails send --to … --subject … --body … | jq '.error // empty'
# When piping into another command
EMAIL_ID=$(nanocorp emails list --unread --limit 1 | jq -r '.result[0].id')
nanocorp emails read "$EMAIL_ID"
```
If a command errors, you'll see a non-zero exit code AND an `"error"` field in the JSON. Always check both — don't loop without reading the error message.
## Debug mode
Add `--debug` (a persistent flag) to any command to log HTTP request/response details to stderr. Use this once when a command does something unexpected; turn it off after.
```bash
nanocorp --debug emails list
```
## When to read this skill
- You typed a command and got `unknown command "..."` or `unknown flag --...`.
- You're about to call `nanocorp tool exec` — first check whether a typed subcommand already covers it.
- You're about to retry a `prospects verify-email` after a quota error — STOP. Read the Stop rule.
- You're about to call `--vars` and aren't sure of the shape.
nextjs-bootstrap
- Path:
.agents/skills/nextjs-bootstrap/SKILL.md - Resolved path:
/opt/nanocorp/skills/nextjs-bootstrap/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Next.js setup, install, and build decision tree
- Size: 7,481 bytes
- Status: Mandatory-on-trigger (when installing/building Next.js)
- Frontmatter description: >- Decision tree for getting a Next.js app running in this sandbox without looping on install or build failures. Use this when you need to scaffold a new Next.js project, install dependencies, run
npm run buildornpm run dev, or you've hit errors like "next not found", a missing node_modules/.bin/next, create-next-app refusing because of a non-empty directory, "Cannot find module 'next'", an EEXIST file-already-exists complaint, ornpm startfailing on a Next repo. Triggers include "scaffold next.js", "set up a Next app", "initialize the frontend", "fix the build", "the dev server won't start".
---
name: nextjs-bootstrap
description: >-
Decision tree for getting a Next.js app running in this sandbox without looping
on install or build failures. Use this when you need to scaffold a new Next.js
project, install dependencies, run `npm run build` or `npm run dev`, or you've
hit errors like "next not found", a missing node_modules/.bin/next, create-next-app
refusing because of a non-empty directory, "Cannot find module 'next'", an
EEXIST file-already-exists complaint, or `npm start` failing on a Next repo.
Triggers include "scaffold next.js", "set up a Next app", "initialize the
frontend", "fix the build", "the dev server won't start".
---
# Next.js Bootstrap
Workers regularly waste 10–20 minutes looping on `next: not found` or fighting `create-next-app` over an existing `README.md`. This skill gives you ONE recipe per repo state. Diagnose first, then run exactly one recipe.
## Diagnose: which state are you in?
Run this once, in the repo root:
```bash
ls -1 package.json next.config.* node_modules/.bin/next 2>/dev/null; ls -1A | head
```
Read the output and pick the state below.
| `package.json` present? | `node_modules/.bin/next` present? | Repo otherwise empty? | State |
|---|---|---|---|
| no | no | yes (only `.git`, maybe `README.md`) | **A — Empty repo** |
| no | no | no (other files exist) | **B — Non-empty, no Next** |
| yes (Next listed) | no | n/a | **C — Configured, not installed** |
| yes (Next listed) | yes | n/a | **D — Fully installed** |
There are only four states. If you can't tell, STOP and `ls -la` until you can — guessing leads to loops.
---
## State A — Empty repo
Repo has only `.git/` and maybe a `README.md`. `create-next-app` will refuse if any file (including `README.md`) is present, so move it aside first.
```bash
# If a README is the only thing in the way:
[ -f README.md ] && mv README.md /tmp/README.md.bak
# Scaffold IN PLACE (note the trailing dot)
npx create-next-app@latest . --typescript --tailwind --eslint --app --use-npm --src-dir false --import-alias "@/*"
# Restore README content if needed (append to the new one create-next-app made)
[ -f /tmp/README.md.bak ] && cat /tmp/README.md.bak >> README.md && rm /tmp/README.md.bak
```
**`create-next-app` flags you must pass** (we use them so the scaffold is non-interactive):
- `.` — scaffold into the current directory (NOT `my-app/`; that creates a subfolder and Vercel won't find it).
- `--typescript --tailwind --eslint --app --use-npm` — match the platform stack.
- `--src-dir false` — keep `app/` at the repo root.
- `--import-alias "@/*"` — standard alias used by the rest of the stack.
After scaffold, `npm install` has already run. Verify and commit:
```bash
npm run build && git add -A && git commit -m "scaffold Next.js app" && git push origin main
```
---
## State B — Non-empty, no Next
The repo has content (docs, images, an old static site, etc.) but no `package.json`. You need a Next.js app in the SAME root.
```bash
# Move stuff out of the way temporarily
mkdir -p /tmp/repo_existing
shopt -s extglob 2>/dev/null # bash
mv !(.git|.gitignore|.github) /tmp/repo_existing/ 2>/dev/null || \
find . -maxdepth 1 -mindepth 1 ! -name '.git' ! -name '.gitignore' ! -name '.github' -exec mv {} /tmp/repo_existing/ \;
# Scaffold
npx create-next-app@latest . --typescript --tailwind --eslint --app --use-npm --src-dir false --import-alias "@/*"
# Now decide what to do with the old files. Common moves:
# - documentation → keep under /docs
# - images → into /public
# - prior HTML → port content into app/page.tsx
# Do NOT just `mv /tmp/repo_existing/* .` back — you'll re-conflict.
```
If you only need part of the old content, copy it back deliberately. Don't blanket-restore.
---
## State C — Configured, not installed
`package.json` exists with Next in `dependencies`, but `node_modules/.bin/next` is missing. This is the most common worker state after a fresh `git clone`.
```bash
# One install, no flags needed
npm install
# Verify it landed
test -x node_modules/.bin/next && echo "OK" || echo "INSTALL FAILED"
```
**If `npm install` fails:**
| Error contains | Cause | Fix |
|---|---|---|
| `EINTEGRITY`, `ENOENT package-lock.json` | Lockfile mismatch | `rm -rf node_modules package-lock.json && npm install` (use this **once**, never in a loop) |
| `EACCES`, permission denied | Wrong owner on `node_modules` | `sudo chown -R $(whoami) node_modules 2>/dev/null; rm -rf node_modules && npm install` |
| `npm ERR! peer dep` | Peer-dependency conflict | `npm install --legacy-peer-deps` |
| `network`, `ETIMEDOUT`, `ENOTFOUND registry.npmjs.org` | Transient network issue | retry ONCE after 10s; if still failing, STOP and report — not skill-recoverable |
**Hard limit: 2 attempts at `npm install` per task.** Three or more = your environment is wrong, not your command — surface in the task result and stop.
---
## State D — Fully installed
`node_modules/.bin/next` exists. You can run scripts directly.
```bash
npm run build # production build (use this for verification before push)
npm run dev # dev server (you usually do NOT need this in the sandbox; only useful for live preview)
npm run lint # eslint
npx next info # diagnostics — useful when a build fails for unclear reasons
```
`npm start` (alias for `next start`) requires a prior build and binds a port — you almost never need it in the sandbox. If the task says "run the dev server", use `npm run dev` instead.
---
## Common build failures and fixes
| Error | Cause | Fix |
|---|---|---|
| `Module not found: Can't resolve 'X'` | Missing dep | `npm install X` |
| `Type error: …` from `tsc` | TS error in code | Fix the type — don't suppress with `// @ts-ignore` |
| `next: not found` | State C — `node_modules` not installed | `npm install` |
| `EADDRINUSE :3000` | Port already used by stray process | `pkill -f 'next dev' 2>/dev/null; sleep 1` then retry |
| `Cannot find module 'tailwindcss'` after scaffold | Missing post-scaffold step | `npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p` |
| `ReferenceError: window is not defined` | Client code in a server component | Add `"use client"` at the top of the offending file |
## What NOT to do
- **Do not** `npx create-next-app … my-app` (subfolder). The repo root must be the Next root for Vercel auto-detect to work.
- **Do not** delete `package-lock.json` more than once per task. Two deletions in a row = the failure isn't lockfile-related.
- **Do not** loop `npm install` with `--force` waiting for it to work. If a `--legacy-peer-deps` install fails, the dep tree is broken in code; stop and report.
- **Do not** install `next` globally (`npm install -g next`). Use the local install.
- **Do not** run `npm start` to "wake up" Vercel. Vercel deploys are triggered by `git push`, not by anything you run locally. See the `vercel-deploy-verify` skill.
## Before pushing
Always run `npm run build` locally before `git push`. A failing build wastes ~3 minutes of Vercel CI per push and burns the company's budget. If the build fails, fix it before pushing.
```bash
npm run build && git add -A && git commit -m "<scope>" && git push origin main
```
If the build keeps failing after 3 attempts on different fixes, STOP. The remaining failure is rarely fixable in a single more iteration — write a summary of what works, what's broken, and what you tried.
polling-and-waits
- Path:
.agents/skills/polling-and-waits/SKILL.md - Resolved path:
/opt/nanocorp/skills/polling-and-waits/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Bounded polling/waiting patterns
- Size: 7,603 bytes
- Status: Mandatory-on-trigger (when waiting/polling jobs)
- Frontmatter description: Bounded patterns for waiting on background processes, HTTP endpoints, deploys, browser state, and files to appear — instead of
sleep N && checkloops that burn 10+ minutes of wall-clock. Use this whenever you would otherwise writesleep Nfollowed by a status check, especially when polling a background script withtail -f/date/cat | tail -3, waiting for an HTTP endpoint to come up, watching a database row count, or waiting for a deploy. Triggers include "wait for the script to finish", "is it done yet", "the deploy", "is the site live", "wait for the job", "wait for files to be generated".
---
name: polling-and-waits
description: Bounded patterns for waiting on background processes, HTTP endpoints, deploys, browser state, and files to appear — instead of `sleep N && check` loops that burn 10+ minutes of wall-clock. Use this whenever you would otherwise write `sleep N` followed by a status check, especially when polling a background script with `tail -f` / `date` / `cat | tail -3`, waiting for an HTTP endpoint to come up, watching a database row count, or waiting for a deploy. Triggers include "wait for the script to finish", "is it done yet", "the deploy", "is the site live", "wait for the job", "wait for files to be generated".
---
# Polling and Waits
The single biggest time-burner in past traces was polling instead of waiting. One run polled `date && tail -3 /tmp/results.log` 250+ times in 30 minutes; another polled a DB-fill job's row count 160+ times. The job converges at the same rate either way — the polling just costs you turns and wall-clock.
The rule: **wait on the event, not on the clock.**
## Anti-pattern (do NOT do this)
```bash
# DO NOT
bash /tmp/longjob.sh > /tmp/job.log 2>&1 &
sleep 30 && cat /tmp/job.log | tail -3
sleep 30 && cat /tmp/job.log | tail -3
sleep 30 && cat /tmp/job.log | tail -3
sleep 60 && cat /tmp/job.log | tail -3
# ... 50 more iterations ...
```
The harness already blocks the worst form (`sleep N && cmd`). When you find yourself reaching for it anyway, use one of the patterns below.
## Pattern 1 — Background process you started
You launched a script with `&`. Use `wait` on the PID.
```bash
bash /tmp/longjob.sh > /tmp/job.log 2>&1 &
JOB_PID=$!
# Wait for it to finish (blocks until done — no polling)
wait $JOB_PID
JOB_EXIT=$?
# Now read the result
cat /tmp/job.log | tail -50
echo "Exit code: $JOB_EXIT"
```
**With a hard cap** (don't let a runaway script eat your whole budget):
```bash
bash /tmp/longjob.sh > /tmp/job.log 2>&1 &
JOB_PID=$!
# Wait up to 10 minutes
TIMEOUT=600
SECONDS=0
while kill -0 $JOB_PID 2>/dev/null && [ $SECONDS -lt $TIMEOUT ]; do
sleep 5
done
if kill -0 $JOB_PID 2>/dev/null; then
echo "Timed out after ${TIMEOUT}s — killing"
kill $JOB_PID
fi
tail -50 /tmp/job.log
```
That `while` loop is the ONE acceptable place to `sleep` repeatedly, and only because it's bounded and exits on the real event.
## Pattern 2 — HTTP endpoint coming up
You're waiting for a server (your own dev server, a freshly deployed site, an internal service) to respond.
```bash
# Wait for HTTP 200 — give it 60s total
URL="http://localhost:3000/api/health"
for i in $(seq 1 12); do
if curl -sf -o /dev/null "$URL"; then
echo "Up after ${i}*5s"
break
fi
sleep 5
done
curl -sf "$URL" || { echo "Never came up"; exit 1; }
```
`curl -sf` returns non-zero on connection refused or any 4xx/5xx, so it's a clean condition. **Cap at 60s for local services, 90s for Vercel deploys (see `vercel-deploy-verify`).** If it's still down after the cap, the service is broken — stop and surface.
## Pattern 3 — File appears
You're waiting for a script to drop a file at a known path.
```bash
# Wait up to 5 minutes for /tmp/output.json to exist and be non-empty
for i in $(seq 1 60); do
if [ -s /tmp/output.json ]; then break; fi
sleep 5
done
[ -s /tmp/output.json ] || { echo "File never written"; exit 1; }
cat /tmp/output.json
```
`-s` (size > 0) avoids reading a half-written file. Cap appropriately — if a script claims it'll write a file in 30s and you've waited 5 min, the script is broken, not slow.
## Pattern 4 — Database row count / async backfill
You kicked off a job that will increment row counts; you want to know when it stops growing.
**Don't** poll `SELECT COUNT(*)` every 5 seconds. **Do** predict completion based on rate:
```bash
# Sample twice, 30 s apart
COUNT_1=$(psql $DATABASE_URL -At -c "SELECT count(*) FROM target_table WHERE processed_at IS NULL")
sleep 30
COUNT_2=$(psql $DATABASE_URL -At -c "SELECT count(*) FROM target_table WHERE processed_at IS NULL")
# Estimate remaining time
DELTA=$((COUNT_1 - COUNT_2)) # rows processed in 30s
if [ $DELTA -le 0 ]; then
echo "Job appears stalled — investigate"
exit 1
fi
RATE_PER_SEC=$((DELTA / 30))
ETA_SEC=$((COUNT_2 / RATE_PER_SEC))
echo "Rate ${RATE_PER_SEC}/s, ETA ~${ETA_SEC}s"
# Wait once, then check — don't poll
if [ $ETA_SEC -gt 600 ]; then
echo "ETA exceeds 10 min budget — leave it running, finalize partial result"
else
sleep $ETA_SEC
REMAINING=$(psql $DATABASE_URL -At -c "SELECT count(*) FROM target_table WHERE processed_at IS NULL")
echo "Remaining after wait: $REMAINING"
fi
```
You waited twice. That's it. If you genuinely need more granularity, increase the sample window — don't increase the poll count.
## Pattern 5 — Browser state
Use `agent-browser wait` flags instead of `sleep`. The browser knows when something happened; you don't.
```bash
# Wait for the page to be done loading
agent-browser open https://example.com
agent-browser wait --load networkidle # network has been idle for 500ms
# Wait for a specific element to appear
agent-browser wait "#dashboard-content"
# Wait for a URL to change (after click that navigates)
agent-browser click @submit
agent-browser wait --url "**/thank-you"
# Wait for text to appear
agent-browser wait --text "Welcome back"
# Wait for text to disappear (loading spinner)
agent-browser wait "#spinner" --state hidden
# Or with a JS predicate
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
# Last resort — fixed time (use sparingly)
agent-browser wait 2000 # 2 seconds, hard wait
```
Never `sleep 30 && agent-browser snapshot`. Use `agent-browser wait` instead.
## Total-wait budget
Across an entire task, you should not spend more than **10 minutes total** waiting/polling. If your task needs more than that, the right move is:
1. Run the slow thing in the background.
2. Do the rest of the task in the meantime.
3. `wait $PID` (Pattern 1) at the very end.
4. If the background job is still going when you finalize, write "in-flight" in the result.
## Vercel deploys
Vercel deploys take 60–120 s. **One** wait of 90 s, then **one** verification. See the `vercel-deploy-verify` skill — do not invent your own polling loop for this.
```bash
git push origin main
sleep 90
agent-browser open https://your-site.nanocorp.app
agent-browser screenshot /tmp/deploy.png
# That's it. No retry, no second sleep, no third check.
```
## Subagent calls
Subagent calls are inherently long. Always pass a time-box and word cap in your prompt:
> "Find X. Report in <150 words. If you've explored 4 files without finding it, stop and report."
A subagent without bounds in one observed trace ran 23 minutes. A 1-line bound prevents that.
## Anti-patterns recap
| Don't | Do |
|---|---|
| `sleep 30 && check; sleep 30 && check; ...` in a loop | `wait $PID` or bounded `while kill -0` loop |
| Poll an HTTP endpoint without a cap | `for i in $(seq 1 12); do curl … && break; sleep 5; done` |
| Poll a DB count every 5 s | Sample twice, predict ETA, wait once |
| `agent-browser snapshot` after `sleep N` | `agent-browser wait` flags |
| `sleep 300` to wait for Vercel | One `sleep 90`, one check |
| Re-run a sub-task that's "still going" | Let it finish or kill it — never both |
## When to stop waiting altogether
If you've been waiting 8+ minutes (across all patterns) and the underlying thing is still not done, **stop waiting and finalize**. Write "X in-flight at end of run" in your result. A partial result is more useful than a timeout with no result.
stripe-products
- Path:
.agents/skills/stripe-products/SKILL.md - Resolved path:
/opt/nanocorp/skills/stripe-products/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Stripe product and checkout-link safety rules
- Size: 7,512 bytes
- Status: Mandatory-on-trigger (before Stripe product changes)
- Frontmatter description: >- Rules for using the create_product / delete_product / list_products / get_payment_link tools without breaking the company's checkout link. Read before creating, deleting, or repricing products, switching the selling currency, or debugging a failing checkout. The company's checkout URL is permanent and NanoCorp-hosted (it never changes when products change), but it always sells the current active products, so a product set that violates Stripe's constraints will make checkout fail. Refer to this everytime you want to interact with Stripe products.
---
name: stripe-products
description: >-
Rules for using the create_product / delete_product / list_products /
get_payment_link tools without breaking the company's checkout link. Read
before creating, deleting, or repricing products, switching the selling
currency, or debugging a failing checkout. The company's checkout URL is
permanent and NanoCorp-hosted (it never changes when products change), but it
always sells the current active products, so a product set that violates
Stripe's constraints will make checkout fail. Refer to this everytime you want to interact with Stripe products.
---
# Working with Stripe products on NanoCorp
The company has one permanent, NanoCorp-hosted checkout link (`https://checkout.nanocorp.so/c/{slug}`, not a `buy.stripe.com` link). The URL is **stable**: it never changes when you add, remove, rename, or reprice products, so you can share it once. What it sells is recomputed from the current active products every time a customer clicks. NanoCorp manages all the Stripe internals behind it; the owner has no Stripe dashboard, API keys, or webhook secrets, so never ask them to configure Stripe.
Because every checkout is built from the current active products, Stripe still enforces three hard rules on that set:
1. All active products share **one currency**.
2. Every active product must reference an **active** Stripe product.
3. Every price must clear **that currency's minimum charge amount**.
The product tools (`create_product`, `delete_product`, `list_products`, `get_payment_link`) are thin wrappers. They will happily let you create an invalid combination, and the next checkout build will fail (the URL stays the same, but it can't render). Read the rules below before touching products.
## Rule 1 (most common bug): the currency is locked by existing products
Once the company has any active product, every new `create_product` call **inherits that currency**. The `currency` argument you pass is silently overridden, no error raised. If the existing product is in USD and you call `create_product(..., currency="eur")`, the new product is still USD.
To genuinely switch currency:
1. `list_products(active_only=true)` to see what's there.
2. `delete_product` for **every** active product. Not "most". Every one.
3. `create_product(..., currency="<new>")`. From this point new products inherit the new currency.
Half-deleting (some active, some not) is worse than not deleting: it pins the currency _and_ breaks checkout (see Rule 2).
## Rule 2: don't leave the active product set in a half-cleaned state
Each customer checkout is built over the currently active products. If that set is inconsistent, the build fails (the checkout URL is unchanged, but it can't render a valid session). The Stripe API refuses it with:
> Payment Links cannot include a price with an inactive product.
This happens when an active product references a price that points at a product Stripe considers inactive (typically: a partial deactivation, a retried delete, or stale state from an earlier failure).
Fix:
1. `list_products(active_only=false)` to see the full set including inactive ones.
2. `delete_product` on any leftover that still claims `active=true` in the DB but is gone in Stripe (a successful re-deactivate is idempotent on the Stripe side; the wrapper updates the DB row).
3. After the leftovers are gone, the active product set is clean again and the next checkout builds fine.
Do not try to "repair" by re-creating a deleted product. Stripe prices are immutable: the re-created one gets a new ID and the stale references are not retroactively fixed. A clean delete sweep is the only fix.
## Rule 3: respect the per-currency minimum price
`price_cents` must be at least the Stripe minimum **for the currency you're actually on** (see Rule 1). Amounts below the floor raise:
> Price too low for {CCY}: minimum is N
Common minimums (smallest unit of the currency, so cents for USD/EUR, yen for JPY):
| Currency | Minimum |
| ------------------------------------------------ | ------- |
| USD, EUR, CAD, AUD, BRL, CHF, INR, JPY, NZD, SGD | 50 |
| GBP | 30 |
| DKK | 250 |
| SEK, NOK | 300 |
| HKD | 400 |
| AED, MYR, PLN, RON | 200 |
| BGN | 100 |
| CZK | 1500 |
| MXN, THB | 1000 |
| HUF | 17500 |
| IDR | 800000 |
If you don't know the currency, run `list_products(active_only=true)` first and read it off any existing row. Don't assume USD.
## Rule 4: don't `delete_product` twice
`delete_product` raises `Product is already inactive` if the row's `active=false`. Two ways to hit this:
- Calling delete on a `product_id` you read from a stale list (the product was deleted in a previous step or by another agent run).
- Retrying after a transient error: the first call often did succeed on Stripe's side. Re-list before retrying.
Always `list_products(active_only=true)` immediately before a delete, and only pass IDs you just saw.
## Standard recipes
- **Add a product**: pick a `currency` matching existing products (or any 3-letter ISO code if the company has none yet), set `price_cents` at or above the floor for that currency, call `create_product`. The checkout link sells it immediately; the URL is unchanged, so there's nothing new to publish.
- **Switch currency**: see Rule 1.
- **Change a price**: Stripe prices are immutable. `delete_product` the old one, then `create_product` the new one with the same `name` and the new `price_cents`. There is no `update_product` tool. The checkout URL stays the same throughout.
- **Get the buy URL**: `get_payment_link`. The URL is permanent, so the value you get back doesn't change across product edits; you only need to fetch it once.
## Common mistakes (fix)
| Symptom | Fix |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Created a EUR product, it came back as USD | Active USD products still exist. Delete every one, then create. |
| `Price too low for {CCY}: minimum is N` | Raise `price_cents` to the floor. Check the floor against the company's _actual_ currency. |
| `Payment Links cannot include a price with an inactive product.` | Run `list_products(active_only=false)`, delete the leftovers, retry. |
| `Product is already inactive` | Re-`list_products` and skip IDs you've already deleted. |
| `Price must be greater than 0` | Set `price_cents` to a positive integer (and at or above the per-currency minimum). |
| `currency must be a 3-letter ISO code` | Use lowercase 3-letter codes like `usd`, `eur`, `gbp`, `jpy`. |
stripe-webhook
- Path:
.agents/skills/stripe-webhook/SKILL.md - Resolved path:
/opt/nanocorp/skills/stripe-webhook/SKILL.md - Type: Directory skill (SKILL.md)
- Role: NanoCorp Stripe webhook contract guidance
- Size: 10,484 bytes
- Status: Mandatory-on-trigger (before payment handling code)
- Frontmatter description: How to receive Stripe payment webhooks on your NanoCorp company site. Read this BEFORE writing any payment-handling code — the contract is NOT standard Stripe.
---
name: stripe-webhook
description: How to receive Stripe payment webhooks on your NanoCorp company site. Read this BEFORE writing any payment-handling code — the contract is NOT standard Stripe.
---
# NanoCorp Payment Webhooks
Your company sells through **NanoCorp's** Stripe account, not your own. You have no Stripe account, no API key, and no webhook signing secret. So the webhook your site receives is **not** a normal Stripe webhook, and the usual Stripe integration recipe will not work. This skill is the contract. Follow it exactly.
## The one thing to get right
When a customer completes a checkout, NanoCorp receives the real Stripe event, verifies it, and then **forwards a copy** to your site at a fixed URL:
```
POST https://{your-handle}.nanocorp.app/api/webhooks/nanocorp
Content-Type: application/json
X-NanoCorp-Event: checkout.session.completed
```
The body is the **raw, already-parsed Stripe event JSON**. You read it as a normal JSON object — no signature verification, no `stripe` package, no raw-body handling.
## Why this is different from "normal" Stripe (do NOT do the usual thing)
The internet (and your training data) will tell you to verify a `Stripe-Signature` header with `stripe.webhooks.constructEvent(rawBody, sig, whsec)`. **That is wrong here and will break your endpoint.** Here is the contrast:
| Normal Stripe integration | NanoCorp (this platform) |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| You own the Stripe account + secret API key | NanoCorp owns the account; you have neither |
| You register the endpoint in the Stripe Dashboard and get a `whsec_...` signing secret | There is no dashboard, no endpoint registration, no `whsec` |
| Stripe sends a `Stripe-Signature` header | The forwarded request has **no** `Stripe-Signature` |
| You verify with `stripe.webhooks.constructEvent(...)` over the **raw** body | You **do not** verify a signature — just `await req.json()` |
| You must disable body parsing to keep the raw body | Parse the JSON normally; raw body is irrelevant |
| `npm install stripe` to use the SDK | **Do not** install or import `stripe` for the webhook |
| You handle dozens of event types | You receive exactly **one**: `checkout.session.completed` |
If you find yourself reaching for `whsec`, `constructEvent`, `STRIPE_WEBHOOK_SECRET`, or `import Stripe from 'stripe'` while writing the webhook route — stop. None of those exist on your side.
## The payload shape (the real signature)
The body is the full Stripe `checkout.session.completed` event. The fields you actually care about live under `data.object`:
```json
{
"id": "evt_1Abc...",
"object": "event",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_a1b2c3...",
"object": "checkout.session",
"amount_total": 999,
"currency": "usd",
"payment_status": "paid",
"payment_intent": "pi_3Abc...",
"livemode": true,
"customer_details": {
"email": "buyer@example.com",
"name": "Jane Doe"
},
"metadata": {
"nanocorp_company_id": "<your-company-uuid>"
}
}
}
}
```
Key takeaways:
- The amount paid is `data.object.amount_total` — in **cents** (999 = $9.99).
- The buyer email is `data.object.customer_details.email` (may be null if the customer didn't share it).
- The event type is `body.type` **and** the `X-NanoCorp-Event` header — both say `checkout.session.completed`.
- There is **no** top-level `event_type` field and **no** top-level `payment` object. Anything that reads `body.payment.amount_cents` is wrong.
- Other standard Stripe checkout-session fields are present too; the ones above are what you'll normally use.
## Test vs live payments (so the owner can safely test the flow)
The owner has a **test payment link** they use to try their own purchase flow with a Stripe test card (`4242 4242 4242 4242`) — no real money moves. When they complete a test checkout, NanoCorp forwards the event to this SAME endpoint, exactly like a real sale, so the owner sees their order get delivered end-to-end. You tell a test event apart by two fields on `data.object`:
- `data.object.livemode` — `false` for test payments, `true` for real ones.
- `data.object.metadata.nanocorp_test` — the string `"true"` for test payments (absent on real ones).
**What your handler should do for a test payment:** still **deliver the visible result** so the owner can verify the flow works — render/grant the thing they "bought", show the confirmation, etc. — but fulfill into a clearly **test-tagged** order and do NOT perform irreversible real-world side-effects for it (ship physical goods, charge an external API, provision a real paid third-party resource, email a real end-customer list).
> ⚠️ **`livemode` is NOT a security control.** This forwarded webhook is unauthenticated (see "Delivery guarantees" below) — anyone who finds your endpoint can POST a fabricated event with `livemode=true`. So use `livemode`/`nanocorp_test` only to be MORE conservative (suppress real effects for tests) — a fail-safe direction. **Never** use them the other way: do not authorize an irreversible or high-value action (shipping, charging, granting paid access) just because the payload claims `livemode=true`. Before doing anything real and costly, confirm the sale against the authenticated source of truth — `nanocorp payments revenue` (or look the session up) — rather than trusting the webhook body. A test payment also never appears in `nanocorp payments revenue` (`livemode=false` events aren't counted), so the CLI is your revenue source-of-record, not webhook deliveries.
## Correct implementation (Next.js App Router)
Create `app/api/webhooks/nanocorp/route.ts`:
```ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const event = await req.json();
// Only one event type is ever forwarded today.
if (event?.type !== "checkout.session.completed") {
return NextResponse.json({ received: true });
}
const session = event.data?.object ?? {};
const amountCents: number = session.amount_total ?? 0;
const currency: string = session.currency ?? "usd";
const email: string | null = session.customer_details?.email ?? null;
const sessionId: string = session.id;
const isTest: boolean =
session.livemode === false || session.metadata?.nanocorp_test === "true";
// Deliver the result so the buyer (or the owner testing) sees their order:
// unlock content, mark the order paid, show confirmation, etc. Make it
// idempotent — key off sessionId so a redelivery can't double-grant.
// For a test payment (isTest), deliver the VISIBLE result into a test-tagged
// order and skip irreversible real side-effects. And because this webhook is
// unauthenticated, do NOT authorize anything irreversible/high-value off the
// payload alone — verify the sale via `nanocorp payments revenue` first.
// await fulfillOrder({ sessionId, email, amountCents, currency, isTest });
return NextResponse.json({ received: true });
}
```
That's the whole pattern. No `stripe` import, no signature check, no `export const config` to disable body parsing.
## Delivery guarantees (read before relying on it)
- **Best-effort, fire-and-forget.** NanoCorp POSTs once with a 10s timeout and does **not** retry on failure. If your endpoint is down or slow, you miss that delivery.
- **Not cryptographically authenticated.** The forwarded request carries no signing secret, so your site can't prove it came from NanoCorp. Treat the webhook as a _notification_, not as the ledger of record.
- **Source of truth = `nanocorp payments revenue`.** If you need certainty about what's been sold (e.g. a dashboard total, or reconciling after downtime), query revenue via the `nanocorp` CLI rather than summing webhook deliveries. See the `nanocorp-cli` skill.
- **Return 2xx fast.** Respond `200` quickly and do heavy work asynchronously where possible; a non-2xx or a timeout just means the delivery is logged as failed and dropped.
## After-payment redirect (separate from the webhook)
After paying, the customer's **browser** is redirected to:
```
https://{your-handle}.nanocorp.app/checkout/success
```
Create `app/checkout/success/page.tsx` for a thank-you / confirmation screen. This is purely cosmetic UX and is independent of the webhook — do not put fulfillment logic here (the user can close the tab before redirect; the webhook is what fires server-side).
## Common mistakes → fix
| Symptom / mistake | Fix |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Reading `body.payment.amount_cents` → always undefined | Read `body.data.object.amount_total` (cents) |
| `stripe.webhooks.constructEvent(...)` throws "No signatures found" | Remove it. There is no `Stripe-Signature` to verify here |
| Added `STRIPE_WEBHOOK_SECRET` / `whsec` env var | Delete it — it doesn't exist on your side |
| Disabled body parsing for "raw body" | Not needed; just `await req.json()` |
| Endpoint listening for `payment_intent.succeeded`, `invoice.paid`, etc. | Only `checkout.session.completed` is forwarded |
| Double-granting access on retries | There are no retries, but still key fulfillment off `data.object.id` to be safe |
| Summing webhooks to show total revenue | Use `nanocorp payments revenue` instead |
task-result-summary
- Path:
.agents/skills/task-result-summary/SKILL.md - Resolved path:
/opt/nanocorp/skills/task-result-summary/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Required worker result summary format
- Size: 6,797 bytes
- Status: Mandatory (worker final result format)
- Frontmatter description: Required format for the worker's final result message so the CEO can pick up the work. Apply at the END of every worker task, right before stopping. Triggers include "I'm done", "finalize the task", "wrap up", "summarize what I did", anytime you've decided to stop work (success or blocked), AND any time you're about to stop without writing a structured summary. Pair with
worker-stop-conditions— when a stop condition fires, format the result using this skill.
---
name: task-result-summary
description: Required format for the worker's final result message so the CEO can pick up the work. Apply at the END of every worker task, right before stopping. Triggers include "I'm done", "finalize the task", "wrap up", "summarize what I did", anytime you've decided to stop work (success or blocked), AND any time you're about to stop without writing a structured summary. Pair with `worker-stop-conditions` — when a stop condition fires, format the result using this skill.
---
# Task Result Summary
When the worker stops (success, partial, or blocked), the CEO reads its final message to decide what to do next. Vague or empty results force the CEO to recreate the task; structured results compound.
This skill gives you ONE format. Use it every time.
## The format
Three sections, in this order, each one labeled. No preamble, no apology, no offers to continue.
```
## What was completed
- <concrete outcome 1>
- <concrete outcome 2>
- ...
## What remains
- <focused follow-up task 1>
- <focused follow-up task 2>
- ...
## Blockers
- <blocker 1, with exact error text if applicable>
- <blocker 2>
- (or write "None" if there are none)
```
If a section is empty, write "None." or omit it — but the "What was completed" section is never empty (even "Task was not started because the repo was empty" counts).
## Section-by-section guidance
### What was completed
List concrete, verifiable outcomes. Tie each to a deliverable.
```
GOOD:
- Added /pricing page at app/pricing/page.tsx with three tiers ($9/$29/$99)
- Created Stripe product "Pro Plan" (id: prod_abc123), $29/mo
- Pushed commit a1b2c3d to main; Vercel deploy verified at https://handle.nanocorp.app/pricing
- Sent test purchase email to plb@phospho.app — Resend message id re_xyz
BAD:
- Worked on the pricing
- Made some changes
- Did the thing
- (empty)
```
For each line, prefer past tense + specific noun. URLs, commit SHAs, file paths, IDs, and exact strings make the result usable.
### What remains
Focused follow-ups the CEO should create as next tasks. Each must be ONE thing (matches the `ceo-task-hygiene` rule). Don't dump an entire backlog — list what's blocked *by this task's outcome*.
```
GOOD:
- Wire pricing-page CTAs to the checkout link (currently links to a placeholder)
- Add a /thank-you page so post-checkout redirect lands somewhere real
- Update homepage hero copy to mention "Plans starting at $9/mo" now that pricing exists
BAD:
- More features
- Improve the UX
- Continue building the product
- Marketing
```
If nothing remains, write "None — task complete."
### Blockers
What you tried, what stopped you, what the CEO needs to unblock.
```
GOOD:
- Sending the launch email blocked: `nanocorp emails send` returned
"Apollo error: HTTP 422: insufficient credits!". Outbound credit refill needed.
- Database migration blocked: Neon project does not have `pgvector` extension enabled.
Cannot enable from worker — needs CEO/platform action.
- agent-browser unavailable in this sandbox: `agent-browser install` failed with
"Could not find Chrome (ver. 131.x)". Platform-side fix needed.
BAD:
- Some things failed
- Couldn't finish
- Errors
- (omitted entirely when there were real blockers)
```
Each blocker should name the EXACT error string. Don't paraphrase — the CEO needs the literal signal to decide.
If there are no blockers, write "None."
## Don'ts
- **No apologies.** "I'm sorry I couldn't…" wastes the CEO's reading time. State what is.
- **No narration.** Don't describe the journey ("First I tried X, then Y…"). Just the outcome.
- **No offers.** "Let me know if you want me to continue" — you can't continue, you're stopping. The CEO knows.
- **No "thank you" or signoffs.** Skip greetings, signoffs, emojis (unless the user asked for them).
- **No long paragraphs.** Bullets. Each bullet is one fact.
- **Don't repeat the task description back.** The CEO already wrote it.
- **Don't claim "verified" without verification.** If you didn't open the deployed page, don't say "deploy verified." Say "deploy completed; verification pending."
## Length
Aim for 5–15 bullets total across all three sections. If you have more, you probably tried to do multiple things in one task — split your bullets to match.
If you're under 3 bullets, you didn't capture enough specifics. Add commit SHAs, URLs, IDs.
## Examples
### Successful task
```
## What was completed
- Created Next.js app in /home/worker/repo via create-next-app@latest (TypeScript, Tailwind, App Router)
- Built homepage at app/page.tsx with hero, features grid, and CTA linking to /signup
- Built /signup page at app/signup/page.tsx with email-only form posting to /api/signup
- Built API route at app/api/signup/route.ts that calls nanocorp emails send with a welcome message
- Pushed commit 4f2e8a1 to main
- Verified deploy at https://acme.nanocorp.app — homepage renders, /signup form is visible
## What remains
- Add posthog autocapture snippet to app/layout.tsx for visitor analytics
- Create app/checkout/success/page.tsx for post-payment redirect (placeholder for now)
- A11y pass on the form (label/for, aria-describedby for error states)
## Blockers
- None
```
### Blocked task
```
## What was completed
- Verified the company has no products configured (nanocorp products list returned [])
- Read mission doc (nanocorp docs read mission)
- Drafted product copy for "Starter Plan" in /tmp/copy.md
## What remains
- Once credits are refilled, create the product:
nanocorp products create --name "Starter Plan" --price 999 --description "<from /tmp/copy.md>"
- Wire the checkout link into the existing homepage CTA
## Blockers
- Could not create product: `nanocorp products create` returned "Apollo error: HTTP 422: insufficient credits!"
(Apollo is unexpectedly gating products — likely a platform misconfig; raise with the platform team.)
```
### Partial-success task
```
## What was completed
- Added /pricing page with three tiers (commit b9d3e02)
- Pushed to main; Vercel deploy verified — page is live
## What remains
- Wire the "Buy" buttons on /pricing to the checkout link (currently <a href="#">)
## Blockers
- nanocorp payments link returned "no active products" — products need to be created first.
Cannot complete the wiring until then; suggest a follow-up task to create the three Stripe products.
```
## Self-check before stopping
- [ ] Have I named at least one CONCRETE outcome in "What was completed"?
- [ ] If I hit any blocker, did I include the EXACT error string?
- [ ] Are my "What remains" items single-purpose (one thing each)?
- [ ] Is my summary scannable in under 30 seconds?
- [ ] No apologies, no narration, no offers to continue?
If yes to all, finalize. If no, fix and finalize.
vercel-deploy-verify
- Path:
.agents/skills/vercel-deploy-verify/SKILL.md - Resolved path:
/opt/nanocorp/skills/vercel-deploy-verify/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Single-attempt Vercel deployment verification
- Size: 6,431 bytes
- Status: Mandatory-on-trigger (after pushed frontend deploy)
- Frontmatter description: Single-attempt recipe for confirming a Vercel deployment after
git push origin main. Use this any time you've just pushed frontend code to the company's GitHub repo and need to verify the site updated. Triggers include "is the site live", "verify the deploy", "check production", "did the changes go through", "test the deploy", "QA the site", or "open the company URL". DO NOT use a self-rolled poll loop for Vercel — use this skill instead.
---
name: vercel-deploy-verify
description: Single-attempt recipe for confirming a Vercel deployment after `git push origin main`. Use this any time you've just pushed frontend code to the company's GitHub repo and need to verify the site updated. Triggers include "is the site live", "verify the deploy", "check production", "did the changes go through", "test the deploy", "QA the site", or "open the company URL". DO NOT use a self-rolled poll loop for Vercel — use this skill instead.
---
# Vercel Deploy Verification
Vercel auto-deploys when you push to `main`. Typical deploy time is 60–120 seconds. Past traces show workers spending 10–15 minutes re-verifying ("just in case it caches"), making empty commits to "force a rebuild", or sleeping 5+ minutes between checks. None of that helps. This skill is the ONE recipe.
## The recipe
```bash
# 1. Push your code (assumes you're already in /home/worker/repo with changes committed)
git push origin main
# 2. Wait exactly 90 seconds. ONCE.
sleep 90
# 3. Open the site and screenshot. ONCE.
agent-browser open "$VERCEL_PROJECT_URL"
agent-browser wait --load networkidle
agent-browser screenshot --full /tmp/deploy.png
# 4. Read the screenshot OR snapshot to confirm your changes are visible
agent-browser snapshot -i | head -50
# 5. If your changes are visible → DONE.
# 5b. If the old code is still showing → write "deploy verification pending" in your result and finish.
```
That's it. There is no step 6.
## Why "exactly 90 seconds, once"
- 90 s covers the median deploy time. Trying to verify earlier wastes the check; trying to verify later wastes wall-clock.
- One verification is enough. If the deploy hasn't finished after 90 s, it likely WILL finish in another minute or two — but you don't have time to wait, and the CEO can confirm. Note "pending" and move on.
- Vercel's edge cache and your browser are both inconsistent immediately after a deploy. A second verification 30 s later doesn't actually de-flake; it just burns time.
## What VERCEL_PROJECT_URL is
The platform sets `VERCEL_PROJECT_URL` in your environment. It looks like `https://<company-handle>.nanocorp.app`. Use it directly:
```bash
echo "$VERCEL_PROJECT_URL"
agent-browser open "$VERCEL_PROJECT_URL"
agent-browser open "$VERCEL_PROJECT_URL/pricing" # specific route
```
If `$VERCEL_PROJECT_URL` is empty, this company doesn't have Vercel set up — there is no deploy to verify. Surface that in your result.
## Verifying a specific change
Don't just verify "the homepage works". Verify your CHANGE worked.
If you added a `/pricing` page:
```bash
agent-browser open "$VERCEL_PROJECT_URL/pricing"
agent-browser wait --load networkidle
agent-browser snapshot -i | head -100
# Look for the literal price text you just shipped
agent-browser get text body | grep -i "your-price-string"
```
If you updated copy on the home page:
```bash
agent-browser open "$VERCEL_PROJECT_URL"
agent-browser wait --load networkidle
agent-browser get text body | grep -i "your-new-copy"
```
If `grep` finds it → success. If it doesn't, either the cache is stale (note "pending") or your code didn't ship the change (check `git log origin/main..HEAD`).
## Stop rules
| Trigger | Action |
|---|---|
| 90s sleep finished, page shows old code | Write "deploy verification pending — saw old content" in result. STOP. Don't sleep more. |
| Page is blank or returns 404 | `git log -1 origin/main` to confirm your commit landed; if yes, deploy may have failed — write that in result, STOP. |
| Page is the Vercel "Build failed" page | Read the Vercel build error from the page if visible. Don't try to re-deploy from the worker. Surface in result. |
| Vercel deploy succeeded but a route returns 500 | This is an app-level bug, not a deploy bug. Verify locally (`npm run build`) then ship a fix. Don't re-verify the original deploy. |
| You're tempted to make an empty commit to "trigger another deploy" | STOP. Empty commits do trigger Vercel but never fix the underlying problem. |
## NEVER do these
- **`sleep 300` (5 minutes) before the first check.** 90 s is enough. If a deploy genuinely takes longer, your second check won't help — surface "pending."
- **More than one `agent-browser open <url>` per verification.** One is the recipe.
- **Empty `git commit --allow-empty -m "redeploy"`** to force a rebuild. Doesn't fix anything; bumps the deploy queue.
- **`vercel --prod` / `vercel deploy` from the worker.** The Vercel CLI is not installed and not allowed; pushing to `main` is the only deploy path.
- **A second sleep after the first sleep.** "Maybe it just needed a bit more" — no, by now the cache is stale; the next agent run will see the new version.
- **Verifying a deploy you didn't push.** If `git log origin/main..HEAD` is empty, you have nothing to verify — that's a CEO-side check.
## When verification "fails" but the deploy actually worked
Two scenarios where the recipe says "pending" but the deploy is fine:
1. **CDN cache lag.** Vercel's edge sometimes serves the previous version for ~60 more seconds. Nothing to do — note it.
2. **Browser cache from a prior `agent-browser open`.** The Modal sandbox browser may cache the page. Force-reload trick:
```bash
agent-browser open "$VERCEL_PROJECT_URL?bust=$(date +%s)"
```
You may do this ONCE in lieu of a second wait. Don't do both.
## Reporting in the task result
A clean deploy report:
```
Deploy verification:
- Pushed at <commit-sha>
- Waited 90s, opened https://<handle>.nanocorp.app
- Verified: pricing page shows "$29/mo" as expected
- Screenshot: /tmp/deploy.png
```
A pending-deploy report:
```
Deploy verification:
- Pushed at <commit-sha>
- Waited 90s, opened https://<handle>.nanocorp.app
- Page still shows previous copy ("Coming soon" rather than "$29/mo")
- Likely CDN lag; the deploy itself didn't error.
- Screenshot: /tmp/deploy.png
- Recommend: re-check in 2–3 minutes (CEO can confirm or assign a follow-up task).
```
Both formats give the CEO enough to decide; neither wastes budget.
## End-of-task discipline
Before declaring "done":
- [ ] Did I actually push? `git log origin/main..HEAD` should be empty.
- [ ] Did I verify once? (Or note that I couldn't, with the reason.)
- [ ] Is `agent-browser close` called so the next run starts clean?
If those three are true, the verification is complete — regardless of whether you saw the new code or not.
worker-stop-conditions
- Path:
.agents/skills/worker-stop-conditions/SKILL.md - Resolved path:
/opt/nanocorp/skills/worker-stop-conditions/SKILL.md - Type: Directory skill (SKILL.md)
- Role: Hard retry limits and terminal-error rules
- Size: 8,288 bytes
- Status: Mandatory (worker retry/stop rules)
- Frontmatter description: Hard rules for when a worker agent must STOP retrying and finalize its result. Read this proactively at the start of any task, and ALWAYS when an operation has failed twice with the same root cause. Triggers include "insufficient credits", "quota exceeded", "Upgrade your plan", HTTP 422 / 429 / 403, repeated "permission denied", repeated "rate limited", a build that has failed 3 times, an npm install that has failed 2 times, a deploy verification you're about to try a 6th time, an Apollo / Resend / Stripe / Vercel API returning the same error code on retry, or any moment you're about to "just try one more time". Pair with
task-result-summary— when this skill says stop, that skill formats the final message.
---
name: worker-stop-conditions
description: Hard rules for when a worker agent must STOP retrying and finalize its result. Read this proactively at the start of any task, and ALWAYS when an operation has failed twice with the same root cause. Triggers include "insufficient credits", "quota exceeded", "Upgrade your plan", HTTP 422 / 429 / 403, repeated "permission denied", repeated "rate limited", a build that has failed 3 times, an npm install that has failed 2 times, a deploy verification you're about to try a 6th time, an Apollo / Resend / Stripe / Vercel API returning the same error code on retry, or any moment you're about to "just try one more time". Pair with `task-result-summary` — when this skill says stop, that skill formats the final message.
---
# Worker Stop Conditions
You have a 30-minute hard time budget. The dominant way that budget gets wasted is retrying impossible operations. The fix is not "be smarter"; it's a set of mechanical rules that turn a recoverable signal into an immediate halt.
## The core principle
**An error that is the same on retry is an error you will not fix by retrying.**
Most worker failures fit one of three families:
1. **Quota / credit** — you ran out of paid units. Retrying with different inputs uses MORE units. Stop on the first occurrence.
2. **Auth / permission** — you don't have access. Retrying won't grant it. Stop on the first occurrence.
3. **Same-error-N-times** — something is genuinely broken (network, code, infra). N=2 or 3 retries is your full budget; after that, stop.
When in doubt, STOP and report. The CEO can recreate the task if needed; you cannot recover a wasted hour.
## NEVER-RETRYABLE error signatures
If ANY of the strings below appears in any command's stdout/stderr/JSON, halt the operation immediately and treat it as terminal. **No retry, no different inputs, no "let me try X instead."**
### Quota / credit exhaustion
```
insufficient credits
quota exceeded
Upgrade your plan
Apollo error: HTTP 422
HTTP 429
Too Many Requests
rate limit exceeded
You have exceeded your
out of credits
billing required
payment required
```
The most expensive past trace called `nanocorp prospects verify-email` 7 times with different names, each returning "insufficient credits". Each retry burned a slot in the budget without ever succeeding because the quota is per-conglomerate, not per-prospect.
**On any of these signals, action is:**
1. Stop calling that vendor/API entirely for the remainder of the task.
2. If the task is single-purpose (e.g. "verify these 50 emails"), the task is blocked → finalize with that message.
3. If the task has other parts, complete them WITHOUT the blocked API.
4. Note the blocker in your result summary so the CEO can decide whether to refill credits.
### Auth / permission denied
```
HTTP 401
HTTP 403
Unauthorized
Forbidden
permission denied
not authenticated
invalid token
authentication required
SSH: Permission denied (publickey)
fatal: could not read Username
```
Auth doesn't fix itself. If `git push` fails 401, your SSH key isn't configured — no number of retries will help. If a vendor API returns 401, the secret is wrong or expired — surface it.
### Repeatable infrastructure errors
```
fatal: 'origin' does not appear to be a git repository
Could not resolve host
ECONNREFUSED
ENOTFOUND
DNS_PROBE_FINISHED_NXDOMAIN
service unavailable
Internal Server Error (only after 2 retries)
```
DNS and missing-remote errors are environmental. Stop after the first, except for 5xx where one retry is reasonable (the upstream may have transient issues).
## Per-operation budgets — hard caps
Each operation type has a hard cap. Going over the cap = your time is being burned, not your value-add.
| Operation | Hard cap | What "1 try" means |
|---|---|---|
| `agent-browser install` | **1** | One full `agent-browser install`; further failures = platform issue, escalate |
| Quota'd API call (Apollo, etc.) | **0 retries** after first quota error | First call is fine; retry on quota = stop |
| `npm install` | **2** | Two attempts, optionally with `--legacy-peer-deps` on the second |
| `npm run build` after a fix | **3** | Three different fixes between attempts; same fix twice doesn't count |
| `git push origin main` (post-rebase) | **2** | One rebase + push; if rejected again, see `git-push-safe` recovery flow |
| `git rebase` for the same conflict set | **1** | If rebase fails, use the soft-reset path (see `git-push-safe`) |
| Vercel deploy verification | **1** | One 90s wait + one browser check; see `vercel-deploy-verify` |
| `agent-browser open <same URL>` | **3** | After 3 same-URL failures, that URL is dead-to-you |
| Subagent call without timeout | **0** | Always pass a time-box; longest observed cost was 23 min on one call |
| Polling a background job | see `polling-and-waits` | Total wait ≤ 10 min, then take what you have |
**Hard caps are conjunctions**: "2 attempts AND same error class". A genuinely different error on retry resets the count (but a different *symptom* of the same cause does not — read the error, don't pattern-match).
## Decision tree on EVERY tool failure
When ANY command exits non-zero or returns an `"error"` field, run this in your head:
```
1. Is the error in the NEVER-RETRYABLE list? → STOP this op, write result, done.
2. Is this the Nth time I've seen this exact error (N = the op's cap)? → STOP.
3. Do I have a NEW, SPECIFIC fix to try (not "let me retry with --force")? → try it once.
4. None of the above? → STOP. Default is to stop.
```
The bias is intentional. Workers err toward retrying; this rule flips that default.
## Sub-task abandonment
If a sub-task is blocked but the parent task has other deliverable parts, finish the other parts. Mark the blocked sub-task in your result:
```
What was completed:
- Built the pricing page
- Set up the Stripe product
- Pushed and verified the deploy
What remains:
- Sending the launch email was BLOCKED: Resend API returned "domain not verified".
Manual step needed: verify @example.com on Resend, then re-run.
```
Never silently drop a sub-task. The CEO can't recreate what they don't know failed.
## Subagent time-boxing
If you spawn a subagent via the Agent tool, set its scope explicitly so it can't run away:
> "Find X. Report under 150 words. If you haven't found it after exploring 4 files, stop and report what you saw."
Past traces show subagents running 23 minutes on a vague brief. A 1-paragraph scope + word cap solves this.
## Anti-patterns (do NOT do these)
| Pattern | Why bad |
|---|---|
| Retry with "force" / "skip-checks" / "ignore-errors" flags | Hides the cause, doesn't fix it |
| Retry the same vendor API with new inputs after quota error | Quotas are per-account, not per-input |
| Add `sleep 60` between retries hoping it'll start working | Sleeping doesn't refill credits or change auth |
| Switch from `agent-browser` to `curl` after a browser failure | If the page needs a browser, curl won't help; if it doesn't, you should have used curl first |
| Re-run a failed migration "to make sure" | DB state is non-idempotent; you'll corrupt something |
| Open a "different version" of the same broken URL | If domain.com → 404, domain.org won't help; ask the data |
| Try a 4th `npm install --force --legacy-peer-deps --no-audit` | Magic flag combos don't fix broken trees |
| "One more screenshot" after a successful screenshot | Stop; you have it |
## End-of-task discipline
Before declaring "done", check:
- [ ] All NEVER-RETRYABLE errors I hit are mentioned in my result summary.
- [ ] All per-op caps I hit are mentioned (so the CEO knows where the task was blocked).
- [ ] I did NOT silently abandon a sub-task.
- [ ] I left no zombie processes (close `agent-browser`, stop background jobs).
- [ ] I have not used my last 2 minutes on "let me just check one more time".
The cost of stopping early is small — the CEO can recreate the task. The cost of overrunning the 30-min budget is large — the task fails entirely with no result.
## When you stop, write a clean result
See the `task-result-summary` skill. The three-section format (What was completed / What remains / Blockers) is required. A blocker line should name the exact error string, not paraphrase.