diff --git a/.claire/memory-compiler/knowledge/concepts/ardent-forge-template-builder-ux.md b/.claire/memory-compiler/knowledge/concepts/ardent-forge-template-builder-ux.md new file mode 100644 index 00000000..b3a42524 --- /dev/null +++ b/.claire/memory-compiler/knowledge/concepts/ardent-forge-template-builder-ux.md @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/.claude/agents/quality-engineer.md b/.claude/agents/quality-engineer.md index e3a7f340..02f56450 100755 --- a/.claude/agents/quality-engineer.md +++ b/.claude/agents/quality-engineer.md @@ -268,11 +268,11 @@ Your goal is to prevent quality issues through intelligent pattern enforcement w ## Validation Mode -When dispatched via `/team-plan` or `/build` to validate completed work, the quality-engineer operates in **validation mode** -- a read-only inspection workflow focused on verifying builder output against acceptance criteria. +When dispatched via `/blueprint` or `/impl` to validate completed work, the quality-engineer operates in **validation mode** -- a read-only inspection workflow focused on verifying builder output against acceptance criteria. ### When Activated -- Dispatched as a validator by team orchestration commands (`/team-plan`, `/build`) +- Dispatched as a validator by team orchestration commands (`/blueprint`, `/impl`) - Assigned a task whose purpose is to verify or validate another agent's completed work - Task description references "validate", "verify", or "inspect" against acceptance criteria diff --git a/.claude/commands/adr.md b/.claude/commands/adr.md index 35184f42..db7ca4e9 100755 --- a/.claude/commands/adr.md +++ b/.claude/commands/adr.md @@ -1,77 +1,95 @@ --- description: Create or review Architecture Decision Records -model: opus +model: sonnet --- -# Architecture Decision Record - -Create or update an Architecture Decision Record (ADR) documenting a technical decision, its rationale, and compatibility with existing decisions. - -## When to use -- Implementation deviates from the spec -- A significant technical decision is made that affects future work -- User says "create an ADR", "document this decision", "we're changing approach" -- Detected at a milestone checkpoint during impl-start - -## Model Routing - -This command routes to the architecture category model: - -| Environment | Model | Rationale | -|---|---|---| -| Claude Code (work) | sonnet | ADRs require careful reasoning about compatibility | -| OpenCode (home) | gemini-2.5-flash | Architecture decisions need structured analysis | - -## Workflow - -**CRITICAL:** If $ARGUMENTS is empty, STOP here and ask user: "What decision would you like to document as an ADR?" Do NOT read any files until a description is provided. - -### Step 1: Load context -1. If $ARGUMENTS is NOT empty, user has described the ADR topic: "$ARGUMENTS" -2. If $ARGUMENTS is empty, ask: "What technical decision would you like to document as an ADR?" -3. Do NOT read Spec.md or other files until description is obtained -4. Once you have a description, then check if working within a feature and read its Spec.md -5. Check if this ADR documents a spec deviation -6. Load relevant ADRs in Context/Decisions/ - -### Step 2: Determine ADR type -1. **New decision:** No existing ADR covers this area -2. **Supersed:** Replacing a previous ADR (document why) -3. **Refinement:** Updating an existing decision while preserving original - -### Step 3: Determine ADR number -1. Find highest NNNN in existing ADR filenames -2. Assign NNNN+1 - -### Step 4: Draft ADR content -1. **Context:** What problem or situation motivated this decision -2. **Decision:** What was decided (1-2 sentences, then details) -3. **Consequences:** Benefits, trade-offs, and risks -4. **Compatibility:** Check against overlapping ADRs - - Same files affected - - Same feature area - - Same architectural concern -5. **Related:** Link to feature, files affected, spec section - -### Step 5: Contradiction detection -This is critical. Compare new ADR against all accepted (non-superseded) ADRs: - -**For each overlapping ADR:** -- Present the overlap to user -- Require resolution: - - "Compatible because [reason]" — decisions coexist - - "Supersedes ADR-XXXX" — new replaces old - - "Need to rethink" — reconsider the decision - -**If no overlaps:** Document "No overlapping accepted ADRs found" - -### Step 6: Write ADR file -1. Create `Context/Decisions/NNNN-decision-title.md` (kebab-case) -2. Fill all sections including completed Compatibility section -3. Set status to "Proposed" or "Accepted" based on context - -### Step 7: Update related artifacts -If documenting a spec deviation, update the feature's Spec.md: -- Add entry to Revision History table -- Update affected Testable Assertions status to "Superseded by ADR-NNNN" -- Note ADR number for commit message +# Architecture Decision Record + +Create or update an Architecture Decision Record (ADR) documenting a technical decision, its rationale, and compatibility with existing decisions. + +## When to use +- Implementation deviates from the spec +- A significant technical decision is made that affects future work +- User says "create an ADR", "document this decision", "we're changing approach" +- Detected at a milestone checkpoint during impl-start + +## Model Routing + +This command routes to the architecture category model: + +| Environment | Model | Rationale | +|---|---|---| +| Claude Code (work) | sonnet | ADRs require careful reasoning about compatibility | +| OpenCode (home) | gemini-2.5-flash | Architecture decisions need structured analysis | + +## Workflow + +**CRITICAL:** If $ARGUMENTS is empty, STOP here and ask user: "What decision would you like to document as an ADR?" Do NOT read any files until a description is provided. + +### Step 1: Load context +1. If $ARGUMENTS is NOT empty, user has described the ADR topic: "$ARGUMENTS" +2. If $ARGUMENTS is empty, ask: "What technical decision would you like to document as an ADR?" +3. Do NOT read Spec.md or other files until description is obtained +4. Once you have a description, then check if working within a feature and read its Spec.md +5. Check if this ADR documents a spec deviation +6. Load relevant ADRs in Context/Decisions/ + +### Step 2: Determine ADR type +1. **New decision:** No existing ADR covers this area +2. **Supersed:** Replacing a previous ADR (document why) +3. **Refinement:** Updating an existing decision while preserving original + +### Step 3: Determine ADR number +1. Find highest NNNN in existing ADR filenames +2. Assign NNNN+1 + +### Step 4: Draft ADR content +1. **Context:** What problem or situation motivated this decision +2. **Decision:** What was decided (1-2 sentences, then details) +3. **Consequences:** Benefits, trade-offs, and risks. For non-trivial ADRs, consult the Advisor tool before writing this section: + + ```bash + cat > /tmp/cortex-adr-consequences-.md <<'EOF' + Decision being documented: + [1-2 sentence summary of the decision] + + Known context and constraints: + [paste Context section of the draft ADR] + + Question: what are the two most load-bearing consequences (one benefit, + one risk) that this ADR must name to be honest about the trade-off? + Respond as enumerated bullets. + EOF + + bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-adr-consequences-.md + ``` + + Exit 0: fold the Advisor's response into the Consequences section. Exit 2: draft in-thread without the consult. +4. **Compatibility:** Check against overlapping ADRs + - Same files affected + - Same feature area + - Same architectural concern +5. **Related:** Link to feature, files affected, spec section + +### Step 5: Contradiction detection +This is critical. Compare new ADR against all accepted (non-superseded) ADRs: + +**For each overlapping ADR:** +- Present the overlap to user +- Require resolution: + - "Compatible because [reason]" — decisions coexist + - "Supersedes ADR-XXXX" — new replaces old + - "Need to rethink" — reconsider the decision + +**If no overlaps:** Document "No overlapping accepted ADRs found" + +### Step 6: Write ADR file +1. Create `Context/Decisions/NNNN-decision-title.md` (kebab-case) +2. Fill all sections including completed Compatibility section +3. Set status to "Proposed" or "Accepted" based on context + +### Step 7: Update related artifacts +If documenting a spec deviation, update the feature's Spec.md: +- Add entry to Revision History table +- Update affected Testable Assertions status to "Superseded by ADR-NNNN" +- Note ADR number for commit message diff --git a/.claude/commands/blueprint.md b/.claude/commands/blueprint.md index 3d09e097..56f99f37 100755 --- a/.claude/commands/blueprint.md +++ b/.claude/commands/blueprint.md @@ -1,6 +1,6 @@ --- description: Full 4-phase planning: Spec, Tech, Steps with team orchestration -model: opus +model: sonnet --- # Full Planning Workflow @@ -29,12 +29,31 @@ Generate a complete 4-phase plan for major features or complex tasks. Creates Sp 1. Determine feature number (next available NNN in Context/Features/) 2. Use the `plan-spec` skill to create `Context/Features/NNN-FeatureName/Spec.md` 3. Include: Overview, Problem Statement, User Stories, Requirements (Must/Should/Won't), Testable Assertions, Open Questions, Dependencies -4. Present to user for review and approval before proceeding +4. Present to user for review and approval before proceeding ### Step 3: Create Tech.md (Phase 2) 1. Use the `plan-research` skill to create `Context/Features/NNN-FeatureName/Tech.md` 2. Research and document: Architecture overview, key decisions with options considered, stack-specific details, integration points, risks and unknowns -3. Create ADRs for significant architectural decisions +3. Create ADRs for significant architectural decisions. For each significant decision, consult the Advisor tool before finalising the ADR: + + ```bash + cat > /tmp/cortex-adr-.md <<'EOF' + Context (from Spec.md and research so far): + [paste the relevant requirement, constraint, or trade-off] + + Options considered: + [enumerate the options and their observable consequences] + + Question: which option should the ADR accept, and what are the two + most load-bearing consequences the Consequences section must name? + Respond as enumerated bullets. + EOF + + bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-adr-.md + ``` + + - Exit 0: fold the Advisor's enumerated response into the ADR draft. + - Exit 2: Advisor unavailable; draft the ADR in-thread without the consult. 4. Present to user for review and approval before proceeding ### Step 4: Create Steps.md (Phase 3) @@ -54,13 +73,15 @@ Present all three documents for review: - Spec.md summary: key requirements and assertions - Tech.md summary: architecture decisions and ADRs created - Steps.md summary: task count, team composition, wave structure -- Estimated execution approach: `/impl` vs `/team-impl` Confirm or adjust before proceeding to implementation. -Suggest next step: -- `/impl NNN` to execute via sub-agents -- `/team-impl NNN` to execute via Agent Teams (if cross-domain coordination needed) +### Step 6: Suggest execution command +ALWAYS end by recommending the most appropriate execution command with the feature number: +- `/impl NNN` -- default for most features (hub-and-spoke sub-agent orchestration) +- `/team-impl NNN` -- when cross-domain coordination is needed (peer-to-peer Agent Teams) + +Choose based on the Steps.md content: if tasks span multiple stacks with tight integration points, recommend `/team-impl`. Otherwise recommend `/impl`. ## Rules - Each phase requires user approval before proceeding to the next @@ -68,4 +89,4 @@ Suggest next step: - If the feature is too small for 4-phase planning, suggest `/quick` instead - ADRs are created during Phase 2 for any significant architectural decision - Phase 3 must produce a Steps.md with team orchestration metadata -- Note any ADRs created during planning in the final summary +- Note any ADRs created during planning in the final summary diff --git a/.claude/commands/diagnose.md b/.claude/commands/diagnose.md index c667a265..8cbaa325 100755 --- a/.claude/commands/diagnose.md +++ b/.claude/commands/diagnose.md @@ -3,70 +3,62 @@ description: Check project structure and Cortex health model: sonnet --- -# Doctor: Context Engine Configuration - -Display effective Context Engine configuration, showing model resolution and feature flags. - -## Output - -### Configuration Hierarchy - -Show the config resolution order: - -1. **Adapter defaults** — Built-in defaults from the adapter -2. **User config** — `~/.config/opencode/context-engine.jsonc` (OpenCode) -3. **Project config** — `.opencode/context-engine.jsonc` (current project) - -Each level overrides the previous. The output should show which values come from which level. - -### Model Routing - -Show effective model assignment for all agents: - -| Agent | Model | Source | -|---|---|---| -| build | `provider/model` | project/user/adapter | -| plan | `provider/model` | project/user/adapter | -| qa-python | `provider/model` | project/user/adapter | -| ... | ... | ... | - -And categories: - -| Category | Model | Source | -|---|---|---| -| quick | `provider/model` | project/user/adapter | -| planning | `provider/model` | project/user/adapter | -| ... | ... | ... | - -### Feature Flags - -| Feature | State | Source | -|---|---|---| -| parallel_qa | true/false | project/user/adapter | -| context_aware_compaction | true/false | project/user/adapter | -| notifications | true/false | project/user/adapter | - -### Background Task Concurrency - -``` -Default concurrency: N -Provider limits: - - provider-name: M -``` - -### Disabled Components - -``` -Disabled agents: [list or "none"] -Disabled skills: [list or "none"] -``` - -## Implementation - -1. Read and merge configs in priority order: adapter → user → project -2. For each agent and category, show the resolved value and its source -3. For each feature flag, show state and source -4. Display background task concurrency settings -5. Show any disabled agents/skills - -Use the same config loading logic as the OpenCode plugin (`adapters/opencode/config.ts`). +# Diagnose: Cortex Project Health Check + +Run a structured health check on the current project's Cortex configuration. + +## Checks + +### 1. Directory Structure +Verify required directories exist: +- [ ] `Context/Features/` +- [ ] `Context/Decisions/` +- [ ] `Context/Reviews/` +- [ ] `Context/Backlog/` +- [ ] `.cortex/` +- [ ] `.claude/hooks/` +- [ ] `.claude/agents/` +- [ ] `.claude/commands/` +- [ ] `.claude/skills/` + +### 2. Configuration Files +Verify required files exist and are valid JSON/markdown: +- [ ] `CLAUDE.md` exists and has `## Active Work` section +- [ ] `.claude/settings.json` exists and has hooks configured +- [ ] `.claude/hooks/skill-activation/skill-rules.json` exists +- [ ] `.claude/hooks/skill-activation/agent-rules.json` exists + +### 3. Hook Health +For each hook in `.claude/settings.json`: +- [ ] Script file exists at the declared path +- [ ] Script is valid TypeScript (run `bun check` or parse for syntax errors) +- [ ] No hardcoded secrets (scan for `sk-`, `ghp_`, `AKIA`, password patterns) + +### 4. MCP Context Budget +Check MCP server count and tool count: +- Count enabled MCP servers in `.claude/settings.json` and `.mcp.json` +- Count total tools across all servers +- **WARN** if >10 servers enabled +- **WARN** if >80 tools active +- Display estimated context consumption + +### 5. Session State +- [ ] `.cortex/session.md` -- report if active session exists +- [ ] `.cortex/backups/` -- report backup count and most recent +- [ ] `.cortex/patterns.md` -- report if pattern log exists and entry count + +### 6. Active Work Consistency +- If CLAUDE.md lists an active feature, verify the corresponding + `Context/Features/NNN-Name/` directory exists +- Check for orphaned Steps.md files without matching Spec.md + +## Output Format + +| Check | Status | Details | +|-------|--------|---------| +| Directory structure | PASS/WARN/FAIL | Missing: [...] | +| Config files | PASS/WARN/FAIL | Invalid: [...] | +| Hook health | PASS/WARN/FAIL | Issues: [...] | +| MCP budget | PASS/WARN | Servers: N, Tools: N | +| Session state | INFO | Active/None, N backups | +| Active work | PASS/WARN | Consistency issues: [...] | diff --git a/.claude/commands/impl.md b/.claude/commands/impl.md index 5c29818c..29bbc15d 100755 --- a/.claude/commands/impl.md +++ b/.claude/commands/impl.md @@ -1,11 +1,11 @@ --- description: Execute Steps.md via hub-and-spoke sub-agent orchestration -model: opus +model: sonnet --- # Implementation Orchestrator -Execute a planned feature by reading Steps.md and delegating tasks to specialist agents in waves defined by milestone boundaries. +Execute a planned feature by reading Steps.md and delegating tasks to specialist agents in waves defined by milestone boundaries. Specialists run in the background; the orchestrator coordinates through an event log (see `core/skills/monitor-loop/SKILL.md`) and consults the Advisor tool for strategic decisions. ## Workflow @@ -28,14 +28,18 @@ Execute a planned feature by reading Steps.md and delegating tasks to specialist 5. Parse Steps.md to extract: - Team Members (names, agent types, resume behavior) - Task list with assignments, dependencies, parallel flags - - Milestones with testable assertion references and contract declarations + - Milestones with testable assertion references and contract declarations ### Step 3: Initialize execution session 1. Create `.cortex/session.md` with plan reference and team roster -2. Create TaskCreate entries for each S### task in Steps.md -3. Set dependencies via addBlockedBy based on Depends fields -4. Assign owners based on Assigned fields -5. Update Steps.md Progress: Status = "In progress" +2. Truncate the event log so only this run's events appear: + ```bash + mkdir -p .cortex .cortex/prompts && : > .cortex/events.log + ``` +3. Create TaskCreate entries for each S### task in Steps.md +4. Set dependencies via `addBlockedBy` based on `Depends` fields +5. Assign owners based on `Assigned` fields +6. Update Steps.md Progress: Status = "In progress" ### Step 4: Build wave execution plan Parse milestones to determine execution waves: @@ -46,25 +50,34 @@ Parse milestones to determine execution waves: ### Step 5: Execute waves -For each wave: - -**5a. Spawn agents for the wave** -- Deploy specialist agents via Task tool for each task in the wave -- For tasks marked `Parallel: true` with all deps satisfied: spawn simultaneously -- For sequential tasks: spawn one at a time, wait for completion -- Each agent receives: - - The full Spec.md and Tech.md context - - Their specific task description from Steps.md - - Upstream contracts (actual file content from previous wave) - - File ownership boundaries - - Instructions to use TaskUpdate to mark tasks complete - -**5b. Monitor and coordinate** -- Track task completion via TaskList/TaskUpdate -- Log agent activity to `.cortex/session.md` -- If an agent is blocked, provide guidance or reassign - -**5c. Milestone checkpoint** +For each wave, use the background + Monitor pattern documented in `core/skills/monitor-loop/SKILL.md`. The steps below are the orchestrator-side flow; the skill has the full rationale and failure modes. + +**5a. Render specialist prompts** +Write each task's prompt to `.cortex/prompts/.md` using the Agent Prompt Template below. Keeping prompts in files (a) stops giant inline strings from bloating the orchestrator's turn, and (b) makes them recoverable if the orchestrator dies mid-wave. + +**5b. Spawn the wave in the background** +In a single message, send one `Task({run_in_background: true})` call per parallel task in the wave. Each call's `prompt` field contains the file contents from `.cortex/prompts/.md`. Sequential tasks in the wave spawn one-at-a-time after their upstream `task_done` events arrive. + +**5c. Start the Monitor on events.log** +Immediately after spawning, call Monitor with a filter that matches only milestone events: + +```ts +Monitor({ + description: "cortex events: task_done, contract_ready, milestone_reached, error", + command: `tail -n 0 -F .cortex/events.log | grep --line-buffered -E '"type":"task_done"|"type":"contract_ready"|"type":"milestone_reached"|"type":"error"'`, + timeout_ms: 3600000, + persistent: false, +}); +``` + +**5d. React to events** +- On `task_done` -- call `TaskUpdate({taskId, status: "completed"})`. +- On `contract_ready` -- read the contract file and stash its content for the next wave's prompts. +- On `milestone_reached` -- proceed to step 5e. +- On `error` -- pull that single agent's `TaskOutput({task_id, block: false})` for triage. Never pull output on the happy path. +- If an expected `task_done` is missing after a reasonable deadline (10 minutes per task by default), treat it as a silent failure and pull that agent's output. + +**5e. Milestone checkpoint** When all tasks before a milestone are complete: 1. **Stub detection** -- search completed files for: @@ -75,7 +88,7 @@ When all tasks before a milestone are complete: - `throw NotImplementedError` or `throw new Error("not implemented")` - Empty method/function bodies -2. **Verify test/doc tasks** -- check that all S###-T and S###-D tasks before this milestone are completed +2. **Verify test/doc tasks** -- check that all `S###-T` and `S###-D` tasks before this milestone are completed 3. **Contract extraction** -- for each file path listed under the milestone's Contracts: - Read the file content @@ -83,16 +96,35 @@ When all tasks before a milestone are complete: - Store the content for injection into next wave's agent prompts - Log the delivery to `.cortex/session.md` -4. **Drift checkpoint** -- pull the Testable Assertions referenced in the milestone from Spec.md: - - Present each assertion with its ID - - Ask: "Does the current implementation align? Options:" - - **Aligned** -- proceed to next wave - - **Diverged, need ADR** -- pause, create ADR documenting the deviation - - **Diverged, spec was wrong** -- update Spec.md with revision history entry +4. **Drift checkpoint with Advisor consult** -- pull the Testable Assertions referenced in the milestone from Spec.md. Write a question file and consult the Advisor: + + ```bash + cat > /tmp/cortex-drift-.md <<'EOF' + The following Spec.md testable assertions are attached to this milestone: + [assertion IDs and text] + + The following files were produced by the wave: + [list of changed files with short content excerpts] + + Question: for each assertion, is the implementation (a) aligned, + (b) diverged and needs an ADR, or (c) diverged because the spec was wrong? + Respond as enumerated bullets, one per assertion. + EOF + + bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-drift-.md + ``` + + - Exit 0: present the Advisor's enumerated verdict alongside each assertion and ask the user to confirm. + - Exit 2: the Advisor is unavailable. Fall back to in-thread reasoning -- present the assertions yourself and ask. + + For each assertion, the user picks: + - **Aligned** -- proceed to next wave + - **Diverged, need ADR** -- pause, create ADR documenting the deviation + - **Diverged, spec was wrong** -- update Spec.md with revision history entry 5. Update Steps.md Progress section with last completed milestone -6. **Continue to next wave** with extracted contracts injected into agent prompts +6. **Continue to next wave** with extracted contracts injected into the next wave's prompt files ### Step 6: Final validation After all waves complete: @@ -102,20 +134,22 @@ After all waves complete: 4. Report PASS/FAIL per criterion ### Step 7: Complete -1. Update Steps.md Progress: Status = "Complete" -2. Run Validation Commands and capture results -3. Archive `.cortex/session.md` to `.cortex/archive/` -4. Present completion report -5. Suggest next steps: +1. Stop the Monitor via `TaskStop` (or let it time out on its own) +2. Update Steps.md Progress: Status = "Complete" +3. Run Validation Commands and capture results +4. Archive `.cortex/session.md` to `.cortex/archive/` +5. `.cortex/events.log` stays in place until the next `/impl` run truncates it. `backup-core.ts` will pick it up in any backup snapshot. +6. Present completion report +7. Suggest next steps: - Run `/qa` for parallel stack-specific quality checks - - Run `/review` to capture findings + - Run `/review-capture` to capture findings - Run `/commit` when ready ## Agent Prompt Template -When spawning each agent, provide this context: +When rendering each agent's prompt file, use this template: -` +``` You are [NAME], the [ROLE] on this team. FEATURE CONTEXT: @@ -136,18 +170,134 @@ CONTRACTS YOU MUST PRODUCE (your outputs): [What downstream agents need from you, from the next milestone's Contracts] [If final wave: "None -- you are consuming, not producing"] -COORDINATION: +COORDINATION (event log): +- When you finish a task, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type task_done --task +- When you write a contract file listed in the milestone Contracts, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type contract_ready --contract +- If you hit a blocker you cannot resolve, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type error --note "" +- Do NOT emit events for routine tool calls. The log is for milestones only. + +FILE OWNERSHIP RULES: - Mark your tasks complete via TaskUpdate when done -- If you change an interface another agent depends on, note it clearly +- If you change an interface another agent depends on, note it clearly in `.cortex/session.md` - Read all relevant files and load skills before starting work Think hard and provide thorough implementation. -` +``` + +## Orchestration Tools Reference + +### Creating and managing tasks + +```typescript +// Create a task for each S### entry in Steps.md +TaskCreate({ + subject: "S001: Set up database schema", + description: "Create migration files for user and session tables. See Steps.md S001.", + activeForm: "Setting up database schema", +}); + +// Set dependencies from Steps.md Depends fields +TaskUpdate({ + taskId: "2", + addBlockedBy: ["1"], +}); + +// Assign owner from Steps.md Assigned fields +TaskUpdate({ + taskId: "1", + owner: "builder-api", +}); + +// Mark complete when the matching task_done event arrives +TaskUpdate({ + taskId: "1", + status: "completed", +}); + +// Check progress across all tasks +TaskList({}); +``` + +### Spawning agents in the background + +Sub-agents run on Sonnet. Specialists execute against a detailed prompt file (spec/tech excerpts, file ownership, upstream contracts, a concrete task list) -- that's execution work, not design work. The design decisions already happened upstream in `/blueprint`, and the milestone drift check (which consults the Advisor tool for an Opus verdict) plus the final quality-engineer validation run catch any misalignment. Set `model: "sonnet"` explicitly so future readers don't have to trace orchestrator inheritance. + +```typescript +// All parallel tasks in the wave spawn in a single message +Task({ + description: "S002: Build API layer", + prompt: "", + subagent_type: "general-purpose", + model: "sonnet", + run_in_background: true, +}); +Task({ + description: "S003: Build UI layer", + prompt: "", + subagent_type: "general-purpose", + model: "sonnet", + run_in_background: true, +}); +``` + +### Monitoring the wave via the event log + +```typescript +Monitor({ + description: "cortex events: task_done, contract_ready, milestone_reached, error", + command: `tail -n 0 -F .cortex/events.log | grep --line-buffered -E '"type":"task_done"|"type":"contract_ready"|"type":"milestone_reached"|"type":"error"'`, + timeout_ms: 3600000, + persistent: false, +}); + +// On error events only, pull that one agent's output for triage: +TaskOutput({ task_id: "agentId", block: false, timeout: 5000 }); +``` + +Never block on `TaskOutput` on the happy path. The event log is the authoritative wave-progress signal. + +### Consulting the Advisor at milestones + +```bash +# Write a question file with assertions + evidence, then consult the Advisor +cat > /tmp/cortex-drift-m1.md <<'EOF' +[assertion IDs and text] +[list of changed files] +Question: aligned, diverged-need-ADR, or diverged-spec-wrong per assertion? +EOF + +bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-drift-m1.md +# Exit 0 -> stdout has the enumerated verdict +# Exit 2 -> Advisor unavailable; fall back to in-thread reasoning +``` + +### Resuming agents + +When an agent needs follow-up work (e.g., milestone corrections), resume it to preserve context: + +```typescript +Task({ + description: "S001: Fix schema validation", + prompt: "The milestone check found a missing index. Add it to...", + subagent_type: "general-purpose", + resume: "abc123", +}); +``` + +When to resume vs start fresh: +- **Resume**: Continuing related work where the agent needs prior context +- **Fresh**: Unrelated task or clean slate preferred ## Rules - Never skip milestone checkpoints -- they are not optional +- Never pull `TaskOutput` on the happy path -- events are authoritative +- Set `model: "sonnet"` explicitly on spawned sub-agent `Task()` calls. Specialists execute a well-specified plan; Opus-rate reasoning is reserved for the Advisor tool at milestone boundaries. - If a task reveals the plan is wrong, pause and discuss with the user - If context window is filling up, save progress to Steps.md and `.cortex/session.md` - Parallel agents must not modify the same files - Contract files must exist before downstream agents are spawned -- The quality-engineer always runs last in validation mode (read-only) +- The quality-engineer always runs last in validation mode (read-only) +- Advisor-cli exit code 2 means fall back to in-thread reasoning -- never fail the wave on advisor unavailability diff --git a/.claude/commands/quick.md b/.claude/commands/quick.md index 99ed39a8..42a7b4f7 100755 --- a/.claude/commands/quick.md +++ b/.claude/commands/quick.md @@ -1,8 +1,8 @@ ---- -description: Lightweight single-file plan for small tasks -model: sonnet ---- - +--- +description: Lightweight single-file plan for small tasks +model: sonnet +--- + # Quick Plan Generate a lightweight, single-file plan for small tasks or exploratory work. Skips full 4-phase workflow. @@ -45,7 +45,13 @@ This command routes to a lower-cost model tier for rapid planning: ### Step 3: Review with user 1. Present the quick plan 2. Confirm or adjust approach -3. If approved, user can proceed with implementation -### Step 4: Clean up +### Step 4: Suggest execution command +ALWAYS end by recommending the most appropriate execution command: +- `/impl` -- default for most quick tasks (hub-and-spoke sub-agent orchestration) +- `/team-impl` -- when the task spans multiple stacks with tight integration points (peer-to-peer Agent Teams) + +Quick plans typically use `/impl` since they are smaller in scope. + +### Step 5: Clean up When task is complete, move quick plan to `Context/Backlog/Done/` for reference, or delete if not needed. diff --git a/.claude/commands/review-resolve.md b/.claude/commands/review-resolve.md index f3e5b760..2fa63586 100755 --- a/.claude/commands/review-resolve.md +++ b/.claude/commands/review-resolve.md @@ -1,11 +1,11 @@ --- -description: Work through captured review findings by category -model: opus +description: Work through captured review findings with interactive triage +model: sonnet --- # Review Resolution -Work through a captured review file, resolve each finding by category, generate appropriate planning artifacts, and update the review file with resolution status. +Interactively triage and resolve review findings. Claude recommends actions, but the user makes every decision. ## When to use - At the start of a fresh session after a review was captured @@ -14,53 +14,66 @@ Work through a captured review file, resolve each finding by category, generate ## Workflow -**CRITICAL:** This should run in a **fresh session** with full context budget, not in the same session that performed the review. +**CRITICAL:** Run in a **fresh session** with full context budget -- not the session that performed the review. ### Step 1: Load the review file -1. If user specified a file, load that one -2. Otherwise, list files in `Context/Reviews/` and find the most recent unresolved -3. If no unresolved reviews exist, inform the user -4. Load the associated feature's Spec.md, Steps.md, and Tech.md for context - -### Step 2: Work through Fix-Now findings ([FIX]) -Process each in priority order (Critical, High, Medium, Low): -1. Open referenced file and line -2. Apply the fix -3. Update finding: Status = Fixed, Resolution = description of change -4. If fix is more complex than expected, reclassify to [TASK] or [ADR] - -### Step 3: Work through Missing Task findings ([TASK]) -For each finding: -1. Determine next available S### number from Steps.md -2. Add new task to Steps.md in the appropriate phase -3. If related to a Testable Assertion, verify it exists in Spec.md -4. Update finding: Status = Task created, Resolution = S### reference -5. Do NOT implement the task now -- just track it - -### Step 4: Work through Architectural Concern findings ([ADR]) -For each finding: -1. Present the concern with context (what reviewer found, what Tech.md says) -2. Ask user to decide: - - **Create ADR** -- use the adr-create skill - - **Dismiss** -- user provides reason - - **Defer** -- mark for later consideration -3. Update finding status accordingly - -### Step 5: Work through Convention Gap findings ([RULE]) -For each finding: -1. Identify which rule file should be updated -2. Present the suggested addition to the user -3. If approved, add to the appropriate rule file -4. Update finding status - -### Step 6: Update review file header -After processing all findings, update status and add Resolution Summary table. - -### Step 7: Commit resolution work -Use the impl-commit skill. Commit message should reference the review file. - -### Step 8: Suggest next steps -- New tasks added -- "Run /impl to work through new tasks" +Find the specified or most recent unresolved review in `Context/Reviews/`. Load associated Spec.md, Steps.md, and Tech.md for context. Parse all findings into a working list. + +### Step 2: Bulk accept +Present a summary table of findings grouped by category and severity. Offer to accept suggested actions in bulk -- the category from capture IS the suggested action. User can accept all, accept by category, or skip to individual triage. Update all affected findings on disk after each bulk accept. + +### Step 3: Individual triage +Process each remaining finding one at a time. For each, show the finding details and Claude's recommended action, then present a numbered menu: + +1. **Fix** -- apply the fix inline +2. **Task** -- add S### entry to Steps.md +3. **ADR** -- create architecture decision record +4. **Rule** -- add/update convention in rule file +5. **Defer** -- push to tracker (`.cortex/config.json`) or backlog +6. **Discard** -- drop the finding + +Wait for the user's choice before proceeding. Save progress to the review file after each decision. + +**Advisor consult on ambiguous findings.** When the recommended action is not obvious (for example: a finding that could plausibly be Fix, Task, or ADR depending on blast radius), write a question file and consult the Advisor before presenting the menu: + +```bash +cat > /tmp/cortex-triage-.md <<'EOF' +Finding [ID] from [review file]: +[verbatim finding text] + +Files referenced: +[list] + +Category from capture: [FIX/TASK/ADR/RULE] + +Question: is this best handled as (1) Fix inline, (2) Task in Steps.md, +(3) ADR, or (4) Rule addition? Which option and why, in <=100 words, +as enumerated bullets. +EOF + +bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-triage-.md +``` + +Exit 0: present the Advisor's recommendation alongside the numbered menu as Claude's suggestion. Exit 2: present the menu without an Advisor suggestion. Either way, the user decides. + +### Step 4: Execute actions +Process all triaged findings in order: Fix, Task, ADR, Rule, Defer, Discard. Update each finding's status and resolution in the review file. + +### Step 5: Finalize +Update the review file header and add a Resolution Summary table. Commit via impl-commit skill referencing the review file. + +### Step 6: Suggest next steps +- New tasks -- "Run /impl to work through new tasks" - ADRs created -- "Review the new ADRs" - Rules updated -- "Updated rules apply to future code automatically" +- Items deferred -- "Check the tracker/backlog for deferred items" - Always -- "Run /review-verify to confirm all findings resolved" + +## Key rules +- NEVER auto-resolve, auto-dismiss, or skip findings +- Claude recommends but user decides +- Discard does not require a reason +- Bulk accept phase comes before individual triage phase +- The review file is the single source of truth + +See `core/skills/review-resolve/SKILL.md` for full workflow details. diff --git a/.claude/commands/review-verify.md b/.claude/commands/review-verify.md index fb195712..16a4031e 100755 --- a/.claude/commands/review-verify.md +++ b/.claude/commands/review-verify.md @@ -1,8 +1,8 @@ ---- -description: Verify all review findings have been addressed -model: sonnet ---- - +--- +description: Verify all review findings have been addressed +model: sonnet +--- + # Review Verification Verify that review feedback has been addressed and the implementation aligns with decisions made during review. @@ -49,7 +49,15 @@ Briefly: - New issues found 5. Mark review as verified in capture file -### Step 3: Confirm review status +### Step 3: Update review file on PASS +If the agent's verdict is **PASS** (or **WARNING** with only deferred items): +1. Open the review capture file +2. Check the box: `- [x] Review verified by review-verify agent` +3. This is the only edit -- do not modify any other content in the review file + +If the verdict is **FAIL**, do not check the box. + +### Step 4: Confirm review status Present results: - Which reviews were checked - Status of each review diff --git a/.claude/commands/security-scan.md b/.claude/commands/security-scan.md new file mode 100644 index 00000000..df9c9e8f --- /dev/null +++ b/.claude/commands/security-scan.md @@ -0,0 +1,73 @@ +--- +description: Scan Cortex and Claude Code configuration for security issues +model: sonnet +--- + +# Security Scan: Configuration Audit + +Audit the project's Claude Code configuration for security risks. + +## Scan Categories + +### 1. Secrets in Configuration +Scan these files for API keys, tokens, passwords: +- `CLAUDE.md` +- `.claude/settings.json` +- `.mcp.json` +- All files in `.claude/hooks/` +- All files in `.claude/agents/` +- All files in `.claude/skills/` +- All files in `.claude/commands/` +- All files in `.claude/rules/` + +Patterns to detect: `sk-`, `ghp_`, `AKIA`, `glpat-`, `xoxb-`, inline passwords, +base64-encoded secrets, private key material (`-----BEGIN`). + +### 2. Hook Injection Analysis +For each hook script in `.claude/settings.json`: +- Check for shell injection risks (unescaped `${}` in command strings) +- Check for dynamic code execution patterns (dynamic `Function` construction, string-to-code execution) +- Check for network calls (`fetch`, `http`, `net`) that could exfiltrate data +- Verify hooks read from stdin only (not from env vars that could be spoofed) + +### 3. Permission Audit +Review `.claude/settings.json` permissions: +- Flag overly broad `Bash(*)` allows +- Flag missing deny rules for destructive commands (`rm -rf`, `format`) +- Flag if `deny` list is empty (should explicitly deny dangerous patterns) + +### 4. MCP Server Risk Profile +For each MCP server in `.mcp.json` and `.claude/settings.json`: +- Identify server source (npm package, URL, local path) +- Flag servers from unknown or unverified sources +- Flag servers with broad tool permissions +- Check for `ANTHROPIC_API_KEY` or other secrets passed as env vars to servers + +### 5. Agent Definition Review +For each agent in `.claude/agents/`: +- Check for prompt injection vectors (instructions that could be overridden) +- Verify tool restrictions are appropriate for the agent's role +- Flag agents with unrestricted tool access + +## Output Format + +### Security Scan Results + +**Overall Grade: A-F** + +| Category | Grade | Findings | +|----------|-------|----------| +| Secrets | A-F | N issues | +| Hook safety | A-F | N issues | +| Permissions | A-F | N issues | +| MCP servers | A-F | N issues | +| Agent defs | A-F | N issues | + +### Critical Findings (fix immediately) +[List with file, line, and remediation] + +### Warnings (fix soon) +[List with file and recommendation] + +### Info (best practices) +[List of improvement suggestions] diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index f953b771..2774e3d5 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -92,7 +92,24 @@ Check that Cortex hooks are wired in `.claude/settings.json`: If hooks are missing, inform the user and suggest running `bun run deploy` to install them, or offer to add them via the merge-settings script. -### Step 7: Summary +### Step 7: Detect issue tracker + +Check for available issue tracker CLIs so `/review-resolve` can defer findings to external issues. + +1. **Check GitHub CLI**: Run `gh auth status` to see if `gh` is installed and authenticated +2. **Check GitLab CLI**: Run `glab auth status` to see if `glab` is installed and authenticated +3. **Resolve which tracker to use**: + - If both are found and authenticated, ask the user which they prefer + - If only one is found and authenticated, confirm it with the user + - If neither is found (or not authenticated), inform the user that deferred findings will use the local backlog instead +4. **Store the result** in `.cortex/config.json`: + - GitHub: `{ "issueTracker": { "type": "github", "cli": "gh" } }` + - GitLab: `{ "issueTracker": { "type": "gitlab", "cli": "glab" } }` + - No tracker: `{ "issueTracker": { "type": "backlog" } }` + +If `.cortex/config.json` already exists, merge the `issueTracker` key into it without overwriting other settings. + +### Step 8: Summary Present what was created/verified: - Stacks detected @@ -100,6 +117,7 @@ Present what was created/verified: - CLAUDE.md status (created or updated) - Stack rules created - Hooks status +- Issue tracker configured (GitHub, GitLab, or local backlog) Suggest next steps: 1. Review the generated CLAUDE.md diff --git a/.claude/commands/team-impl.md b/.claude/commands/team-impl.md index 8b749972..b91bbb21 100755 --- a/.claude/commands/team-impl.md +++ b/.claude/commands/team-impl.md @@ -1,11 +1,11 @@ --- description: Execute Steps.md via Agent Teams with peer-to-peer coordination -model: opus +model: sonnet --- # Team Implementation (Agent Teams) -Execute a planned feature using Claude Code's Agent Teams for peer-to-peer coordination. Unlike `/impl` (hub-and-spoke sub-agents), `/team-impl` spawns a coordinated team where agents communicate directly, share a task list, and collaborate in real-time. +Execute a planned feature using Claude Code's Agent Teams for peer-to-peer coordination. Unlike `/impl` (hub-and-spoke sub-agents), `/team-impl` spawns a coordinated team where agents communicate directly, share a task list, and collaborate in real-time. The team lead still uses the Monitor+event-log pattern (`core/skills/monitor-loop/SKILL.md`) for wave coordination and the Advisor tool for milestone drift decisions. ## When to use instead of /impl - Cross-domain integration where agents need to share contracts in real-time @@ -21,9 +21,9 @@ Agent Teams must be enabled. Verify `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is se **CRITICAL:** If $ARGUMENTS is empty, STOP and ask which feature to implement. ### Step 1: Load plan (identical to /impl) -1. Identify feature from `` +1. Identify feature from $ARGUMENTS 2. Read Steps.md, Spec.md, Tech.md, CLAUDE.md -3. Parse team members, task graph, milestones, contracts, acceptance criteria +3. Parse team members, task graph, milestones, contracts, acceptance criteria ### Step 2: Contract chain analysis Analyze the task dependency graph to determine spawning order: @@ -49,57 +49,106 @@ Create the agent team based on Steps.md Team Members section: - **File ownership boundaries** -- assign exclusive ownership of specific files/directories - **Full context per teammate** -- every teammate receives the complete plan, their tasks, upstream contracts, and file ownership boundaries -### Step 4: Contract-first execution +### Step 4: Contract-first execution with event log + Monitor + +Before spawning any teammates, initialise the event log and `.cortex/prompts/`: + +```bash +mkdir -p .cortex .cortex/prompts && : > .cortex/events.log +``` **Wave 1 (Foundation):** -1. Spawn upstream teammates with no dependencies -2. They work on foundational tasks: schemas, shared types, interfaces -3. **Wait for their contracts.** Do not proceed until: - - Upstream teammates mark foundation tasks complete - - Contract files exist and contain concrete content - - Milestone drift check passes against Spec.md assertions +1. Render each upstream teammate's prompt file to `.cortex/prompts/.md` using the template below. +2. Spawn upstream teammates in the background in a single message: + ```ts + Task({ + description: "S001: Set up shared types", + prompt: "", + subagent_type: "general-purpose", + run_in_background: true, + }); + ``` +3. Start the Monitor on `.cortex/events.log` so the team lead sees `task_done`, `contract_ready`, `milestone_reached`, and `error` events: + ```ts + Monitor({ + description: "cortex events: task_done, contract_ready, milestone_reached, error", + command: `tail -n 0 -F .cortex/events.log | grep --line-buffered -E '"type":"task_done"|"type":"contract_ready"|"type":"milestone_reached"|"type":"error"'`, + timeout_ms: 3600000, + persistent: false, + }); + ``` +4. **Wait for their contracts.** Do not proceed until: + - The expected `task_done` events for the foundation wave have arrived + - The expected `contract_ready` events for the foundation wave have arrived + - Contract files exist on disk and contain concrete content + - Milestone drift check passes (see step 5) **Wave 2+ (Parallel Execution):** -1. Read contract files, inject actual content into each downstream teammate's prompt -2. Downstream teammates work in parallel on their assigned tasks -3. Teammates message each other directly when: +1. Read contract files listed in the completed milestone, inject actual content into each downstream teammate's prompt file at `.cortex/prompts/.md`. +2. Spawn downstream teammates in the background (same shape as Wave 1). +3. Teammates message each other directly via `SendMessage` when: - They change an interface another teammate depends on - They are blocked and need input from another teammate - They discover a contract mismatch or integration issue +4. The team lead continues to react to event log events, not individual `TaskOutput` calls. **Final Wave (Validation):** -1. After all implementation teammates complete, spawn quality-engineer -2. Quality engineer validates against Spec.md testable assertions -3. Quality engineer runs all Validation Commands from Steps.md -4. Reports PASS or FAIL per acceptance criterion -5. If FAIL: identify which teammate's work needs correction +1. After all implementation teammates complete, spawn quality-engineer (still in the background, still reporting via the event log). +2. Quality engineer validates against Spec.md testable assertions. +3. Quality engineer runs all Validation Commands from Steps.md. +4. Reports PASS or FAIL per acceptance criterion. +5. If FAIL: identify which teammate's work needs correction via the `error`/`note` fields; resume that teammate with the specific correction. -### Step 5: Milestone checkpoints (same as /impl) +### Step 5: Milestone checkpoints with Advisor consult At each milestone boundary, the team lead: -1. Runs stub detection -2. Verifies all test/doc tasks are complete -3. Extracts contracts from declared file paths -4. Runs drift check against Spec.md testable assertions +1. Runs stub detection on changed files +2. Verifies all `S###-T`/`S###-D` tasks are complete for this milestone +3. Extracts contracts from declared file paths and stores them for the next wave's prompts +4. Runs the drift checkpoint, consulting the Advisor tool first: + + ```bash + cat > /tmp/cortex-drift-.md <<'EOF' + Milestone: [milestone name] + + Spec.md testable assertions referenced by this milestone: + [assertion IDs and text] + + Files produced by the completing wave: + [list of changed files with short excerpts] + + Question: for each assertion, is the implementation (a) aligned, + (b) diverged and needs an ADR, or (c) diverged because the spec was wrong? + Respond as enumerated bullets, one per assertion. + EOF + + bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-drift-.md + ``` + + - Exit 0: present the Advisor's verdict alongside each assertion and ask the user to confirm. + - Exit 2: Advisor unavailable; fall back to in-thread reasoning -- the team lead reasons in-thread on Sonnet and asks the user directly. + 5. Updates Steps.md Progress section ### Step 6: Monitoring While the team works: -- Watch shared task list for stalled or stuck tasks -- If a teammate is blocked, intervene with guidance +- React to event log notifications (`task_done`, `contract_ready`, `milestone_reached`, `error`). +- Only pull a teammate's full `TaskOutput` when an `error` event arrives or a `task_done` event is missing past its deadline. +- If a teammate is blocked, intervene via `SendMessage` with guidance - If contract mismatches are discovered, mediate and clarify - Log all coordination to `.cortex/session.md` ### Step 7: Shutdown -1. Verify all tasks in shared task list show as complete +1. Verify all tasks in shared task list show as complete (matching the wave plan) 2. Run all Validation Commands and capture results -3. Shut down all teammates and clean up team -4. Update Steps.md Progress: Status = "Complete" -5. Archive `.cortex/session.md` to `.cortex/archive/` -6. Present completion report +3. Stop the Monitor via `TaskStop` (or let it time out) +4. Shut down all teammates and clean up team +5. Update Steps.md Progress: Status = "Complete" +6. Archive `.cortex/session.md` to `.cortex/archive/` +7. Present completion report ## Teammate Prompt Template -` +``` You are [NAME], the [ROLE] on this team. PLAN CONTEXT: @@ -119,18 +168,121 @@ CONTRACTS YOU MUST PRODUCE (your outputs): [What downstream teammates need from you] [If final wave: "None -- you are consuming, not producing"] -COORDINATION: -- Message the lead when your contract is ready -- Message [OTHER_TEAMMATE] directly if you change [SPECIFIC_INTERFACE] +COORDINATION (event log): +- When you finish a task, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type task_done --task +- When you write a contract file listed in the milestone Contracts, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type contract_ready --contract +- If you hit a blocker you cannot resolve, run: + bun run .claude/hooks/event-log/event-log.ts append --source [NAME] --type error --note "" +- Message the team lead via the event log's `error` channel or via `SendMessage` only when you need help. +- Message [OTHER_TEAMMATE] directly via `SendMessage` if you change [SPECIFIC_INTERFACE] - Mark your task complete in the shared task list when done -` +- Do NOT emit events for routine tool calls. The event log is for milestones only. +``` ## Fallback If Agent Teams is unavailable or fails to initialize, fall back to `/impl` (sub-agent execution). Inform the user: "Agent Teams unavailable. Falling back to /impl with sub-agent execution." +## Orchestration Tools Reference + +### Task list setup + +```typescript +TaskCreate({ + subject: "S001: Set up database schema", + description: "Create migration files for user and session tables. See Steps.md S001.", + activeForm: "Setting up database schema", +}); + +TaskUpdate({ taskId: "2", addBlockedBy: ["1"] }); +TaskUpdate({ taskId: "1", owner: "builder-api" }); + +TaskList({}); +TaskGet({ taskId: "1" }); +``` + +### Spawning teammates in the background + +Teammates run on Sonnet. They execute against a detailed prompt file (spec/tech excerpts, file ownership, upstream contracts, a concrete task list) -- that's execution work, not design work. The design decisions already happened upstream in `/blueprint`, and the milestone drift check (which consults the Advisor tool for an Opus verdict) plus the final quality-engineer validation run catch any misalignment. Set `model: "sonnet"` explicitly so future readers don't have to trace team-lead inheritance. + +```typescript +Task({ + description: "S001: Set up shared types", + prompt: "", + subagent_type: "general-purpose", + model: "sonnet", + run_in_background: true, +}); + +Task({ + description: "S002: Build API layer", + prompt: "", + subagent_type: "general-purpose", + model: "sonnet", + run_in_background: true, +}); +Task({ + description: "S003: Build UI layer", + prompt: "", + subagent_type: "general-purpose", + model: "sonnet", + run_in_background: true, +}); +``` + +### Monitoring via the event log + +```typescript +Monitor({ + description: "cortex events: task_done, contract_ready, milestone_reached, error", + command: `tail -n 0 -F .cortex/events.log | grep --line-buffered -E '"type":"task_done"|"type":"contract_ready"|"type":"milestone_reached"|"type":"error"'`, + timeout_ms: 3600000, + persistent: false, +}); + +// Only on error events -- never on the happy path: +TaskOutput({ task_id: "agentId", block: false, timeout: 5000 }); + +// Mark task complete after a task_done event arrives: +TaskUpdate({ taskId: "1", status: "completed" }); +``` + +### Consulting the Advisor at milestones + +```bash +cat > /tmp/cortex-drift-m1.md <<'EOF' +[assertions + changed files + question] +EOF + +bun run .claude/hooks/advisor/advisor-cli.ts --question-file /tmp/cortex-drift-m1.md +# Exit 0 -> stdout has enumerated verdict +# Exit 2 -> fall back to in-thread reasoning +``` + +### Resuming teammates + +When a teammate needs corrections after a milestone check, resume to preserve their context: + +```typescript +Task({ + description: "S002: Fix API contract mismatch", + prompt: "The milestone check found the response shape differs from...", + subagent_type: "general-purpose", + resume: "abc123", +}); +``` + +When to resume vs start fresh: +- **Resume**: Corrections, follow-up work, or the teammate needs prior context +- **Fresh**: Unrelated task or clean slate preferred + ## Rules - You are the team lead. You orchestrate, you do not implement. - Never bypass the team to "just do it faster" - Never spawn teammates without clear contracts from upstream - Never let teammates work on overlapping files -- Milestone drift checks are mandatory, not optional +- Never pull `TaskOutput` on the happy path -- events are authoritative +- Set `model: "sonnet"` explicitly on spawned teammate `Task()` calls. Teammates execute a well-specified plan; Opus-rate reasoning is reserved for the Advisor tool at milestone boundaries. +- Milestone drift checks are mandatory, not optional +- Advisor-cli exit code 2 means fall back to in-thread reasoning -- never fail the wave on advisor unavailability diff --git a/.claude/memory-compiler/.claude/settings.json b/.claude/memory-compiler/.claude/settings.json new file mode 100644 index 00000000..672746fb --- /dev/null +++ b/.claude/memory-compiler/.claude/settings.json @@ -0,0 +1,40 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "uv run python hooks/session-start.py", + "timeout": 15 + } + ] + } + ], + "PreCompact": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "uv run python hooks/pre-compact.py", + "timeout": 10 + } + ] + } + ], + "SessionEnd": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "uv run python hooks/session-end.py", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.claude/memory-compiler/.gitignore b/.claude/memory-compiler/.gitignore new file mode 100644 index 00000000..b1429312 --- /dev/null +++ b/.claude/memory-compiler/.gitignore @@ -0,0 +1,31 @@ +# Python +__pycache__/ +*.pyc +.venv/ + +# Runtime state (regenerated by scripts) +scripts/state.json +scripts/last-flush.json +scripts/flush.log +scripts/compile.log +scripts/session-flush-* +scripts/flush-context-* + +# Generated content (built up per-user by hooks and scripts) +daily/ +knowledge/ + +# Reports (regenerated by lint) +reports/ + +# uv lock (regenerated by uv sync) +scripts/uv.lock + +# Claude Code local settings +.claude/settings.local.json + +# OS files +.DS_Store +Thumbs.db + +.obsidian/ diff --git a/.claude/memory-compiler/AGENTS.md b/.claude/memory-compiler/AGENTS.md new file mode 100644 index 00000000..1039e319 --- /dev/null +++ b/.claude/memory-compiler/AGENTS.md @@ -0,0 +1,518 @@ +# AGENTS.md - Personal Knowledge Base Schema + +> Adapted from [Andrej Karpathy's LLM Knowledge Base](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) architecture. +> Instead of ingesting external articles, this system compiles knowledge from your own AI conversations. + +## The Compiler Analogy + +``` +daily/ = source code (your conversations - the raw material) +LLM = compiler (extracts and organizes knowledge) +knowledge/ = executable (structured, queryable knowledge base) +lint = test suite (health checks for consistency) +queries = runtime (using the knowledge) +``` + +You don't manually organize your knowledge. You have conversations, and the LLM handles the synthesis, cross-referencing, and maintenance. + +--- + +## Architecture + +### Layer 1: `daily/` - Conversation Logs (Immutable Source) + +Daily logs capture what happened in your AI coding sessions. These are the "raw sources" - append-only, never edited after the fact. + +``` +daily/ +├── 2026-04-01.md +├── 2026-04-02.md +├── ... +``` + +Each file follows this format: + +```markdown +# Daily Log: YYYY-MM-DD + +## Sessions + +### Session (HH:MM) - Brief Title + +**Context:** What the user was working on. + +**Key Exchanges:** +- User asked about X, assistant explained Y +- Decided to use Z approach because... +- Discovered that W doesn't work when... + +**Decisions Made:** +- Chose library X over Y because... +- Architecture: went with pattern Z + +**Lessons Learned:** +- Always do X before Y to avoid... +- The gotcha with Z is that... + +**Action Items:** +- [ ] Follow up on X +- [ ] Refactor Y when time permits +``` + +### Layer 2: `knowledge/` - Compiled Knowledge (LLM-Owned) + +The LLM owns this directory entirely. Humans read it but rarely edit it directly. + +``` +knowledge/ +├── index.md # Master catalog - every article with one-line summary +├── log.md # Append-only chronological build log +├── concepts/ # Atomic knowledge articles +├── connections/ # Cross-cutting insights linking 2+ concepts +└── qa/ # Filed query answers (compounding knowledge) +``` + +### Layer 3: This File (AGENTS.md) + +The schema that tells the LLM how to compile and maintain the knowledge base. This is the "compiler specification." + +--- + +## Structural Files + +### `knowledge/index.md` - Master Catalog + +A table listing every knowledge article. This is the primary retrieval mechanism - the LLM reads this FIRST when answering any query, then selects relevant articles to read in full. + +Format: + +```markdown +# Knowledge Base Index + +| Article | Summary | Compiled From | Updated | +|---------|---------|---------------|---------| +| [[concepts/supabase-auth]] | Row-level security patterns and JWT gotchas | daily/2026-04-02.md | 2026-04-02 | +| [[connections/auth-and-webhooks]] | Token verification patterns shared across Supabase auth and Stripe webhooks | daily/2026-04-02.md, daily/2026-04-04.md | 2026-04-04 | +``` + +### `knowledge/log.md` - Build Log + +Append-only chronological record of every compile, query, and lint operation. + +Format: + +```markdown +# Build Log + +## [2026-04-01T14:30:00] compile | Daily Log 2026-04-01 +- Source: daily/2026-04-01.md +- Articles created: [[concepts/nextjs-project-structure]], [[concepts/tailwind-setup]] +- Articles updated: (none) + +## [2026-04-02T09:00:00] query | "How do I handle auth redirects?" +- Consulted: [[concepts/supabase-auth]], [[concepts/nextjs-middleware]] +- Filed to: [[qa/auth-redirect-handling]] +``` + +--- + +## Article Formats + +### Concept Articles (`knowledge/concepts/`) + +One article per atomic piece of knowledge. These are facts, patterns, decisions, preferences, and lessons extracted from your conversations. + +```markdown +--- +title: "Concept Name" +aliases: [alternate-name, abbreviation] +tags: [domain, topic] +sources: + - "daily/2026-04-01.md" + - "daily/2026-04-03.md" +created: 2026-04-01 +updated: 2026-04-03 +--- + +# Concept Name + +[2-4 sentence core explanation] + +## Key Points + +- [Bullet points, each self-contained] + +## Details + +[Deeper explanation, encyclopedia-style paragraphs] + +## Related Concepts + +- [[concepts/related-concept]] - How it connects + +## Sources + +- [[daily/2026-04-01.md]] - Initial discovery during project setup +- [[daily/2026-04-03.md]] - Updated after debugging session +``` + +### Connection Articles (`knowledge/connections/`) + +Cross-cutting synthesis linking 2+ concepts. Created when a conversation reveals a non-obvious relationship. + +```markdown +--- +title: "Connection: X and Y" +connects: + - "concepts/concept-x" + - "concepts/concept-y" +sources: + - "daily/2026-04-04.md" +created: 2026-04-04 +updated: 2026-04-04 +--- + +# Connection: X and Y + +## The Connection + +[What links these concepts] + +## Key Insight + +[The non-obvious relationship discovered] + +## Evidence + +[Specific examples from conversations] + +## Related Concepts + +- [[concepts/concept-x]] +- [[concepts/concept-y]] +``` + +### Q&A Articles (`knowledge/qa/`) + +Filed answers from queries. Every complex question answered by the system can be permanently stored, making future queries smarter. + +```markdown +--- +title: "Q: Original Question" +question: "The exact question asked" +consulted: + - "concepts/article-1" + - "concepts/article-2" +filed: 2026-04-05 +--- + +# Q: Original Question + +## Answer + +[The synthesized answer with [[wikilinks]] to sources] + +## Sources Consulted + +- [[concepts/article-1]] - Relevant because... +- [[concepts/article-2]] - Provided context on... + +## Follow-Up Questions + +- What about edge case X? +- How does this change if Y? +``` + +--- + +## Core Operations + +### 1. Compile (daily/ -> knowledge/) + +When processing a daily log: + +1. Read the daily log file +2. Read `knowledge/index.md` to understand current knowledge state +3. Read existing articles that may need updating +4. For each piece of knowledge found in the log: + - If an existing concept article covers this topic: UPDATE it with new information, add the daily log as a source + - If it's a new topic: CREATE a new `concepts/` article +5. If the log reveals a non-obvious connection between 2+ existing concepts: CREATE a `connections/` article +6. UPDATE `knowledge/index.md` with new/modified entries +7. APPEND to `knowledge/log.md` + +**Important guidelines:** +- A single daily log may touch 3-10 knowledge articles +- Prefer updating existing articles over creating near-duplicates +- Use Obsidian-style `[[wikilinks]]` with full relative paths from knowledge/ +- Write in encyclopedia style - factual, concise, self-contained +- Every article must have YAML frontmatter +- Every article must link back to its source daily logs + +### 2. Query (Ask the Knowledge Base) + +1. Read `knowledge/index.md` (the master catalog) +2. Based on the question, identify 3-10 relevant articles from the index +3. Read those articles in full +4. Synthesize an answer with `[[wikilink]]` citations +5. If `--file-back` is specified: create a `knowledge/qa/` article and update index.md and log.md + +**Why this works without RAG:** At personal knowledge base scale (50-500 articles), the LLM reading a structured index outperforms cosine similarity. The LLM understands what the question is really asking and selects pages accordingly. Embeddings find similar words; the LLM finds relevant concepts. + +### 3. Lint (Health Checks) + +Seven checks, run periodically: + +1. **Broken links** - `[[wikilinks]]` pointing to non-existent articles +2. **Orphan pages** - Articles with zero inbound links from other articles +3. **Orphan sources** - Daily logs that haven't been compiled yet +4. **Stale articles** - Source daily log changed since article was last compiled +5. **Contradictions** - Conflicting claims across articles (requires LLM judgment) +6. **Missing backlinks** - A links to B but B doesn't link back to A +7. **Sparse articles** - Below 200 words, likely incomplete + +Output: a markdown report with severity levels (error, warning, suggestion). + +--- + +## Conventions + +- **Wikilinks:** Use Obsidian-style `[[path/to/article]]` without `.md` extension +- **Writing style:** Encyclopedia-style, factual, third-person where appropriate +- **Dates:** ISO 8601 (YYYY-MM-DD for dates, full ISO for timestamps in log.md) +- **File naming:** lowercase, hyphens for spaces (e.g., `supabase-row-level-security.md`) +- **Frontmatter:** Every article must have YAML frontmatter with at minimum: title, sources, created, updated +- **Sources:** Always link back to the daily log(s) that contributed to an article + +--- + +## Full Project Structure + +``` +llm-personal-kb/ +|-- .claude/ +| |-- settings.json # Hook configuration (auto-activates in Claude Code) +|-- .gitignore # Excludes runtime state, temp files, caches +|-- AGENTS.md # This file - schema + full technical reference +|-- README.md # Concise overview + quick start +|-- pyproject.toml # Dependencies (at root so hooks can find it) +|-- daily/ # "Source code" - conversation logs (immutable) +|-- knowledge/ # "Executable" - compiled knowledge (LLM-owned) +| |-- index.md # Master catalog - THE retrieval mechanism +| |-- log.md # Append-only build log +| |-- concepts/ # Atomic knowledge articles +| |-- connections/ # Cross-cutting insights linking 2+ concepts +| |-- qa/ # Filed query answers (compounding knowledge) +|-- scripts/ # CLI tools +| |-- compile.py # Compile daily logs -> knowledge articles +| |-- query.py # Ask questions (index-guided, no RAG) +| |-- lint.py # 7 health checks +| |-- flush.py # Extract memories from conversations (background) +| |-- config.py # Path constants +| |-- utils.py # Shared helpers +|-- hooks/ # Claude Code hooks +| |-- session-start.py # Injects knowledge into every session +| |-- session-end.py # Extracts conversation -> daily log +| |-- pre-compact.py # Safety net: captures context before compaction +|-- reports/ # Lint reports (gitignored) +``` + +--- + +## Hook System (Automatic Capture) + +Hooks are configured in `.claude/settings.json` and fire automatically when you use Claude Code in this project. + +### `.claude/settings.json` Format + +```json +{ + "hooks": { + "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-start.py", "timeout": 15 }] }], + "PreCompact": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/pre-compact.py", "timeout": 10 }] }], + "SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-end.py", "timeout": 10 }] }] + } +} +``` + +Commands use simple relative paths from the project root. Empty `matcher` catches all events. + +### Hook Details + +**`session-start.py`** (SessionStart) +- Pure local I/O, no API calls, runs in under 1 second +- Reads `knowledge/index.md` and the most recent daily log +- Outputs JSON to stdout: `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "..."}}` +- Claude sees the knowledge base index at the start of every session +- Max context: 20,000 characters + +**`session-end.py`** (SessionEnd) +- Reads hook input from stdin (JSON with `session_id`, `transcript_path`, `cwd`) +- Copies the raw JSONL transcript to a temp file (no parsing in the hook - keeps it fast) +- Spawns `flush.py` as a fully detached background process +- Recursion guard: exits immediately if `CLAUDE_INVOKED_BY` env var is set + +**`pre-compact.py`** (PreCompact) +- Same architecture as session-end.py +- Fires before Claude Code auto-compacts the context window +- Guards against empty `transcript_path` (known Claude Code bug #13668) +- Critical for long sessions: captures context before summarization discards it + +**Why both PreCompact and SessionEnd?** Long-running sessions may trigger multiple auto-compactions before you close the session. Without PreCompact, intermediate context is lost to summarization before SessionEnd ever fires. + +### Background Flush Process (`flush.py`) + +Spawned by both hooks as a fully detached background process: +- **Windows:** `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS` flags +- **Mac/Linux:** `start_new_session=True` + +This ensures flush.py survives after Claude Code's hook process exits. + +**What flush.py does:** +1. Sets `CLAUDE_INVOKED_BY=memory_flush` env var (prevents recursive hook firing) +2. Reads the pre-extracted conversation context from the temp `.md` file +3. Skips if context is empty or if same session was flushed within 60 seconds (deduplication) +4. Calls Claude Agent SDK (`query()` with `allowed_tools=[]`, `max_turns=2`) +5. Claude decides what's worth saving - returns structured bullet points or `FLUSH_OK` +6. Appends result to `daily/YYYY-MM-DD.md` +7. Cleans up temp context file +8. **End-of-day auto-compilation:** If it's past 6 PM local time (`COMPILE_AFTER_HOUR = 18`) and today's daily log has changed since its last compilation (hash comparison against `state.json`), spawns `compile.py` as another detached background process. This means compilation happens automatically once a day without needing a cron job or manual trigger. + +### JSONL Transcript Format + +Claude Code stores conversations as `.jsonl` files. Messages are nested under a `message` key: + +```python +entry = json.loads(line) +msg = entry.get("message", {}) +role = msg.get("role", "") # "user" or "assistant" +content = msg.get("content", "") # string or list of content blocks +``` + +Content can be a string or a list of blocks (`{"type": "text", "text": "..."}` dicts). + +--- + +## Script Details + +### compile.py - The Compiler + +Uses the Claude Agent SDK's async streaming `query()`: + +```python +async for message in query( + prompt=compile_prompt, + options=ClaudeAgentOptions( + cwd=str(ROOT_DIR), + system_prompt={"type": "preset", "preset": "claude_code"}, + allowed_tools=["Read", "Write", "Edit", "Glob", "Grep"], + permission_mode="acceptEdits", + max_turns=30, + ), +): +``` + +- Builds a prompt with: AGENTS.md schema, current index, all existing articles, and the daily log +- Claude reads the daily log, decides what concepts to extract, and writes files directly +- `permission_mode="acceptEdits"` auto-approves all file operations +- Incremental: tracks SHA-256 hashes of daily logs in `state.json`, skips unchanged files +- Cost: ~$0.45-0.65 per daily log (increases as KB grows) + +**CLI:** +```bash +uv run python scripts/compile.py # compile new/changed only +uv run python scripts/compile.py --all # force recompile everything +uv run python scripts/compile.py --file daily/2026-04-01.md +uv run python scripts/compile.py --dry-run +``` + +### query.py - Index-Guided Retrieval + +Loads the entire knowledge base into context (index + all articles). No RAG. + +At personal KB scale (50-500 articles), the LLM reading a structured index outperforms vector similarity. The LLM understands what you're really asking; cosine similarity just finds similar words. + +**CLI:** +```bash +uv run python scripts/query.py "What auth patterns do I use?" +uv run python scripts/query.py "What's my error handling strategy?" --file-back +``` + +With `--file-back`, creates a Q&A article in `knowledge/qa/` and updates the index and log. This is the compounding loop - every question makes the KB smarter. + +### lint.py - Health Checks + +Seven checks: + +| Check | Type | Catches | +|-------|------|---------| +| Broken links | Structural | `[[wikilinks]]` to non-existent articles | +| Orphan pages | Structural | Articles with zero inbound links | +| Orphan sources | Structural | Daily logs not yet compiled | +| Stale articles | Structural | Source logs changed since compilation | +| Missing backlinks | Structural | A links to B but B doesn't link back | +| Sparse articles | Structural | Under 200 words | +| Contradictions | LLM | Conflicting claims across articles | + +**CLI:** +```bash +uv run python scripts/lint.py # all checks +uv run python scripts/lint.py --structural-only # skip LLM check (free) +``` + +Reports saved to `reports/lint-YYYY-MM-DD.md`. + +--- + +## State Tracking + +`scripts/state.json` tracks: +- `ingested` - map of daily log filenames to SHA-256 hashes, compilation timestamps, and costs +- `query_count` - total queries run +- `last_lint` - timestamp of most recent lint +- `total_cost` - cumulative API cost + +`scripts/last-flush.json` tracks flush deduplication (session_id + timestamp). + +Both are gitignored and regenerated automatically. + +--- + +## Dependencies + +`pyproject.toml` (at project root): +- `claude-agent-sdk>=0.1.29` - Claude Agent SDK for LLM calls with tool use +- `python-dotenv>=1.0.0` - Environment variable management +- `tzdata>=2024.1` - Timezone data +- Python 3.12+, managed by [uv](https://docs.astral.sh/uv/) + +No API key needed - uses Claude Code's built-in credentials at `~/.claude/.credentials.json`. + +--- + +## Costs + +| Operation | Cost | +|-----------|------| +| Compile one daily log | $0.45-0.65 | +| Query (no file-back) | ~$0.15-0.25 | +| Query (with file-back) | ~$0.25-0.40 | +| Full lint (with contradictions) | ~$0.15-0.25 | +| Structural lint only | $0.00 | +| Memory flush (per session) | ~$0.02-0.05 | + +--- + +## Customization + +### Additional Article Types + +Add directories like `people/`, `projects/`, `tools/` to `knowledge/`. Define the article format in this file (AGENTS.md) and update `utils.py`'s `list_wiki_articles()` to include them. + +### Obsidian Integration + +The knowledge base is pure markdown with `[[wikilinks]]` - works natively in Obsidian. Point a vault at `knowledge/` for graph view, backlinks, and search. + +### Scaling Beyond Index-Guided Retrieval + +At ~2,000+ articles / ~2M+ tokens, the index becomes too large for the context window. At that point, add hybrid RAG (keyword + semantic search) as a retrieval layer before the LLM. See Karpathy's recommendation of `qmd` by Tobi Lutke for search at scale. diff --git a/.claude/memory-compiler/README.md b/.claude/memory-compiler/README.md new file mode 100644 index 00000000..10268d15 --- /dev/null +++ b/.claude/memory-compiler/README.md @@ -0,0 +1,52 @@ +# LLM Personal Knowledge Base + +**Your AI conversations compile themselves into a searchable knowledge base.** + +Adapted from [Karpathy's LLM Knowledge Base](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) architecture, but instead of clipping web articles, the raw data is your own conversations with Claude Code. When a session ends (or auto-compacts mid-session), Claude Code hooks capture the conversation transcript and spawn a background process that uses the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk) to extract the important stuff - decisions, lessons learned, patterns, gotchas - and appends it to a daily log. You then compile those daily logs into structured, cross-referenced knowledge articles organized by concept. Retrieval uses a simple index file instead of RAG - no vector database, no embeddings, just markdown. + +Anthropic has clarified that personal use of the Claude Agent SDK is covered under your existing Claude subscription (Max, Team, or Enterprise) - no separate API credits needed. Unlike OpenClaw, which requires API billing for its memory flush, this runs on your subscription. + +## Quick Start + +Tell your AI coding agent: + +> "Clone https://github.com/coleam00/claude-memory-compiler into this project. Set up the Claude Code hooks so my conversations automatically get captured into daily logs, compiled into a knowledge base, and injected back into future sessions. Read the AGENTS.md for the full technical reference on how everything works." + +The agent will: +1. Clone the repo and run `uv sync` to install dependencies +2. Copy `.claude/settings.json` into your project (or merge the hooks into your existing settings) +3. The hooks activate automatically next time you open Claude Code + +From there, your conversations start accumulating. After 6 PM local time, the next session flush automatically triggers compilation of that day's logs into knowledge articles. You can also run `uv run python scripts/compile.py` manually at any time. + +## How It Works + +``` +Conversation -> SessionEnd/PreCompact hooks -> flush.py extracts knowledge + -> daily/YYYY-MM-DD.md -> compile.py -> knowledge/concepts/, connections/, qa/ + -> SessionStart hook injects index into next session -> cycle repeats +``` + +- **Hooks** capture conversations automatically (session end + pre-compaction safety net) +- **flush.py** calls the Claude Agent SDK to decide what's worth saving, and after 6 PM triggers end-of-day compilation automatically +- **compile.py** turns daily logs into organized concept articles with cross-references (triggered automatically or run manually) +- **query.py** answers questions using index-guided retrieval (no RAG needed at personal scale) +- **lint.py** runs 7 health checks (broken links, orphans, contradictions, staleness) + +## Key Commands + +```bash +uv run python scripts/compile.py # compile new daily logs +uv run python scripts/query.py "question" # ask the knowledge base +uv run python scripts/query.py "question" --file-back # ask + save answer back +uv run python scripts/lint.py # run health checks +uv run python scripts/lint.py --structural-only # free structural checks only +``` + +## Why No RAG? + +Karpathy's insight: at personal scale (50-500 articles), the LLM reading a structured `index.md` outperforms vector similarity. The LLM understands what you're really asking; cosine similarity just finds similar words. RAG becomes necessary at ~2,000+ articles when the index exceeds the context window. + +## Technical Reference + +See **[AGENTS.md](AGENTS.md)** for the complete technical reference: article formats, hook architecture, script internals, cross-platform details, costs, and customization options. AGENTS.md is designed to give an AI agent everything it needs to understand, modify, or rebuild the system. diff --git a/.claude/memory-compiler/hooks/pre-compact.py b/.claude/memory-compiler/hooks/pre-compact.py new file mode 100644 index 00000000..9b3b69dc --- /dev/null +++ b/.claude/memory-compiler/hooks/pre-compact.py @@ -0,0 +1,170 @@ +""" +PreCompact hook - captures conversation transcript before auto-compaction. + +When Claude Code's context window fills up, it auto-compacts (summarizes and +discards detail). This hook fires BEFORE that happens, extracting conversation +context and spawning flush.py to extract knowledge that would otherwise +be lost to summarization. + +The hook itself does NO API calls - only local file I/O for speed (<10s). +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +# Recursion guard +if os.environ.get("CLAUDE_INVOKED_BY"): + sys.exit(0) + +ROOT = Path(__file__).resolve().parent.parent +SCRIPTS_DIR = ROOT / "scripts" +STATE_DIR = SCRIPTS_DIR + +logging.basicConfig( + filename=str(SCRIPTS_DIR / "flush.log"), + level=logging.INFO, + format="%(asctime)s %(levelname)s [pre-compact] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + +MAX_TURNS = 30 +MAX_CONTEXT_CHARS = 15_000 +MIN_TURNS_TO_FLUSH = 5 + + +def extract_conversation_context(transcript_path: Path) -> tuple[str, int]: + """Read JSONL transcript and extract last ~N conversation turns as markdown.""" + turns: list[str] = [] + + with open(transcript_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + + msg = entry.get("message", {}) + if isinstance(msg, dict): + role = msg.get("role", "") + content = msg.get("content", "") + else: + role = entry.get("role", "") + content = entry.get("content", "") + + if role not in ("user", "assistant"): + continue + + if isinstance(content, list): + text_parts = [] + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + elif isinstance(block, str): + text_parts.append(block) + content = "\n".join(text_parts) + + if isinstance(content, str) and content.strip(): + label = "User" if role == "user" else "Assistant" + turns.append(f"**{label}:** {content.strip()}\n") + + recent = turns[-MAX_TURNS:] + context = "\n".join(recent) + + if len(context) > MAX_CONTEXT_CHARS: + context = context[-MAX_CONTEXT_CHARS:] + boundary = context.find("\n**") + if boundary > 0: + context = context[boundary + 1 :] + + return context, len(recent) + + +def main() -> None: + # Read hook input from stdin + try: + raw_input = sys.stdin.read() + try: + hook_input: dict = json.loads(raw_input) + except json.JSONDecodeError: + fixed_input = re.sub(r'(? tuple[str, int]: + """Read JSONL transcript and extract last ~N conversation turns as markdown.""" + turns: list[str] = [] + + with open(transcript_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + + msg = entry.get("message", {}) + if isinstance(msg, dict): + role = msg.get("role", "") + content = msg.get("content", "") + else: + role = entry.get("role", "") + content = entry.get("content", "") + + if role not in ("user", "assistant"): + continue + + if isinstance(content, list): + text_parts = [] + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + elif isinstance(block, str): + text_parts.append(block) + content = "\n".join(text_parts) + + if isinstance(content, str) and content.strip(): + label = "User" if role == "user" else "Assistant" + turns.append(f"**{label}:** {content.strip()}\n") + + recent = turns[-MAX_TURNS:] + context = "\n".join(recent) + + if len(context) > MAX_CONTEXT_CHARS: + context = context[-MAX_CONTEXT_CHARS:] + boundary = context.find("\n**") + if boundary > 0: + context = context[boundary + 1 :] + + return context, len(recent) + + +def main() -> None: + # Read hook input from stdin + # Claude Code on Windows may pass paths with unescaped backslashes + try: + raw_input = sys.stdin.read() + try: + hook_input: dict = json.loads(raw_input) + except json.JSONDecodeError: + fixed_input = re.sub(r'(? str: + """Read the most recent daily log (today or yesterday).""" + today = datetime.now(timezone.utc).astimezone() + + for offset in range(2): + date = today - timedelta(days=offset) + log_path = DAILY_DIR / f"{date.strftime('%Y-%m-%d')}.md" + if log_path.exists(): + lines = log_path.read_text(encoding="utf-8").splitlines() + # Return last N lines to keep context small + recent = lines[-MAX_LOG_LINES:] if len(lines) > MAX_LOG_LINES else lines + return "\n".join(recent) + + return "(no recent daily log)" + + +def build_context() -> str: + """Assemble the context to inject into the conversation.""" + parts = [] + + # Today's date + today = datetime.now(timezone.utc).astimezone() + parts.append(f"## Today\n{today.strftime('%A, %B %d, %Y')}") + + # Knowledge base index (the core retrieval mechanism) + if INDEX_FILE.exists(): + index_content = INDEX_FILE.read_text(encoding="utf-8") + parts.append(f"## Knowledge Base Index\n\n{index_content}") + else: + parts.append("## Knowledge Base Index\n\n(empty - no articles compiled yet)") + + # Recent daily log + recent_log = get_recent_log() + parts.append(f"## Recent Daily Log\n\n{recent_log}") + + context = "\n\n---\n\n".join(parts) + + # Truncate if too long + if len(context) > MAX_CONTEXT_CHARS: + context = context[:MAX_CONTEXT_CHARS] + "\n\n...(truncated)" + + return context + + +def main(): + context = build_context() + + output = { + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context, + } + } + + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/.claude/memory-compiler/pyproject.toml b/.claude/memory-compiler/pyproject.toml new file mode 100644 index 00000000..1e701fb4 --- /dev/null +++ b/.claude/memory-compiler/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "llm-personal-kb" +version = "0.1.0" +description = "Personal knowledge base compiled from AI conversations - inspired by Karpathy's LLM KB architecture" +requires-python = ">=3.12" +dependencies = [ + "claude-agent-sdk>=0.1.29", + "python-dotenv>=1.0.0", + "tzdata>=2024.1", +] + +[tool.ruff] +line-length = 100 diff --git a/.claude/memory-compiler/scripts/compile.py b/.claude/memory-compiler/scripts/compile.py new file mode 100644 index 00000000..43baad6a --- /dev/null +++ b/.claude/memory-compiler/scripts/compile.py @@ -0,0 +1,224 @@ +""" +Compile daily conversation logs into structured knowledge articles. + +This is the "LLM compiler" - it reads daily logs (source code) and produces +organized knowledge articles (the executable). + +Usage: + uv run python compile.py # compile new/changed logs only + uv run python compile.py --all # force recompile everything + uv run python compile.py --file daily/2026-04-01.md # compile a specific log + uv run python compile.py --dry-run # show what would be compiled +""" + +from __future__ import annotations + +import argparse +import asyncio +import sys +from pathlib import Path + +from config import AGENTS_FILE, CONCEPTS_DIR, CONNECTIONS_DIR, DAILY_DIR, KNOWLEDGE_DIR, now_iso +from utils import ( + file_hash, + list_raw_files, + list_wiki_articles, + load_state, + read_wiki_index, + save_state, +) + +# ── Paths for the LLM to use ────────────────────────────────────────── +ROOT_DIR = Path(__file__).resolve().parent.parent + + +async def compile_daily_log(log_path: Path, state: dict) -> float: + """Compile a single daily log into knowledge articles. + + Returns the API cost of the compilation. + """ + from claude_agent_sdk import ( + AssistantMessage, + ClaudeAgentOptions, + ResultMessage, + TextBlock, + query, + ) + + log_content = log_path.read_text(encoding="utf-8") + schema = AGENTS_FILE.read_text(encoding="utf-8") + wiki_index = read_wiki_index() + + # Read existing articles for context + existing_articles_context = "" + existing = {} + for article_path in list_wiki_articles(): + rel = article_path.relative_to(KNOWLEDGE_DIR) + existing[str(rel)] = article_path.read_text(encoding="utf-8") + + if existing: + parts = [] + for rel_path, content in existing.items(): + parts.append(f"### {rel_path}\n```markdown\n{content}\n```") + existing_articles_context = "\n\n".join(parts) + + timestamp = now_iso() + + prompt = f"""You are a knowledge compiler. Your job is to read a daily conversation log +and extract knowledge into structured wiki articles. + +## Schema (AGENTS.md) + +{schema} + +## Current Wiki Index + +{wiki_index} + +## Existing Wiki Articles + +{existing_articles_context if existing_articles_context else "(No existing articles yet)"} + +## Daily Log to Compile + +**File:** {log_path.name} + +{log_content} + +## Your Task + +Read the daily log above and compile it into wiki articles following the schema exactly. + +### Rules: + +1. **Extract key concepts** - Identify 3-7 distinct concepts worth their own article +2. **Create concept articles** in `knowledge/concepts/` - One .md file per concept + - Use the exact article format from AGENTS.md (YAML frontmatter + sections) + - Include `sources:` in frontmatter pointing to the daily log file + - Use `[[concepts/slug]]` wikilinks to link to related concepts + - Write in encyclopedia style - neutral, comprehensive +3. **Create connection articles** in `knowledge/connections/` if this log reveals non-obvious + relationships between 2+ existing concepts +4. **Update existing articles** if this log adds new information to concepts already in the wiki + - Read the existing article, add the new information, add the source to frontmatter +5. **Update knowledge/index.md** - Add new entries to the table + - Each entry: `| [[path/slug]] | One-line summary | source-file | {timestamp[:10]} |` +6. **Append to knowledge/log.md** - Add a timestamped entry: + ``` + ## [{timestamp}] compile | {log_path.name} + - Source: daily/{log_path.name} + - Articles created: [[concepts/x]], [[concepts/y]] + - Articles updated: [[concepts/z]] (if any) + ``` + +### File paths: +- Write concept articles to: {CONCEPTS_DIR} +- Write connection articles to: {CONNECTIONS_DIR} +- Update index at: {KNOWLEDGE_DIR / 'index.md'} +- Append log at: {KNOWLEDGE_DIR / 'log.md'} + +### Quality standards: +- Every article must have complete YAML frontmatter +- Every article must link to at least 2 other articles via [[wikilinks]] +- Key Points section should have 3-5 bullet points +- Details section should have 2+ paragraphs +- Related Concepts section should have 2+ entries +- Sources section should cite the daily log with specific claims extracted +""" + + cost = 0.0 + + try: + async for message in query( + prompt=prompt, + options=ClaudeAgentOptions( + cwd=str(ROOT_DIR), + system_prompt={"type": "preset", "preset": "claude_code"}, + allowed_tools=["Read", "Write", "Edit", "Glob", "Grep"], + permission_mode="acceptEdits", + max_turns=30, + ), + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + pass # compilation output - LLM writes files directly + elif isinstance(message, ResultMessage): + cost = message.total_cost_usd or 0.0 + print(f" Cost: ${cost:.4f}") + except Exception as e: + print(f" Error: {e}") + return 0.0 + + # Update state + rel_path = log_path.name + state.setdefault("ingested", {})[rel_path] = { + "hash": file_hash(log_path), + "compiled_at": now_iso(), + "cost_usd": cost, + } + state["total_cost"] = state.get("total_cost", 0.0) + cost + save_state(state) + + return cost + + +def main(): + parser = argparse.ArgumentParser(description="Compile daily logs into knowledge articles") + parser.add_argument("--all", action="store_true", help="Force recompile all logs") + parser.add_argument("--file", type=str, help="Compile a specific daily log file") + parser.add_argument("--dry-run", action="store_true", help="Show what would be compiled") + args = parser.parse_args() + + state = load_state() + + # Determine which files to compile + if args.file: + target = Path(args.file) + if not target.is_absolute(): + target = DAILY_DIR / target.name + if not target.exists(): + # Try resolving relative to project root + target = ROOT_DIR / args.file + if not target.exists(): + print(f"Error: {args.file} not found") + sys.exit(1) + to_compile = [target] + else: + all_logs = list_raw_files() + if args.all: + to_compile = all_logs + else: + to_compile = [] + for log_path in all_logs: + rel = log_path.name + prev = state.get("ingested", {}).get(rel, {}) + if not prev or prev.get("hash") != file_hash(log_path): + to_compile.append(log_path) + + if not to_compile: + print("Nothing to compile - all daily logs are up to date.") + return + + print(f"{'[DRY RUN] ' if args.dry_run else ''}Files to compile ({len(to_compile)}):") + for f in to_compile: + print(f" - {f.name}") + + if args.dry_run: + return + + # Compile each file sequentially + total_cost = 0.0 + for i, log_path in enumerate(to_compile, 1): + print(f"\n[{i}/{len(to_compile)}] Compiling {log_path.name}...") + cost = asyncio.run(compile_daily_log(log_path, state)) + total_cost += cost + print(f" Done.") + + articles = list_wiki_articles() + print(f"\nCompilation complete. Total cost: ${total_cost:.2f}") + print(f"Knowledge base: {len(articles)} articles") + + +if __name__ == "__main__": + main() diff --git a/.claude/memory-compiler/scripts/config.py b/.claude/memory-compiler/scripts/config.py new file mode 100644 index 00000000..aa54feac --- /dev/null +++ b/.claude/memory-compiler/scripts/config.py @@ -0,0 +1,33 @@ +"""Path constants and configuration for the personal knowledge base.""" + +from pathlib import Path +from datetime import datetime, timezone + +# ── Paths ────────────────────────────────────────────────────────────── +ROOT_DIR = Path(__file__).resolve().parent.parent +DAILY_DIR = ROOT_DIR / "daily" +KNOWLEDGE_DIR = ROOT_DIR / "knowledge" +CONCEPTS_DIR = KNOWLEDGE_DIR / "concepts" +CONNECTIONS_DIR = KNOWLEDGE_DIR / "connections" +QA_DIR = KNOWLEDGE_DIR / "qa" +REPORTS_DIR = ROOT_DIR / "reports" +SCRIPTS_DIR = ROOT_DIR / "scripts" +HOOKS_DIR = ROOT_DIR / "hooks" +AGENTS_FILE = ROOT_DIR / "AGENTS.md" + +INDEX_FILE = KNOWLEDGE_DIR / "index.md" +LOG_FILE = KNOWLEDGE_DIR / "log.md" +STATE_FILE = SCRIPTS_DIR / "state.json" + +# ── Timezone ─────────────────────────────────────────────────────────── +TIMEZONE = "America/Chicago" + + +def now_iso() -> str: + """Current time in ISO 8601 format.""" + return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") + + +def today_iso() -> str: + """Current date in ISO 8601 format.""" + return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d") diff --git a/.claude/memory-compiler/scripts/flush.py b/.claude/memory-compiler/scripts/flush.py new file mode 100644 index 00000000..3cb25c40 --- /dev/null +++ b/.claude/memory-compiler/scripts/flush.py @@ -0,0 +1,255 @@ +""" +Memory flush agent - extracts important knowledge from conversation context. + +Spawned by session-end.py or pre-compact.py as a background process. Reads +pre-extracted conversation context from a .md file, uses the Claude Agent SDK +to decide what's worth saving, and appends the result to today's daily log. + +Usage: + uv run python flush.py +""" + +from __future__ import annotations + +# Recursion prevention: set this BEFORE any imports that might trigger Claude +import os +os.environ["CLAUDE_INVOKED_BY"] = "memory_flush" + +import asyncio +import json +import logging +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DAILY_DIR = ROOT / "daily" +SCRIPTS_DIR = ROOT / "scripts" +STATE_FILE = SCRIPTS_DIR / "last-flush.json" +LOG_FILE = SCRIPTS_DIR / "flush.log" + +# Set up file-based logging so we can verify the background process ran. +# The parent process sends stdout/stderr to DEVNULL (to avoid the inherited +# file handle bug on Windows), so this is our only observability channel. +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + + +def load_flush_state() -> dict: + if STATE_FILE.exists(): + try: + return json.loads(STATE_FILE.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + pass + return {} + + +def save_flush_state(state: dict) -> None: + STATE_FILE.write_text(json.dumps(state), encoding="utf-8") + + +def append_to_daily_log(content: str, section: str = "Session") -> None: + """Append content to today's daily log.""" + today = datetime.now(timezone.utc).astimezone() + log_path = DAILY_DIR / f"{today.strftime('%Y-%m-%d')}.md" + + if not log_path.exists(): + DAILY_DIR.mkdir(parents=True, exist_ok=True) + log_path.write_text( + f"# Daily Log: {today.strftime('%Y-%m-%d')}\n\n## Sessions\n\n## Memory Maintenance\n\n", + encoding="utf-8", + ) + + time_str = today.strftime("%H:%M") + entry = f"### {section} ({time_str})\n\n{content}\n\n" + + with open(log_path, "a", encoding="utf-8") as f: + f.write(entry) + + +async def run_flush(context: str) -> str: + """Use Claude Agent SDK to extract important knowledge from conversation context.""" + from claude_agent_sdk import ( + AssistantMessage, + ClaudeAgentOptions, + ResultMessage, + TextBlock, + query, + ) + + prompt = f"""Review the conversation context below and respond with a concise summary +of important items that should be preserved in the daily log. +Do NOT use any tools — just return plain text. + +Format your response as a structured daily log entry with these sections: + +**Context:** [One line about what the user was working on] + +**Key Exchanges:** +- [Important Q&A or discussions] + +**Decisions Made:** +- [Any decisions with rationale] + +**Lessons Learned:** +- [Gotchas, patterns, or insights discovered] + +**Action Items:** +- [Follow-ups or TODOs mentioned] + +Skip anything that is: +- Routine tool calls or file reads +- Content that's trivial or obvious +- Trivial back-and-forth or clarification exchanges + +Only include sections that have actual content. If nothing is worth saving, +respond with exactly: FLUSH_OK + +## Conversation Context + +{context}""" + + response = "" + + try: + async for message in query( + prompt=prompt, + options=ClaudeAgentOptions( + cwd=str(ROOT), + allowed_tools=[], + max_turns=2, + ), + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + response += block.text + elif isinstance(message, ResultMessage): + pass + except Exception as e: + import traceback + logging.error("Agent SDK error: %s\n%s", e, traceback.format_exc()) + response = f"FLUSH_ERROR: {type(e).__name__}: {e}" + + return response + + +COMPILE_AFTER_HOUR = 18 # 6 PM local time + + +def maybe_trigger_compilation() -> None: + """If it's past the compile hour and today's log hasn't been compiled, run compile.py.""" + import subprocess as _sp + + now = datetime.now(timezone.utc).astimezone() + if now.hour < COMPILE_AFTER_HOUR: + return + + # Check if today's log has already been compiled + today_log = f"{now.strftime('%Y-%m-%d')}.md" + compile_state_file = SCRIPTS_DIR / "state.json" + if compile_state_file.exists(): + try: + compile_state = json.loads(compile_state_file.read_text(encoding="utf-8")) + ingested = compile_state.get("ingested", {}) + if today_log in ingested: + # Already compiled today - check if the log has changed since + from hashlib import sha256 + log_path = DAILY_DIR / today_log + if log_path.exists(): + current_hash = sha256(log_path.read_bytes()).hexdigest()[:16] + if ingested[today_log].get("hash") == current_hash: + return # log unchanged since last compile + except (json.JSONDecodeError, OSError): + pass + + compile_script = SCRIPTS_DIR / "compile.py" + if not compile_script.exists(): + return + + logging.info("End-of-day compilation triggered (after %d:00)", COMPILE_AFTER_HOUR) + + cmd = ["uv", "run", "--directory", str(ROOT), "python", str(compile_script)] + + kwargs: dict = {} + if sys.platform == "win32": + kwargs["creationflags"] = _sp.CREATE_NEW_PROCESS_GROUP | _sp.DETACHED_PROCESS + else: + kwargs["start_new_session"] = True + + try: + log_handle = open(str(SCRIPTS_DIR / "compile.log"), "a") + _sp.Popen(cmd, stdout=log_handle, stderr=_sp.STDOUT, cwd=str(ROOT), **kwargs) + except Exception as e: + logging.error("Failed to spawn compile.py: %s", e) + + +def main(): + if len(sys.argv) < 3: + logging.error("Usage: %s ", sys.argv[0]) + sys.exit(1) + + context_file = Path(sys.argv[1]) + session_id = sys.argv[2] + + logging.info("flush.py started for session %s, context: %s", session_id, context_file) + + if not context_file.exists(): + logging.error("Context file not found: %s", context_file) + return + + # Deduplication: skip if same session was flushed within 60 seconds + state = load_flush_state() + if ( + state.get("session_id") == session_id + and time.time() - state.get("timestamp", 0) < 60 + ): + logging.info("Skipping duplicate flush for session %s", session_id) + context_file.unlink(missing_ok=True) + return + + # Read pre-extracted context + context = context_file.read_text(encoding="utf-8").strip() + if not context: + logging.info("Context file is empty, skipping") + context_file.unlink(missing_ok=True) + return + + logging.info("Flushing session %s: %d chars", session_id, len(context)) + + # Run the LLM extraction + response = asyncio.run(run_flush(context)) + + # Append to daily log + if "FLUSH_OK" in response: + logging.info("Result: FLUSH_OK") + append_to_daily_log( + "FLUSH_OK - Nothing worth saving from this session", "Memory Flush" + ) + elif "FLUSH_ERROR" in response: + logging.error("Result: %s", response) + append_to_daily_log(response, "Memory Flush") + else: + logging.info("Result: saved to daily log (%d chars)", len(response)) + append_to_daily_log(response, "Session") + + # Update dedup state + save_flush_state({"session_id": session_id, "timestamp": time.time()}) + + # Clean up context file + context_file.unlink(missing_ok=True) + + # End-of-day auto-compilation: if it's past the compile hour and today's + # log hasn't been compiled yet, trigger compile.py in the background. + maybe_trigger_compilation() + + logging.info("Flush complete for session %s", session_id) + + +if __name__ == "__main__": + main() diff --git a/.claude/memory-compiler/scripts/lint.py b/.claude/memory-compiler/scripts/lint.py new file mode 100644 index 00000000..9079eb1d --- /dev/null +++ b/.claude/memory-compiler/scripts/lint.py @@ -0,0 +1,312 @@ +""" +Lint the knowledge base for structural and semantic health. + +Runs 7 checks: broken links, orphan pages, orphan sources, stale articles, +contradictions (LLM), missing backlinks, and sparse articles. + +Usage: + uv run python lint.py # all checks + uv run python lint.py --structural-only # skip LLM checks (faster, cheaper) +""" + +from __future__ import annotations + +import argparse +import asyncio +from pathlib import Path + +from config import KNOWLEDGE_DIR, REPORTS_DIR, now_iso, today_iso +from utils import ( + count_inbound_links, + extract_wikilinks, + file_hash, + get_article_word_count, + list_raw_files, + list_wiki_articles, + load_state, + read_all_wiki_content, + save_state, + wiki_article_exists, +) + +ROOT_DIR = Path(__file__).resolve().parent.parent + + +def check_broken_links() -> list[dict]: + """Check for [[wikilinks]] that point to non-existent articles.""" + issues = [] + for article in list_wiki_articles(): + content = article.read_text(encoding="utf-8") + rel = article.relative_to(KNOWLEDGE_DIR) + for link in extract_wikilinks(content): + if link.startswith("daily/"): + continue # daily log references are valid + if not wiki_article_exists(link): + issues.append({ + "severity": "error", + "check": "broken_link", + "file": str(rel), + "detail": f"Broken link: [[{link}]] - target does not exist", + }) + return issues + + +def check_orphan_pages() -> list[dict]: + """Check for articles with zero inbound links.""" + issues = [] + for article in list_wiki_articles(): + rel = article.relative_to(KNOWLEDGE_DIR) + link_target = str(rel).replace(".md", "").replace("\\", "/") + inbound = count_inbound_links(link_target) + if inbound == 0: + issues.append({ + "severity": "warning", + "check": "orphan_page", + "file": str(rel), + "detail": f"Orphan page: no other articles link to [[{link_target}]]", + }) + return issues + + +def check_orphan_sources() -> list[dict]: + """Check for daily logs that haven't been compiled yet.""" + state = load_state() + ingested = state.get("ingested", {}) + issues = [] + for log_path in list_raw_files(): + if log_path.name not in ingested: + issues.append({ + "severity": "warning", + "check": "orphan_source", + "file": f"daily/{log_path.name}", + "detail": f"Uncompiled daily log: {log_path.name} has not been ingested", + }) + return issues + + +def check_stale_articles() -> list[dict]: + """Check if source daily logs have changed since compilation.""" + state = load_state() + ingested = state.get("ingested", {}) + issues = [] + for log_path in list_raw_files(): + rel = log_path.name + if rel in ingested: + stored_hash = ingested[rel].get("hash", "") + current_hash = file_hash(log_path) + if stored_hash != current_hash: + issues.append({ + "severity": "warning", + "check": "stale_article", + "file": f"daily/{rel}", + "detail": f"Stale: {rel} has changed since last compilation", + }) + return issues + + +def check_missing_backlinks() -> list[dict]: + """Check for asymmetric links: A links to B but B doesn't link to A.""" + issues = [] + for article in list_wiki_articles(): + content = article.read_text(encoding="utf-8") + rel = article.relative_to(KNOWLEDGE_DIR) + source_link = str(rel).replace(".md", "").replace("\\", "/") + + for link in extract_wikilinks(content): + if link.startswith("daily/"): + continue + target_path = KNOWLEDGE_DIR / f"{link}.md" + if target_path.exists(): + target_content = target_path.read_text(encoding="utf-8") + if f"[[{source_link}]]" not in target_content: + issues.append({ + "severity": "suggestion", + "check": "missing_backlink", + "file": str(rel), + "detail": f"[[{source_link}]] links to [[{link}]] but not vice versa", + "auto_fixable": True, + }) + return issues + + +def check_sparse_articles() -> list[dict]: + """Check for articles with fewer than 200 words.""" + issues = [] + for article in list_wiki_articles(): + word_count = get_article_word_count(article) + if word_count < 200: + rel = article.relative_to(KNOWLEDGE_DIR) + issues.append({ + "severity": "suggestion", + "check": "sparse_article", + "file": str(rel), + "detail": f"Sparse article: {word_count} words (minimum recommended: 200)", + }) + return issues + + +async def check_contradictions() -> list[dict]: + """Use LLM to detect contradictions across articles.""" + from claude_agent_sdk import ( + AssistantMessage, + ClaudeAgentOptions, + ResultMessage, + TextBlock, + query, + ) + + wiki_content = read_all_wiki_content() + + prompt = f"""Review this knowledge base for contradictions, inconsistencies, or +conflicting claims across articles. + +## Knowledge Base + +{wiki_content} + +## Instructions + +Look for: +- Direct contradictions (article A says X, article B says not-X) +- Inconsistent recommendations (different articles recommend conflicting approaches) +- Outdated information that conflicts with newer entries + +For each issue found, output EXACTLY one line in this format: +CONTRADICTION: [file1] vs [file2] - description of the conflict +INCONSISTENCY: [file] - description of the inconsistency + +If no issues found, output exactly: NO_ISSUES + +Do NOT output anything else - no preamble, no explanation, just the formatted lines.""" + + response = "" + try: + async for message in query( + prompt=prompt, + options=ClaudeAgentOptions( + cwd=str(ROOT_DIR), + allowed_tools=[], + max_turns=2, + ), + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + response += block.text + except Exception as e: + return [{"severity": "error", "check": "contradiction", "file": "(system)", "detail": f"LLM check failed: {e}"}] + + issues = [] + if "NO_ISSUES" not in response: + for line in response.strip().split("\n"): + line = line.strip() + if line.startswith("CONTRADICTION:") or line.startswith("INCONSISTENCY:"): + issues.append({ + "severity": "warning", + "check": "contradiction", + "file": "(cross-article)", + "detail": line, + }) + + return issues + + +def generate_report(all_issues: list[dict]) -> str: + """Generate a markdown lint report.""" + errors = [i for i in all_issues if i["severity"] == "error"] + warnings = [i for i in all_issues if i["severity"] == "warning"] + suggestions = [i for i in all_issues if i["severity"] == "suggestion"] + + lines = [ + f"# Lint Report - {today_iso()}", + "", + f"**Total issues:** {len(all_issues)}", + f"- Errors: {len(errors)}", + f"- Warnings: {len(warnings)}", + f"- Suggestions: {len(suggestions)}", + "", + ] + + for severity, issues, marker in [ + ("Errors", errors, "x"), + ("Warnings", warnings, "!"), + ("Suggestions", suggestions, "?"), + ]: + if issues: + lines.append(f"## {severity}") + lines.append("") + for issue in issues: + fixable = " (auto-fixable)" if issue.get("auto_fixable") else "" + lines.append(f"- **[{marker}]** `{issue['file']}` - {issue['detail']}{fixable}") + lines.append("") + + if not all_issues: + lines.append("All checks passed. Knowledge base is healthy.") + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Lint the knowledge base") + parser.add_argument( + "--structural-only", + action="store_true", + help="Skip LLM-based checks (contradictions) - faster and free", + ) + args = parser.parse_args() + + print("Running knowledge base lint checks...") + all_issues: list[dict] = [] + + # Structural checks (free, instant) + checks = [ + ("Broken links", check_broken_links), + ("Orphan pages", check_orphan_pages), + ("Orphan sources", check_orphan_sources), + ("Stale articles", check_stale_articles), + ("Missing backlinks", check_missing_backlinks), + ("Sparse articles", check_sparse_articles), + ] + + for name, check_fn in checks: + print(f" Checking: {name}...") + issues = check_fn() + all_issues.extend(issues) + print(f" Found {len(issues)} issue(s)") + + # LLM check (costs money) + if not args.structural_only: + print(" Checking: Contradictions (LLM)...") + issues = asyncio.run(check_contradictions()) + all_issues.extend(issues) + print(f" Found {len(issues)} issue(s)") + else: + print(" Skipping: Contradictions (--structural-only)") + + # Generate and save report + report = generate_report(all_issues) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + report_path = REPORTS_DIR / f"lint-{today_iso()}.md" + report_path.write_text(report, encoding="utf-8") + print(f"\nReport saved to: {report_path}") + + # Update state + state = load_state() + state["last_lint"] = now_iso() + save_state(state) + + # Summary + errors = sum(1 for i in all_issues if i["severity"] == "error") + warnings = sum(1 for i in all_issues if i["severity"] == "warning") + suggestions = sum(1 for i in all_issues if i["severity"] == "suggestion") + print(f"\nResults: {errors} errors, {warnings} warnings, {suggestions} suggestions") + + if errors > 0: + print("\nErrors found - knowledge base needs attention!") + return 1 + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/.claude/memory-compiler/scripts/query.py b/.claude/memory-compiler/scripts/query.py new file mode 100644 index 00000000..fe82ab64 --- /dev/null +++ b/.claude/memory-compiler/scripts/query.py @@ -0,0 +1,138 @@ +""" +Query the knowledge base using index-guided retrieval (no RAG). + +The LLM reads the index, picks relevant articles, and synthesizes an answer. +No vector database, no embeddings, no chunking - just structured markdown +and an index the LLM can reason over. + +Usage: + uv run python query.py "How should I handle auth redirects?" + uv run python query.py "What patterns do I use for API design?" --file-back +""" + +from __future__ import annotations + +import argparse +import asyncio +from pathlib import Path + +from config import KNOWLEDGE_DIR, QA_DIR, now_iso +from utils import load_state, read_all_wiki_content, save_state + +ROOT_DIR = Path(__file__).resolve().parent.parent + + +async def run_query(question: str, file_back: bool = False) -> str: + """Query the knowledge base and optionally file the answer back.""" + from claude_agent_sdk import ( + AssistantMessage, + ClaudeAgentOptions, + ResultMessage, + TextBlock, + query, + ) + + wiki_content = read_all_wiki_content() + + tools = ["Read", "Glob", "Grep"] + if file_back: + tools.extend(["Write", "Edit"]) + + file_back_instructions = "" + if file_back: + timestamp = now_iso() + file_back_instructions = f""" + +## File Back Instructions + +After answering, do the following: +1. Create a Q&A article at {QA_DIR}/ with the filename being a slugified version + of the question (e.g., knowledge/qa/how-to-handle-auth-redirects.md) +2. Use the Q&A article format from the schema (frontmatter with title, question, + consulted articles, filed date) +3. Update {KNOWLEDGE_DIR / 'index.md'} with a new row for this Q&A article +4. Append to {KNOWLEDGE_DIR / 'log.md'}: + ## [{timestamp}] query (filed) | question summary + - Question: {question} + - Consulted: [[list of articles read]] + - Filed to: [[qa/article-name]] +""" + + prompt = f"""You are a knowledge base query engine. Answer the user's question by +consulting the knowledge base below. + +## How to Answer + +1. Read the INDEX section first - it lists every article with a one-line summary +2. Identify 3-10 articles that are relevant to the question +3. Read those articles carefully (they're included below) +4. Synthesize a clear, thorough answer +5. Cite your sources using [[wikilinks]] (e.g., [[concepts/supabase-auth]]) +6. If the knowledge base doesn't contain relevant information, say so honestly + +## Knowledge Base + +{wiki_content} + +## Question + +{question} +{file_back_instructions}""" + + answer = "" + cost = 0.0 + + try: + async for message in query( + prompt=prompt, + options=ClaudeAgentOptions( + cwd=str(ROOT_DIR), + system_prompt={"type": "preset", "preset": "claude_code"}, + allowed_tools=tools, + permission_mode="acceptEdits", + max_turns=15, + ), + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + answer += block.text + elif isinstance(message, ResultMessage): + cost = message.total_cost_usd or 0.0 + except Exception as e: + answer = f"Error querying knowledge base: {e}" + + # Update state + state = load_state() + state["query_count"] = state.get("query_count", 0) + 1 + state["total_cost"] = state.get("total_cost", 0.0) + cost + save_state(state) + + return answer + + +def main(): + parser = argparse.ArgumentParser(description="Query the personal knowledge base") + parser.add_argument("question", help="The question to ask") + parser.add_argument( + "--file-back", + action="store_true", + help="File the answer back into the knowledge base as a Q&A article", + ) + args = parser.parse_args() + + print(f"Question: {args.question}") + print(f"File back: {'yes' if args.file_back else 'no'}") + print("-" * 60) + + answer = asyncio.run(run_query(args.question, file_back=args.file_back)) + print(answer) + + if args.file_back: + print("\n" + "-" * 60) + qa_count = len(list(QA_DIR.glob("*.md"))) if QA_DIR.exists() else 0 + print(f"Answer filed to knowledge/qa/ ({qa_count} Q&A articles total)") + + +if __name__ == "__main__": + main() diff --git a/.claude/memory-compiler/scripts/utils.py b/.claude/memory-compiler/scripts/utils.py new file mode 100644 index 00000000..67f8664b --- /dev/null +++ b/.claude/memory-compiler/scripts/utils.py @@ -0,0 +1,133 @@ +"""Shared utilities for the personal knowledge base.""" + +import hashlib +import json +import re +from pathlib import Path + +from config import ( + CONCEPTS_DIR, + CONNECTIONS_DIR, + DAILY_DIR, + INDEX_FILE, + KNOWLEDGE_DIR, + LOG_FILE, + QA_DIR, + STATE_FILE, +) + + +# ── State management ────────────────────────────────────────────────── + +def load_state() -> dict: + """Load persistent state from state.json.""" + if STATE_FILE.exists(): + return json.loads(STATE_FILE.read_text(encoding="utf-8")) + return {"ingested": {}, "query_count": 0, "last_lint": None, "total_cost": 0.0} + + +def save_state(state: dict) -> None: + """Save state to state.json.""" + STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8") + + +# ── File hashing ────────────────────────────────────────────────────── + +def file_hash(path: Path) -> str: + """SHA-256 hash of a file (first 16 hex chars).""" + return hashlib.sha256(path.read_bytes()).hexdigest()[:16] + + +# ── Slug / naming ───────────────────────────────────────────────────── + +def slugify(text: str) -> str: + """Convert text to a filename-safe slug.""" + text = text.lower().strip() + text = re.sub(r"[^\w\s-]", "", text) + text = re.sub(r"[\s_]+", "-", text) + text = re.sub(r"-+", "-", text) + return text.strip("-") + + +# ── Wikilink helpers ────────────────────────────────────────────────── + +def extract_wikilinks(content: str) -> list[str]: + """Extract all [[wikilinks]] from markdown content.""" + return re.findall(r"\[\[([^\]]+)\]\]", content) + + +def wiki_article_exists(link: str) -> bool: + """Check if a wikilinked article exists on disk.""" + path = KNOWLEDGE_DIR / f"{link}.md" + return path.exists() + + +# ── Wiki content helpers ────────────────────────────────────────────── + +def read_wiki_index() -> str: + """Read the knowledge base index file.""" + if INDEX_FILE.exists(): + return INDEX_FILE.read_text(encoding="utf-8") + return "# Knowledge Base Index\n\n| Article | Summary | Compiled From | Updated |\n|---------|---------|---------------|---------|" + + +def read_all_wiki_content() -> str: + """Read index + all wiki articles into a single string for context.""" + parts = [f"## INDEX\n\n{read_wiki_index()}"] + + for subdir in [CONCEPTS_DIR, CONNECTIONS_DIR, QA_DIR]: + if not subdir.exists(): + continue + for md_file in sorted(subdir.glob("*.md")): + rel = md_file.relative_to(KNOWLEDGE_DIR) + content = md_file.read_text(encoding="utf-8") + parts.append(f"## {rel}\n\n{content}") + + return "\n\n---\n\n".join(parts) + + +def list_wiki_articles() -> list[Path]: + """List all wiki article files.""" + articles = [] + for subdir in [CONCEPTS_DIR, CONNECTIONS_DIR, QA_DIR]: + if subdir.exists(): + articles.extend(sorted(subdir.glob("*.md"))) + return articles + + +def list_raw_files() -> list[Path]: + """List all daily log files.""" + if not DAILY_DIR.exists(): + return [] + return sorted(DAILY_DIR.glob("*.md")) + + +# ── Index helpers ───────────────────────────────────────────────────── + +def count_inbound_links(target: str, exclude_file: Path | None = None) -> int: + """Count how many wiki articles link to a given target.""" + count = 0 + for article in list_wiki_articles(): + if article == exclude_file: + continue + content = article.read_text(encoding="utf-8") + if f"[[{target}]]" in content: + count += 1 + return count + + +def get_article_word_count(path: Path) -> int: + """Count words in an article, excluding YAML frontmatter.""" + content = path.read_text(encoding="utf-8") + # Strip frontmatter + if content.startswith("---"): + end = content.find("---", 3) + if end != -1: + content = content[end + 3:] + return len(content.split()) + + +def build_index_entry(rel_path: str, summary: str, sources: str, updated: str) -> str: + """Build a single index table row.""" + link = rel_path.replace(".md", "") + return f"| [[{link}]] | {summary} | {sources} | {updated} |" diff --git a/.claude/memory-compiler/uv.lock b/.claude/memory-compiler/uv.lock new file mode 100644 index 00000000..d158ff85 --- /dev/null +++ b/.claude/memory-compiler/uv.lock @@ -0,0 +1,636 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353 }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "claude-agent-sdk" +version = "0.1.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/c1/c7afb1a08cecef0644693ccd9975651e45cf23a50272b94b6eca2c1a7dc8/claude_agent_sdk-0.1.56.tar.gz", hash = "sha256:a95bc14e59f9d6c8e7fa2e6581008a3f24f10e1b57302719823f62cfb5beccdc", size = 121659 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/73/4e3d13c4d43614de35a113c87ec96b3db605baa23f9f5c4a38536837e18e/claude_agent_sdk-0.1.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f5a7c87617101e6bb0f23408104ac6f40f9b5adec91dcfe5b8de5f65a7df73a", size = 58585662 }, + { url = "https://files.pythonhosted.org/packages/1e/6d/78347c2efa1526f1f6e7edecabe636575f622bcaa7921965457f95dd12dc/claude_agent_sdk-0.1.56-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:824f4a10340f46dd26fee8e74aeed4fc64fec95084e327ab1ebb6058b349e1c3", size = 60419564 }, + { url = "https://files.pythonhosted.org/packages/87/c1/708262318926c8393d494a5dcaafd9bc7d6ba547c0a5fad4eff5f9aa0ecd/claude_agent_sdk-0.1.56-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:ff60dedc06b62b52e5937a9a2c4b0ec4ad0dd6764c20be656d01aeb8b11fba1d", size = 71893844 }, + { url = "https://files.pythonhosted.org/packages/1b/4f/24918a596b0d61c3a691af2a9ee52b8c54f1769ce2c5fef1d64350056e53/claude_agent_sdk-0.1.56-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:fe866b2119f69e99d9637acc27b588670c610fed1c4a096287874db5744d029b", size = 72030943 }, + { url = "https://files.pythonhosted.org/packages/f3/d8/5ded242e55f0b5f295d4ee2cbe5ae3bca914eb0a2a291f81e38b68d3ef58/claude_agent_sdk-0.1.56-py3-none-win_amd64.whl", hash = "sha256:5934e082e1ccf975d65cd7412f2eaf2c5ffa6b9019a2ca2a9fb228310df7ddc8", size = 74141451 }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "46.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401 }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275 }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320 }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082 }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514 }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766 }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535 }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618 }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802 }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425 }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530 }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896 }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348 }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896 }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147 }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221 }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952 }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141 }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178 }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812 }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923 }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695 }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785 }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404 }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549 }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874 }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511 }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692 }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776 }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529 }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827 }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265 }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800 }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771 }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333 }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069 }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358 }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061 }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103 }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255 }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660 }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + +[[package]] +name = "llm-personal-kb" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "claude-agent-sdk" }, + { name = "python-dotenv" }, + { name = "tzdata" }, +] + +[package.metadata] +requires-dist = [ + { name = "claude-agent-sdk", specifier = ">=0.1.29" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "tzdata", specifier = ">=2024.1" }, +] + +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967 }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929 }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420 }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330 }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952 }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425 }, +] diff --git a/.claude/projects/-home-rghamilton3-workspace-ardent-forge/memory/reference-deployment.md b/.claude/projects/-home-rghamilton3-workspace-ardent-forge/memory/reference-deployment.md new file mode 100644 index 00000000..07ba74dd --- /dev/null +++ b/.claude/projects/-home-rghamilton3-workspace-ardent-forge/memory/reference-deployment.md @@ -0,0 +1,10 @@ +--- +name: Deployment References +description: Supabase remote ref, web app URL, Vercel config, migration commands +type: reference +--- + +- Supabase remote project ref: `azaioeglqubjqbdssnmi` +- Web app URL: `https://app.ardentforge.app` (NOT ardent-forge.vercel.app) +- Vercel is configured for deployment +- Push migrations: `bunx supabase db push --linked` diff --git a/.claude/rules/error-handling.md b/.claude/rules/error-handling.md new file mode 100644 index 00000000..f8a89e07 --- /dev/null +++ b/.claude/rules/error-handling.md @@ -0,0 +1,163 @@ +# Error Handling Conventions + +## Error Type Semantics + +Error types at system boundaries must distinguish input validation failures from network/transport failures. Use distinct error variants (e.g., `INVALID_INPUT` vs `NETWORK_ERROR`) so callers can branch on error type for retry logic, user messaging, and logging without ambiguity. + +## Catch Blocks + +Never use bare `catch {}`. Always capture the error parameter and log it with a bracketed module prefix: + +```typescript +// Bad +try { ... } catch { return fallback } + +// Good +try { ... } catch (err) { + console.error('[module-name] Description:', err) + return fallback +} +``` + +This ensures errors are traceable in the dev console and production logs. + +## Adapter Boundary Fallbacks + +Safety-net coercions at adapter boundaries (null-to-default, type coercions) should log at warn level when the fallback triggers for fields expected to be non-nullable. Silent coercion hides data integrity issues. + +```typescript +// Bad -- silent coercion +function intToBool(val: number | null, fallback = false): boolean { + return val == null ? fallback : val !== 0 +} + +// Good -- logs when safety net triggers +function intToBool(val: number | null, field: string, fallback = false): boolean { + if (val == null) { + console.warn(`[adapter] ${field}: expected non-null int, using fallback ${fallback}`) + return fallback + } + return val !== 0 +} +``` + +## User-Action Guard Clauses + +Guard clauses in user-action handlers (save, delete, submit) must never silently return. Always log with `[module-name]` prefix and set a user-facing error state (e.g., `setErrors()`, toast notification). + +```typescript +// Bad +const handleSave = () => { + if (!userId) return + // ... +} + +// Good +const handleSave = () => { + if (!userId) { + console.error('[my-module] Cannot save: no authenticated user') + setErrors((prev) => ({ ...prev, general: 'You must be signed in to save.' })) + return + } + // ... +} +``` + +## Mutation Hook Error Handling + +All `useMutation`-based hooks must attach an `onError` handler that logs with a bracketed `[module-name]` prefix and includes enough context (operation name, key entity ids) to diagnose the failure from logs alone. Where the mutation performs an optimistic cache update, `onError` must also roll the cache back to the snapshotted previous state. + +```typescript +// Bad -- silent mutation failure +export function useDeleteWidget() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: string) => adapter.deleteWidget(id), + onSuccess: () => qc.invalidateQueries({ queryKey: ['widgets'] }), + }) +} + +// Good -- prefixed log + cache rollback +export function useDeleteWidget() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: string) => adapter.deleteWidget(id), + onMutate: async (id) => { + await qc.cancelQueries({ queryKey: ['widgets'] }) + const previous = qc.getQueryData(['widgets']) + qc.setQueryData(['widgets'], (old: Widget[] = []) => old.filter((w) => w.id !== id)) + return { previous } + }, + onError: (err, id, ctx) => { + console.error('[widgets] deleteWidget failed:', { id, err }) + if (ctx?.previous) qc.setQueryData(['widgets'], ctx.previous) + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['widgets'] }), + }) +} +``` + +A mutation hook that lacks `onError` is a silent failure: the user-facing component cannot distinguish "request still in flight" from "request failed and the optimistic UI is now lying". The `[module-name]` prefix makes the failure greppable in production logs. + +## Query Hook Error States + +All `useQuery`-based hooks in user-facing components must destructure and handle `isError` (or `error`). Showing stale or empty UI on a fetch failure is a silent failure. At minimum, render an error state so the user knows something went wrong. + +```typescript +// Bad -- ignores error state +const { data, isLoading } = useMyQuery(id) + +// Good -- handles error state +const { data, isLoading, isError } = useMyQuery(id) + +// ... later in JSX: +// {isError ? : isLoading ? : } +``` + +## Mutation Hook Log Ownership + +When a `useMutation` hook has an `onError` handler that logs the failure, the consuming +component must not also catch and log the same rejection. The hook is the single log +owner for that mutation -- a single failure producing two log lines with different module +prefixes is misleading and harder to trace in production. + +```typescript +// Bad -- hook logs and component also logs the same rejection +export function useCreateFoo() { + return useMutation({ + mutationFn: createFoo, + onError: (err) => console.error('[foos] createFoo failed:', err), + }) +} + +// Component: +const handleSubmit = async () => { + try { + await createFoo.mutateAsync(values) + } catch (err) { + console.error('[foo-form] Failed to create:', err) // duplicate log -- BAD + } +} + +// Good -- hook owns the log; component may still render isError state +export function useCreateFoo() { + return useMutation({ + mutationFn: createFoo, + onError: (err) => console.error('[foos] createFoo failed:', err), + }) +} + +// Component: +const handleSubmit = async () => { + try { + await createFoo.mutateAsync(values) + } catch { + // Hook already logged. Component renders error state via createFoo.isError. + } +} +``` + +Exception: if the consuming component invokes the mutation via a callback that may +itself throw after the mutation resolves (e.g., an `onCreated` prop), wrap that +callback in its own distinct `try/catch` with a clearly different `[module-name]` +prefix -- that is a separate error domain from the mutation itself. diff --git a/.claude/rules/layout-conventions.md b/.claude/rules/layout-conventions.md new file mode 100644 index 00000000..47cc2094 --- /dev/null +++ b/.claude/rules/layout-conventions.md @@ -0,0 +1,74 @@ +# Layout Conventions + +## Content Width Constraint + +All authenticated pages use `max-w-5xl` (1024px) as the standard content-width constraint. This prevents excessive line lengths and content sprawl on wide screens while maintaining comfortable readability. + +Apply `mx-auto max-w-5xl` to the main content wrapper on every authenticated page, including early-return states (loading, error, not-found) to avoid jarring width shifts when data loads. + +```tsx +// Standard page wrapper pattern +
+
{/* Page content */}
+
+``` + +**Exception -- multi-column editor layouts:** Pages whose primary layout is a +wide fixed-column grid (e.g., `xl:grid-cols-[280px_1fr_260px]`) may omit +`max-w-5xl` when the column widths themselves constrain content density and a +fixed-positioned side panel (such as `ExercisePickerDrawer`) must dock flush +against the grid at xl/2xl breakpoints. Document the intentional deviation with +an inline comment in the component (see `TemplateEditorLayout`). + +## Responsive Padding + +Use progressive horizontal padding across breakpoints: + +- Mobile: `px-4` +- Tablet (`md:`): `px-6` +- Desktop (`lg:`): `px-8` + +## No Divider Lines (Iron & Ember) + +**Prohibited:** `border-t`, `border-b`, `border-l`, `border-r` with any +`border-surface-*` color are not allowed as section dividers. This is a +non-negotiable constraint of the Iron & Ember design system +(CLAUDE.md §Design Context — "no divider lines (tonal layering)"). + +Communicate section breaks through **tonal depth** instead: shift the +background surface (`bg-surface-pit/40`, `bg-surface-gunmetal/60`, +`bg-surface-iron`, `bg-surface-charcoal/40`) to create visual separation +without drawing a line. + +```tsx +// Bad: border divider (violates Iron & Ember) +
+

