initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,404 @@
---
name: subagent-driven-development
description: "Execute plans via delegate_task subagents (2-stage review)."
version: 1.1.0
author: Hermes Agent (adapted from obra/superpowers)
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [delegation, subagent, implementation, workflow, parallel]
related_skills: [writing-plans, requesting-code-review, test-driven-development]
---
# Subagent-Driven Development
## Overview
Execute implementation plans by dispatching fresh subagents per task with systematic two-stage review.
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration.
## When to Use
Use this skill when:
- You have an implementation plan (from writing-plans skill or user requirements)
- Tasks are mostly independent
- Quality and spec compliance are important
- You want automated review between tasks
**Parallel fan-out preference:** When the user explicitly asks to delegate or the task involves building multiple independent pages/components (scaffolding, page builds, rewrites), use **parallel fan-out** (`delegate_task` with `tasks` array, up to 3 concurrent). Dispatch all independent pieces at once — no need for sequential 2-stage review on every single task. Follow up manually on anything the subagents didn't complete. The user prefers speed to perfect review discipline for bulk codegen.
**When NOT to use subagents — direct execution is better when:**
• The plan contains **exact, copy-pasteable code** for every file (mechanical extraction / file-creation tasks). A subagent that receives fully specified code and just has to write it adds delegation overhead with zero decision-making value.
• The task is a **pure refactor** that moves existing code between files without changing behavior. Writing the files yourself is faster, you keep awareness of structural interdependencies, and there's nothing for a reviewer to evaluate (spec = "move verbatim").
• You have already read all the files and know the exact state. Spinning up a subagent means re-explaining context they'd need to re-read anyway.
**Decision rule:** When the plan's task description includes the full file contents (`write_file`, `patch` snippets), execute directly. Reserve subagents for tasks where the plan gives a *spec* and expects the implementer to *design and write* code — that's where the review cycle adds value.
**vs. manual execution when subagents ARE the right tool:**
- Fresh context per task (no confusion from accumulated state)
- Automated review process catches issues early
- Consistent quality checks across all tasks
- Subagents can ask questions before starting work
## The Process
### 1. Read and Parse Plan
Read the plan file. Extract ALL tasks with their full text and context upfront. Create a todo list:
```python
# Read the plan
read_file("docs/plans/feature-plan.md")
# Create todo list with all tasks
todo([
{"id": "task-1", "content": "Create User model with email field", "status": "pending"},
{"id": "task-2", "content": "Add password hashing utility", "status": "pending"},
{"id": "task-3", "content": "Create login endpoint", "status": "pending"},
])
```
**Key:** Read the plan ONCE. Extract everything. Don't make subagents read the plan file — provide the full task text directly in context.
### 2. Per-Task Workflow
For EACH task in the plan:
#### Step 1: Dispatch Implementer Subagent
Use `delegate_task` with complete context:
```python
delegate_task(
goal="Implement Task 1: Create User model with email and password_hash fields",
context="""
TASK FROM PLAN:
- Create: src/models/user.py
- Add User class with email (str) and password_hash (str) fields
- Use bcrypt for password hashing
- Include __repr__ for debugging
FOLLOW TDD:
1. Write failing test in tests/models/test_user.py
2. Run: pytest tests/models/test_user.py -v (verify FAIL)
3. Write minimal implementation
4. Run: pytest tests/models/test_user.py -v (verify PASS)
5. Run: pytest tests/ -q (verify no regressions)
6. Commit: git add -A && git commit -m "feat: add User model with password hashing"
PROJECT CONTEXT:
- Python 3.11, Flask app in src/app.py
- Existing models in src/models/
- Tests use pytest, run from project root
- bcrypt already in requirements.txt
""",
toolsets=['terminal', 'file']
)
```
#### Step 2: Dispatch Spec Compliance Reviewer
After the implementer completes, verify against the original spec:
```python
delegate_task(
goal="Review if implementation matches the spec from the plan",
context="""
ORIGINAL TASK SPEC:
- Create src/models/user.py with User class
- Fields: email (str), password_hash (str)
- Use bcrypt for password hashing
- Include __repr__
CHECK:
- [ ] All requirements from spec implemented?
- [ ] File paths match spec?
- [ ] Function signatures match spec?
- [ ] Behavior matches expected?
- [ ] Nothing extra added (no scope creep)?
OUTPUT: PASS or list of specific spec gaps to fix.
""",
toolsets=['file']
)
```
**If spec issues found:** Fix gaps, then re-run spec review. Continue only when spec-compliant.
#### Step 3: Dispatch Code Quality Reviewer
After spec compliance passes:
```python
delegate_task(
goal="Review code quality for Task 1 implementation",
context="""
FILES TO REVIEW:
- src/models/user.py
- tests/models/test_user.py
CHECK:
- [ ] Follows project conventions and style?
- [ ] Proper error handling?
- [ ] Clear variable/function names?
- [ ] Adequate test coverage?
- [ ] No obvious bugs or missed edge cases?
- [ ] No security issues?
OUTPUT FORMAT:
- Critical Issues: [must fix before proceeding]
- Important Issues: [should fix]
- Minor Issues: [optional]
- Verdict: APPROVED or REQUEST_CHANGES
""",
toolsets=['file']
)
```
**If quality issues found:** Fix issues, re-review. Continue only when approved.
#### Step 4: Mark Complete
```python
todo([{"id": "task-1", "content": "Create User model with email field", "status": "completed"}], merge=True)
```
### 3. Final Review
After ALL tasks are complete, dispatch a final integration reviewer:
```python
delegate_task(
goal="Review the entire implementation for consistency and integration issues",
context="""
All tasks from the plan are complete. Review the full implementation:
- Do all components work together?
- Any inconsistencies between tasks?
- All tests passing?
- Ready for merge?
""",
toolsets=['terminal', 'file']
)
```
### 4. Verify and Commit
```bash
# Run full test suite
pytest tests/ -q
# Review all changes
git diff --stat
# Final commit if needed
git add -A && git commit -m "feat: complete [feature name] implementation"
```
## Task Granularity
**Each task = 2-5 minutes of focused work.**
**Too big:**
- "Implement user authentication system"
**Right size:**
- "Create User model with email and password fields"
- "Add password hashing function"
- "Create login endpoint"
- "Add JWT token generation"
- "Create registration endpoint"
## Red Flags — Never Do These
- Start implementation without a plan
- Skip reviews (spec compliance OR code quality)
- Proceed with unfixed critical/important issues
- Dispatch multiple implementation subagents for tasks that touch the same files
- Make subagent read the plan file (provide full text in context instead)
- Never skip scene-setting context (subagent needs to understand where the task fits)
- IGNORE subagent questions (answer before letting them proceed)
- Wait for ALL subagents in a parallel batch before testing/reviewing — React pages often share imports; a missing page import in App.tsx breaks the build
- After parallel fan-out completes: check for React context providers. Subagents may use hooks like `useToast()` that require a provider wrapper. If any page crashes (blank screen), check if `<ToastProvider>` wraps the routes. **Before dispatching**, scan the task specs for hooks that need providers (useToast, useAuth, useTheme) and include a reminder in each subagent's context to either use them safely or note the required wrapper
- After parallel fan-out where one subagent creates a library module (like `ai.ts`) and another consumes it (like `QuoteGenerator.tsx`), verify that function signatures match between the two. Subagents don't share context, so their exported/imported interfaces can drift. Check the actual call sites against the actual function signatures before building
- When the project uses `React.lazy()` for code splitting, import errors in lazy-loaded chunks surface as blank screens with empty console errors. Use eager imports first to isolate the failing module, then switch back to lazy once verified
- Accept "close enough" on spec compliance
- Skip review loops (reviewer found issues → implementer fixes → review again)
- Let implementer self-review replace actual review (both are needed)
- **Start code quality review before spec compliance is PASS** (wrong order)
- Move to next task while either review has open issues
- **Trust subagent generated UI buttons have wired handlers** — always verify interactive elements (buttons, links, forms) have onClick/onSubmit handlers after subagent work. Subagents frequently generate `<button>` elements with no action attached.
## Common Subagent Output Defects to Catch
Subagents produce code that compiles cleanly (TypeScript passes, bundler accepts it) but is functionally broken. After parallel fan-outs, scan for these common defects:
### Missing interactive handlers
Buttons, links, and form elements rendered in the DOM but with no `onClick`/`onSubmit` handler attached. The element appears on screen but clicking does nothing. **Check:** grep each new page file for `<button` and verify every interactive button has an `onClick` or `type="submit"`.
### Hardcoded mock/fallback responses
Subagents sometimes build "smart fallbacks" (simulated responses, canned data, mock delays) instead of calling the real API endpoint. **Check:** grep for keywords like `setTimeout`, `Math.random`, `fallback`, `simulate`, or hardcoded response objects. If the task spec says "call API X", the implementation must call API X — not simulate it.
### Function signature drift between modules
When subagent A creates a library and subagent B consumes it, the exported function signatures often don't match the call sites. `aiWriteExplanation(name, reason)` vs `aiWriteExplanation({serviceName, recommendation})`. **Check:** for each imported function in consumer files, verify the actual call argument shapes match the exported parameter types.
### Dependency stealth
Subagents add imports to `package.json` (`tesseract.js`, chart libraries, etc.) and use them in code without the package actually being installed. TypeScript may not catch this if the module isn't directly imported at compile time (e.g., dynamic `import()`). **Check:** compare new imports against `package.json` dependencies.
### React nested-component remount (input focus loss)
Subagents define child components as nested functions inside parent components. When the parent rerenders (e.g., due to a zustand store update from typing in an input), React destroys and recreates the nested component because its function identity changed — causing inputs to lose focus on every keystroke. **Symptom:** typing one character dismisses focus; the user must click the field again for each keypress. **Fix:** extract the nested component to a filelevel `const Component = memo(function Component({...}) {...})` with stable props. Pass callbacks as individual props rather than capturing parent closure variables.
**Variant: duplicate definitions (shadowing).** When a subagent adds a new feature to an existing file, it may create BOTH:
1. A **standalone** (prop-based) component **outside** the parent component function, AND
2. An **inline** (closure-based) component **inside** the parent component function
The inline definition **shadows** the proper standalone one because JavaScript scope resolution finds the inner binding first. The inner component is recreated every render (destroying focus), while the outer one is never used. **Diagnosis:** Search for `function ComponentName(` — if two definitions exist (one inside the parent component, one outside), delete the inline one. **Prevention:** when adding a feature to a file that already has standalone components, check whether the parent component ALSO contains an inline version that shadows the outer one.
### tsc clean ≠ build clean
TypeScript `--noEmit` passing does not guarantee the bundler (Vite/Rolldown, webpack) will succeed. Bundlers enforce module resolution, circular imports, and chunk splitting that tsc ignores. After type-checking, always run the actual build command.
### JSX fragment conversion during component extraction
When extracting JSX that uses fragment shorthand (`<>...</>`) into a named component and converting to `<Fragment key={...}>`, the brace/paren structure around the return expression often changes because the fragment shorthand doesn't carry a `key` prop but a moved `<Fragment>` needs one. **Common failure:** leaving the original closing pattern (`));` or `);`) unchanged when the Fragment's structural role changed.
**Root cause:** The original `<>...</>` sits directly inside `return ( ... )` and the closing `)` is obvious. After converting to `<Fragment key={expr}>`, the Fragment's open tag may introduce an extra nesting level or change how the return expression relates to the arrow function body. The old closing `);` may need to become just `);` (one paren fewer/more) depending on whether the fragment is the single child or wraps multiple elements.
**Prevention:** After extracting any JSX block that involved `<>...</>`, trace the return path: `return ( <Fragment> ... </Fragment> )` should close with `);` then `})}` for a block-arrow `.map()`. Count parens explicitly. The `;` ends the return statement, the `}` closes the arrow body, the `)` closes `.map()`, and the `}` closes the JSX expression.
## Handling Issues
### If Subagent Asks Questions
- Answer clearly and completely
- Provide additional context if needed
- Don't rush them into implementation
### If Reviewer Finds Issues
- Implementer subagent (or a new one) fixes them
- Reviewer reviews again
- Repeat until approved
- Don't skip the re-review
### If Subagent Fails a Task
- Dispatch a new fix subagent with specific instructions about what went wrong
- Don't try to fix manually in the controller session (context pollution)
## Efficiency Notes
**Why fresh subagent per task:**
- Prevents context pollution from accumulated state
- Each subagent gets clean, focused context
- No confusion from prior tasks' code or reasoning
**Why two-stage review:**
- Spec review catches under/over-building early
- Quality review ensures the implementation is well-built
- Catches issues before they compound across tasks
**Cost trade-off:**
- More subagent invocations (implementer + 2 reviewers per task)
- But catches issues early (cheaper than debugging compounded problems later)
## Integration with Other Skills
### With writing-plans
This skill EXECUTES plans created by the writing-plans skill:
1. User requirements → writing-plans → implementation plan
2. Implementation plan → subagent-driven-development → working code
### With test-driven-development
Implementer subagents should follow TDD:
1. Write failing test first
2. Implement minimal code
3. Verify test passes
4. Commit
Include TDD instructions in every implementer context.
### With requesting-code-review
The two-stage review process IS the code review. For final integration review, use the requesting-code-review skill's review dimensions.
### With systematic-debugging
If a subagent encounters bugs during implementation:
1. Follow systematic-debugging process
2. Find root cause before fixing
3. Write regression test
4. Resume implementation
## Example Workflow
```
[Read plan: docs/plans/auth-feature.md]
[Create todo list with 5 tasks]
--- Task 1: Create User model ---
[Dispatch implementer subagent]
Implementer: "Should email be unique?"
You: "Yes, email must be unique"
Implementer: Implemented, 3/3 tests passing, committed.
[Dispatch spec reviewer]
Spec reviewer: ✅ PASS — all requirements met
[Dispatch quality reviewer]
Quality reviewer: ✅ APPROVED — clean code, good tests
[Mark Task 1 complete]
--- Task 2: Password hashing ---
[Dispatch implementer subagent]
Implementer: No questions, implemented, 5/5 tests passing.
[Dispatch spec reviewer]
Spec reviewer: ❌ Missing: password strength validation (spec says "min 8 chars")
[Implementer fixes]
Implementer: Added validation, 7/7 tests passing.
[Dispatch spec reviewer again]
Spec reviewer: ✅ PASS
[Dispatch quality reviewer]
Quality reviewer: Important: Magic number 8, extract to constant
Implementer: Extracted MIN_PASSWORD_LENGTH constant
Quality reviewer: ✅ APPROVED
[Mark Task 2 complete]
... (continue for all tasks)
[After all tasks: dispatch final integration reviewer]
[Run full test suite: all passing]
[Done!]
```
## Remember
```
Fresh subagent per task
Two-stage review every time
Spec compliance FIRST
Code quality SECOND
Never skip reviews
Catch issues early
```
**Quality is not an accident. It's the result of systematic process.**
## Further reading (load when relevant)
When the orchestration involves significant context usage, long review loops, or complex validation checkpoints, load these references for the specific discipline:
- **`references/context-budget-discipline.md`** — Four-tier context degradation model (PEAK / GOOD / DEGRADING / POOR), read-depth rules that scale with context window size, and early warning signs of silent degradation. Load when a run will clearly consume significant context (multi-phase plans, many subagents, large artifacts).
- **`references/gates-taxonomy.md`** — The four canonical gate types (Pre-flight, Revision, Escalation, Abort) with behavior, recovery, and examples. Load when designing or reviewing any workflow that has validation checkpoints — use the vocabulary explicitly so each gate has defined entry, failure behavior, and resumption rules.
- **`references/pocketbase-proxy-pitfalls.md`** — PocketBase SDK path construction trap (double `/api`), Python http.server query string behavior, and admin auth endpoint differences across versions. Load when debugging PocketBase proxy 404 errors or setting up a local SPA dev server that proxies to PocketBase.
- **`references/spa-proxy-server.md`** — Template and pitfalls for a Python HTTP proxy server that serves a SPA's static dist/ while proxying API paths to a backend on a different port. Load when you need to serve a local build for browser testing and the SPA's relative API paths need to reach a backend like PocketBase.
- **`references/pocketbase-sequencing-pitfalls.md`** — PB migrations must be applied before frontend code that queries new collections can work. Load when dispatching subagents that write both PB migrations and frontend UI in the same batch — apply migrations first, then dispatch frontend subagents.
Both references adapted from gsd-build/get-shit-done (MIT © 2025 Lex Christopherson).
@@ -0,0 +1,53 @@
# Context Budget Discipline
Practical rules for keeping orchestrator context lean when spawning subagents or reading large artifacts. Use these whenever you're running a multi-step agent loop that will consume significant context — plan execution, subagent orchestration, review pipelines, multi-file refactors.
Adapted from the GSD (Get Shit Done) project's context-budget reference — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)).
## Universal rules
Every workflow that spawns agents or reads significant content must follow these:
1. **Never read agent definition files.** `delegate_task` auto-loads them — you reading them too just doubles the cost.
2. **Never inline large files into subagent prompts.** Tell the agent to read the file from disk with `read_file` instead. The subagent gets full content; your context stays lean.
3. **Read depth scales with context window.** See the table below.
4. **Delegate heavy work to subagents.** The orchestrator routes; it doesn't execute.
5. **Proactively warn** the user when you've consumed significant context ("Context is getting heavy — consider checkpointing progress before we continue").
## Read depth by context window
Check the model's actual context window (not "it's Claude so 200K"). Some Sonnet deployments are 1M, some are 200K. If you don't know, assume the smaller one — err toward leanness.
| Context window | Subagent output reading | Summary files | Verification files | Plans for other phases |
|----------------|-------------------------|---------------|--------------------|-----------------------|
| < 500k (e.g. 200k) | Frontmatter only | Frontmatter only | Frontmatter only | Current phase only |
| >= 500k (1M models) | Full body permitted | Full body permitted | Full body permitted | Current phase only |
"Frontmatter only" means: read enough to see the final status/verdict/conclusion. If the subagent wrote a 3000-line debug log, read the summary section it produced, not the log.
## Four-tier degradation model
Monitor your context usage and shift behavior as you climb the tiers. The point is to notice *before* you hit the wall, not when responses start truncating.
| Tier | Usage | Behavior |
|------|-------|----------|
| **PEAK** | 0 30% | Full operations. Read bodies, spawn multiple agents in parallel, inline results freely. |
| **GOOD** | 30 50% | Normal operations. Prefer frontmatter reads. Delegate aggressively. |
| **DEGRADING** | 50 70% | Economize. Frontmatter-only reads, minimal inlining, **warn the user** about budget. |
| **POOR** | 70%+ | Emergency mode. **Checkpoint progress immediately.** No new reads unless critical. Finish the current task and stop cleanly. |
## Early warning signs (before panic thresholds fire)
Quality degrades *gradually* before hard limits hit. Watch for these:
- **Silent partial completion.** Subagent claims done but implementation is incomplete. Self-checks catch file existence, not semantic completeness. Always verify subagent output against the plan's must-haves, not just "did a file appear?"
- **Increasing vagueness.** Agent starts using phrases like "appropriate handling" or "standard patterns" instead of specific code. This is context pressure showing up before budget warnings fire.
- **Skipped protocol steps.** Agent omits steps it would normally follow. If success criteria has 8 items and the report covers 5, suspect context pressure, not "the agent decided 5 was enough."
When these signs appear, checkpoint the work and either reset context or hand off to a fresh subagent.
## Fundamental limitation
When you orchestrate, you cannot verify semantic correctness of subagent output — only structural completeness ("did the file appear?", "does the test pass?"). Semantic verification requires either running the code yourself or delegating a review pass to another fresh subagent.
**Mitigation:** in every task you delegate, include explicit "must-have" truths the subagent must confirm in its response (e.g., "confirm your test actually tests X, not just that X was imported"). The subagent re-asserting concrete facts is evidence; vague summaries are not.
@@ -0,0 +1,93 @@
# Gates Taxonomy
Canonical gate types for validation checkpoints across any workflow that spawns subagents, runs review loops, or has human-approval pauses. Every validation checkpoint maps to one of these four types — naming them explicitly makes the workflow legible and prevents "what happens when this check fails?" confusion.
Adapted from the GSD (Get Shit Done) project's gates reference — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)).
## The four gate types
### 1. Pre-flight gate
**Purpose:** Validates preconditions before starting an operation.
**Behavior:** Blocks entry if conditions unmet. No partial work created — bail before anything changes.
**Recovery:** Fix the missing precondition, then retry.
**Examples:**
- Implementation phase checks that the plan file exists before it starts writing code.
- Delegated subagent checks that required env vars are set before making API calls.
- Commit checks that tests passed before pushing.
### 2. Revision gate
**Purpose:** Evaluates output quality and routes to revision if insufficient.
**Behavior:** Loops back to the producer with specific feedback. Bounded by an iteration cap (typically 3).
**Recovery:** Producer addresses feedback; checker re-evaluates. The loop escalates early if issue count does not decrease between consecutive iterations (stall detection). After max iterations, escalates to the user unconditionally — never loop forever.
**Examples:**
- Plan reviewer reads a draft plan, returns specific issues, planner revises, reviewer re-reads (max 3 cycles).
- Code reviewer checks subagent-produced code against must-haves; dispatches fixes back to the implementer if any must-have failed.
- Test coverage checker validates new tests exercise the new paths; if not, sends back to author.
### 3. Escalation gate
**Purpose:** Surfaces unresolvable issues to the human for a decision.
**Behavior:** Pauses workflow, presents options, waits for human input. Never guesses, never picks a default.
**Recovery:** Human chooses action; workflow resumes on the selected path.
**Examples:**
- Revision loop exhausted after 3 iterations.
- Merge conflict during automated worktree cleanup.
- Ambiguous requirement — two reasonable interpretations and the choice changes the approach.
- Subagent reports "the plan says X but the codebase actually does Y" — human decides which is right.
### 4. Abort gate
**Purpose:** Terminates the operation to prevent damage or waste.
**Behavior:** Stops immediately, preserves state (checkpoint current progress), reports the specific reason.
**Recovery:** Human investigates root cause, fixes, restarts from checkpoint.
**Examples:**
- Context window critically low during execution (POOR tier, >70%) — abort cleanly rather than produce truncated output.
- Critical dependency unavailable mid-run (network down, API key revoked).
- Unrecoverable filesystem state (disk full, permissions lost).
- Safety invariant violated (agent attempted an irreversible destructive action outside approved scope).
## How to use this in a skill
When you write an orchestration skill that has validation checkpoints, **name each checkpoint by its gate type explicitly** and answer three questions:
1. **What condition triggers this gate?** (e.g., "plan file missing", "issue count didn't decrease", "context >70%")
2. **What happens when it fails?** (block / loop back / ask human / abort)
3. **Who resumes, and from where?** (fix precondition + retry, revise + re-check, human decision, restart from checkpoint)
Answering these three up front means your skill never hits "what do we do now?" at runtime.
## Example — a review loop with all four gate types
```
[Pre-flight] plan.md exists and is non-empty? → no: bail, ask user to write a plan first
↓ yes
[Execute] subagent implements task
[Revision] reviewer checks against must-haves → fail: loop back to subagent (max 3)
↓ pass
[Pre-flight] tests pass? → no: bail, report failing tests
↓ yes
[Commit]
(on revision loop exhaustion)
[Escalation] "3 review cycles failed to converge on issue X — pick: force-merge, rewrite task, abandon"
↓ user picks
(on any tier-POOR context pressure during loop)
[Abort] "context at 73%, checkpointing and stopping"
```
The vocabulary is small on purpose. Every gate in every workflow should fit one of these four. If you find yourself inventing a fifth, it's probably a revision gate with extra branching, or an escalation gate in disguise.
@@ -0,0 +1,75 @@
# PocketBase Data Normalization Pitfalls
## JSON fields come back as strings
PocketBase's `json` field type does NOT guarantee parsed JSON arrays on read. The SDK may return JSON fields as **serialized strings** instead of parsed arrays/objects. Always normalize after fetching:
```typescript
const records = await pb.collection('repairOrders').getList(1, 200, { ... });
const normalized = records.items.map((item: any) => {
let services = item.services || [];
if (typeof services === 'string' && services.trim()) {
try { services = JSON.parse(services); } catch { services = []; }
}
if (!Array.isArray(services)) services = [];
return { ...item, services };
});
```
**Symptom:** `e.reduce is not a function` or `e.map is not a function` on fields you expected to be arrays.
**Also affects:** edit modals that pre-populate from fetched records — apply the same normalization before `setState()`.
## Empty/malformed JSON strings
Even when a JSON field has a value, it might be an empty string `""` or partially corrupted. JSON.parse on these throws `"Unexpected end of JSON input"`. Always wrap in try/catch:
```typescript
try { parsed = JSON.parse(raw); } catch { parsed = []; }
```
## Missing system fields (created, updated)
Some PocketBase collections lack the standard `created`/`updated` auto-fields. This happens when collections were created via raw SQL or imported. **Symptom:** queries with `sort: '-created'` or `fields: '...,created'` return 400 errors.
**Fix:** Use `sort: '-id'` (PocketBase IDs are time-sortable) and avoid requesting `created`/`updated` in the `fields` parameter.
**Detection:** Test with `curl` first:
```bash
curl -s "http://127.0.0.1:8091/api/collections/NAME/records?sort=-created&perPage=1" \
-H "Authorization: $TOKEN"
```
If it returns 400, the collection lacks `created`.
## Collection naming conventions
PocketBase collection names are case-sensitive. `repairOrders` and `repair_orders` are different collections. When porting from one naming convention to another, test each collection name directly against the API.
**Detection:**
```python
for name in ['repairOrders', 'repair_orders', 'repairorders']:
r = fetch(f'http://127.0.0.1:8091/api/collections/{name}/records?perPage=1')
print(f"{name}: {'EXISTS' if r.status == 200 else 'MISSING'}")
```
## PocketBase SDK error structure
The `ClientResponseError` thrown by the PocketBase JS SDK has this shape:
```
error.message → top-level message ("Failed to create record.")
error.status → HTTP status (400)
error.response → full API response body: {
data: { email: { message: "Value must be unique." } },
message: "Failed to create record.",
status: 400
}
```
For field-level validation errors, access `error.response.data`. Do NOT assume `error.data` is the field errors — in some SDK versions `error.data` is an alias for `error.response` (the full response body), so field errors are at `error.response.data` or `error.data.data`.
Simple reliable pattern:
```typescript
const message = err instanceof Error ? err.message : 'Failed';
```
The PocketBase SDK's `.message` already includes the user-facing error text.
@@ -0,0 +1,126 @@
# PocketBase SDK Path Construction
## The double `/api` trap
When building a reverse proxy for a PocketBase-backed SPA, the PB JS SDK constructs its own API paths. Understanding this avoids the most common proxy bug.
### How the PB SDK builds URLs
The PocketBase JS SDK's `buildURL` method concatenates `baseURL` + `path`:
```js
// If you create: new PocketBase('/pb')
// Then calling pb.collection('users').authWithPassword(email, password)
// SDK builds: GET /pb/api/collections/users/auth-with-password
// ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// baseURL SDK-added /api/ path
```
The SDK ALWAYS prepends `/api/` to its paths. Your proxy must strip only the `/pb` prefix and forward the rest as-is — including the `/api/` that the SDK already added.
### The bug
```python
# WRONG — this adds a second /api/, producing /api/api/collections/...
if self.path.startswith('/pb'):
url = PB_BACKEND + '/api' + self.path[3:] # DOUBLE /api!
```
PocketBase returns `404 {"message": "The requested resource wasn't found."}` for the double-path URL, which is the same error message as genuinely missing resources — making this a silent, confusing failure.
### The fix
```python
# CORRECT — preserve the original path, which already includes /api/
if self.path.startswith('/pb'):
url = PB_BACKEND + self.path[3:] # /pb/X → /X on backend
```
## Python `http.server` path behavior
`BaseHTTPRequestHandler.path` includes the query string. When forwarding, the query string is already part of the path. `urllib.request.Request(url)` handles query strings correctly, so no special parsing is needed.
## PocketBase admin auth
PocketBase admin/superuser authentication is version-dependent:
- **v0.22+**: `POST /api/collections/_superusers/auth-with-password`
- **Older**: `POST /api/admins/auth-with-password` (may be a 404)
- The admin dashboard is at `/_/` (web UI only, not an API endpoint)
If neither endpoint works, check the PocketBase version. The admin user must be created via the `./pocketbase superuser` CLI command or through the web UI on first run.
## PocketBase Query Field Errors (400 "Something went wrong")
PocketBase returns `400 {"message": "Something went wrong while processing your request."}` when a query references fields that don't exist in the collection schema. This is NOT a 404 — it's a 400, and the error message is generic, making it easy to misdiagnose as a permissions or setup issue.
### The `created` / `updated` system field trap
Collections created via the PocketBase API (rather than the admin UI) may lack the `created` and `updated` system fields. Any query that sorts or filters on these fields will fail with 400.
**Symptom:** `sort: '-created'` succeeds via curl against a valid collection but returns 400 when tested against the same collection on a different PB instance. The collection EXISTS but the query fails because the field doesn't.
**Detection:** Test the exact query parameters one at a time:
```python
# Isolate which parameter breaks
# Test: ?page=1 (should work)
# Test: ?page=1&sort=-created (fails → field missing)
# Test: ?page=1&fields=created (may fail or be silently ignored)
```
**Workaround:** Replace `sort: '-created'` with `sort: '-id'` — PocketBase record IDs are sortable and roughly chronological. For `fields`, remove any references to `created`/`updated` if the collection lacks them.
**Root cause:** Subagents building against one PB instance may use system fields that work there, but the target deployment's PB instance has collections created via API without auto-system-fields enabled.
### Collection naming mismatches
PocketBase collection names are case-sensitive and can use either camelCase (`repairOrders`) or snake_case (`repair_orders`) depending on how they were created. The API silently returns 404 for the wrong case, and the error message is identical to a genuinely missing collection. Always verify the exact collection name by querying `GET /api/collections` before assuming a collection doesn't exist.
## PocketBase Sign-Up Error Handling
When `pb.collection('users').create()` fails, the PocketBase SDK throws a `ClientResponseError` with a generic top-level `.message` (e.g., `"Failed to create record."`) and field-level details in `.data`:
```json
{
"data": {
"email": {"code": "validation_not_unique", "message": "Value must be unique."}
},
"message": "Failed to create record.",
"status": 400
}
```
**Never use the raw `.message` directly** — it's always `"Failed to create record."` for any validation failure. Instead, unwrap `.data` to show the user which field failed:
```typescript
} catch (err: unknown) {
let message = 'Failed to create account.';
if (err && typeof err === 'object' && 'data' in err) {
const pbErr = err as { data?: Record<string, { message: string }> };
if (pbErr.data) {
const fieldErrors = Object.entries(pbErr.data)
.map(([field, info]) => `${field}: ${info.message}`)
.join('; ');
if (fieldErrors) message = fieldErrors;
}
}
setError(message);
}
```
This produces user-friendly messages like `email: Value must be unique.` instead of the unhelpful `Failed to create record.`
Common PocketBase validation errors on user creation:
- **email not unique** → `email: Value must be unique.`
- **password too short** → `password: Must be at least 8 characters.`
- **missing required field** → `email: Cannot be blank.`
## PocketBase SMTP Configuration
PocketBase requires SMTP to be configured for email verification to work. Check the `_params` table in the PocketBase data directory:
```sql
SELECT value FROM _params WHERE id='settings'
```
If `smtp.enabled` is `false`, verification emails will never send. Configure SMTP via the PocketBase admin UI (`/_/`) or by updating the `_params` row directly with valid SMTP credentials (host, port, username, password, TLS).
@@ -0,0 +1,46 @@
# PB Migration Sequencing Pitfalls (Subagent-Driven Development)
## The Problem
When a plan involves creating/updating PocketBase collections AND frontend UI in the same batch, the frontend code inevitably references the new collection name. If the PB migration hasn't been applied yet, the frontend will:
- Type-check fine (no compile-time dependency on PB schema)
- Build fine (no import dependency)
- **Crash at runtime** with "Missing collection" errors
## When This Happens
The standard batch-parallel pattern (`delegate_task` with independent goals) is susceptible because:
1. Subagent A creates `pb_migrations/M25_new_collection.js`
2. Subagent B writes `pb.collection('newCollection').getList(...)` in a UI component
3. Both execute in parallel — neither knows about the other
4. The PB migration still needs to be `docker cp`'d and `migrate up` applied
5. The frontend code is deployed, but the collection doesn't exist in PB yet
## Mitigation Strategy
**For the agent orchestrating the batch:**
- **Deploy PB migrations BEFORE dispatching frontend subagents.** Wrap the sequence as:
1. Write all PB migration files
2. Copy to container and `migrate up`
3. Verify success with `docker exec` / `--dir` check
4. THEN dispatch frontend subagents
- **If forced to parallelize** (e.g., PB and frontend for different features are interleaved), add a guard in the frontend subagent's context: "The collection [name] already exists in PB — query it directly." Verify the collection exists first with a curl health check before dispatching.
- **For deployment safety**, add a try/catch around PB queries in new frontend code:
```typescript
try {
const records = await pb.collection('newCollection').getFullList({...});
} catch (err) {
console.warn('[newCollection] not available yet');
return [];
}
```
## Why This Is Tricky
Unlike module imports (which fail at build time), PB collection references are runtime API calls. The JS bundler doesn't know about them. In the SPQ-v2 project:
- All PB queries go through `pb.collection('name').method()`
- The collection name is a runtime string — no compile-time validation
- Missing collections produce a `404` response from PB, which the frontend may or may not handle gracefully
@@ -0,0 +1,54 @@
# React Component Extraction — Mega-File Split Pattern
## When you need this
A single `.tsx` file has grown past ~1,000 lines with inline sub-components, inline helper functions, and inline type definitions. The established pattern is:
1. **Pure helpers → `src/lib/<domain>.ts`** — formatters, PB query builders, status label/color mappers, date formatters
2. **Types → shared module** — interfaces can stay in the page file if re-exported (for external importers), or move to `src/components/<domain>/types.ts`
3. **Inline components → `src/components/<domain>/`** — one file per component, barrel `index.ts`
4. **Page becomes orchestrator** — imports from barrel, keeps all state + handlers
## Proven pattern (from Appointments.tsx, Customers.tsx splits)
| Extraction target | Destination | Pattern |
|---|---|---|
| `function HelperFn(...)` | `src/lib/<domain>.ts` | Pure function, no hooks, no JSX |
| `interface X` | `src/components/<domain>/types.ts` (or re-export from page) | Re-export from page if external files import from the old path |
| `function Skeleton()`, `function EmptyState()` | `EmptyStates.tsx` | Plain function component |
| `function Card(props): JSX` | `<Name>Card.tsx` | `memo(function NameCardImpl(props) { ... })` |
| `function FormModal(props)` | `<Name>FormModal.tsx` | Keep as-is (has internal state — memo adds nothing) |
| `function DetailView(props)` | `<Name>DetailView.tsx` | `memo(function DetailViewImpl(props) { ... })` |
| `function DeleteConfirmModal(props)` | `DeleteConfirmModal.tsx` | Keep as-is |
| Barrel | `index.ts` | Re-export all components and types |
## Import compatibility pattern
When external files import types from the old page path (e.g., `import type { CustomerWithVehicles } from '../pages/Customers'`), the page MUST re-export those types:
```tsx
// src/pages/Customers.tsx — at the top level
export type { CustomerRecord, CustomerWithVehicles, CustomerFormData }
from '../components/customers/types';
```
Do NOT change the import paths in the 5 external files — the re-export keeps them working.
## Pitfalls discovered during extraction
### 1. Fragment shorthand → Fragment key mismatch
The original file uses `<>...</>` (no key prop). The extracted equivalent needs `<Fragment key={expr}>`. But the closing paren/bracket structure differs because the Fragment opening tag is part of the JSX tree rather than being a structural wrapper.
**Trace template:**
```
Original: return ( <><tr/><tr/></> );
After extract: return ( <Fragment key={x}><tr/><tr/></Fragment> );
```
The closing `));` after `</>` in a `.map()` callback becomes `);` after `</Fragment>`, then `})}` for block-end, `.map()` close, JSX expression close. **Count explicitly.**
### 2. `useCallback` dependency drift
When moving a handler from the orchestrator page that references a callback defined in the same component, the `useCallback` deps array may reference a function that's now imported. The import is stable (never changes identity), so the dep IS needed in the array to be correct, but TypeScript won't warn either way. **Check:** every `useCallback(fn, deps)` where `fn` calls an imported function — that imported function should be in the deps array, or the callback is stale.
### 3. `formatDate` name collision
The page likely has its own `function formatDate(...)` helper. The extracted lib module uses the same name. Rename to `formatCustomerDate` or `formatDomainDate` — the app's `src/lib/format.ts` may already export a global `formatDate`.
@@ -0,0 +1,50 @@
# React Runtime Crash Patterns
## Invalid Date → RangeError (white screen)
`new Date(undefined)` or `new Date(null)` creates an Invalid Date. Calling `.toLocaleDateString()` or `Intl.DateTimeFormat.format()` on it throws `RangeError: Invalid time value` — an uncaught runtime error that blanks the page.
**Fix:** Guard all date formatting functions:
```typescript
function formatDate(iso: string | undefined | null) {
if (!iso) return '—';
return new Intl.DateTimeFormat('en-US', { ... }).format(new Date(iso));
}
```
**Detection:** grep for `new Date(` calls in page components and verify each has a null/undefined guard.
## Nested component re-mount (input focus loss)
Defining a child component as a nested function inside a parent causes React to destroy/recreate it on every parent render. If the parent re-renders from a state update (e.g., zustand store change while typing), the input loses focus on every keystroke.
**Symptom:** Each character typed dismisses focus; user must click back into the field for the next character.
**Fix:** Extract to a file-level `memo` component:
```typescript
const ServiceRow = memo(function ServiceRow({ service, onUpdate, ... }: Props) {
return <input value={service.price} onChange={e => onUpdate({ price: parseFloat(e.target.value) })} />
});
```
**Anti-pattern (causes focus loss):**
```typescript
function Parent() {
const Child = ({ item }) => <input ... />; // New identity every render
return items.map(i => <Child key={i.id} item={i} />);
}
```
## Blank screen with empty console
When a React lazy-loaded chunk has an import error, the page shows blank with zero console errors. The error is swallowed by the Suspense boundary. **Fix:** temporarily switch to eager imports in App.tsx to surface the error, then revert to lazy once fixed.
## tsc clean ≠ runtime clean
TypeScript compilation success does not catch:
- JSON.parse on potentially-malformed strings
- `new Date(undefined)` → RangeError
- `.reduce()` / `.map()` on non-array values
- Missing context providers (useToast without ToastProvider)
These all surface as blank pages or runtime crashes. After parallel fan-outs, smoke-test each page by navigating to its URL.
@@ -0,0 +1,121 @@
# Local SPA Proxy Server Pattern
When testing a React/Vite SPA locally that needs to reach a backend API on a different port (e.g., PocketBase on 8091, or any API server), use a Python HTTP server that serves static files AND proxies API paths to the real backend.
## Problem
SPAs built with Vite often reference backend APIs at relative paths like `/pb` or `/api`. When served from a simple `python3 -m http.server`, these requests go to the wrong origin and fail with 404 or CORS errors.
## Solution
A ~60-line Python script that:
1. Serves static files from the `dist/` directory
2. Proxies API paths (`/pb/*`, `/api/*`) to the real backend
3. Adds CORS headers to all responses
4. Handles SPA fallback (serves `index.html` for unknown paths)
5. Supports GET, POST, PUT, PATCH, DELETE, OPTIONS methods
## Template
Save as `serve-proxy.py` in the project root:
```python
#!/usr/bin/env python3
"""Serve SPA dist + proxy /pb to backend."""
from http.server import HTTPServer, SimpleHTTPRequestHandler
import urllib.request, os
DIST = './dist'
BACKEND = 'http://127.0.0.1:8091'
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIST, **kwargs)
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
super().end_headers()
def do_OPTIONS(self):
self.send_response(204)
self.end_headers()
def do_GET(self):
if self.path.startswith('/pb') or self.path.startswith('/api'):
self.proxy()
else:
full = os.path.join(DIST, self.path.lstrip('/'))
if not os.path.exists(full) or os.path.isdir(full) and self.path != '/':
self.path = '/index.html'
super().do_GET()
def do_POST(self): self.proxy() if self.is_api_path() else self.send_error(405)
def do_PUT(self): self.proxy()
def do_PATCH(self): self.proxy()
def do_DELETE(self): self.proxy()
def is_api_path(self):
return self.path.startswith('/pb') or self.path.startswith('/api')
def proxy(self):
url = BACKEND + self.path
# Strip proxy prefix if the SDK already includes it
if self.path.startswith('/pb'):
url = BACKEND + self.path[3:]
if not url.startswith(BACKEND + '/api'):
url = BACKEND + '/api' + self.path[3:]
body = None
if self.headers.get('Content-Length'):
body = self.rfile.read(int(self.headers['Content-Length']))
req = urllib.request.Request(url, data=body, method=self.command)
for k, v in self.headers.items():
if k.lower() not in ('host', 'connection', 'origin', 'referer'):
req.add_header(k, v)
try:
resp = urllib.request.urlopen(req)
self.send_response(resp.status)
for k, v in resp.headers.items():
if k.lower() not in ('transfer-encoding', 'connection'):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(e.code)
self.end_headers()
self.wfile.write(e.read())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 4173), Handler)
print(f'Serving on http://0.0.0.0:4173 → {BACKEND}')
server.serve_forever()
```
## Common Pitfalls
### Double `/api` prefix
The PocketBase JS SDK constructs paths like `/pb/api/collections/...`. The proxy must strip `/pb` and append the remainder, NOT add another `/api/`. Check what paths the SDK actually builds by inspecting the built JS or testing with curl.
### PocketBase SDK adds `/api/` to the base URL
`new PocketBase('/pb')` builds URLs as: `/pb/api/collections/...`. The SDK always inserts `/api/` between the base URL and the collection path.
### Missing CORS on error responses
If `urllib.error.HTTPError` is caught, the error response body is written but CORS headers from `end_headers()` may not be called in the right order. Ensure `end_headers()` is called before `wfile.write()` in error paths.
### Empty browser console errors
React lazy-loaded chunk failures often produce empty exceptions in browser consoles. If a page renders blank with no visible error, revert to eager imports temporarily to surface the actual error message.
## Usage
```bash
# Start server in background
python3 serve-proxy.py &
# Access at http://localhost:4173 or http://<lan-ip>:4173
```
## When to Use
- Testing a local SPA build that needs API access
- Debugging frontend-backend integration without nginx reverse proxy
- Quick demo serving with `python3 -m http.server` + CORS + API proxy
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Serve a SPA's dist/ directory + proxy API paths to a backend.
Usage: python3 proxy-server.py [--port PORT] [--dist DIST_DIR] [--backend BACKEND_URL]
Proxies:
/pb/* → BACKEND/api/* (PocketBase SDK path)
/api/* → BACKEND/api/* (direct API)
/deepseek/* → https://api.deepseek.com/* (with API key from /tmp/deepseek_key.txt)
/* → SPA static files (fallback to index.html for client-side routes)
Environment:
DEEPSEEK_KEY — API key for DeepSeek (overrides /tmp/deepseek_key.txt)
"""
from http.server import HTTPServer, SimpleHTTPRequestHandler
import urllib.request, os, sys, argparse
def parse_args():
p = argparse.ArgumentParser(description='SPA dev server with API proxy')
p.add_argument('--port', type=int, default=4173)
p.add_argument('--dist', default=os.path.join(os.path.dirname(__file__), 'dist'))
p.add_argument('--backend', default='http://127.0.0.1:8091')
return p.parse_args()
ARGS = parse_args()
DIST = ARGS.dist
PB = ARGS.backend
DEEPSEEK_KEY = os.environ.get('DEEPSEEK_KEY')
if not DEEPSEEK_KEY:
try:
with open('/tmp/deepseek_key.txt') as f:
DEEPSEEK_KEY = f.read().strip()
except FileNotFoundError:
pass
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIST, **kwargs)
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
super().end_headers()
def do_OPTIONS(self):
self.send_response(204)
self.end_headers()
def do_GET(self):
if self._is_api_path():
self._proxy()
else:
full = os.path.join(DIST, self.path.lstrip('/'))
if not os.path.exists(full) or (os.path.isdir(full) and self.path != '/'):
self.path = '/index.html'
super().do_GET()
def do_POST(self):
if self._is_api_path():
self._proxy()
else:
self.send_error(405)
def do_PUT(self): self._proxy()
def do_PATCH(self): self._proxy()
def do_DELETE(self): self._proxy()
def _is_api_path(self):
return self.path.startswith(('/pb', '/api', '/deepseek', '/llm', '/vision'))
def _proxy(self):
if self.path.startswith('/deepseek'):
url = 'https://api.deepseek.com' + self.path[len('/deepseek'):]
elif self.path.startswith('/llm') or self.path.startswith('/vision'):
url = 'http://127.0.0.1:11434' + self.path.split('/', 2)[-1] if '/' in self.path[1:] else self.path
url = f'http://127.0.0.1:11434/{url}' if not url.startswith('http') else url
elif self.path.startswith('/pb'):
rest = self.path[3:] # /pb/api/collections/... → /api/collections/...
url = PB + rest
if not rest.startswith('/api'):
url = PB + '/api' + rest
else:
url = PB + self.path
body = None
if self.headers.get('Content-Length'):
body = self.rfile.read(int(self.headers['Content-Length']))
req = urllib.request.Request(url, data=body, method=self.command)
for k, v in self.headers.items():
if k.lower() not in ('host', 'connection', 'origin', 'referer'):
req.add_header(k, v)
if self.path.startswith('/deepseek') and DEEPSEEK_KEY:
req.add_header('Authorization', f'Bearer {DEEPSEEK_KEY}')
try:
resp = urllib.request.urlopen(req)
self.send_response(resp.status)
for k, v in resp.headers.items():
if k.lower() not in ('transfer-encoding', 'connection'):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(e.code)
self.end_headers()
self.wfile.write(e.read())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', ARGS.port), Handler)
print(f'Serving {DIST} on http://0.0.0.0:{ARGS.port} (API → {PB})')
server.serve_forever()