initial commit
This commit is contained in:
@@ -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 re‑mount (input focus loss)
|
||||
Subagents define child components as nested functions inside parent components. When the parent re‑renders (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 file‑level `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).
|
||||
+53
@@ -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.
|
||||
+75
@@ -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.
|
||||
+126
@@ -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).
|
||||
+46
@@ -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
|
||||
+54
@@ -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`.
|
||||
+50
@@ -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.
|
||||
+121
@@ -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()
|
||||
Reference in New Issue
Block a user