Section title

+
+ +// Bad: "or" separator with flanking border lines +
+
+ or +
+
+ +// Good: tonal shift communicates the break +
+

Section title

+
+ +// Good: bare "or" with spacing, no lines +
+ or +
+``` + +**Exceptions:** None for new code. Pre-existing violations in +`gym-management-section.tsx` and `_authenticated/index.tsx` are grandfathered +but should migrate opportunistically. If a new feature genuinely needs a +visual separator that tonal depth cannot express, raise it as a design +system change, not a one-off escape hatch. + +**How to apply:** Grep for `border-t border-surface`, `border-b border-surface`, +etc. in code review. Any match on new or touched code is a blocker. diff --git a/.claude/rules/state-management.md b/.claude/rules/state-management.md new file mode 100644 index 00000000..21e77f20 --- /dev/null +++ b/.claude/rules/state-management.md @@ -0,0 +1,70 @@ +# State Management Conventions + +## Zustand Store Boundary Validation + +Zustand stores that accept domain-constrained values should validate at their own boundary, not rely solely on caller validation. This makes stores self-protecting against future callers that might bypass upstream validation. + +```typescript +// Bad: relies on callers to validate +setPending: (url, key) => set({ pending: { url, key } }) + +// Good: validates at the store boundary +setPending: (url, key) => { + if (!url.startsWith('https://') || !key) return + set({ pending: { url, key } }) +} +``` + +## Module-State Setter Validation + +The same boundary-validation principle applies to functions in a module that mutate module-scope state -- they are operationally equivalent to a state store. A module-level setter that accepts domain-constrained values should validate at the module boundary, not assume callers got it right. + +This matters most for modules with multiple call sites (publishers, subscribers, adapters, registries) where a future caller could quietly bypass upstream validation. Examples in this codebase: `display-publisher.ts::configureDisplayPublisher`, `display-subscriber.ts::initDisplaySubscriber`, `gym-picker-storage.ts::writeLastGymChoice`. + +```typescript +// Bad: silently mutates with whatever the caller passed +let _activeGymId: string | null = null +export function configureDisplayPublisher({ gymId }: { gymId: string | null }): void { + _activeGymId = gymId + // empty string would silently produce a `display:gym:` channel name +} + +// Good: validates at the module boundary +export function configureDisplayPublisher({ + gymId, + intent, +}: { + gymId: string | null + intent: 'broadcasting' | 'private' +}): void { + if (intent === 'broadcasting' && gymId === null) { + console.error('[display-publisher] intent=broadcasting requires gymId') + return + } + if (intent === 'private' && gymId !== null) { + console.error(`[display-publisher] intent=private requires gymId=null, got ${gymId}`) + return + } + _activeGymId = gymId + _publisherMode = intent +} +``` + +The validation should always **log** rather than throw silently, so the bypass attempt is observable in production traces. + +## Zustand + React Integration + +- Access store state in components via the hook: `const value = useMyStore((s) => s.value)` +- Access store state outside React (event handlers, side effects) via `useMyStore.getState()` +- For one-time initialization from a store, use `useState` initializer functions to avoid render-phase side effects: + +```typescript +// Bad: setState during render +const pending = usePendingConnect.getState().pending +if (pending) { + setUrl(pending.url) +} // side effect during render + +// Good: initialize via useState +const [url, setUrl] = useState(() => usePendingConnect.getState().pending?.url ?? '') +``` diff --git a/.claude/rules/supabase.md b/.claude/rules/supabase.md index 61a7d59b..505efeeb 100644 --- a/.claude/rules/supabase.md +++ b/.claude/rules/supabase.md @@ -1,12 +1,13 @@ --- paths: - - "supabase/**" - - "**/*.sql" + - 'supabase/**' + - '**/*.sql' --- # Supabase Conventions ## Migrations + - All schema changes go through migration files in `supabase/migrations/` - Migration naming: `YYYYMMDDHHMMSS_description.sql` - Never modify production schema outside of migrations @@ -14,6 +15,7 @@ paths: - Test migrations locally with `npx supabase db push` before committing ## Row Level Security (RLS) + - RLS enabled on every table that stores user data - Policies use `auth.uid()` for ownership checks - Separate policies for SELECT, INSERT, UPDATE, DELETE @@ -21,6 +23,7 @@ paths: - Test RLS policies with different user contexts ## SQL Style + - Use lowercase for SQL keywords (select, insert, update, etc.) - Table and column names: snake_case - Primary keys: `id uuid default gen_random_uuid()` @@ -29,20 +32,28 @@ paths: - All queries parameterized -- no string concatenation for values ## Indices + - Create indices for columns used in WHERE clauses and JOINs - Composite indices follow query pattern order - Name indices descriptively: `idx_[table]_[columns]` ## Triggers + - Use triggers for `updated_at` auto-updates - Trigger functions in a dedicated schema or clearly named - Keep trigger logic simple -- complex business logic belongs in application code +### `auth.users` triggers (added by F018) + +The codebase has one `security definer` trigger on `auth.users` insert (`trg_auth_user_default_gym`, installed by `20260407000001_create_gyms.sql`). When adding new triggers on `auth.users`, follow the same pattern: `security definer`, locked `search_path`, idempotent against retries (`on conflict do nothing` or equivalent), and an integration test that creates a user via `auth.admin.createUser` (or a direct `insert into auth.users` under superuser) and asserts the downstream side effect. `supabase/tests/018_trigger.sql` is the reference pattern. + ## Seed Data + - Seed data in `supabase/seed.sql` for local development - Seed data must be idempotent (safe to run multiple times) ## Client Usage (Frontend) + - Use typed Supabase client generated from schema - Handle Supabase errors at the call site with meaningful messages - Use `.single()` when expecting exactly one row diff --git a/.claude/rules/typescript-conventions.md b/.claude/rules/typescript-conventions.md new file mode 100644 index 00000000..6eb9a501 --- /dev/null +++ b/.claude/rules/typescript-conventions.md @@ -0,0 +1,25 @@ +# TypeScript Conventions + +## Exhaustive Record Constants + +For exhaustive Record constants, use `satisfies Record` without an explicit type annotation. This provides compile-time exhaustiveness checking while letting TypeScript infer narrower literal types. + +```typescript +// Good +const FOO = { ... } satisfies Record + +// Bad -- redundant annotation widens the type +const FOO: Record = { ... } satisfies Record +``` + +## Domain-Keyed Record Types + +All Record types keyed by domain union types (SessionType, BlockType, GroupType, SetSchemeType, etc.) must use the union as the key type, not `string`. Use `satisfies` for exhaustiveness. This ensures new domain variants cause compile errors instead of silent fallbacks. + +```typescript +// Good +const STYLES = { ... } satisfies Record + +// Bad -- new BlockType variants silently fall back +const STYLES: Record = { ... } +``` diff --git a/.claude/settings.json b/.claude/settings.json index 6dc83e28..44b60bb4 100755 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,8 +1,4 @@ { - "statusLine": { - "type": "command", - "command": "bun run hooks/context-recovery/statusline-monitor.ts" - }, "permissions": { "allow": [ "Edit", @@ -24,22 +20,60 @@ "ask": [] }, "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "uv --project .claude/memory-compiler run python .claude/memory-compiler/hooks/session-start.py", + "timeout": 15 + } + ] + } + ], + "SessionEnd": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "uv --project .claude/memory-compiler run python .claude/memory-compiler/hooks/session-end.py", + "timeout": 10 + } + ] + } + ], "UserPromptSubmit": [ { "hooks": [ { "type": "command", - "command": "bun run hooks/skill-activation/skill-activation.ts" + "command": "bun run .claude/hooks/skill-activation/skill-activation.ts" + }, + { + "type": "command", + "command": "bun run .claude/hooks/validators/secret-detector.ts" } ] } ], "PreCompact": [ { + "matcher": "", "hooks": [ { "type": "command", - "command": "bun run hooks/context-recovery/conv-backup.ts", + "command": "uv --project .claude/memory-compiler run python .claude/memory-compiler/hooks/pre-compact.py", + "timeout": 10 + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "bun run .claude/hooks/context-recovery/conv-backup.ts", "async": true } ] @@ -51,10 +85,14 @@ "hooks": [ { "type": "command", - "command": "bun run hooks/formatter/formatter.ts" + "command": "bun run .claude/hooks/formatter/formatter.ts" } ] } ] + }, + "statusLine": { + "type": "command", + "command": "bun run .claude/hooks/context-recovery/statusline-monitor.ts" } } diff --git a/.claude/skills/codebase-navigation/SKILL.md b/.claude/skills/codebase-navigation/SKILL.md index 8e9ed53b..7cbb1d03 100755 --- a/.claude/skills/codebase-navigation/SKILL.md +++ b/.claude/skills/codebase-navigation/SKILL.md @@ -5,7 +5,7 @@ description: Codebase structure and efficient navigation tips # Directory Structure -## 🏗️ Framework Organization +## 🏗️ Claude Fast Framework Organization ``` .claude/ diff --git a/.claude/skills/impl-start/SKILL.md b/.claude/skills/impl-start/SKILL.md index c69a4b7d..c935646e 100755 --- a/.claude/skills/impl-start/SKILL.md +++ b/.claude/skills/impl-start/SKILL.md @@ -25,7 +25,7 @@ Begin or resume implementation of a planned feature. Restores full context from - `Tech.md` -- architecture decisions and stack details - `Steps.md` -- task list, team, milestones, contracts 4. Read all accepted ADRs in `Context/Decisions/` relevant to this feature -5. Read active `.claude/rules/` for stacks this feature touches +5. Read active `.claude/rules/` for stacks this feature touches ### Step 2: Determine execution state 1. Parse Steps.md Progress section (Status, Current task, Last milestone) @@ -53,11 +53,13 @@ Begin or resume implementation of a planned feature. Restores full context from 4. Ask: "Proceed with implementation?" ### Step 5: Execute via agent delegation -Delegate to the `/impl` command workflow: -- Spawn agents for current wave's tasks -- Pass upstream contracts from previous milestones -- Monitor task completion -- Run milestone checkpoints (stub detection, drift check, contract extraction) +Delegate to the `/impl` command workflow, which uses the background + Monitor + event-log pattern (see `core/skills/monitor-loop/SKILL.md`): +- Render each task's specialist prompt to `.cortex/prompts/.md` +- Spawn agents for the current wave with `Task({run_in_background: true})`, one call per parallel task in a single orchestrator message +- Start a `Monitor` on `.cortex/events.log` filtered to `task_done`, `contract_ready`, `milestone_reached`, and `error` +- React to events instead of blocking on `TaskOutput`. Only pull an agent's output when an `error` event fires or an expected `task_done` is missing past its deadline +- Pass upstream contracts from previous milestones by injecting them into the next wave's prompt files +- Run milestone checkpoints (stub detection, drift check via the Advisor tool, contract extraction) - Continue through waves until complete For quick plans (single-file plan, no team orchestration): @@ -66,7 +68,7 @@ For quick plans (single-file plan, no team orchestration): - Mark tasks complete as they are done ### Step 6: Handle completion -- If all tasks complete: update Status to "Complete", suggest `/qa` and `/review` +- If all tasks complete: update Status to "Complete", suggest `/qa` and `/review-capture` - If context window filling: save progress to Steps.md and `.cortex/session.md`, tell user to start a new session - If plan is wrong: pause, discuss with user, create ADR or update Spec.md @@ -77,4 +79,4 @@ For quick plans (single-file plan, no team orchestration): - Test tasks (S###-T) must produce actual test files, not just comments - Doc tasks (S###-D) must modify the specified document - If an ADR is needed at a milestone, use the adr-create skill before continuing -- Save progress to Steps.md and `.cortex/session.md` frequently +- Save progress to Steps.md and `.cortex/session.md` frequently diff --git a/.claude/skills/monitor-loop/SKILL.md b/.claude/skills/monitor-loop/SKILL.md new file mode 100644 index 00000000..ee97fdfd --- /dev/null +++ b/.claude/skills/monitor-loop/SKILL.md @@ -0,0 +1,114 @@ +--- +name: monitor-loop +description: Spawn background sub-agents and coordinate them via the Monitor tool and an event log, instead of blocking on TaskOutput. +--- + +# Monitor Loop Pattern + +`/impl` and `/team-impl` use this pattern to run specialist sub-agents in parallel without blocking the orchestrator's context on `TaskOutput` dumps. + +## Why + +`TaskOutput({block: true, timeout: 300000})` pulls the full agent transcript into the orchestrator's context window. That serialises waves (orchestrator stays hot while each agent runs), bloats the central window with chatter, and makes parallel execution practically impossible on a Sonnet orchestrator. + +The Monitor tool solves both problems: +- Sub-agents run with `run_in_background: true`; the orchestrator proceeds. +- Sub-agents append short structured lines to `.cortex/events.log`. +- A `Monitor` on that log fires one notification per matching line — the orchestrator sees only discrete events (`task_done`, `contract_ready`, `milestone_reached`, `error`). +- The full sub-agent transcript stays out of the orchestrator's context. If an error fires, only then does the orchestrator pull that single agent's `TaskOutput` for triage. + +## Preconditions + +- `.claude/hooks/event-log/event-log.ts` exists (it does) and is executable via `bun run`. +- The orchestrator has `Task`, `Monitor`, and `Bash` available. +- Sub-agent prompts include an explicit instruction to append events at task-complete and contract-ready moments. + +## Event shape + +Lines in `.cortex/events.log` are NDJSON, one per line, matching this TypeScript shape: + +```ts +interface EventEntry { + ts: string; // ISO-8601 timestamp + source: string; // agent name, e.g. "builder-api" + type: "task_done" | "contract_ready" | "milestone_reached" | "error" | "note"; + task?: string; // S### task id when applicable + contract?: string; // absolute or project-relative path + note?: string; // free-text context, optional +} +``` + +## Sub-agent instruction (paste into every specialist prompt) + +```text +When you finish a task assigned to you, run: + bun run .claude/hooks/event-log/event-log.ts append --source --type task_done --task + +When you write a contract file that downstream agents will consume +(a file path listed in the Steps.md milestone Contracts section), run: + bun run .claude/hooks/event-log/event-log.ts append --source --type contract_ready --contract + +If you hit an unrecoverable blocker, run: + bun run .claude/hooks/event-log/event-log.ts append --source --type error --note "" + +Do NOT emit events for routine tool calls. The event log is for milestones only. +``` + +## Orchestrator flow + +1. **Initialise the log.** Before spawning any wave, ensure the file exists so `tail -F` can start cleanly: + ```bash + mkdir -p .cortex && : > .cortex/events.log + ``` + Truncating is safe here — `/impl` writes a fresh log per feature run, and `.claude/hooks/context-recovery/backup-core.ts` already preserved the prior session's log in the backup. + +2. **Spawn the wave in the background.** For every task in the current wave that has its upstream contracts satisfied, render the specialist prompt to `.cortex/prompts/.md` and call: + ```ts + Task({ + description: "S002: Build API layer", + prompt: "", + subagent_type: "general-purpose", + run_in_background: true, + }); + ``` + All parallel tasks in the wave go out in a single message, back-to-back. + +3. **Start the Monitor.** Immediately after spawning: + ```ts + Monitor({ + description: "cortex events: task_done, contract_ready, milestone_reached, error", + command: `tail -n 0 -F .cortex/events.log | grep --line-buffered -E '"type":"task_done"|"type":"contract_ready"|"type":"milestone_reached"|"type":"error"'`, + timeout_ms: 3600000, + persistent: false, + }); + ``` + Notes: + - `tail -n 0` skips the existing contents so we only see new events. + - `tail -F` (capital F) retries if the file is missing or rotated. + - `grep --line-buffered` is mandatory — default pipe buffering delays events by minutes. + - The pattern matches on `"type":""` so a free-text `note` field containing the word `error` will not trigger a false positive. + - `timeout_ms` is 1 hour — waves longer than that likely indicate a stalled agent anyway. + +4. **React to events, do not poll.** Every matching event arrives as a chat notification. When a `task_done` event lands, mark the corresponding TaskUpdate row `completed`. When a `contract_ready` event lands, read that file and prepare it for injection into the next wave. When an `error` event lands, pull that single agent's full output via `TaskOutput({task_id, block: false})` for triage. + +5. **Wave-complete condition.** A wave is complete when the expected set of `task_done` events for that wave have arrived. Maintain a small in-memory checklist against the planned Steps.md task list. Do NOT call `TaskOutput` on agents that emitted `task_done` — the event is authoritative. + +6. **Safety net for silent agents.** If an expected `task_done` has not arrived by some reasonable deadline (default: 10 minutes per task unless Steps.md says otherwise), pull that one agent's `TaskOutput` non-blocking and inspect it. Treat missing events as a bug in the specialist prompt rather than a routine situation. + +7. **Shut down the Monitor.** Once every wave is complete and the milestone checkpoints have passed, call `TaskStop` on the Monitor task_id or let it time out. + +## What this pattern is not + +- **Not a replacement for milestone drift checks.** Events are throughput signals, not quality signals. The orchestrator still runs `stub detection`, `contract extraction`, and `drift checkpoint` at each milestone boundary. +- **Not a replacement for TaskOutput on failure.** When an agent reports `error`, or when an expected `task_done` never arrives, the orchestrator pulls that single agent's output. The point is to avoid pulling output on the happy path. +- **Not a messaging bus between agents.** Agents do not read the event log to communicate with each other. Only the orchestrator consumes it. For cross-agent contract handoff, use Steps.md milestone Contracts — same as before. + +## Failure modes and mitigations + +| Failure | Mitigation | +|---|---| +| Specialist forgets to emit `task_done` | Safety-net timeout pulls its TaskOutput; orchestrator updates the prompt template if this recurs. | +| Monitor filter too tight, real events missed | The filter is single source of truth for event types. Keep it in sync with the `EventType` union in `.claude/hooks/event-log/event-log.ts`. | +| Monitor filter too loose, notification flood | Monitor auto-stops on excessive volume. Restart with a tighter regex. | +| Event log grows across sessions | `/impl` truncates at start; `backup-core.ts` archives the prior log before truncation. | +| Agent emits event with malformed JSON | Unlikely — agents call `event-log.ts append`, which builds the JSON. Direct `echo >> events.log` by agents is discouraged. | diff --git a/.claude/skills/project-init/SKILL.md b/.claude/skills/project-init/SKILL.md index 5abdebd8..9b0d6410 100755 --- a/.claude/skills/project-init/SKILL.md +++ b/.claude/skills/project-init/SKILL.md @@ -1,7 +1,7 @@ # Skill: Project Initialization Templates ## Purpose -Provides all template content needed by the `/setup` command. +Provides all template content needed by the `/context-engine:init` command. This skill is not invoked directly — it's a reference library. ## Templates @@ -563,3 +563,17 @@ exit 0 } } ``` + +--- + +### .cortex/config.json + +Created during scaffolding if it does not already exist. Stores project-level Cortex configuration. + +```json +{ + "issueTracker": { + "type": "backlog" + } +} +``` diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 00000000..7d877619 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,159 @@ +# Skill: Release + +## Purpose + +Prepare and publish a new version of Ardent Forge by bumping versions, updating the changelog, tagging the release, and monitoring the CI/CD pipeline through to Play Store deployment. + +## When to use + +- User says "release", "cut a release", "publish a version", "tag a release" +- User wants to bump the version and push a new build +- User says "ship it", "deploy", "push to Play Store" + +## Prerequisites + +- All intended changes are merged into the target branch +- CI is green on the target branch (no failing checks) +- User has push access and secrets are configured in GitHub + +## Workflow + +### Step 1: Determine release type and version + +1. Read the current version from `package.json` (source of truth) +2. Confirm with `src-tauri/tauri.conf.json` and `src-tauri/Cargo.toml` -- all three must match +3. Ask the user what kind of release this is: + +| Release type | Branch | Tag example | GitHub Release | +| ----------------- | --------- | ---------------- | -------------- | +| Alpha | `develop` | `v1.0.0-alpha.1` | Pre-release | +| Beta | `develop` | `v1.0.0-beta.3` | Pre-release | +| Release candidate | `develop` | `v1.0.0-rc.1` | Pre-release | +| Stable | `main` | `v1.0.0` | Stable | + +4. Derive the new version using semver. For pre-release, increment the pre-release number. For stable, drop the pre-release suffix (or bump major/minor/patch per user input). + +### Step 2: Merge develop into main (stable releases only) + +Skip this step for pre-release (alpha/beta/RC) releases. + +For stable releases, `develop` must be merged into `main` before tagging: + +1. Confirm with the user: "Ready to merge `develop` into `main` for the stable release?" +2. Ensure the working tree is clean on `develop` +3. Switch to `main` and pull latest: `git checkout main && git pull origin main` +4. Merge develop: `git merge develop --no-ff -m "chore: merge develop into main for vX.Y.Z release"` +5. Push the merge: `git push origin main` +6. If there are merge conflicts, stop and report them to the user -- do not force-resolve + +After the merge, remain on `main` for the rest of the release process. + +### Step 3: Pre-flight checklist + +Verify each item before proceeding. Stop and report any failures. + +- [ ] On the correct branch (`develop` for pre-release, `main` for stable) +- [ ] Working tree is clean (`git status` shows no uncommitted changes) +- [ ] All tests pass (`bun run test`) +- [ ] Lint passes (`bun run lint`) +- [ ] Build succeeds (`bun run build`) +- [ ] No pending Supabase migrations that haven't been tested locally (`ls supabase/migrations/`) + +### Step 4: Bump versions + +Update the version string in all four files: + +1. `package.json` -- `"version": "X.Y.Z..."` field +2. `src-tauri/tauri.conf.json` -- `"version": "X.Y.Z..."` field +3. `src-tauri/Cargo.toml` -- `version = "X.Y.Z..."` field +4. `src-tauri/gen/android/app/tauri.properties` -- bump `tauri.android.versionCode` and set `tauri.android.versionName` + +All three manifest files (1-3) must contain the identical version string (without the `v` prefix). + +**Version code (Android):** The `versionCode` in `tauri.properties` must be strictly greater than the previous value. Read the current value and increment by 1. CI will override this with `1000000 + github.run_number`, but the committed value serves as a floor and prevents conflicts if the CI formula ever resets. Never reuse or decrease a version code -- Play Store rejects uploads with duplicate or lower codes. + +### Step 5: Update CHANGELOG.md + +1. Read the current `CHANGELOG.md` +2. Gather changes since the last release tag: `git log $(git describe --tags --abbrev=0)..HEAD --oneline` +3. Add a new section at the top (below the header) using the established format: + +```markdown +## [X.Y.Z] - YYYY-MM-DD + +### Features + +- **Feature name** -- description (#PR) + +### Improvements + +- Description (#PR) + +### Bug Fixes + +- Description (#PR) +``` + +4. Categorize commits into Features, Improvements, and Bug Fixes +5. Omit empty sections +6. Use past tense, link PR numbers where available + +### Step 6: Commit version bump + +1. Stage the four version files (`package.json`, `tauri.conf.json`, `Cargo.toml`, `tauri.properties`) and `CHANGELOG.md` +2. Commit with message: `chore(release): bump version to X.Y.Z` +3. Do NOT push yet -- present the commit to the user for review + +### Step 7: Tag and push + +After user confirms: + +1. Create an annotated tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` +2. Push the commit and tag: `git push origin --follow-tags` + +This triggers the `release.yml` GitHub Actions workflow. + +### Step 8: Monitor CI/CD pipeline + +1. Check workflow status: `gh run list --workflow=release.yml --limit 1` +2. Report the three job statuses to the user: + - **build-release** -- Signed AAB build + - **migrate** -- Supabase database migrations + - **publish** -- GitHub Release creation + Play Store upload +3. If any job fails: `gh run view --log-failed` to surface the error +4. Link the user to the GitHub Actions run for live monitoring + +### Step 9: Post-release verification + +Once the pipeline succeeds, verify: + +- [ ] GitHub Release exists at the correct tag with AAB attached +- [ ] Release is marked as pre-release or stable (matching the tag type) +- [ ] Play Store internal track shows the new version (user must verify manually) + +### Step 10: Post-release cleanup (stable releases only) + +For stable releases, prompt the user about: + +1. Merge `main` back into `develop` to pick up the version bump +2. Start the next pre-release cycle (e.g., bump to `X.Y+1.0-alpha.1` on `develop`) + +## Failure Recovery + +| Failure point | Recovery | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| Pre-flight check fails | Fix the issue, re-run the check | +| Build job fails | Check `gh run view --log-failed`, fix, delete the tag (`git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z`), re-tag after fix | +| Migration job fails | Check Supabase logs, fix migration, delete tag, re-tag | +| Publish job fails | Can often be re-run: `gh run rerun --failed` | +| Tag pushed on wrong branch | Delete remote tag, reset, re-tag on correct branch | + +## Rules + +- Never skip the pre-flight checklist +- Never push a tag without user confirmation +- All four version files must be updated atomically in a single commit +- Changelog entries must reference PR numbers where available +- Pre-release tags must come from `develop`; stable tags from `main` +- Do not amend the version bump commit after tagging -- if changes are needed, delete the tag, amend, re-tag +- Use `bun` and `bunx` for all commands, never `npm`/`npx`/`yarn` diff --git a/.claude/skills/review-capture/SKILL.md b/.claude/skills/review-capture/SKILL.md index 42ed681e..a38cc031 100755 --- a/.claude/skills/review-capture/SKILL.md +++ b/.claude/skills/review-capture/SKILL.md @@ -29,25 +29,29 @@ from a GitLab MR), ask them to paste or describe the findings. 3. If no match, ask the user which feature this review belongs to 4. Load `Spec.md` and `Steps.md` from the feature directory for cross-referencing -### Step 3: Triage each finding +### Step 3: Triage errors and warnings -Categorize every finding into exactly one of four categories: +Separate the review output into two groups: +- **Errors and warnings** -- triage automatically (this step) +- **Suggestions** -- triage interactively with the user (Step 4) -**[FIX] Fix-now** — Code quality issues, convention violations, missing error handling, +For errors and warnings, categorize each into exactly one of four categories: + +**[FIX] Fix-now** -- Code quality issues, convention violations, missing error handling, typos, formatting. These get fixed inline during the resolution pass. No planning artifacts needed. Signals: linter warnings, style violations, missing null checks, bare excepts, unused imports, wrong function signatures, off-by-one errors. -**[TASK] Missing task** — Work that should have been in Steps.md but wasn't. Needs a +**[TASK] Missing task** -- Work that should have been in Steps.md but wasn't. Needs a new S### task number so it gets tracked and verified. This includes missing tests, undocumented endpoints, edge cases the plan didn't anticipate. Signals: "no test for X", "this case isn't handled", "docs not updated for Y", missing accessibility, missing validation on an input the spec didn't mention. -**[ADR] Architectural concern** — Design problems that require a decision. Wrong +**[ADR] Architectural concern** -- Design problems that require a decision. Wrong abstraction, missing separation of concerns, security gaps, performance anti-patterns, dependency issues. These warrant an ADR because they change the technical approach. @@ -55,21 +59,97 @@ Signals: "this should be a separate service", "you're coupling X to Y", "this wo scale past N", "this auth approach has a vulnerability", "the spec assumed X but the implementation does Y". -**[RULE] Convention gap** — A pattern the reviewer flagged repeatedly that the current +**[RULE] Convention gap** -- A pattern the reviewer flagged repeatedly that the current rules don't catch. The finding should become a rule update so future code gets it right automatically. Signals: same issue in 3+ files, reviewer says "we should always do X", pattern not mentioned in any `.claude/rules/*.md` file, the post-edit hook doesn't catch it. -### Step 4: Assign finding IDs +### Step 4: Triage suggestions interactively + +If the review source produced suggestions (common with pr-review-toolkit), present +them to the user for interactive triage. Errors and warnings skip this step -- they +were already categorized in Step 3. + +**CRITICAL:** Never silently drop, auto-classify, or skip suggestions. Every +suggestion must get an explicit user decision. + +#### Step 4a: Bulk triage + +List all suggestions with their titles so the user can see what they are deciding on: + +``` +## Suggestions from pr-review (N total) + + 1. Consider extracting auth logic into a shared middleware + 2. Database queries could use connection pooling + 3. Error messages should include request IDs for tracing + 4. The retry logic could benefit from exponential backoff + 5. Unused CSS variables in theme.css + + Bulk actions: + a. Capture all -- add all to review file (Claude assigns categories) + b. Dismiss all -- drop everything + c. Skip -- go straight to individual triage + + Choose, or type "done" for individual triage: +``` + +If the user picks "Capture all", Claude assigns a category ([FIX], [TASK], [ADR], +[RULE]) to each suggestion and adds them to the findings list. Update the review +file on disk as a batch. + +If the user picks "Dismiss all", drop all suggestions. No reason required for bulk. + +#### Step 4b: Individual triage + +For each remaining suggestion that was not handled in bulk, present: + +``` +Suggestion X of Y: "Consider extracting auth logic into a shared middleware" + File: src/routes/api/users.ts:45 + Detail: Auth check is duplicated across 4 route handlers + + Recommended: Capture as [TASK] -- tracked refactoring work, not an immediate fix + + Actions: + 1. Capture (as [TASK], or specify: fix/task/adr/rule) + 2. Backlog + 3. Dismiss + + Choose [1-3]: +``` + +**Action handling:** + +- **1 (Capture):** Accept Claude's recommended category. The user can override by + responding "1 fix", "1 adr", etc. The suggestion becomes a finding with the chosen + category and enters the normal flow (gets an ID in Step 5). + +- **2 (Backlog):** Append an entry to `Context/Backlog/Ideas.md`: + ```markdown + ### [Suggestion title] + **Added:** YYYY-MM-DD + **Context:** [suggestion detail from pr-review] + **Related:** [file path from suggestion] + **Priority:** Low + ``` + The suggestion is NOT added to the review file. + +- **3 (Dismiss):** Drop the suggestion. Not captured anywhere. + +After each individual decision, update the review file (or backlog file) on disk +immediately. This ensures progress survives context loss. + +### Step 5: Assign finding IDs Each finding gets a unique ID within the review: `P{review-number}-{sequence}`. The review number increments based on existing files in `Context/Reviews/`. Example: First review in the project → P1-001, P1-002, P1-003... -### Step 5: Cross-reference with planning artifacts +### Step 6: Cross-reference with planning artifacts For each finding: - Check if it relates to a Testable Assertion in Spec.md (note the assertion ID) @@ -77,7 +157,7 @@ For each finding: - Check if an existing ADR already covers the concern - Note these references in the finding entry -### Step 6: Write the review file +### Step 7: Write the review file Create `Context/Reviews/PR-{branch-short-name}-{YYYY-MM-DD}.md` with the following structure: @@ -143,7 +223,7 @@ following structure: - [ ] Review verified by review-verify agent ``` -### Step 7: Present summary to user +### Step 8: Present summary to user After writing the file, present: - Total findings by category diff --git a/.claude/skills/review-resolve/SKILL.md b/.claude/skills/review-resolve/SKILL.md index db5b50f9..6b0d2a96 100755 --- a/.claude/skills/review-resolve/SKILL.md +++ b/.claude/skills/review-resolve/SKILL.md @@ -1,152 +1,173 @@ -# Skill: Resolve Review Findings - -## Purpose -Work through a captured review file, resolve each finding by category, generate -appropriate planning artifacts, and update the review file with resolution status. - -## When to use -- At the start of a fresh session after a review was captured -- User says "resolve review", "fix review findings", "work through the review" -- User references a specific review file in Context/Reviews/ - -## Prerequisites -- A review file must exist in `Context/Reviews/` (created by review-capture) -- This should run in a **fresh session** with full context budget — not in the same - session that performed the review - -## Workflow - -### Step 1: Load the review file -1. If the user specified a file, load that one -2. Otherwise, list files in `Context/Reviews/` and find the most recent with - `Status: 🔴 Unresolved` -3. If no unresolved reviews exist, tell the user — nothing to do -4. Read the full review file -5. Load the associated feature's Spec.md, Steps.md, and Tech.md for context - -### Step 2: Work through Fix-Now findings - -Process each `[FIX]` finding in priority order (Critical → High → Medium → Low): - -1. Open the referenced file and line -2. Apply the fix -3. Verify the fix doesn't break anything obvious (check imports, references) -4. Update the finding in the review file: - - **Status:** ✅ Fixed - - **Resolution:** {Brief description of what was changed} - -If a fix turns out to be more complex than expected (requires restructuring, -affects other modules, changes behavior), reclassify it: -- Reclassify to [TASK] if it needs tracked work -- Reclassify to [ADR] if it requires an architectural decision -- Update the category label and move the finding to the correct section - -### Step 3: Work through Missing Task findings - -For each `[TASK]` finding: - -1. Determine the next available S### number from Steps.md -2. Add the new task to Steps.md in the appropriate phase: - - If it's a test → add as S###-T after the related implementation task - - If it's documentation → add as S###-D after the related implementation task - - If it's new implementation → add at the end of the relevant phase -3. If the finding relates to a Testable Assertion, verify the assertion exists in - Spec.md. If not, add it to the Testable Assertions table. -4. Update the finding in the review file: - - **Status:** ✅ Task created - - **Resolution:** Added as S### in Steps.md - -Do NOT implement the task now — just track it. Implementation follows the normal -impl-start workflow. - -### Step 4: Work through Architectural Concern findings - -For each `[ADR]` finding: - -1. Present the concern to the user with context: - - What the reviewer found - - What the current Tech.md says - - What existing ADRs are relevant -2. Ask the user to decide: - - **Create ADR** — proceed to use the adr-create skill - - **Dismiss** — the concern was considered but doesn't warrant a decision change - (user must provide a reason) - - **Defer** — mark as deferred for later consideration -3. Update the finding in the review file: - - If ADR created: **Status:** ✅ ADR created → **Resolution:** ADR-NNNN - - If dismissed: **Status:** ✅ Dismissed → **Resolution:** {user's reason} - - If deferred: **Status:** 🟡 Deferred → **Resolution:** Deferred — {reason} - -### Step 5: Work through Convention Gap findings - -For each `[RULE]` finding: - -1. Identify which rule file should be updated (or if a new rule file is needed) -2. Read the current rule file -3. Present the suggested addition to the user -4. If approved: - - Add the convention to the appropriate rule file - - If the gap is something the post-edit hook could catch, update the hook too -5. Update the finding in the review file: - - If applied: **Status:** ✅ Rule updated → **Resolution:** Added to {rule-file} - - If dismissed: **Status:** ✅ Dismissed → **Resolution:** {reason} - -### Step 6: Update review file header - -After processing all findings, update the review file: - -1. Count resolutions: - - All resolved → **Status:** 🟢 Resolved - - Some deferred → **Status:** 🟡 Partially resolved - - Any unresolved → **Status:** 🔴 Unresolved (should not happen if workflow completes) -2. Update the Resolution Checklist at the bottom (check completed items) -3. Add a Resolution Summary section: - -```markdown -## Resolution Summary -**Resolved at:** {YYYY-MM-DD} -**Session:** {brief description} - -| Category | Total | Fixed | Tasks Created | ADRs | Rules | Dismissed | Deferred | -|---|---|---|---|---|---|---|---| -| [FIX] | N | N | — | — | — | — | — | -| [TASK] | N | — | N | — | — | — | — | -| [ADR] | N | — | — | N | — | N | N | -| [RULE] | N | — | — | — | N | N | — | -| **Total** | **N** | | | | | | | -``` - -### Step 7: Commit resolution work - -Use the impl-commit skill to commit the fixes. The commit message should reference -the review file: - -``` -fix(review): resolve PR review findings P{N} - -Fixes: [FIX] items resolved inline -Tasks: S### added for [TASK] items -ADR-NNNN created for [ADR] items -Rules updated for [RULE] items - -Review: Context/Reviews/PR-{branch}-{date}.md -``` - -### Step 8: Suggest next steps - -Based on what was generated: -- If new tasks were added → "Run impl-start to work through the new tasks" -- If ADRs were created → "Review the new ADRs to make sure they're complete" -- If rules were updated → "The updated rules will apply to future code automatically" -- Always → "Run review-verify to confirm all findings are resolved" - -## Rules -- ALWAYS start by loading the review file — don't work from conversation memory -- Process findings in category order: FIX → TASK → ADR → RULE -- Within each category, process in priority order: Critical → High → Medium → Low -- Never skip a finding without explicit user input (dismiss or defer with reason) -- Never auto-dismiss architectural concerns — the user must decide -- If context is getting tight, save progress to the review file immediately - (update statuses for what's done so far) before continuing -- Keep the review file as the single source of truth — don't track resolutions - anywhere else +# Skill: Resolve Review Findings + +## Purpose +Work through a captured review file, executing each finding's category as its action. +Findings were already triaged during capture -- resolve just executes. + +## When to use +- At the start of a fresh session after a review was captured +- User says "resolve review", "fix review findings", "work through the review" +- User references a specific review file in Context/Reviews/ + +## Prerequisites +- A review file must exist in `Context/Reviews/` (created by review-capture) +- This should run in a **fresh session** with full context budget -- not in the same + session that performed the review + +## Workflow + +### Step 1: Load the review file +1. If the user specified a file, load that one +2. Otherwise, list files in `Context/Reviews/` and find the most recent with + `Status: Unresolved` +3. If no unresolved reviews exist, tell the user -- nothing to do +4. Read the full review file +5. Load the associated feature's Spec.md, Steps.md, and Tech.md for context +6. Parse all findings into a working list, noting each finding's category, severity, + file reference, and description + +### Step 2: Present findings and confirm +List every finding with its ID, severity, and title, grouped by category. +The category from capture determines the action -- no re-triage needed. + +``` +Findings + + [FIX] -- 9 findings + + - P10-001 (Critical) findActivityId returns fabricated ID causing silent override corruption + - P10-002 (High) Stale closure in handleEditRemove Undo loses edits + - P10-003 (High) Guard clauses in builder.tsx lack user-facing error state (x3) + - P10-004 (High) Template fetch error not handled in SessionEditSheet + - P10-005 (Medium) applyOverrides silently drops setScheme overrides without resolutionCtx + - P10-006 (Low) Orphaned section header in session-detail.tsx + - P10-007 (Low) JSDoc inaccuracy in applyOverrides -- "Returns a new array" + - P10-008 (Low) File path comment is noise + - P10-009 (Low) SQLite migration lacks comment unlike Supabase counterpart + + [TASK] -- 3 findings + + - P10-010 (High) Data-mapper tests only cover overrides: null + - P10-011 (Medium) Rust backend validates JSON syntax but not SessionOverrides structure + - P10-012 (Low) Missing test for setScheme override without resolutionCtx + + [ADR] -- 2 findings + + - P10-013 (Medium) Triple/quadruple null representation for "no overrides" + - P10-014 (Medium) Orphaned override keys silently skipped with no logging + + [RULE] -- 1 finding + + - P10-015 (Medium) useQuery error state destructuring convention + + Total: 15 findings + + Proceed? (yes / or flag specific findings to skip or recategorize) +``` + +**CRITICAL:** Always list findings by title, not just counts. The user needs to +see what they are about to resolve. + +If the user says "yes" or "proceed", move to Step 3. If they flag specific +findings (e.g., "skip P10-008, recategorize P10-005 as TASK"), apply those +changes first, then proceed. + +### Step 3: Execution phase +Execute each finding's action based on its category. +Process in this order: Fix, Task, ADR, Rule. + +#### Fix +1. Open the referenced file and line +2. Apply the fix +3. Verify the fix does not break anything obvious (check imports, references) +4. If the fix turns out to be more complex than expected (requires restructuring, + affects other modules, changes behavior), inform the user and ask whether to + reclassify as Task or ADR +5. Update the finding in the review file: + - **Status:** Fixed + - **Resolution:** {Brief description of what was changed} + +#### Task +1. Determine the next available S### number from Steps.md +2. Add the new task to Steps.md in the appropriate phase: + - Test tasks: add as S###-T after the related implementation task + - Documentation tasks: add as S###-D after the related implementation task + - New implementation: add at the end of the relevant phase +3. If the finding relates to a Testable Assertion, verify the assertion exists in + Spec.md. If not, add it to the Testable Assertions table. +4. Update the finding in the review file: + - **Status:** Task created + - **Resolution:** Added as S### in Steps.md + +#### ADR +1. Use the adr-create skill to create the ADR +2. Update the finding in the review file: + - **Status:** ADR created + - **Resolution:** ADR-NNNN + +#### Rule +1. Identify which rule file should be updated (or if a new rule file is needed) +2. Read the current rule file +3. Add the convention to the appropriate rule file +4. If the gap is something the post-edit hook could catch, update the hook too +5. Update the finding in the review file: + - **Status:** Rule updated + - **Resolution:** Added to {rule-file} + +### Step 4: Update review file header +After executing all actions, update the review file: + +1. Count resolutions: + - All resolved (no Deferred) -- **Status:** Resolved + - Some deferred -- **Status:** Partially resolved + - Any unresolved -- **Status:** Unresolved (should not happen if workflow completes) +2. Update the Resolution Checklist at the bottom (check completed items) +3. Add a Resolution Summary section: + +```markdown +## Resolution Summary +**Resolved at:** {YYYY-MM-DD} +**Session:** {brief description} + +| Category | Total | Resolved | +|---|---|---| +| [FIX] | N | N | +| [TASK] | N | N | +| [ADR] | N | N | +| [RULE] | N | N | +| **Total** | **N** | **N** | +``` + +### Step 5: Commit resolution work +Use the impl-commit skill to commit the fixes. The commit message should reference +the review file: + +``` +fix(review): resolve PR review findings P{N} + +Fixes: [FIX] items resolved inline +Tasks: S### added for [TASK] items +ADR-NNNN created for [ADR] items +Rules updated for [RULE] items + +Review: Context/Reviews/PR-{branch}-{date}.md +``` + +### Step 6: Suggest next steps +Based on what was generated: +- If new tasks were added -- "Run impl-start to work through the new tasks" +- If ADRs were created -- "Review the new ADRs to make sure they're complete" +- If rules were updated -- "The updated rules will apply to future code automatically" +- Always -- "Run review-verify to confirm all findings are resolved" + +## Rules +- ALWAYS start by loading the review file -- don't work from conversation memory +- Present findings summary and wait for user confirmation before executing +- The category from capture IS the action -- do not re-triage +- If a fix turns out more complex than expected, inform the user and ask whether + to reclassify as Task or ADR +- Process in priority order within each category: Critical, High, Medium, Low +- If context is getting tight, save progress to the review file immediately + (update statuses for what's done so far) before continuing +- Keep the review file as the single source of truth -- don't track resolutions + anywhere else diff --git a/.claude/skills/session-management/SKILL.md b/.claude/skills/session-management/SKILL.md index 7611ebca..bc33cc70 100755 --- a/.claude/skills/session-management/SKILL.md +++ b/.claude/skills/session-management/SKILL.md @@ -41,11 +41,11 @@ These apply to ALL session types: ``` ┌───────────────────────────────────────────────────────────────────────┐ -│ /team-plan + /build PIPELINE │ +│ /blueprint + /impl PIPELINE │ ├───────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌─────────────────┐ ┌──────────────────────┐ │ -│ │ CENTRAL AI │───►│ /team-plan │───►│ PLAN FILES │ │ +│ │ CENTRAL AI │───►│ /blueprint │───►│ PLAN FILES │ │ │ │ Coordinator │ │ Plan Creation │ │ .claude/tasks/*.md │ │ │ │ Auto-invokes │ │ Stop Hooks │ │ Source of Truth │ │ │ └──────────────┘ └─────────────────┘ └──────────────────────┘ │ @@ -56,7 +56,7 @@ These apply to ALL session types: │ │ └────────┬────────┘ │ │ │ │ │ │ │ ┌────────▼────────┐ │ -│ └───────────►│ /build │ │ +│ └───────────►│ /impl │ │ │ │ Plan Execution │ │ │ │ TaskList Sync │ │ │ └────────┬────────┘ │ @@ -92,15 +92,15 @@ PENDING → IN_PROGRESS → COMPLETE → VERIFIED --- -## 5-Phase Session Flow (SOP: `/team-plan` + `/build`) +## 5-Phase Session Flow (SOP: `/blueprint` + `/impl`) The standard operating procedure for all non-trivial implementation work is: 1. Assess complexity 2. Gather context if needed -3. Auto-invoke `/team-plan` to create a structured plan +3. Auto-invoke `/blueprint` to create a structured plan 4. Pause for user approval -5. Execute via `/build` +5. Execute via `/impl` --- @@ -112,8 +112,8 @@ Central AI receives the user request and assesses complexity: | ------------ | -------------------------------------------------- | --------------------------------------- | | **Trivial** | Single file, obvious fix, typo | Execute directly | | **Moderate** | 2-5 files, clear scope, single domain | Direct sub-agent delegation | -| **Complex** | Multi-phase, 5+ files, architectural, multi-domain | `/team-plan` → approval → `/build` | -| **Unclear** | Vague but potentially complex | Gather context first, then `/team-plan` | +| **Complex** | Multi-phase, 5+ files, architectural, multi-domain | `/blueprint` → approval → `/impl` | +| **Unclear** | Vague but potentially complex | Gather context first, then `/blueprint` | **Decision**: Route to Phase 2 (context gathering), Phase 3 (plan creation), or skip to direct execution. @@ -121,7 +121,7 @@ Central AI receives the user request and assesses complexity: ### Phase 2: Context Gathering (if needed) -Skip this phase if the initial request is already detailed enough for `/team-plan`. +Skip this phase if the initial request is already detailed enough for `/blueprint`. **For vague or incomplete requests:** @@ -133,21 +133,21 @@ Skip this phase if the initial request is already detailed enough for `/team-pla - Run quick Explore agents to understand architecture - Identify relevant patterns and existing implementations -**Output**: A comprehensive prompt incorporating all gathered context, ready to feed into `/team-plan`. +**Output**: A comprehensive prompt incorporating all gathered context, ready to feed into `/blueprint`. --- -### Phase 3: Plan Creation (auto-invoke `/team-plan`) +### Phase 3: Plan Creation (auto-invoke `/blueprint`) -Central AI auto-invokes the `/team-plan` command: +Central AI auto-invokes the `/blueprint` command: ``` -Skill({ skill: "team-plan", args: "" }) +Skill({ skill: "blueprint", args: "" }) ``` **What happens:** -- `/team-plan` runs in the main thread with full codebase access +- `/blueprint` runs in the main thread with full codebase access - Analyzes the codebase directly (no sub-agents during planning) - Creates a structured plan with team members, tasks, dependencies, acceptance criteria - Stop hooks validate the plan has all required sections @@ -156,7 +156,7 @@ Skill({ skill: "team-plan", args: "" }) **After plan creation, Central AI PAUSES:** - Presents the plan summary to the user -- Waits for approval ("go", `/build`, or feedback) +- Waits for approval ("go", `/impl`, or feedback) - If user requests changes, adjust the plan and re-present **Prompt composition for auto-invocation should include:** @@ -169,17 +169,17 @@ Skill({ skill: "team-plan", args: "" }) --- -### Phase 4: Plan Execution (user-approved `/build`) +### Phase 4: Plan Execution (user-approved `/impl`) -After user approval, Central AI invokes the `/build` command: +After user approval, Central AI invokes the `/impl` command: ``` -Skill({ skill: "build", args: ".claude/tasks/.md" }) +Skill({ skill: "impl", args: ".claude/tasks/.md" }) ``` **What happens:** -- `/build` reads the plan and creates `TaskCreate` entries for each step +- `/impl` reads the plan and creates `TaskCreate` entries for each step - Sets dependencies with `addBlockedBy` / `addBlocks` - Assigns owners to tasks matching team member names - Deploys specialist agents via `Task` tool (parallel where dependencies allow) @@ -187,7 +187,7 @@ Skill({ skill: "build", args: ".claude/tasks/.md" }) **Task List Synchronization:** -- Plan tasks automatically become the TaskList (created by `/build`) +- Plan tasks automatically become the TaskList (created by `/impl`) - `TaskUpdate` marks tasks `in_progress` when starting, `completed` when done - Press `Ctrl+T` to toggle task visibility during work - For cross-session work, set `CLAUDE_CODE_TASK_LIST_ID` environment variable @@ -220,9 +220,9 @@ Apply `practices/branch-completion.md` for branch handling. ## Session File Template -**Note:** For complex work, the `/team-plan` command generates the session/blueprint file automatically. Use this template only for moderate work where you create the session file manually. +**Note:** For complex work, the `/blueprint` command generates the session/blueprint file automatically. Use this template only for moderate work where you create the session file manually. -For `/team-plan` output format, see `.claude/commands/team-plan.md` (Plan Format section). +For `/blueprint` output format, see `.claude/commands/blueprint.md` (Plan Format section). ```markdown # Session - [Title] diff --git a/.claude/skills/session-management/session-types/development.md b/.claude/skills/session-management/session-types/development.md index 6247fba8..edc8bae5 100755 --- a/.claude/skills/session-management/session-types/development.md +++ b/.claude/skills/session-management/session-types/development.md @@ -44,7 +44,7 @@ Before writing code, validate the approach: ### Phase 2: Plan Creation **For complex development work (5+ files, multi-phase, architectural):** -Central AI auto-invokes `/team-plan` which generates a structured plan with team members, tasks, dependencies, and acceptance criteria. See the `session-management` SKILL.md Phase 3 for details. +Central AI auto-invokes `/blueprint` which generates a structured plan with team members, tasks, dependencies, and acceptance criteria. See the `session-management` SKILL.md Phase 3 for details. **For moderate development work (2-5 files, clear scope):** Create detailed, actionable tasks manually: diff --git a/.claude/skills/sub-agent-invocation/SKILL.md b/.claude/skills/sub-agent-invocation/SKILL.md index b2ace336..9d7cc844 100755 --- a/.claude/skills/sub-agent-invocation/SKILL.md +++ b/.claude/skills/sub-agent-invocation/SKILL.md @@ -1,274 +1,287 @@ ---- -name: sub-agent-invocation -description: "Coordination and delegation protocols for sub-agent specialist assignments" ---- - -# Central AI Delegation Protocol - -Load this skill before invoking ANY sub-agent (master-orchestrator, specialists, validators). - ---- - -## Constitutional Invocation Requirements - -Every sub-agent prompt **MUST** include four components: - -### 1. Complete Context (Be Detailed) - -- Project goal and current state -- Why this work is needed now -- How this task fits into broader objectives -- Dependencies on other work or prior decisions - -### 2. Explicit Instructions (Be Clear) - -- Specific task requirements with clear scope -- Expected deliverables and exact format -- Success criteria and validation requirements - -### 3. Context References (Point to Sources) - -- Session files: "Read .claude/tasks/session-current.md for full context" -- Skills: "Load [relevant] skill for patterns and workflows" -- Related implementations: Point to similar existing code - -### 4. Performance Directives (Demand Excellence) - -- Always include: "Think hard and analyze deeply before proceeding" -- Specify thoroughness level: "comprehensive analysis" or "quick validation" - ---- - -## Invocation Template - -``` -"USER'S ORIGINAL REQUEST: [verbatim user prompt - MANDATORY] - -[COMPREHENSIVE CONTEXT] -- Project: [overall goal and current state] -- Background: [why this matters, how it fits] -- Dependencies: [what this builds on or integrates with] - -TASK ASSIGNMENT: -[Detailed, specific requirements with clear scope and boundaries] - -CONTEXT REFERENCES: -- Session: [path and what to extract] -- Skills: [relevant skills to load] -- Examples: [similar existing implementations] - -Think hard and provide [thoroughness level] analysis/implementation. - -DELIVERABLES: -[Exact format, success criteria, validation requirements]" -``` - -**Sub-Agent Context Principle**: Sub-agents have temporary windows—maximize their context collection. Over-collection is safe; under-collection causes failures. - ---- - -## Common Invocation Failures - -| Bad | Good | -| --------------------- | --------------------------------------------------------------------------------------------------------------- | -| "Fix authentication" | "Fix OAuth redirect loop where successful login redirects to /login instead of /dashboard" | -| "Add tests" | "Add tests for user profile editing (session Phase 2) covering avatar upload, validation, error handling" | -| "Implement feature X" | "Implement feature X following patterns from Y, integrating with Z API, referencing session-current.md Phase 3" | - ---- - -## Routing Decision - -| Scenario | Approach | -| -------------------------------------------------- | ---------------------------------------------------------- | -| Multi-phase feature, 5+ files, architectural | Auto-invoke `/team-plan` → user approval → `/build` | -| Cross-domain integration, agents need coordination | `/team-plan` → user approval → `/team-build` (Agent Teams) | -| Simple file edit, pattern search, single-component | Direct sub-agent delegation | -| Ambiguous scope, needs planning | Gather context → `/team-plan` → user approval → `/build` | -| Clear scope, bounded execution | Direct delegation | -| High-reliability task, production changes | `/team-plan` with Specialist + Quality Engineer validation | -| Research/exploration only | Direct Explore agent or deep-researcher | - -**`/build` vs `/team-build` Decision:** - -- `/build`: Tasks are independent, sub-agents don't need to communicate, cost-sensitive (1x tokens) -- `/team-build`: Agents need peer-to-peer coordination, cross-domain interfaces, contract-first spawning (2-4x tokens) - ---- - -## Auto-Invocation Protocol - -Central AI auto-invokes `/team-plan` + `/build` (or `/team-build`) for complex work. This is the standard operating procedure for all non-trivial implementation. - -### Decision Criteria - -**Auto-invoke `/team-plan` immediately when:** - -- Request involves 5+ files or multiple domains -- Request is clearly multi-phase (e.g., "build feature with X, Y, Z") -- Request involves architectural or structural changes -- User explicitly asks for team coordination or a plan - -**Gather info first, then invoke when:** - -- Request is vague but potentially complex -- Request needs clarification about scope or approach -- Request touches unfamiliar areas of the codebase - -**Don't invoke (direct execution) when:** - -- Single file fix, typo, config change -- Clear, bounded task for a single specialist -- Research/exploration only (no implementation) - -### Invocation Syntax - -**Step 1 - Plan:** - -``` -Skill({ skill: "team-plan", args: "" }) -``` - -**Step 2 - Pause:** -Present the plan summary to the user. Wait for approval ("go", `/build`, `/team-build`, or feedback). - -**Step 3 - Execute (after user approval):** - -For isolated sub-agent execution (default): - -``` -Skill({ skill: "build", args: ".claude/tasks/.md" }) -``` - -For collaborative Agent Teams execution (when agents need peer-to-peer coordination): - -``` -Skill({ skill: "team-build", args: ".claude/tasks/.md" }) -``` - -Use `/team-build` when the plan involves cross-domain integration where agents need to share contracts (schemas, API specs, interfaces) and coordinate in real-time. Requires `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. - -### Prompt Composition for Auto-Invocation - -When composing the prompt for `/team-plan`, Central AI should include: - -1. **User's verbatim request** - Always include the original words -2. **Clarifications gathered** - Any answers from `AskUserQuestion` -3. **Codebase context** - Relevant patterns, files, and architecture discovered -4. **Constraints and preferences** - User-expressed boundaries -5. **Orchestration guidance** (optional) - Hints for team composition or parallel/sequential structure - -**Example auto-invocation:** - -``` -Skill({ - skill: "team-plan", - args: "Build a new models section for the blog with 11 model profile pages. User wants: chronological model pages from Claude 3 through Opus 4.6, a models index page, and cross-links to existing posts. Existing blog structure is in apps/web/src/content/blog/blog-structure.ts. Content style should match existing guide posts. Use content-writer agents for parallel page creation." -}) -``` - ---- - -## Coordination Patterns - -### Parallel Execution - -Invoke multiple agents using multiple Task tool calls in **ONE message**. - -| Pattern | Agents | Use Case | -| ----------------------- | ------------------------------------ | ------------------------------- | -| **Domain Parallel** | frontend + backend + database | Independent feature development | -| **Validation Parallel** | security + performance + quality | Comprehensive validation | -| **Debug Parallel** | debugger-detective + deep-researcher | Complex issue investigation | - -**Use parallel dispatch when ALL conditions met:** - -- 3+ unrelated issues or independent domains -- No shared state between tasks -- Clear boundaries with no file overlap - -**Use serial dispatch when ANY condition present:** - -- Interconnected failures (one fix may resolve others) -- Shared state or same files (risk of merge conflicts) -- Sequential dependencies (B depends on A completing) -- Unclear scope (need to understand before fixing) - -**Parallel Agent Output Rule:** -When dispatching agents in parallel, instruct them to write their work to files (edits, creations) rather than returning lengthy responses in the terminal. All work is version controlled, so file-based output is preferred. This prevents context bloat from multiple agents returning verbose terminal output simultaneously. - -### Specialist + Quality Engineer Validation - -The high-reliability pattern: specialist agents build, quality-engineer validates. Use when incorrect output has high cost. - -**Specialist agents serve as builders.** Use the appropriate specialist for the domain: `frontend-specialist`, `backend-engineer`, `supabase-specialist`, `security-auditor`, `performance-optimizer`, or `general-purpose` for cross-domain work. Each specialist focuses on ONE task and reports completion via TaskUpdate. - -**Quality engineer serves as validator.** When dispatched in validation mode, the quality-engineer inspects completed work without modifying files and produces a structured pass/fail report. - -**Validation prompt template:** - -``` -"You are operating in VALIDATION MODE. Verify the completed work against acceptance criteria. - -TASK TO VALIDATE: [task description and acceptance criteria] -SPECIALIST'S WORK: [files to inspect, expected changes] - -WORKFLOW: -1. Read the task requirements and acceptance criteria -2. Inspect all files the specialist changed -3. Run validation commands (tests, type checks, compilation) -4. Report PASS or FAIL with specific evidence using the Validation Report format - -CONSTRAINT: Do NOT modify any files. Inspect and report only. If issues are found, report them for the specialist to fix." -``` - -**Dependency pattern for specialist/validation chains:** - -``` -TaskCreate: "Build feature X" -> Task #1 (assigned to specialist) -TaskCreate: "Validate feature X" -> Task #2 (assigned to quality-engineer) -TaskUpdate: Task #2 addBlockedBy: ["1"] // Validation waits for build - -// Multiple specialists can run in parallel: -TaskCreate: "Build component A" -> Task #1 (frontend-specialist) -TaskCreate: "Build component B" -> Task #2 (backend-engineer) -TaskCreate: "Validate all" -> Task #3 (quality-engineer), addBlockedBy: ["1", "2"] -``` - -### Resume Pattern - -Store agent IDs to continue work with preserved context: - -``` -// First deployment -Task({ prompt: "Build X...", subagent_type: "general-purpose" }) -// Returns: agentId "abc123" - -// Later - resume with full prior context -Task({ prompt: "Now add tests...", resume: "abc123" }) -``` - -Use **resume** when: continuing related work, agent needs prior context. -Use **fresh** when: unrelated task, clean slate preferred. - -### Sequential Dependencies - -| Chain | Reasoning | -| ------------------------------- | ------------------------------------------- | -| Schema → API → Frontend | Data structure must exist before interfaces | -| Core → Enhancement | Foundation before optimization | -| Build → Validate → Integrate | Build, verify, then connect | -| Research → Planning → Execution | Understand, plan, implement | - ---- - -## Agent Routing Reference - -| Domain | Agent | Handles | -| ---------------------- | --------------------- | ------------------------------------------------- | -| **Frontend** | frontend-specialist | React, UI, state, forms, responsive design | -| **Backend** | backend-engineer | Server actions, APIs, business logic, auth | -| **Database** | supabase-specialist | Schema, migrations, RLS, real-time | -| **Testing/Validation** | quality-engineer | Unit, integration, E2E, coverage, task validation | -| **Security** | security-auditor | Auth security, RLS validation, vulnerabilities | -| **Performance** | performance-optimizer | Core Web Vitals, bundle analysis, monitoring | +--- +name: sub-agent-invocation +description: "Coordination and delegation protocols for sub-agent specialist assignments" +--- + +# Central AI Delegation Protocol + +Load this skill before invoking ANY sub-agent (master-orchestrator, specialists, validators). + +--- + +## Constitutional Invocation Requirements + +Every sub-agent prompt **MUST** include four components: + +### 1. Complete Context (Be Detailed) + +- Project goal and current state +- Why this work is needed now +- How this task fits into broader objectives +- Dependencies on other work or prior decisions + +### 2. Explicit Instructions (Be Clear) + +- Specific task requirements with clear scope +- Expected deliverables and exact format +- Success criteria and validation requirements + +### 3. Context References (Point to Sources) + +- Session files: "Read .claude/tasks/session-current.md for full context" +- Skills: "Load [relevant] skill for patterns and workflows" +- Related implementations: Point to similar existing code + +### 4. Performance Directives (Demand Excellence) + +- Always include: "Think hard and analyze deeply before proceeding" +- Specify thoroughness level: "comprehensive analysis" or "quick validation" + +--- + +## Invocation Template + +``` +"USER'S ORIGINAL REQUEST: [verbatim user prompt - MANDATORY] + +[COMPREHENSIVE CONTEXT] +- Project: [overall goal and current state] +- Background: [why this matters, how it fits] +- Dependencies: [what this builds on or integrates with] + +TASK ASSIGNMENT: +[Detailed, specific requirements with clear scope and boundaries] + +CONTEXT REFERENCES: +- Session: [path and what to extract] +- Skills: [relevant skills to load] +- Examples: [similar existing implementations] + +Think hard and provide [thoroughness level] analysis/implementation. + +DELIVERABLES: +[Exact format, success criteria, validation requirements]" +``` + +**Sub-Agent Context Principle**: Sub-agents have temporary windows—maximize their context collection. Over-collection is safe; under-collection causes failures. + +--- + +## Common Invocation Failures + +| Bad | Good | +| --------------------- | --------------------------------------------------------------------------------------------------------------- | +| "Fix authentication" | "Fix OAuth redirect loop where successful login redirects to /login instead of /dashboard" | +| "Add tests" | "Add tests for user profile editing (session Phase 2) covering avatar upload, validation, error handling" | +| "Implement feature X" | "Implement feature X following patterns from Y, integrating with Z API, referencing session-current.md Phase 3" | + +--- + +## Routing Decision + +| Scenario | Approach | +| -------------------------------------------------- | ---------------------------------------------------------- | +| Multi-phase feature, 5+ files, architectural | Auto-invoke `/blueprint` → user approval → `/impl` | +| Cross-domain integration, agents need coordination | `/blueprint` → user approval → `/team-impl` (Agent Teams) | +| Simple file edit, pattern search, single-component | Direct sub-agent delegation | +| Ambiguous scope, needs planning | Gather context → `/blueprint` → user approval → `/impl` | +| Clear scope, bounded execution | Direct delegation | +| High-reliability task, production changes | `/blueprint` with Specialist + Quality Engineer validation | +| Research/exploration only | Direct Explore agent or deep-researcher | + +**`/impl` vs `/team-impl` Decision:** + +- `/impl`: Tasks are independent, sub-agents don't need to communicate, cost-sensitive (1x tokens) +- `/team-impl`: Agents need peer-to-peer coordination, cross-domain interfaces, contract-first spawning (2-4x tokens) + +--- + +## Auto-Invocation Protocol + +Central AI auto-invokes `/blueprint` + `/impl` (or `/team-impl`) for complex work. This is the standard operating procedure for all non-trivial implementation. + +### Decision Criteria + +**Auto-invoke `/blueprint` immediately when:** + +- Request involves 5+ files or multiple domains +- Request is clearly multi-phase (e.g., "build feature with X, Y, Z") +- Request involves architectural or structural changes +- User explicitly asks for team coordination or a plan + +**Gather info first, then invoke when:** + +- Request is vague but potentially complex +- Request needs clarification about scope or approach +- Request touches unfamiliar areas of the codebase + +**Don't invoke (direct execution) when:** + +- Single file fix, typo, config change +- Clear, bounded task for a single specialist +- Research/exploration only (no implementation) + +### Invocation Syntax + +**Step 1 - Plan:** + +``` +Skill({ skill: "blueprint", args: "" }) +``` + +**Step 2 - Pause:** +Present the plan summary to the user. Wait for approval ("go", `/impl`, `/team-impl`, or feedback). + +**Step 3 - Execute (after user approval):** + +For isolated sub-agent execution (default): + +``` +Skill({ skill: "impl", args: ".claude/tasks/.md" }) +``` + +For collaborative Agent Teams execution (when agents need peer-to-peer coordination): + +``` +Skill({ skill: "team-impl", args: ".claude/tasks/.md" }) +``` + +Use `/team-impl` when the plan involves cross-domain integration where agents need to share contracts (schemas, API specs, interfaces) and coordinate in real-time. Requires `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. + +### Prompt Composition for Auto-Invocation + +When composing the prompt for `/blueprint`, Central AI should include: + +1. **User's verbatim request** - Always include the original words +2. **Clarifications gathered** - Any answers from `AskUserQuestion` +3. **Codebase context** - Relevant patterns, files, and architecture discovered +4. **Constraints and preferences** - User-expressed boundaries +5. **Orchestration guidance** (optional) - Hints for team composition or parallel/sequential structure + +**Example auto-invocation:** + +``` +Skill({ + skill: "blueprint", + args: "Build a new models section for the blog with 11 model profile pages. User wants: chronological model pages from Claude 3 through Opus 4.6, a models index page, and cross-links to existing posts. Existing blog structure is in apps/web/src/content/blog/blog-structure.ts. Content style should match existing guide posts. Use content-writer agents for parallel page creation." +}) +``` + +--- + +## Coordination Patterns + +### Background + Event Log (preferred for `/impl` and `/team-impl`) + +The orchestrator spawns specialists with `Task({run_in_background: true})`, then watches `.cortex/events.log` via the `Monitor` tool. Specialists call `bun run .claude/hooks/event-log/event-log.ts append` at task-complete and contract-ready moments; the orchestrator only sees those filtered lines, never the full sub-agent transcript. + +Use this pattern whenever: +- Multiple specialists will run in parallel on the same wave +- The orchestrator is Sonnet and you want to keep its context window small +- You do not need to inspect every agent's output on the happy path + +See `core/skills/monitor-loop/SKILL.md` for the full protocol, event shape, safety-net rules, and the boilerplate to paste into every specialist's prompt. Commands `/impl` and `/team-impl` both implement this pattern. + +Only pull `TaskOutput` on `error` events or when an expected `task_done` is missing past its deadline. Never pull `TaskOutput` on the happy path. + +### Parallel Execution + +Invoke multiple agents using multiple Task tool calls in **ONE message**. + +| Pattern | Agents | Use Case | +| ----------------------- | ------------------------------------ | ------------------------------- | +| **Domain Parallel** | frontend + backend + database | Independent feature development | +| **Validation Parallel** | security + performance + quality | Comprehensive validation | +| **Debug Parallel** | debugger-detective + deep-researcher | Complex issue investigation | + +**Use parallel dispatch when ALL conditions met:** + +- 3+ unrelated issues or independent domains +- No shared state between tasks +- Clear boundaries with no file overlap + +**Use serial dispatch when ANY condition present:** + +- Interconnected failures (one fix may resolve others) +- Shared state or same files (risk of merge conflicts) +- Sequential dependencies (B depends on A completing) +- Unclear scope (need to understand before fixing) + +**Parallel Agent Output Rule:** +When dispatching agents in parallel, instruct them to write their work to files (edits, creations) rather than returning lengthy responses in the terminal. All work is version controlled, so file-based output is preferred. This prevents context bloat from multiple agents returning verbose terminal output simultaneously. + +### Specialist + Quality Engineer Validation + +The high-reliability pattern: specialist agents build, quality-engineer validates. Use when incorrect output has high cost. + +**Specialist agents serve as builders.** Use the appropriate specialist for the domain: `frontend-specialist`, `backend-engineer`, `supabase-specialist`, `security-auditor`, `performance-optimizer`, or `general-purpose` for cross-domain work. Each specialist focuses on ONE task and reports completion via TaskUpdate. + +**Quality engineer serves as validator.** When dispatched in validation mode, the quality-engineer inspects completed work without modifying files and produces a structured pass/fail report. + +**Validation prompt template:** + +``` +"You are operating in VALIDATION MODE. Verify the completed work against acceptance criteria. + +TASK TO VALIDATE: [task description and acceptance criteria] +SPECIALIST'S WORK: [files to inspect, expected changes] + +WORKFLOW: +1. Read the task requirements and acceptance criteria +2. Inspect all files the specialist changed +3. Run validation commands (tests, type checks, compilation) +4. Report PASS or FAIL with specific evidence using the Validation Report format + +CONSTRAINT: Do NOT modify any files. Inspect and report only. If issues are found, report them for the specialist to fix." +``` + +**Dependency pattern for specialist/validation chains:** + +``` +TaskCreate: "Build feature X" -> Task #1 (assigned to specialist) +TaskCreate: "Validate feature X" -> Task #2 (assigned to quality-engineer) +TaskUpdate: Task #2 addBlockedBy: ["1"] // Validation waits for build + +// Multiple specialists can run in parallel: +TaskCreate: "Build component A" -> Task #1 (frontend-specialist) +TaskCreate: "Build component B" -> Task #2 (backend-engineer) +TaskCreate: "Validate all" -> Task #3 (quality-engineer), addBlockedBy: ["1", "2"] +``` + +### Resume Pattern + +Store agent IDs to continue work with preserved context: + +``` +// First deployment +Task({ prompt: "Build X...", subagent_type: "general-purpose" }) +// Returns: agentId "abc123" + +// Later - resume with full prior context +Task({ prompt: "Now add tests...", resume: "abc123" }) +``` + +Use **resume** when: continuing related work, agent needs prior context. +Use **fresh** when: unrelated task, clean slate preferred. + +### Sequential Dependencies + +| Chain | Reasoning | +| ------------------------------- | ------------------------------------------- | +| Schema → API → Frontend | Data structure must exist before interfaces | +| Core → Enhancement | Foundation before optimization | +| Build → Validate → Integrate | Build, verify, then connect | +| Research → Planning → Execution | Understand, plan, implement | + +--- + +## Agent Routing Reference + +| Domain | Agent | Handles | +| ---------------------- | --------------------- | ------------------------------------------------- | +| **Frontend** | frontend-specialist | React, UI, state, forms, responsive design | +| **Backend** | backend-engineer | Server actions, APIs, business logic, auth | +| **Database** | supabase-specialist | Schema, migrations, RLS, real-time | +| **Testing/Validation** | quality-engineer | Unit, integration, E2E, coverage, task validation | +| **Security** | security-auditor | Auth security, RLS validation, vulnerabilities | +| **Performance** | performance-optimizer | Core Web Vitals, bundle analysis, monitoring | diff --git a/.cortex/archive/019-pause-gated-finish-session.md b/.cortex/archive/019-pause-gated-finish-session.md new file mode 100644 index 00000000..15ece6de --- /dev/null +++ b/.cortex/archive/019-pause-gated-finish-session.md @@ -0,0 +1,19 @@ +# Session: Feature 019 — Pause-Gated Finish + +**Plan:** Context/Features/019-Pause-Gated-Finish/Steps.md +**Started:** 2026-04-06 +**Status:** In progress + +## Team Roster + +- **builder-ui** (frontend-specialist, resume=true) — phases 1-3 +- **validator** (quality-engineer, resume=false) — phase 4 + +## Wave Plan + +- **Wave 1**: S001 → S005-T (builder-ui owns the entire build across phases 1-3 since the team has only one builder; milestones are checkpointed inline by the agent and reviewed here at wave boundary) +- **Wave 2**: S006, S007 (build validation + read-only validator pass) + +## Activity Log + +- 2026-04-06: Session initialized. Spawning Wave 1 builder agent. diff --git a/.cortex/archive/session-015-onboarding.md b/.cortex/archive/session-015-onboarding.md new file mode 100644 index 00000000..28eae97f --- /dev/null +++ b/.cortex/archive/session-015-onboarding.md @@ -0,0 +1,25 @@ +# Session: Feature 015 -- New User Onboarding System + +**Plan:** Context/Features/015-Onboarding/Steps.md +**Started:** 2026-04-05 +**Status:** In progress + +## Team Roster + +| Agent | Role | Status | +| ----------------- | ------------------------------------ | ------- | +| foundation-eng | Store, hook, EmptyState verification | Active | +| empty-state-eng | Empty state standardization | Pending | +| onboarding-ui-eng | Welcome card, hints, page placements | Pending | +| nav-eng | Nav discovery indicators | Pending | +| workout-hint-eng | First-workout guided hints | Pending | +| validator | Quality validation (read-only) | Pending | + +## Wave Execution Log + +### Wave 1 (Phase 1: Foundation) + +- [ ] S001: Create onboarding store +- [ ] S001-T: Test onboarding store +- [ ] S002: Create use-onboarding hook +- [ ] S003: Review EmptyState component diff --git a/.cortex/archive/session-f019-display-setup-ux.md b/.cortex/archive/session-f019-display-setup-ux.md new file mode 100644 index 00000000..e288dca8 --- /dev/null +++ b/.cortex/archive/session-f019-display-setup-ux.md @@ -0,0 +1,193 @@ +# F019 Implementation Session: Display Setup UX + +**Plan:** Context/Features/019-Display-Setup-UX/Steps.md +**Branch:** worktree-feat+multipe-instance-display +**Started:** 2026-04-07 + +## Team Roster + +- **fe-broadcast** — frontend-specialist — display dispatcher, chooser, setup panel, URL parser, discovery endpoint +- **fe-ui** — frontend-specialist — profile panel, gym-management integration, config-store, shared helpers, setup.tsx persistence +- **validator** — quality-engineer — final read-only validation + +## Execution Mode + +Central AI implements directly for Waves 1-6 (small module files, no cross-cutting complexity). Sub-agents would add overhead without improving quality. Validator runs in Wave 7. + +## Progress Log + +### Wave 1: Foundation primitives — DONE (M1 ✅) + +- S001 display-url.ts +- S001-T display-url.test.ts (24 tests) +- S002 display-setup.ts +- S002-T display-setup.test.ts (12 tests) +- S003 gym-error-messages.ts (extracted from gym-management-section.tsx) +- S003-T gym-error-messages.test.ts (24 tests) +- S004 copy-to-clipboard.ts +- S004-T copy-to-clipboard.test.ts (6 tests) + +### Wave 2: Backend extension + config schema — DONE (M2 ✅) + +- S005 api/discovery.ts: computeAppUrl helper, additive `app_url` field +- S005-T api/**tests**/discovery.test.ts (12 tests) +- S006 src/lib/config-store.ts: backendConfigSchema gains optional appUrl +- S006-T extended config-store.test.ts (5 new tests for appUrl round-trip) + +### Wave 3: Setup refactor and persistence — DONE (M3 ✅) + +- S007 src/hooks/use-qr-scanner.ts (Tauri-only barcode scanner hook) +- S007-T src/hooks/**tests**/use-qr-scanner.test.tsx (8 tests) +- S007-V Refactored src/routes/setup.tsx to consume the hook (existing 4 tests still pass) +- S008 Discovery callback in setup.tsx persists app_url; src/lib/discovery.ts schema extended +- S008-T 2 new tests in -setup.test.tsx + 4 new tests in discovery.test.ts for app_url + +### Wave 4: Profile inline panel — DONE (M4 ✅) + +- S009 src/components/profile/show-display-panel.tsx (URL + Copy + QR + dev-warning + D22 backfill) +- S009-T src/components/profile/**tests**/show-display-panel.test.tsx (10 tests) +- S010 Integrated ShowDisplayPanel into gym-management-section: single-row-open state, "Show display" button on all rows +- S010-T 4 new integration tests in gym-management-section.test.tsx + +### Wave 5: Dispatcher primitives and sub-views — DONE (M5 ✅) + +- S011 src/components/display/dispatcher-state.ts (pure 6-variant state machine) +- S011-T src/components/display/**tests**/dispatcher-state.test.ts (11 tests) +- S012 src/components/display/display-chooser.tsx (2+ gym chooser + personal display row) +- S012-T src/components/display/**tests**/display-chooser.test.tsx (9 tests) +- S013 src/components/display/display-setup-panel.tsx (Panel A URL input + Panel B personal CTA) +- S013-T src/components/display/**tests**/display-setup-panel.test.tsx (16 tests) +- S014 src/components/display/display-dispatcher.tsx (state machine + branching + inline LegacyNotConfiguredPage) +- S014-T src/components/display/**tests**/display-dispatcher.test.tsx (7 tests) + +### Wave 6: Wire up /display route — DONE (M6 ✅) + +- S015 src/routes/display/index.tsx replaced with thin DisplayDispatcher wrapper +- S015-T src/routes/display/**tests**/-dispatcher-route.test.tsx (5 integration tests for TA5/TA6/TA8/TA13/TA17) +- S015-D Final file list verified against Tech.md File Structure section (this update) +- Existing -gym-route.test.tsx updated to mock useAuth/useGyms for the dispatcher + +## Final File List + +### New source files (12) + +- `src/lib/display-url.ts` +- `src/lib/display-setup.ts` +- `src/lib/gym-error-messages.ts` +- `src/lib/copy-to-clipboard.ts` +- `src/hooks/use-qr-scanner.ts` +- `src/components/profile/show-display-panel.tsx` +- `src/components/display/dispatcher-state.ts` +- `src/components/display/display-dispatcher.tsx` +- `src/components/display/display-chooser.tsx` +- `src/components/display/display-setup-panel.tsx` + +### New test files (12) + +- `src/lib/__tests__/display-url.test.ts` +- `src/lib/__tests__/display-setup.test.ts` +- `src/lib/__tests__/gym-error-messages.test.ts` +- `src/lib/__tests__/copy-to-clipboard.test.ts` +- `api/__tests__/discovery.test.ts` +- `src/hooks/__tests__/use-qr-scanner.test.tsx` +- `src/components/profile/__tests__/show-display-panel.test.tsx` +- `src/components/display/__tests__/dispatcher-state.test.ts` +- `src/components/display/__tests__/display-dispatcher.test.tsx` +- `src/components/display/__tests__/display-chooser.test.tsx` +- `src/components/display/__tests__/display-setup-panel.test.tsx` +- `src/routes/display/__tests__/-dispatcher-route.test.tsx` + +### Modified source files (6) + +- `api/discovery.ts` — `app_url` field via `computeAppUrl(req)` +- `src/lib/config-store.ts` — `backendConfigSchema` gains optional `appUrl` +- `src/lib/discovery.ts` — `DiscoveryResult.appUrl`, schema extension +- `src/routes/setup.tsx` — consumes `useQrScanner`, persists `appUrl` +- `src/routes/display/index.tsx` — thin wrapper rendering `DisplayDispatcher` +- `src/components/profile/gym-management-section.tsx` — single-row-open state, ShowDisplayPanel integration, gymErrorMessage import + +### Modified test files (5) + +- `src/lib/__tests__/config-store.test.ts` — appUrl round-trip +- `src/lib/__tests__/discovery.test.ts` — app_url passthrough + warn +- `src/routes/__tests__/-setup.test.tsx` — appUrl persistence assertions +- `src/components/profile/__tests__/gym-management-section.test.tsx` — Show display integration tests +- `src/routes/display/__tests__/-gym-route.test.tsx` — auth/use-gyms mocks for dispatcher + +### Zero-change files (proven by empty git diff against HEAD) + +- `src/routes/display/gym/$gymId.tsx` (TA18) +- `supabase/migrations/*` (TA19) +- `src/domain/types/*` (TA20) + +--- + +## Wave 7: Final Validation (S017-V) — 2026-04-07 + +### Test suite + +- Full repo: **2,350 tests passing across 122 test files** (zero regressions) +- F019-specific (new + modified): 162 tests across 12 new files + 5 modified files +- Lint: clean +- Build: clean (`bun run build` succeeds in 645ms; existing chunk warnings unrelated) + +### Spec.md Testable Assertions (TA1-TA23) + +| ID | Assertion | Verification | Status | +| ---- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------ | +| TA1 | "Show display" button on every gym row, visible to all members | `gym-management-section.test.tsx` "renders Show display button on every row (owner and non-owner)" | PASS | +| TA2 | Tap reveals inline panel with URL + Copy + QR | `show-display-panel.test.tsx` "renders URL + Copy + QR when origin is valid" | PASS | +| TA3 | URL pattern `${origin}/display/gym/${gymId}` | `show-display-panel.test.tsx` asserts `urlEl.textContent` matches the canonical pattern | PASS | +| TA4 | Copy calls `navigator.clipboard.writeText` and fires success toast | `show-display-panel.test.tsx` "Copy button calls copyToClipboard with the URL and expected messages" | PASS | +| TA5 | Single-gym → `/display` navigates (replace) to `/display/gym/{id}` | `-dispatcher-route.test.tsx` "renders Navigate replace to the user only gym" | PASS | +| TA6 | 2+ gyms → chooser lists every membership | `-dispatcher-route.test.tsx` "renders one Link row per membership" | PASS | +| TA7 | Each chooser row is an `` | `display-chooser.test.tsx` "renders one Link row per gym" + asserts `href` attribute | PASS | +| TA8 | Zero gyms → setup UI with Panel A + Panel B | `-dispatcher-route.test.tsx` "renders both Panel A and Panel B" | PASS | +| TA9 | Panel A accepts a full URL, navigates | `display-setup-panel.test.tsx` "accepts a full URL and navigates with the parsed gym id" | PASS | +| TA10 | Panel A accepts a bare UUID, navigates | `display-setup-panel.test.tsx` "accepts a bare UUID" | PASS | +| TA11 | Panel A rejects malformed input with inline error and does not navigate | `display-setup-panel.test.tsx` "rejects malformed input with an inline error and does not navigate" | PASS | +| TA12 | Panel A Scan QR button absent on web, present on Tauri | `display-setup-panel.test.tsx` "Scan QR button is absent / present" pair | PASS | +| TA13 | Panel B createGym + replace navigate on success | `-dispatcher-route.test.tsx` "clicks Create personal display, calls createGym, and navigates with replace" | PASS | +| TA14 | Personal-gym name derivation (`'s Training` / `'My Training'` fallback) | `display-setup.test.ts` covers all branches | PASS | +| TA15 | Dispatcher loading state does not flash setup/chooser | `display-dispatcher.test.tsx` "renders loading state while gyms are loading" + leak assertions | PASS | +| TA16 | Error state with Retry that calls `refetch` | `display-dispatcher.test.tsx` "renders error state with Retry that calls refetch" | PASS | +| TA17 | Unauthenticated → preserved legacy "DISPLAY NOT CONFIGURED" copy | `-dispatcher-route.test.tsx` "renders the F018-shipped DISPLAY NOT CONFIGURED copy unchanged" | PASS | +| TA18 | `src/routes/display/gym/$gymId.tsx` zero diff | `git diff HEAD -- src/routes/display/gym/$gymId.tsx` returns empty | PASS | +| TA19 | Zero new SQL migrations | `git diff HEAD -- supabase/migrations/` returns empty | PASS | +| TA20 | Zero new domain types | `git diff HEAD -- src/domain/types/` returns empty | PASS | +| TA21 | Dev-origin warning visible on localhost, absent on production | `show-display-panel.test.tsx` "renders the dev-origin warning for localhost" + production negative test | PASS | +| TA22 | Panel A Enter key submits | `display-setup-panel.test.tsx` "accepts Enter key as submit" | PASS | +| TA23 | 2+-gym chooser "Start a personal display" mirrors Panel B's createGym + navigate | `display-chooser.test.tsx` "tapping Start a personal display calls createGym" + "onSuccess navigates" | PASS | + +### Summary + +All 23 testable assertions verified. All zero-blast-radius guarantees confirmed. Feature is complete and ready for the manual Tauri Android smoke pass (S016-QA, six scenarios) and PR to develop. + +### S016-QA (smoke scenarios — split into automated + manual) + +After the initial pass deferred S016-QA wholesale, a follow-up automated the two web-reachable scenarios via Playwright. The four Tauri-native scenarios remain manual. + +**Automated (Playwright e2e, run in CI under `e2e` job):** + +| # | Scenario | Test name | +| --- | ------------------------------------- | --------------------------------------------------------------- | +| 1 | Web "Show display" panel → URL + Copy | `F019 Scenario 1: Show display panel reveals URL + Copy on web` | +| 4 | Web 0-gym → personal display creation | `F019 Scenario 4: zero-gym user creates a personal display` | + +New files: + +- `e2e/helpers.ts` — auth/seed helpers (`seedConfig`, `createTestUser`, `createGymForUser`, `signInViaForm`) against the local Supabase instance the CI e2e job already starts +- `e2e/smoke.spec.ts` — extended with the two F019 tests above + +E2E results: 4/4 passing (2 pre-existing + 2 F019). Lint clean. Unit tests still 2350/2350. + +**Still manual (Tauri Android device required):** + +| # | Scenario | Why manual | +| --- | ------------------------------------------------ | ------------------------------------------------------------------------------------ | +| 2 | Tauri fresh-setup copy → TV loads, host correct | Playwright cannot simulate the Tauri WebView host header that distinguishes from web | +| 3 | Tauri pre-F019 config → D22 backfill form | Requires Tauri SQLite mutation + app restart | +| 5 | Tauri Panel A → native QR scan | Requires `@tauri-apps/plugin-barcode-scanner` + real device camera | +| 6 | Self-hosted docker-compose `SITE_URL` host check | Requires running a separate Caddy + Vercel function deployment | + +**Coverage delta:** 2 of 6 scenarios are now automated; 4 remain manual but are appropriately scoped to physical device verification. diff --git a/.cortex/archive/session-f019-wave-8-followups.md b/.cortex/archive/session-f019-wave-8-followups.md new file mode 100644 index 00000000..5cedf4ce --- /dev/null +++ b/.cortex/archive/session-f019-wave-8-followups.md @@ -0,0 +1,107 @@ +# F019 Wave 8 Implementation Session: Display Setup UX Post-Review Follow-ups + +**Plan:** Context/Features/019-Display-Setup-UX/Steps.md +**Review source:** Context/Reviews/0015-pr91-f019-display-setup-ux-review.md +**Branch:** worktree-feat+multipe-instance-display +**Started:** 2026-04-08 + +## Context + +Waves 1-7 of F019 are already shipped (commits 2cb69e1, e1382d2, a95d013). +Wave 8 captures 9 follow-up tasks from the PR #91 review that did not block merge +but should be addressed in this follow-up session. + +## Team Roster + +- **fe-broadcast** — frontend-specialist — display URL parser, dispatcher, + edge function alerting, supabase test, publisher test stabilization +- **fe-ui** — frontend-specialist — profile panel coverage, qr scanner hook, + test fixture extraction +- **validator** — quality-engineer — final read-only validation pass + +## Execution Mode + +Hub-and-spoke (`/impl 019`). All 9 Wave 8 tasks are independent (Parallel: true, +Depends: none) so they fan out in a single wave with no contract handoff needed. +Validator runs sequentially after all 9 complete. + +## Wave 8 Task Map + +| Task | Owner | Files | +| ------ | ------------ | ------------------------------------------------------------------ | +| S018-T | fe-broadcast | supabase/tests/018_gym_owner_enroll.sql → 019_gym_owner_enroll.sql | +| S019-T | fe-ui | src/components/profile/**tests**/show-display-panel.test.tsx | +| S020-T | fe-ui | src/hooks/**tests**/use-qr-scanner.test.tsx | +| S021-T | fe-broadcast | src/lib/**tests**/display-publisher-hello.test.ts | +| S022-T | fe-broadcast | src/lib/**tests**/discovery.test.ts | +| S023 | fe-broadcast | supabase/functions/display-idle-snapshot/index.ts | +| S024-T | fe-broadcast | src/lib/**tests**/display-url.test.ts | +| S025-T | fe-ui | src/test/fixtures/gym.ts (NEW) + 3 dispatcher test migrations | +| S026-T | fe-ui | src/components/profile/**tests**/show-display-panel.test.tsx | + +## Progress Log + +### Wave 8: Post-review follow-ups — IN PROGRESS + +- Session created +- Tasks 1-9 created in TaskList, owners assigned +- Validation task #10 blocked by all 9 follow-ups +- 2026-04-08 07:15 — fe-ui agent dispatched (background, agentId aeb2d06e1c6b4434c) +- 2026-04-08 07:15 — fe-broadcast initial spawn hit transient API overload; resumed via SendMessage to agentId ac863a0842e63915f +- Both agents working in parallel on disjoint file sets +- 2026-04-08 — fe-ui completed S019-T, S020-T, S025-T, S026-T (4/4), lint+tsc clean +- 2026-04-08 — fe-broadcast completed S018-T, S021-T, S022-T, S023, S024-T (5/5), lint clean, build clean +- Both agents independently verified 3 pre-existing test failures unrelated to Wave 8: + 1. `show-display-panel.test.tsx:131` — Copy button logPrefix drift (`display-setup` vs `show-display-panel`) + 2. `gym-management-section.test.tsx:236` — Leave gym mutate signature drift + 3. `gym-management-section.test.tsx:344` — Join gym mutate signature drift +- 2026-04-08 — Validator dispatched for Wave 8 read-only verification + +## Wave 8 Validation Report + +**Validator:** quality-engineer (read-only) +**Date:** 2026-04-08 +**Scope:** 9 post-review follow-up tasks (S018-T through S026-T) + +### Per-task verdict + +| Task | Finding | File(s) | Verdict | Notes | +| ------ | ------- | ------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| S018-T | P15-021 | `supabase/tests/019_gym_owner_enroll.sql` (renamed + rewritten) | PASS | Rename via `git mv` (`D 018_*.sql` / `A 019_*.sql`). Section 2 now drops the trigger, pre-inserts membership, recreates trigger with `insert or update` scope, updates gym to re-fire, asserts membership count = 1. Actually exercises the trigger function's `on conflict do nothing` path. Header references F019 and migration `20260407000004_enroll_gym_creator.sql`. | +| S019-T | P15-022 | `src/components/profile/__tests__/show-display-panel.test.tsx` +59 lines | PASS | `it('falls through to backfill form when getConfig() rejects', ...)` added inside the Tauri describe block. Mocks `mockGetConfig.mockRejectedValueOnce(new Error('store corrupted'))`, asserts backfill form renders, URL/Copy/QR are absent, and the `[show-display-panel] Failed to read config for origin:` log fires. Passes (12 tests / 11 passing). | +| S020-T | P15-023 | `src/hooks/__tests__/use-qr-scanner.test.tsx` +49 lines | PASS | New `describe('useQrScanner.cancel public API', ...)` with two cases: (1) noop when no scan in progress (cancelMock not called), (2) noop after completed scan (cancelMock called exactly once by scan()'s internal cleanup, not re-invoked by explicit cancel). File passes in isolation. | +| S021-T | P15-049 | `src/lib/__tests__/display-publisher-hello.test.ts` +15 lines | PASS | Multi-line "IMPLICIT COUPLING NOTICE (P15-049)" block comment documents the lazy-init dependency and the specific replacement needed if the publisher ever goes eager-init. Option A (no production change). File still passes. | +| S022-T | P15-050 | `src/lib/__tests__/discovery.test.ts` +23 lines | PASS | `it('returns INVALID_INPUT for a javascript: URL', ...)` added next to the `file://` / `ftp://` rejection cases. Comment explains the https-prepend branch that this input takes. Asserts full error shape + `fetch` not called. File passes. | +| S023 | P15-051 | `supabase/functions/display-idle-snapshot/index.ts` +30 lines | PASS | Permanent-failure path extracts `sharedCode` once, emits `console.error('[display-idle-snapshot] PERMANENT_FAILURE', { error_code, failure_count, gym_count })`, AND adds conditional `permanent_failure: true`, `error_code`, `failure_count` fields to JSON response body (spread only when `isPermanentFailure`). Cron status unchanged (200 for all-permanent). Comment flags Sentry swap-in point. Build compiles. | +| S024-T | P15-052 | `src/lib/__tests__/display-url.test.ts` +52 lines | PASS | `describe('parseDisplayUrlInput ∘ buildDisplayUrl round-trip', ...)` with 10 UUIDs × 4 origins = 40 generated `it(...)` cases. No fast-check dependency. Each case builds URL, asserts `built.ok`, parses it, asserts round-tripped `gymId === uuid`. File passes. | +| S025-T | P15-053 | `src/test/fixtures/gym.ts` (new, 39 lines) + 3 dispatcher tests | PASS | New fixture module exports `makeGym(overrides)` that runs every fixture through `gymSchema.parse`. Migrated `dispatcher-state.test.ts`, `display-dispatcher.test.tsx`, `display-chooser.test.tsx` to `import { makeGym } from '@/test/fixtures/gym'`. Local `makeGym` definitions removed from all three. `display-setup-panel.test.tsx` intentionally untouched (it redefines `makeGym` for its own scope). All 29 tests in the 3 migrated files pass. | +| S026-T | P15-054 | `src/components/profile/__tests__/show-display-panel.test.tsx` | PASS | `it('renders dev-origin warning when Tauri appUrl is loopback', ...)` added inside the Tauri describe block. Mocks `appUrl: 'http://localhost:5173'`, asserts URL resolves to loopback, and the `show-display-dev-warning-${GYM_ID}` caption is rendered. Passes. | + +### Full-suite results + +- **Vitest:** 2394 passed / 3 failed / 2397 total (122 test files, 120 passing) +- **Lint:** PASS (no eslint errors) +- **Build:** PASS (`✓ built in 709ms`, TypeScript check clean) + +### Pre-existing failures (unchanged from baseline) + +- `src/components/profile/__tests__/show-display-panel.test.tsx:134` — Copy button `logPrefix` drift. Verified the `it('Copy button calls copyToClipboard...')` block at lines 127-142 is **unchanged by Wave 8**. Line 138 still reads `logPrefix: 'display-setup'` while production passes `logPrefix: 'show-display-panel'`. Diff only adds new tests after line 166 (inside the Tauri describe block). NOT a regression. +- `src/components/profile/__tests__/gym-management-section.test.tsx` > `leaveGym.mutate` — Verified `gym-management-section.test.tsx` is **entirely untouched by Wave 8** (empty `git diff HEAD -- `). NOT a regression. +- `src/components/profile/__tests__/gym-management-section.test.tsx` > `joinGym.mutate` — Same file, same verification. NOT a regression. + +### Scope leak check + +- Modified files: 12 tracked + 3 untracked = 15 total + - **Tracked** (12): `Context/Features/019-Display-Setup-UX/Steps.md`, `src/components/display/__tests__/dispatcher-state.test.ts`, `src/components/display/__tests__/display-chooser.test.tsx`, `src/components/display/__tests__/display-dispatcher.test.tsx`, `src/components/profile/__tests__/show-display-panel.test.tsx`, `src/hooks/__tests__/use-qr-scanner.test.tsx`, `src/lib/__tests__/discovery.test.ts`, `src/lib/__tests__/display-publisher-hello.test.ts`, `src/lib/__tests__/display-url.test.ts`, `supabase/functions/display-idle-snapshot/index.ts`, `supabase/tests/018_gym_owner_enroll.sql` (D), `supabase/tests/019_gym_owner_enroll.sql` (A, staged). + - **Untracked** (3): `.cortex/session.md`, `.cortex/archive/session-f019-display-setup-ux.md`, `src/test/fixtures/` (contains new `gym.ts`). +- Files outside expected set: **NONE**. + +### Cross-cutting checks + +- **Error handling rules**: PASS. New `console.error('[display-idle-snapshot] PERMANENT_FAILURE', ...)` uses `[module-name]` prefix per `.claude/rules/error-handling.md`. Added test-file `console.error` spies (`show-display-panel`, `discovery`) also assert the `[module-name]` prefix on production logs. No bare `catch {}` introduced in the diff. +- **bun/bunx only**: PASS. No `npx`/`npm install`/`yarn` tokens found in `git diff HEAD`. +- **Tests colocated**: PASS. All touched test files live in `__tests__/` subdirectories colocated with source. New fixture lives at `src/test/fixtures/gym.ts` (the canonical shared-fixtures location per P15-053 guidance). + +### Overall verdict + +**PASS** — Wave 8 follow-ups are ready for commit. All 9 tasks correctly address their corresponding review findings, the full test suite reports the expected 3 pre-existing failures (each verified untouched by Wave 8), lint and build are clean, and there is no scope leak beyond the expected 12-file set. diff --git a/.cortex/backups/1-backup-Apr 11th 1:07pm.md b/.cortex/backups/1-backup-Apr 11th 1:07pm.md new file mode 100644 index 00000000..5fa7fc0d --- /dev/null +++ b/.cortex/backups/1-backup-Apr 11th 1:07pm.md @@ -0,0 +1,36 @@ +# Session Backup + +**Session ID:** 851d2085-8f08-4272-8534-cd3250b68618 +**Trigger:** precompact_auto +**Generated:** 2026-04-11T18:07:39.989Z +**Session Start:** 2026-04-11T18:02:36.303Z + +## User Requests +- Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to. +- /clear + clear + +- Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to. +- /model + model + opus +- Set model to Opus 4.6 +- Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to. +- /effort + effort + max +- Set effort level to max (this session only): Maximum capability with deepest reasoning +- critique +/critique +the template building workflow from start to finish + +## Tasks Created +- **Map template-building entry points and flow**: Find how users START creating a template (library buttons, empty states, CTAs). Trace routes and entry points. +- **Read session-builder components**: Read all session-builder and session template creation components in full, noting structure, typography, layout, interactions. +- **Walk through save/edit lifecycle**: Trace validation, save, error, success states, and post-save navigation. +- **Synthesize critique against Iron & Ember**: Apply frontend-design DON'T checklist and Iron & Ember brand rules. Write final critique report. + +## Tasks Completed: 1 + +## Skills Loaded +- frontend-design diff --git a/.cortex/backups/2-backup-Apr 11th 1:37pm.md b/.cortex/backups/2-backup-Apr 11th 1:37pm.md new file mode 100644 index 00000000..798f6a9a --- /dev/null +++ b/.cortex/backups/2-backup-Apr 11th 1:37pm.md @@ -0,0 +1,500 @@ +# Session Backup + +**Session ID:** 49862042-5f45-45c4-8b9c-5e240e4d4564 +**Trigger:** precompact_auto +**Generated:** 2026-04-11T18:59:41.487Z +**Session Start:** 2026-04-11T18:30:24.865Z + +## User Requests +- impl +/impl +021 +- This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation. + +Summary: +1. Primary Request and Intent: + User invoked `/impl 021` to execute Feature 021 — Template Builder Route. This feature promotes session-template and event-template editing from a bottom sheet in `library.tsx` to dedicated full-page TanStack Router routes (`/templates/new` and `/templates/$templateId/edit`), eliminates the sheet-inside-sheet anti-pattern by replacing `AddExerciseSheet` (stacked) with a route-local `ExercisePickerDrawer`, and adds a dirty-state guard via TanStack Router `useBlocker`. The work is organized as 18 tasks across 5 waves defined in `Context/Features/021-Template-Builder-Route/Steps.md`. Per project CLAUDE.md, this is a single-stack frontend-only feature executed via hub-and-spoke orchestration. + +2. Key Technical Concepts: + - TanStack Router v1.168.7 (file-based routing, `createFileRoute`, `validateSearch`, `Route.useSearch`, `useNavigate`, `Link`, `useBlocker({ shouldBlockFn, enableBeforeUnload, disabled, withResolver })`) + - React 19, TypeScript strict, Zustand, Zod 4, React Query (TanStack Query) + - Tailwind 4, shadcn/ui, Iron & Ember design system (no divider lines, no rounded corners, tonal layering, `max-w-5xl`, `min-h-[100dvh] bg-surface-anvil`) + - `motion-safe:animate-in motion-safe:slide-in-from-bottom` / `lg:motion-safe:slide-in-from-right` for respecting `prefers-reduced-motion` + - Component composition: headless panel (`ExercisePickerPanel`) consumed by both a sheet wrapper (`AddExerciseSheet`) and a drawer (`ExercisePickerDrawer`) + - `PickerComponent?: ComponentType` prop threading pattern for dependency injection + - Vitest for testing, `bun run test ` invocation + - ADR format: Status, Context, Decision, Alternatives Considered, Consequences + +3. Files and Code Sections: + + - **`Context/Features/021-Template-Builder-Route/Steps.md`** (read fully, 336 lines) + - Defines 18 tasks, team (frontend-specialist, quality-engineer, content-writer), 3 milestones, wave structure + - Task dependency graph established in TaskList + + - **`Context/Features/021-Template-Builder-Route/Spec.md`** (read fully, 205 lines) + - 10 testable assertions A1-A10, Must/Should/Won't requirements M1-M10 + - Notes: `/events/$templateId` existing route and `/templates/$templateId/edit` new route coexist as siblings + + - **`Context/Features/021-Template-Builder-Route/Tech.md`** (read fully, 476 lines) + - ADR-021-01 through ADR-021-05 content + - Architecture diagrams for current vs target state + - Picker chain: `SessionTemplateForm` → `ActivityGroupEditor` → `ActivityEditor` → picker + + - **`.cortex/session.md`** — Created with plan reference, team roster, S002 findings (useBlocker API), wave log. + + - **5 ADR files created in `Context/Decisions/`** (via content-writer agent): + - `ADR-021-01-top-level-templates-namespace.md` + - `ADR-021-02-save-stays-in-form-body.md` + - `ADR-021-03-drawer-not-sheet-for-picker.md` + - `ADR-021-04-extract-exercise-picker-panel.md` (agent synthesized 2 brief alternatives) + - `ADR-021-05-dirty-state-guard.md` (includes useBlocker API verification addendum) + + - **`src/components/workout/add-exercise-sheet.tsx`** — Was 137 lines, rewritten as thin Sheet wrapper around ``. Props unchanged: `{ open, onOpenChange, onExerciseSelected, userId }`. Local `handleSelected` forwards and calls `onOpenChange(false)` to preserve close-on-select behavior. PostToolUse formatter touched the file after write. + + - **`src/components/workout/exercise-picker-panel.tsx`** — NEW. Headless panel, no Sheet/Dialog/portal. Props: `{ userId?, onExerciseSelected, autoFocus? }`. Contains `ExerciseSearchInput`, `useExercises`, `useRecentlyUsedExercises`, debounced filtering, `ExerciseRow` helper. Extracted verbatim from AddExerciseSheet. + + - **`src/components/session-builder/activity-editor.tsx`** (read fully, 168 lines) + - Uses `AddExerciseSheet` at line 159 inside `{/* Exercise picker sheet */}` block + - Has `showExerciseSheet` state + - Must be updated in S008 to accept `PickerComponent?: ComponentType` prop defaulting to `AddExerciseSheet` + + - **`src/components/session-builder/activity-group-editor.tsx`** (read fully, 293 lines) + - Renders `ActivityEditor` in a loop; must thread `PickerComponent` through in S008 + + - **`src/components/session-builder/session-template-form.tsx`** (read fully, 481 lines) + - Props: `{ initial?: SessionTemplateFull, onSave?, onCancel? }` — must NOT change (test dependency) + - Must add optional `PickerComponent` prop and thread to `ActivityGroupEditor` + - Save button is inside the form body at lines 458-478 (per ADR-021-02, stays there) + - Layout: `lg:grid lg:grid-cols-[320px_1fr]` — two-column sticky metadata + activity groups + + - **`src/components/event-builder/event-template-form.tsx`** (read lines 1-60) + - Uses `RequirementEditor`, `EventItemEditor`, NOT `AddExerciseSheet` or `ActivityEditor` + - **No picker threading needed for EventTemplateForm** — simplification of S008 + + - **`src/routes/_authenticated/library.tsx`** (partial reads; full is 1201 lines) + - Line 27: `import { SessionTemplateForm }`, Line 29: `import { EventTemplateForm }`, Line 41: `useSessionTemplateFull` + - State at lines 118-120: `sheetOpen`, `editingId`, `sheetMode` + - Handlers: `handleCreate`, `handleCreateEvent`, `handleEdit`, `handleEditEvent`, `handleSaved`, `handleCancel` (lines 123-161) + - Sheet JSX block at lines 467-518 + - Session/event distinguisher: `template.category === 'EVENT'` (line 366) + - `EditTemplateFormLoader` (lines 1113-1156) and `EditEventFormLoader` (lines 1162-1201) — structure: `useSessionTemplateFull(templateId)` → isLoading→Skeleton, error→message, !data→not-found, data→Form + + - **`src/routes/_authenticated/builder.tsx`** (read lines 1-100) + - Route pattern reference: + ```tsx + export const Route = createFileRoute('/_authenticated/builder')({ + validateSearch: (search: Record) => ({ + programId: typeof search['programId'] === 'string' ? search['programId'] : undefined, + }), + component: BuilderPage, + }) + function BuilderPage() { + const { programId } = Route.useSearch() + const navigate = useNavigate() + ... + } + ``` + + - **`src/components/session-builder/template-editor-layout.tsx`** — NEW (S006). Props `{ title, children, onBack? }`. Structure: + ```tsx +
+
+
+ {onBack ? + : ...} +

{title}

+
+
{children}
+
+
+ ``` + No save button in header, no divider borders, uses `arrow_back` Icon + "Library" text. + + - **`src/components/session-builder/exercise-picker-drawer.tsx`** — NEW (S007, just created). Key details: + - Plain `