diff --git a/.beads/README.md b/.beads/README.md index daac3c4e..d7831ff2 100644 --- a/.beads/README.md +++ b/.beads/README.md @@ -14,32 +14,32 @@ Beads is issue tracking that lives in your repo, making it perfect for AI coding ```bash # Create new issues -bd create "Add user authentication" +br create "Add user authentication" # View all issues -bd list +br list # View issue details -bd show +br show # Update issue status -bd update --status in_progress -bd update --status done +br update --status in_progress +br update --status done # Sync with git remote -bd sync +br sync ``` ## Daemon (Optional) -Beads has an optional background daemon (`bd daemon`) that auto-syncs issues with git. +Beads has an optional background daemon (`br daemon`) that auto-syncs issues with git. In this repo, the sync branch is configured as `main`. Running the daemon while you are on a different branch can cause `.beads/issues.jsonl` to be rewritten from `main`, leaving your working tree dirty and blocking `git pull --rebase`. Recommended workflow: -- Prefer manual sync: `bd sync` (default). -- If you use the daemon, run it only while you are on `main`, or run it in local-only mode: `bd daemon --start --local`. -- If `.beads/issues.jsonl` is changing unexpectedly, check/stop the daemon: `bd daemon --status` / `bd daemon --stop`. +- Prefer manual sync: `br sync` (default). +- If you use the daemon, run it only while you are on `main`, or run it in local-only mode: `br daemon --start --local`. +- If `.beads/issues.jsonl` is changing unexpectedly, check/stop the daemon: `br daemon --status` / `br daemon --stop`. ### Working with Issues @@ -75,16 +75,16 @@ Try Beads in your own projects: curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash # Initialize in your repo -bd init +br init # Create your first issue -bd create "Try out Beads" +br create "Try out Beads" ``` ## Learn More - **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) -- **Quick Start Guide**: Run `bd quickstart` +- **Quick Start Guide**: Run `br quickstart` - **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) --- diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3d3ac096..86748e75 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -742,7 +742,9 @@ {"id":"agentic_coding_flywheel_setup-zumu","title":"Reduce flakiness in production Playwright smoke tests","description":"Playwright production smoke tests used waitForLoadState(\"networkidle\") directly, which can be flaky on pages with long-lived background requests (analytics, etc). Add a helper that waits for domcontentloaded and then attempts networkidle with a short timeout, without failing if the page never becomes fully idle.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-25T06:26:18.185815Z","updated_at":"2025-12-25T06:26:46.273323Z","closed_at":"2025-12-25T06:26:46.273323Z","close_reason":"Implemented waitForPageSettled helper and replaced strict networkidle waits in production smoke tests.","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"agentic_coding_flywheel_setup-zumu","depends_on_id":"agentic_coding_flywheel_setup-dvt.6","type":"discovered-from","created_at":"2025-12-25T06:26:18.187139Z","created_by":"jemanuel","metadata":"{}","thread_id":""}]} {"id":"agentic_coding_flywheel_setup-zv33","title":"Random deep code exploration audit (Round 3)","description":"Random audit pass: trace manifest generator + installer security paths, then fix any concrete issues found (reserve files before edits).","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-25T09:37:22.568240Z","updated_at":"2025-12-25T09:54:51.415794Z","closed_at":"2025-12-25T09:54:51.415794Z","close_reason":"Fixed manifest generator verified_installer piping for bash/sh args (commit 5b0c67a); bun test + type-check passed.","source_repo":".","compaction_level":0} {"id":"agentic_coding_flywheel_setup-zwg3","title":"TASK: Add final completion certificate to onboard","description":"# TASK: Add final completion certificate to onboard\n\n## Context\nWhen user completes all 9 lessons, show a special completion screen.\n\n## Proposed Screen\n```\n╭─────────────────────────────────────────────────────────────╮\n│ │\n│ 🏆 ACFS ONBOARDING COMPLETE! │\n│ │\n│ Congratulations! You've completed all 9 lessons. │\n│ │\n│ You're now ready to: │\n│ • Launch AI agents with cc, cod, gmi │\n│ • Manage sessions with ntm │\n│ • Search code with rg │\n│ • Use the full flywheel workflow │\n│ │\n│ Next steps: │\n│ • Run 'acfs info' for quick reference │\n│ • Run 'acfs cheatsheet' for all commands │\n│ • Start a project with 'ntm new myproject && cc' │\n│ │\n│ Completed: $(date +'%Y-%m-%d %H:%M') │\n│ │\n│ Happy coding! 🚀 │\n│ │\n╰─────────────────────────────────────────────────────────────╯\n```\n\n## Implementation\n\n### Store Completion Timestamp\n```bash\n# In onboard_progress.json\n{\n \"completed_lessons\": [0,1,2,3,4,5,6,7,8],\n \"completed_at\": \"2025-01-15T14:30:00Z\"\n}\n```\n\n### Trigger Condition\nShow when:\n- User just completed lesson 8 (last)\n- OR user runs `onboard` when all lessons complete\n\n### Re-access\nUser can re-run `onboard` to see certificate again.\n\n## Acceptance Criteria\n- [ ] Certificate shown on final completion\n- [ ] Completion timestamp stored\n- [ ] Shows next steps recommendations\n- [ ] Accessible by running onboard again\n- [ ] Celebration emoji/styling","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-22T19:59:54.304099Z","updated_at":"2025-12-22T20:35:03.037145Z","closed_at":"2025-12-22T20:35:03.037145Z","close_reason":"View Certificate menu option added to onboard. Shows trophy option when all 9 lessons complete.","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"agentic_coding_flywheel_setup-zwg3","depends_on_id":"agentic_coding_flywheel_setup-kyhv","type":"blocks","created_at":"2025-12-22T20:00:16.782605Z","created_by":"jemanuel","metadata":"{}","thread_id":""}]} +{"id":"bd-10sv","title":"Fix SLB installation failure on Ubuntu 25.04","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-03T14:38:30.188603390Z","created_by":"ubuntu","updated_at":"2026-02-03T14:42:35.650533697Z","closed_at":"2026-02-03T14:42:35.650514952Z","close_reason":"Fixed by installing SLB via .deb (commit 75a91c50)","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-12kj","title":"Deep exploration: WA (WezTerm Automata)","description":"## Goal\nPerform deep exploration of WA (Web Agent / WebView Automation) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify WA installation (check for common web automation paths)\n[[ -d /dp/web_agent ]] && echo \"PASS: wa repo exists\" || echo \"INFO: wa repo path differs\"\ncommand -v wa &>/dev/null && echo \"PASS: wa command available\" || echo \"INFO: wa not in PATH\"\n\n# Check browser automation dependencies\ncommand -v playwright &>/dev/null && echo \"INFO: playwright available\" || echo \"INFO: playwright not installed\"\ncommand -v chromium &>/dev/null && echo \"INFO: chromium available\" || echo \"INFO: chromium not installed\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/wa-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- Find WA repo and read README\n- Check for browser automation docs, screenshot capabilities\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Browser automation mechanism (Playwright? Puppeteer?)\n - Screenshot capture capabilities\n - Form filling and interaction\n - Integration with Claude chrome extension\n - Headless vs headed mode\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nwa --help 2>&1 | head -20 || echo \"No wa command\"\n# Check for alternative names\nbrowser-agent --help 2>&1 | head -10 || echo \"No browser-agent\"\n```\n\n### 1.4 External Context Search\n- `/xf search 'web agent OR browser automation OR playwright'` - Twitter archive\n- `cass search 'wa browser screenshot' --robot --limit 10` - Past sessions\n\n### 1.5 Project State Review\n- Find WA repo location\n- Review recent commits\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Browser automation engine\n- [ ] Screenshot capabilities\n- [ ] Form interaction\n- [ ] Claude extension integration\n- [ ] Headless operation\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] claude-chrome - Chrome extension\n- [ ] mail - screenshot sharing\n- [ ] ntm - coordinated browser tasks\n\n### 2.3 Capability Verification\n```bash\n# Check for playwright installation\nnpx playwright --version 2>/dev/null || echo \"playwright not installed\"\n\n# Check for browser binaries\nls ~/.cache/ms-playwright/ 2>/dev/null | head -5 || echo \"No cached browsers\"\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate wa entry with VERIFIED information:\n- `tagline`: Browser automation\n- `description`: Web interaction capabilities\n- `deepDescription`: How automation works\n- `features`: Verified capabilities\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test wa entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst wa = flywheelTools.find(t => t.id === 'wa');\nconsole.log('Testing wa entry...');\nconsole.assert(wa, 'wa entry exists');\nconsole.assert(wa.features?.length > 0, 'has features');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Browser Automation (Safe Mode)\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/wa-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== WA E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Help/availability\necho \"Test 1: Help check...\" | tee -a $LOG\nwa --help 2>&1 | head -10 | tee -a $LOG || echo \"wa not available\"\n\n# Test 2: Screenshot capability\necho \"Test 2: Screenshot help...\" | tee -a $LOG\nwa screenshot --help 2>&1 | head -5 | tee -a $LOG || echo \"No screenshot command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-12kj --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Browser capabilities documented correctly\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:02:06.153300827Z","created_by":"ubuntu","updated_at":"2026-01-27T03:38:10.013400309Z","closed_at":"2026-01-27T03:38:10.013370132Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-144la","title":"Add opt-in agent notification system via ntfy.sh","description":"## Problem\n\nUsers running agents on remote VPS have no way to get notified when agents complete, fail, or need attention, without actively watching terminals. Ref: GH issue #131.\n\n## Solution\n\nOpt-in notification system using ntfy.sh -- zero-config by default, one-line enable, push to any device via HTTP pub/sub. Builds on the existing webhook.sh pattern.\n\n## Architecture\n\n### How ntfy.sh Works\n- Topic-based HTTP pub/sub: curl -d \"message\" ntfy.sh/my-topic\n- No signup, no API keys needed for public server\n- Topic name serves as de facto password (choose unguessable names)\n- Self-hosted option: same API, different server URL\n- Clients: Android, iOS, web, CLI, or any HTTP client\n- Priority levels: min, low, default, high, urgent\n\n### Configuration Storage\nLocation: ~/.config/acfs/config.yaml (XDG-compliant, same as webhook_url)\n\nNew keys:\n notifications:\n enabled: false # Master switch (default: off)\n server: \"https://ntfy.sh\" # Default public server, or self-hosted URL\n topic: \"\" # Auto-generated on enable (e.g., acfs-{hostname}-{random8})\n events: # Which events trigger notifications\n agent_session_end: true # Agent session completed/crashed\n install_complete: true # ACFS install/update finished\n ci_failure: true # GH Actions workflow failed\n doctor_failure: true # acfs doctor found issues\n nightly_update: false # Nightly update results (noisy, default off)\n priority:\n default: \"default\" # Normal events\n failure: \"high\" # Failures get high priority\n crash: \"urgent\" # Agent crashes get urgent priority\n quiet_hours:\n enabled: false\n start: \"23:00\"\n end: \"07:00\"\n\n### Subcommand: acfs notifications\n\nAdd to acfs.zshrc dispatcher and create scripts/lib/notifications.sh:\n\n acfs notifications enable # Generate topic, enable, show subscribe URL\n acfs notifications disable # Turn off\n acfs notifications test # Send test notification\n acfs notifications status # Show current config\n acfs notifications topic # Print the subscribe URL for easy copying\n acfs notifications events # List configurable events\n acfs notifications set-server # Switch to self-hosted\n\n### Enable Flow\n1. acfs notifications enable\n2. Generate random topic: acfs-$(hostname)-$(openssl rand -hex 4) e.g. acfs-devbox-a1b2c3d4\n3. Write to ~/.config/acfs/config.yaml\n4. Print: \"Notifications enabled! Subscribe on your devices:\"\n5. Print: \" Web: https://ntfy.sh/acfs-devbox-a1b2c3d4\"\n6. Print: \" App: Open ntfy app, add topic: acfs-devbox-a1b2c3d4\"\n7. Send test notification: \"ACFS notifications enabled on $(hostname)\"\n\n### Notification Library: scripts/lib/notify.sh\nFollows the same pattern as scripts/lib/webhook.sh:\n- Source with: source ~/.acfs/scripts/lib/notify.sh\n- Function: acfs_notify(title, message, priority, tags)\n- Non-blocking: background curl with 5s timeout, disowned\n- Best-effort: failures are silently ignored (notifications should never block workflows)\n- HTTPS-only validation on server URL\n- Respects quiet_hours config\n- Logs to ~/.acfs/logs/notifications.log (append-only, one line per notification with timestamp)\n\nImplementation:\n acfs_notify() {\n local title=\"$1\" message=\"$2\" priority=\"${3:-default}\" tags=\"${4:-}\"\n local config_file=\"$HOME/.config/acfs/config.yaml\"\n # Check enabled\n local enabled; enabled=$(yq -r '.notifications.enabled // \"false\"' \"$config_file\" 2>/dev/null)\n [ \"$enabled\" = \"true\" ] || return 0\n # Read config\n local server; server=$(yq -r '.notifications.server // \"https://ntfy.sh\"' \"$config_file\")\n local topic; topic=$(yq -r '.notifications.topic // \"\"' \"$config_file\")\n [ -n \"$topic\" ] || return 0\n # Check quiet hours\n if _acfs_in_quiet_hours; then return 0; fi\n # Send (non-blocking)\n curl -sf -o /dev/null \\\n -H \"Title: $title\" \\\n -H \"Priority: $priority\" \\\n ${tags:+-H \"Tags: $tags\"} \\\n -d \"$message\" \\\n \"$server/$topic\" &\n disown\n # Log\n echo \"$(date -Iseconds) [$priority] $title: $message\" >> \"$HOME/.acfs/logs/notifications.log\"\n }\n\n### Integration Points (where to call acfs_notify)\n\n1. Agent session completion hooks:\n - Claude Code: Add a Notification hook in ~/.claude/settings.json (or per-project .claude/settings.json)\n Claude Code fires \"Notification\" hook when it produces a notification. Wire this to acfs_notify.\n - Codex: Codex CLI emits exit codes -- wrap the cod alias to call acfs_notify on exit\n - Gemini: Same wrapping approach for gmi alias\n\n2. ACFS install/update completion:\n - install.sh: Add acfs_notify call after webhook_notify (line ~5590)\n - scripts/lib/update.sh: Add notify at end of print_summary\n\n3. acfs doctor failures:\n - scripts/lib/doctor.sh: Add notify on non-zero exit\n\n4. Nightly update:\n - scripts/lib/nightly_update.sh: Add notify at completion\n\n5. GH Actions CI failure:\n - This requires a webhook receiver or polling -- defer to v2\n - For now, document how users can add ntfy.sh to their own GH Actions workflows\n\n### Agent Alias Wrapping (for session end notifications)\n\nModify the cc, cod, gmi aliases in acfs.zshrc to wrap agent invocations:\n\nCurrent (line ~456): alias cc='claude'\nNew:\n cc() {\n claude \"$@\"\n local exit_code=$?\n if command -v acfs_notify &>/dev/null; then\n if [ $exit_code -eq 0 ]; then\n acfs_notify \"Claude Code\" \"Session completed in $(basename $PWD)\" \"default\" \"white_check_mark\"\n else\n acfs_notify \"Claude Code\" \"Session exited with code $exit_code in $(basename $PWD)\" \"high\" \"warning\"\n fi\n fi\n return $exit_code\n }\n\n## Files to Modify/Create\n\nModify:\n- acfs/zsh/acfs.zshrc (add notifications subcommand routing + agent alias wrapping)\n- scripts/completions/acfs.bash (add notifications completions)\n- scripts/completions/_acfs (add notifications zsh completions)\n\nCreate:\n- scripts/lib/notifications.sh (subcommand handler: enable/disable/test/status/topic/events/set-server)\n- scripts/lib/notify.sh (notification library: acfs_notify function + quiet hours + logging)\n\nOptionally modify:\n- install.sh (add acfs_notify call after webhook_notify at install completion)\n- scripts/lib/update.sh (add notify at update completion)\n- scripts/lib/doctor.sh (add notify on failure)\n- scripts/lib/nightly_update.sh (add notify at completion)\n\n## Testing Plan\n\n### Unit Tests (tests/unit/notifications.bats)\nUsing bats-core framework:\n- test_notify_disabled_by_default: verify acfs_notify is a no-op when notifications.enabled is false\n- test_notify_enabled_sends_curl: mock curl, verify it's called with correct args when enabled\n- test_notify_topic_generation: verify topic format matches acfs-{hostname}-{hex8}\n- test_notify_quiet_hours_respected: set quiet hours, verify no curl call during quiet window\n- test_notify_quiet_hours_outside: verify notifications send outside quiet window\n- test_notify_missing_topic: verify graceful no-op when topic is empty\n- test_notify_missing_config: verify graceful no-op when config file doesn't exist\n- test_notify_https_only: verify non-HTTPS server URLs are rejected\n- test_notify_logging: verify notification is appended to log file\n- test_notify_priority_mapping: verify priority parameter is passed through correctly\n- test_notify_tags: verify tags header is set when tags are provided, absent when not\n\n### Unit Tests (tests/unit/notifications_subcommand.bats)\n- test_notifications_enable: verify config file is created/updated with correct keys\n- test_notifications_disable: verify enabled is set to false\n- test_notifications_test: verify test notification is sent\n- test_notifications_status: verify current config is displayed\n- test_notifications_topic: verify topic URL is printed\n- test_notifications_set_server: verify server URL is updated in config\n- test_notifications_idempotent_enable: verify enabling twice doesn't change topic\n\n### E2E Test Script (new: scripts/e2e/test_notifications.sh)\nComprehensive end-to-end test:\n1. Start with clean state (no notifications config)\n2. Run acfs notifications status -- verify shows disabled\n3. Run acfs notifications enable -- verify config created, topic generated\n4. Parse topic from output\n5. Start a background subscriber: curl -s ntfy.sh/$TOPIC/json &\n6. Run acfs notifications test -- verify subscriber receives notification\n7. Verify log file created at ~/.acfs/logs/notifications.log\n8. Verify log entry matches sent notification\n9. Run acfs notifications disable -- verify config updated\n10. Run acfs notifications test -- verify no notification sent (disabled)\n11. Re-enable, set quiet hours to current time window\n12. Run acfs notifications test -- verify no notification sent (quiet hours)\n13. Test agent wrapper: run cc --version (fast), verify notification fires\n14. Test with self-hosted server URL: acfs notifications set-server https://custom.ntfy.example\n15. Verify config updated with new server\n16. All output logged with timestamps to scripts/e2e/test_notifications.log\n17. Each test prints [PASS] or [FAIL] with descriptive message\n18. Final summary: X/Y tests passed\n\n### Integration Tests\n- Run existing test suite: bats tests/unit/**/*.bats (must not regress)\n- ShellCheck on new scripts: shellcheck scripts/lib/notify.sh scripts/lib/notifications.sh\n- Lint checks: scripts/tests/lint_*.sh must pass on new files\n\n## Edge Cases\n- yq not installed: fall back to grep/sed for YAML parsing, or check for yq in enable flow\n- Config file doesn't exist: create it on enable, no-op otherwise\n- Network unreachable: curl -sf with timeout handles this (non-blocking, disowned)\n- ntfy.sh public server rate limits: document in help, suggest self-hosted for heavy use\n- Multiple VPS instances: each gets its own topic (hostname in topic name ensures uniqueness)\n- Agent crashes (SIGKILL): the wrapper function won't run -- this is acceptable, only clean exits notify\n- Concurrent notifications: each curl is independent, no race conditions\n- Log file growth: append-only, users can rotate/truncate as needed\n- Existing webhook.sh: notifications complement webhooks, don't replace them. Both can be active.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-02-10T00:21:36.447367448Z","created_by":"ubuntu","updated_at":"2026-02-11T16:26:19.695906420Z","closed_at":"2026-02-11T16:26:19.695874250Z","close_reason":"implemented: same as bd-2igt6, ntfy.sh notification system complete","source_repo":".","compaction_level":0,"original_size":0,"labels":["agent-experience","installer","notifications","ntfy"]} {"id":"bd-1493","title":"Subtask: Update WizardNavigation to check validation","status":"closed","priority":2,"issue_type":"subtask","created_at":"2026-01-25T23:24:26.675842784Z","created_by":"ubuntu","updated_at":"2026-01-27T02:12:14.306857584Z","closed_at":"2026-01-27T02:12:14.306839079Z","close_reason":"Implemented as part of parent bd-2gys - wizard/layout.tsx uses useStepValidation for navigation","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1493","depends_on_id":"bd-2gys","type":"blocks","created_at":"2026-01-25T23:25:59.783098123Z","created_by":"ubuntu"}]} {"id":"bd-14l5","title":"Subtask: Add validate() functions to wizard steps","status":"closed","priority":2,"issue_type":"subtask","created_at":"2026-01-25T23:23:49.345900022Z","created_by":"ubuntu","updated_at":"2026-01-27T02:12:31.374169403Z","closed_at":"2026-01-27T02:12:31.374148624Z","close_reason":"Core validators added in parent bd-2gys (OS selection, VPS creation). Infrastructure complete; additional validators can be added incrementally.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-14l5","depends_on_id":"bd-2gys","type":"blocks","created_at":"2026-01-25T23:25:46.360792994Z","created_by":"ubuntu"}]} {"id":"bd-151z","title":"Subtask: Add --dry-run mode for doctor --fix","status":"closed","priority":2,"issue_type":"subtask","created_at":"2026-01-25T23:20:53.384078706Z","created_by":"ubuntu","updated_at":"2026-01-27T02:10:24.013829253Z","closed_at":"2026-01-27T02:10:24.013808233Z","close_reason":"Already implemented - doctor.sh supports --dry-run flag which passes to doctor_fix.sh; all fix functions check DOCTOR_FIX_DRY_RUN and collect to FIXES_DRY_RUN array; help text updated; tests exist","source_repo":".","compaction_level":0,"original_size":0} @@ -867,10 +869,12 @@ {"id":"bd-1tnn","title":"Add all utility tools to tldr-content.ts","description":"Add toon_rust, rust_proxy, rano, xf, mdwb, pt, aadc, s2p, and caut to apps/web/lib/tldr-content.ts with brief descriptions for quick reference.","status":"closed","priority":2,"issue_type":"task","assignee":"ScarletCreek","created_at":"2026-01-25T00:47:48.020115905Z","created_by":"ubuntu","updated_at":"2026-01-27T01:57:43.818010398Z","closed_at":"2026-01-27T01:57:43.817992394Z","close_reason":"All 9 utility tools (tru, rust_proxy, rano, xf, mdwb, pt, aadc, s2p, caut) already exist in tldr-content.ts with comprehensive entries including whatItDoes, whyItsUseful, synergies, etc.","source_repo":".","compaction_level":0,"original_size":0,"labels":["tldr","utilities","website"],"dependencies":[{"issue_id":"bd-1tnn","depends_on_id":"bd-1ega","type":"parent-child","created_at":"2026-01-25T01:10:08.312163998Z","created_by":"ubuntu"},{"issue_id":"bd-1tnn","depends_on_id":"bd-1ega.6","type":"blocks","created_at":"2026-01-25T01:08:54.952591597Z","created_by":"ubuntu"}]} {"id":"bd-1yfv","title":"Help button for stuck users in web wizard","description":"## Overview\nAdd a contextual \"I'm stuck\" help button in the web wizard that provides step-specific troubleshooting guidance.\n\n## Current Problem\n- Users get stuck and don't know what to do\n- Generic FAQs don't address specific step issues\n- No easy way to get help without leaving the wizard\n- Common mistakes aren't addressed proactively\n\n## Proposed Solution\n- Floating \"Need help?\" button on each step\n- Opens modal with step-specific troubleshooting\n- Common issues and solutions for that step\n- Links to relevant documentation\n- Option to copy debug info for support\n\n## Help Content Structure\n```typescript\ninterface StepHelp {\n commonIssues: {\n symptom: string;\n solution: string;\n link?: string;\n }[];\n tips: string[];\n debugInfo: () => string;\n}\n```\n\n## Example: SSH Step Help\n**Common Issues:**\n- \"Permission denied\" → Check file permissions (chmod 600)\n- \"Connection refused\" → Verify VPS is running, check port 22\n- \"Host key verification\" → Add host to known_hosts\n\n**Tips:**\n- Test connection before proceeding\n- Use key-based auth, not passwords\n\n## Implementation Details\n1. Create `HelpButton` component (floating, fixed position)\n2. Create `HelpModal` with tabbed content\n3. Define help content for each wizard step\n4. Track help button usage for analytics\n\n## Debug Info Export\n```\n# ACFS Debug Info\nStep: ssh-setup\nOS: macOS 14.2\nBrowser: Chrome 120\nState: { provider: \"hetzner\", sshKeyPath: \"~/.ssh/id_rsa\" }\nErrors: []\n```\n\n## Test Plan\n- [ ] Test help button appears on all steps\n- [ ] Test modal opens/closes correctly\n- [ ] Test content matches current step\n- [ ] Test debug info generation\n- [ ] Test mobile layout (button position)\n\n## Files to Create\n- apps/web/components/wizard/HelpButton.tsx\n- apps/web/components/wizard/HelpModal.tsx\n- apps/web/lib/stepHelp.ts (help content per step)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T23:02:21.414394609Z","created_by":"ubuntu","updated_at":"2026-01-26T22:43:42.330878329Z","closed_at":"2026-01-26T22:43:42.330809990Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1yfv","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:04:26.493434121Z","created_by":"ubuntu"}],"comments":[{"id":39,"issue_id":"bd-1yfv","author":"Dicklesworthstone","text":"Implemented contextual help panel: per-step help content in lib/stepHelp.ts (common issues + tips for 9 steps), HelpPanel component with native (accessibility built-in), debug info export, integrated into wizard layout (desktop + mobile).","created_at":"2026-01-26T22:43:38Z"}]} {"id":"bd-1zkz","title":"Smoke test issue - DELETE ME","status":"closed","priority":4,"issue_type":"task","created_at":"2026-01-25T16:23:14.588348366Z","created_by":"ubuntu","updated_at":"2026-01-25T16:23:35.774459690Z","closed_at":"2026-01-25T16:23:35.773402068Z","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-216cu","title":"Close GH #119: checksum-monitor already tracks all tools (timing window)","description":"## Context\nGitHub issue #119 filed by frantisek-heca (2026-02-07). Reports that checksum-monitor CI should track Dicklesworthstone-owned tools.\n\n## Finding: ALREADY WORKING\nThe checksum-monitor workflow already monitors ALL tools. The KNOWN_INSTALLERS array in security.sh (lines 188-215) includes ntm, mcp_agent_mail, ubs, bv, cass, cm, caam, slb, dcg, ru, apr, ms, pt, and more.\n\nEvidence: auto-update commits like 83cefc1f (auto-update checksums for zoxide,mcp_agent_mail,uv,pt,rch) prove Dicklesworthstone tools ARE monitored.\n\n## Real Issue\nThe actual gap is the 2-hour cron polling interval creating a timing window where users can hit stale checksums after an upstream release.\n\n## Action Plan\n1. Close GH #119 with explanatory comment:\n - Thank the reporter\n - Explain that the KNOWN_INSTALLERS array already includes all Dicklesworthstone tools\n - Point to the evidence commit (83cefc1f)\n - Acknowledge the 2-hour timing window as a known limitation\n - Suggest repository_dispatch webhooks from upstream repos for instant sync (the workflow already supports repository_dispatch at lines 10-11)\n\n2. Comment text:\n```\nThanks for the suggestion! After investigation, the checksum-monitor workflow **already tracks all Dicklesworthstone-owned tools**. The `KNOWN_INSTALLERS` array in `security.sh` (lines 188-215) includes ntm, mcp_agent_mail, ubs, bv, cass, cm, caam, slb, dcg, ru, apr, ms, pt, and more. See commit 83cefc1f for evidence of auto-updates including these tools.\n\nThe real gap is the 2-hour cron polling interval — there is a timing window after an upstream release where checksums may be stale. The workflow already supports `repository_dispatch` events (lines 10-11), so upstream repos could trigger instant checksum updates. Filed as a follow-up improvement.\n\nClosing as already-working. Thanks for looking into this!\n```\n\n3. Run: `gh issue close 119 -R Dicklesworthstone/agentic_coding_flywheel_setup -c \"...\"`\n\n## No code changes needed. This is a housekeeping close.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-08T18:16:31.854628372Z","created_by":"ubuntu","updated_at":"2026-02-11T16:54:24.051909705Z","closed_at":"2026-02-11T16:54:24.051890549Z","close_reason":"GH#119 already closed. The checksum-monitor workflow already tracks all Dicklesworthstone tools via KNOWN_INSTALLERS array in security.sh. No code changes needed.","external_ref":"gh:Dicklesworthstone/agentic_coding_flywheel_setup#119","source_repo":".","compaction_level":0,"original_size":0,"labels":["checksum","housekeeping"]} {"id":"bd-21kh","title":"Progress bar during tool installation","description":"## Overview\nAdd a visual progress bar during tool installation showing overall progress and current tool being installed.\n\n## Current Problem\n- Installation can take several minutes\n- Users see scrolling text but no sense of overall progress\n- No way to estimate time remaining\n- Anxiety: \"Is it stuck or still working?\"\n\n## Proposed UI\n```\nInstalling ACFS tools...\n[████████████░░░░░░░░] 12/26 tools (46%)\nCurrent: Installing rust toolchain...\n```\n\n## Implementation Details\n1. Count total tools to install upfront\n2. Update progress after each tool completes\n3. Show current tool name during install\n4. Use ANSI escape codes for in-place updates\n5. Respect NO_COLOR environment variable\n\n## Progress Calculation\n- Parse manifest.yaml for tool count\n- Subtract already-installed tools\n- Increment counter after each install_tool() call\n\n## Terminal Handling\n```bash\n# In-place update (same line)\nprintf '\\r[%-20s] %d/%d tools (%d%%)' \"$bar\" \"$done\" \"$total\" \"$pct\"\n\n# With NO_COLOR or non-TTY\necho \"[$done/$total] Installing $tool...\"\n```\n\n## Graceful Degradation\n- Non-interactive: Simple line-by-line output\n- NO_COLOR: Text-only progress\n- Narrow terminal: Abbreviated format\n\n## Test Plan\n- [ ] Test progress updates correctly\n- [ ] Test NO_COLOR fallback\n- [ ] Test non-TTY output (piped)\n- [ ] Test narrow terminal handling\n- [ ] Visual test: progress matches actual completion\n\n## Files to Modify\n- scripts/lib/install_helpers.sh (add progress tracking)\n- scripts/install.sh (initialize progress bar)\n- New: scripts/lib/progress.sh (progress bar utilities)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T23:01:45.195454182Z","created_by":"ubuntu","updated_at":"2026-01-27T02:10:00.570699924Z","closed_at":"2026-01-27T02:10:00.570604384Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-21kh","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:03:54.194943376Z","created_by":"ubuntu"}]} {"id":"bd-223z","title":"Subtask: Add --fix flag parsing to doctor command","status":"closed","priority":1,"issue_type":"subtask","created_at":"2026-01-25T23:20:40.947681729Z","created_by":"ubuntu","updated_at":"2026-01-25T23:45:04.875526830Z","closed_at":"2026-01-25T23:45:04.875275928Z","close_reason":"Duplicate of bd-31ps.6.2","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-22gc","title":"Deep exploration: SLB (Simultaneous Launch Button)","description":"## Goal\nPerform deep exploration of SLB (Simultaneous Launch Button) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify SLB installation\n[[ -d /dp/simultaneous_launch_button ]] && echo \"PASS: slb repo exists\" || { echo \"FAIL: slb repo missing\"; exit 1; }\ncommand -v slb &>/dev/null && echo \"PASS: slb command available\" || { echo \"FAIL: slb not in PATH\"; exit 1; }\n\n# Check if slb daemon/server is available\npgrep -f slb 2>/dev/null && echo \"INFO: slb process running\" || echo \"INFO: slb not currently running\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/slb-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/simultaneous_launch_button/README.md` - Read full README\n- Check for two-person rule docs, coordination protocol\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Two-person rule implementation\n - Confirmation mechanism (how both agents confirm)\n - Timeout handling for confirmations\n - Integration with destructive command protection\n - Coordination protocol (messaging, locks)\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nslb --help 2>&1 | head -20\nslb confirm --help 2>&1 | head -10\nslb status --help 2>&1 | head -10\n\n# Test actual functionality\nslb status 2>&1 | head -5\n```\n\n### 1.4 External Context Search\n- `/xf search 'slb OR simultaneous launch OR two-person rule'` - Twitter archive\n- `cass search 'slb coordination' --robot --limit 10` - Past sessions\n\n### 1.5 Project State Review\n- Check beads in /dp/simultaneous_launch_button/.beads/\n- Review recent commits: `cd /dp/simultaneous_launch_button && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Two-person rule mechanism\n- [ ] Confirmation protocol and timeout\n- [ ] Command types requiring two-person approval\n- [ ] Integration with dcg (destructive command guard)\n- [ ] Recovery from failed confirmations\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] dcg - destructive command protection\n- [ ] mail - confirmation messaging\n- [ ] ntm - multi-agent coordination\n\n### 2.3 Safety Verification\n```bash\n# Check what commands trigger two-person rule\ncat /dp/simultaneous_launch_button/config/*.yaml 2>/dev/null | head -20\n# Or check code for command patterns\ngrep -r \"dangerous\\|destructive\" /dp/simultaneous_launch_button/src/ 2>/dev/null | head -10\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate slb entry with VERIFIED information:\n- `tagline`: Two-person rule for dangerous commands\n- `description`: Coordination protocol\n- `deepDescription`: How confirmation works\n- `features`: Verified safety capabilities\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations (dcg, mail, ntm)\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test slb entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst slb = flywheelTools.find(t => t.id === 'slb');\nconsole.log('Testing slb entry...');\nconsole.assert(slb, 'slb entry exists');\nconsole.assert(slb.features?.length > 0, 'has features');\nconsole.assert(slb.connectsTo?.includes('dcg'), 'connects to dcg');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Coordination Flow (Safe Mode)\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/slb-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== SLB E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Status check\necho \"Test 1: Status check...\" | tee -a $LOG\nslb status 2>&1 | tee -a $LOG\n\n# Test 2: Help for confirm\necho \"Test 2: Confirm help...\" | tee -a $LOG\nslb confirm --help 2>&1 | head -5 | tee -a $LOG\n\n# Test 3: List pending (if available)\necho \"Test 3: Pending operations...\" | tee -a $LOG\nslb pending 2>&1 | tee -a $LOG || echo \"No pending command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-22gc --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Safety mechanisms documented correctly\n- [ ] Synergies are reciprocal (slb->dcg and dcg->slb)\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:01:23.124119983Z","created_by":"ubuntu","updated_at":"2026-01-27T03:14:24.854921039Z","closed_at":"2026-01-27T03:14:24.854835038Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-29fl","title":"acfs status: One-line health summary command","description":"## Overview\nAdd `acfs status` command that outputs a single-line health summary, perfect for shell prompts or quick checks.\n\n## Current Problem\n- `acfs doctor` is verbose and takes time to run\n- No quick way to check \"is everything OK?\"\n- Shell prompt integration requires parseable output\n\n## Proposed Output Format\n```bash\n$ acfs status\nACFS OK: 26 tools, last update 2h ago\n\n$ acfs status # with issues\nACFS WARN: 2 outdated tools, doctor recommended\n```\n\n## Exit Codes\n- 0: Everything healthy\n- 1: Warnings (outdated tools, minor issues)\n- 2: Errors (missing tools, broken state)\n\n## Implementation Details\n1. New file: `scripts/commands/status.sh`\n2. Quick checks only (no network calls by default):\n - State file exists and valid\n - Required tools present in PATH\n - Last update timestamp\n3. Optional `--check-updates` for network-based checks\n\n## Machine-Readable Mode\n```bash\n$ acfs status --json\n{\"status\":\"ok\",\"tools\":26,\"last_update\":\"2026-01-25T21:00:00Z\",\"warnings\":[]}\n```\n\n## Shell Prompt Integration\n```bash\n# Add to .bashrc\nacfs_status() { acfs status --short 2>/dev/null || echo \"?\"; }\nPS1='$(acfs_status) \\w $ '\n```\n\n## Test Plan\n- [ ] Test exit codes for all states\n- [ ] Test --json output is valid JSON\n- [ ] Test runs in < 100ms (no network)\n- [ ] Test with missing state file\n- [ ] Test with outdated state\n\n## Files to Create\n- scripts/commands/status.sh","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-25T23:01:26.052120972Z","created_by":"ubuntu","updated_at":"2026-01-26T22:41:34.694666041Z","closed_at":"2026-01-26T22:41:34.694647606Z","close_reason":"Implemented scripts/lib/status.sh with --json, --short, --check-updates modes. Updated acfs.zshrc routing and help text.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-29fl","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:03:25.805296607Z","created_by":"ubuntu"}]} +{"id":"bd-29u5z","title":"Fresh eyes review pass","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-04T05:54:34.433670424Z","created_by":"ubuntu","updated_at":"2026-02-04T05:54:47.096734647Z","closed_at":"2026-02-04T05:54:47.096717745Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-2a8k","title":"ACFS update should self-update first","description":"The acfs update command should update ACFS itself (git pull) BEFORE updating any other components. Currently no self-update happens.\n\nCritical because:\n1. Bug fixes in update.sh won't apply without manual git pull\n2. Security fixes in checksums.yaml won't apply\n3. New tools in manifest won't be discovered\n\nFix: Add update_acfs_self() as FIRST operation that does git fetch/pull and re-execs if update.sh changed.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-21T21:16:03.928848634Z","created_by":"ubuntu","updated_at":"2026-01-21T21:46:31.334103730Z","closed_at":"2026-01-21T21:46:31.334032095Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0,"labels":["critical","self-update","update"]} {"id":"bd-2bg3","title":"Deep exploration: CAAM (Coding Agent Account Manager)","description":"## Goal\nPerform deep exploration of CAAM (Coding Agent Account Manager) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify CAAM installation\n[[ -d /dp/coding_agent_account_manager ]] && echo \"PASS: caam repo exists\" || { echo \"FAIL: caam repo missing\"; exit 1; }\ncommand -v caam &>/dev/null && echo \"PASS: caam command available\" || { echo \"FAIL: caam not in PATH\"; exit 1; }\n\n# Check supported CLI tool availability\ncommand -v claude &>/dev/null && echo \"INFO: claude CLI available\"\ncommand -v codex &>/dev/null && echo \"INFO: codex CLI available\" || echo \"INFO: codex not installed\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/caam-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/coding_agent_account_manager/README.md` - Read full README\n- Check for account configuration docs, supported profiles\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Account storage mechanism (encrypted? plaintext?)\n - Switching implementation (sub-100ms claim)\n - CLI profiles supported (Claude, GPT, Gemini, Codex)\n - Rate limit detection strategy\n - Environment variable management\n - Config file location and format\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\ncaam --help 2>&1 | head -20\ncaam list --help 2>&1 | head -10\ncaam switch --help 2>&1 | head -10\n\n# Test actual functionality\ncaam list 2>&1 | head -10\n```\n\n### 1.4 Performance Claim Verification\n```bash\n# Verify sub-100ms switching claim\necho \"Testing switch timing...\"\ntime (caam switch test-profile 2>/dev/null || echo \"No test profile\") 2>&1\n```\n\n### 1.5 External Context Search\n- `/xf search 'caam OR account manager OR rate limit'` - Twitter archive\n- `cass search 'caam account switching' --robot --limit 10` - Past sessions\n\n### 1.6 Project State Review\n- Check beads in /dp/coding_agent_account_manager/.beads/\n- Review recent commits: `cd /dp/coding_agent_account_manager && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Account storage format (file location, encryption)\n- [ ] Switching mechanism and actual timing\n- [ ] Supported CLI profiles (complete list)\n- [ ] Rate limit detection method\n- [ ] Environment management approach\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] ntm - multi-agent account coordination\n- [ ] slb - coordinated account switching\n- [ ] mail - account status messaging\n\n### 2.3 Claim Verification\n```bash\n# Verify sub-100ms claim with timing\nSTART=$(date +%s%N)\ncaam list >/dev/null 2>&1\nEND=$(date +%s%N)\nELAPSED=$(( (END - START) / 1000000 ))\necho \"caam list took: ${ELAPSED}ms\"\n\n# Check config location\nls -la ~/.config/caam/ 2>/dev/null || echo \"Config location differs\"\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate caam entry with VERIFIED information:\n- `tagline`: Accurate one-liner (verify sub-100ms claim)\n- `description`: Account management capabilities\n- `deepDescription`: How switching actually works\n- `features`: Verified account operations\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test caam entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst caam = flywheelTools.find(t => t.id === 'caam');\nconsole.log('Testing caam entry...');\nconsole.assert(caam, 'caam entry exists');\nconsole.assert(caam.features?.length > 0, 'has features');\nconsole.assert(caam.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Account Operations\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/caam-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== CAAM E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: List accounts\necho \"Test 1: List accounts...\" | tee -a $LOG\ncaam list 2>&1 | tee -a $LOG\n\n# Test 2: Current account\necho \"Test 2: Current account...\" | tee -a $LOG\ncaam current 2>&1 | tee -a $LOG || echo \"No current command\"\n\n# Test 3: Help for add\necho \"Test 3: Add help...\" | tee -a $LOG\ncaam add --help 2>&1 | head -5 | tee -a $LOG\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-2bg3 --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Performance claims verified (sub-100ms)\n- [ ] Content matches verified tool capabilities\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:01:18.982295810Z","created_by":"ubuntu","updated_at":"2026-01-27T03:27:47.367564776Z","closed_at":"2026-01-27T03:27:47.367538427Z","close_reason":"CAAM deep exploration complete: verified 50+ commands, added project-profile associations, caam run automatic failover, pick command with fzf","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-2cz8","title":"Deep exploration: APR (Automated Plan Reviser Pro)","description":"## Goal\nPerform deep exploration of APR (Automated Plan Reviser Pro) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify APR installation\n[[ -d /dp/automated_plan_reviser_pro ]] && echo \"PASS: apr repo exists\" || { echo \"FAIL: apr repo missing\"; exit 1; }\ncommand -v apr &>/dev/null && echo \"PASS: apr command available\" || { echo \"FAIL: apr not in PATH\"; exit 1; }\n\n# Check for Claude Code integration\n[[ -f ~/.claude/settings.json ]] && echo \"INFO: Claude settings exist\" || echo \"INFO: No Claude settings\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/apr-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/automated_plan_reviser_pro/README.md` - Read full README\n- Check for plan format docs, revision algorithm\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Plan parsing mechanism\n - Revision algorithm (how it improves plans)\n - Integration with Claude Code planning\n - Diff generation for plan changes\n - Checkpoint/rollback support\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\napr --help 2>&1 | head -20\napr revise --help 2>&1 | head -10\napr diff --help 2>&1 | head -10\n\n# Test actual functionality\napr status 2>&1 | head -10 || echo \"No status command\"\n```\n\n### 1.4 External Context Search\n- `/xf search 'apr OR plan reviser OR automated planning'` - Twitter archive\n- `cass search 'apr plan revision' --robot --limit 10` - Past sessions\n\n### 1.5 Project State Review\n- Check beads in /dp/automated_plan_reviser_pro/.beads/\n- Review recent commits: `cd /dp/automated_plan_reviser_pro && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Plan format specification\n- [ ] Revision algorithm details\n- [ ] Diff generation format\n- [ ] Checkpoint mechanism\n- [ ] Claude Code integration\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] br (beads) - plan tracking\n- [ ] mail - plan review coordination\n- [ ] ntm - multi-agent planning\n\n### 2.3 Format Verification\n```bash\n# Check plan format examples\nfind /dp/automated_plan_reviser_pro -name \"*.plan\" -o -name \"example*.md\" 2>/dev/null | head -3 | xargs head -20\n\n# Check revision output format\napr revise --dry-run /tmp/test.plan 2>&1 | head -20 || echo \"No dry-run option\"\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate apr entry with VERIFIED information:\n- `tagline`: Automated plan improvement\n- `description`: Plan parsing and revision\n- `deepDescription`: How revision algorithm works\n- `features`: Verified capabilities\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test apr entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst apr = flywheelTools.find(t => t.id === 'apr');\nconsole.log('Testing apr entry...');\nconsole.assert(apr, 'apr entry exists');\nconsole.assert(apr.features?.length > 0, 'has features');\nconsole.assert(apr.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Plan Revision\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/apr-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== APR E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Help\necho \"Test 1: Help...\" | tee -a $LOG\napr --help 2>&1 | head -10 | tee -a $LOG\n\n# Test 2: Revise help\necho \"Test 2: Revise help...\" | tee -a $LOG\napr revise --help 2>&1 | head -5 | tee -a $LOG\n\n# Test 3: Diff help\necho \"Test 3: Diff help...\" | tee -a $LOG\napr diff --help 2>&1 | head -5 | tee -a $LOG || echo \"No diff command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-2cz8 --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Plan format documented correctly\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:02:24.581011487Z","created_by":"ubuntu","updated_at":"2026-01-27T03:20:12.144878838Z","closed_at":"2026-01-27T03:20:12.144848431Z","close_reason":"Deep exploration complete. Updated flywheel.ts and tldr-content.ts with verified APR information: iterative convergence pattern (architecture→refinement→polish), document bundling, convergence analytics with weighted scoring, pre-flight validation, auto-retry with backoff, session management, robot mode JSON API with semantic error codes, Claude Code integration. Fixed incorrect CLI commands (apr run not apr refine) and install command (removed non-existent --easy-mode). Build passes.","source_repo":".","compaction_level":0,"original_size":0} @@ -880,6 +884,7 @@ {"id":"bd-2gog","title":"Add doctor.sh health checks for all utility tools","description":"## Task\n\nAdd basic health checks to scripts/lib/doctor.sh for all 9 utility tools.\n\n## Checks to Add\n\nEach utility should have at minimum a binary existence check:\n\n| Tool | Check Command |\n|------|---------------|\n| tru | command -v tru |\n| rust_proxy | command -v rust_proxy |\n| rano | command -v rano |\n| xf | command -v xf |\n| mdwb | command -v mdwb |\n| pt | command -v pt |\n| aadc | command -v aadc |\n| s2p | command -v s2p |\n| caut | command -v caut |\n\n## Implementation Pattern\n\nUse the existing check() function pattern:\n```bash\ncheck \"util.tru\" \"tru (token notation)\" \"$(command -v tru &>/dev/null && echo pass || echo warn)\" \"optional utility\"\n```\n\n## Acceptance Criteria\n\n1. All 9 utilities have health checks\n2. Checks are non-fatal (warn, not fail) since utilities are optional\n3. Doctor runs without errors\n4. Proper categorization in doctor output","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T01:09:33.424346589Z","created_by":"ubuntu","updated_at":"2026-01-27T01:56:05.235029921Z","closed_at":"2026-01-27T01:56:05.235011737Z","close_reason":"Added check_utilities() function to scripts/lib/doctor.sh with checks for all 9 utility tools: tru, rust_proxy, rano, xf, mdwb, pt, aadc, s2p, caut. Uses skip status for optional tools not installed. Updated _is_bespoke_covered() to include util.* pattern.","source_repo":".","compaction_level":0,"original_size":0,"labels":["doctor","health-check","utilities"],"dependencies":[{"issue_id":"bd-2gog","depends_on_id":"bd-1ega","type":"parent-child","created_at":"2026-01-25T01:10:12.798545559Z","created_by":"ubuntu"},{"issue_id":"bd-2gog","depends_on_id":"bd-1ega.6","type":"blocks","created_at":"2026-01-25T01:09:47.793624868Z","created_by":"ubuntu"}]} {"id":"bd-2gys","title":"Step validation in web wizard before navigation","description":"## Overview\nAdd pre-navigation validation in the web wizard to ensure required fields are filled before moving to the next step.\n\n## Current Problem\n- Users can click \"Next\" without completing required actions\n- Leads to confusion in later steps that depend on earlier choices\n- No feedback about what's missing\n\n## Proposed Behavior\n1. Each step defines validation rules\n2. \"Next\" button disabled until validation passes\n3. Clear visual indication of incomplete fields\n4. Helpful error messages explaining what's needed\n\n## Validation Rules by Step\n- OS Selection: Must select Ubuntu or Debian\n- VPS Provider: Must select provider (optional: show comparison)\n- SSH Setup: Must have valid SSH key path or generate new\n- Install Options: At least one module selected\n- etc.\n\n## Implementation Details\n1. Add `validate()` function to each wizard step config\n2. Create `useStepValidation` hook\n3. Validation runs on field change and before navigation\n4. Store validation state in wizard context\n\n## Component Changes\n```tsx\n// wizardSteps.ts\nexport interface WizardStep {\n id: number;\n title: string;\n slug: string;\n validate?: (state: WizardState) => ValidationResult;\n}\n\ninterface ValidationResult {\n valid: boolean;\n errors: string[];\n}\n```\n\n## UX Considerations\n- Don't block exploration (allow viewing future steps)\n- Clear distinction between \"incomplete\" and \"invalid\"\n- Inline validation messages, not just on submit\n- Persist partial progress to localStorage\n\n## Test Plan\n- [ ] Test each step's validation rules\n- [ ] Test \"Next\" disabled when invalid\n- [ ] Test error messages display correctly\n- [ ] Test validation on field blur\n- [ ] Test localStorage persistence of partial state\n\n## Files to Modify\n- apps/web/lib/wizardSteps.ts (add validate functions)\n- apps/web/components/wizard/WizardNavigation.tsx\n- apps/web/hooks/useStepValidation.ts (new)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T23:01:36.402265120Z","created_by":"ubuntu","updated_at":"2026-01-26T22:38:19.926575059Z","closed_at":"2026-01-26T22:38:19.926492784Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2gys","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:03:43.392191184Z","created_by":"ubuntu"}],"comments":[{"id":38,"issue_id":"bd-2gys","author":"Dicklesworthstone","text":"Implemented centralized step validation: ValidationResult type + validate() on WizardStep interface, validate functions for steps 1 (OS) and 5 (IP), useStepValidation hook, error banner in layout.tsx. Backward navigation always allowed (don't block exploration). Forward nav validates current step.","created_at":"2026-01-26T22:38:14Z"}]} {"id":"bd-2h1y","title":"Deep exploration: XF (X Archive Search)","description":"## Goal\nPerform deep exploration of XF (X/Twitter Archive Search) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify XF installation\n[[ -d /dp/xf ]] && echo \"PASS: xf repo exists\" || { echo \"FAIL: xf repo missing\"; exit 1; }\ncommand -v xf &>/dev/null && echo \"PASS: xf command available\" || { echo \"FAIL: xf not in PATH\"; exit 1; }\n\n# Check for archive data\n[[ -d ~/.local/share/xf/archive ]] && echo \"INFO: XF archive exists\" || echo \"INFO: No archive yet\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/xf-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/xf/README.md` - Read full README\n- Check for archive format docs, search syntax\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Archive import process (from X/Twitter export)\n - Index building (SQLite FTS5?)\n - Search query syntax\n - Content types supported (tweets, DMs, likes, Grok)\n - Output formats (human, robot)\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nxf --help 2>&1 | head -20\nxf search --help 2>&1 | head -10\nxf import --help 2>&1 | head -10\n\n# Test actual functionality\nxf search \"test\" --limit 3 2>&1 | head -10\n```\n\n### 1.4 External Context Search\n- `cass search 'xf twitter archive' --robot --limit 10` - Past sessions\n- Check for XF usage patterns in existing sessions\n\n### 1.5 Project State Review\n- Check beads in /dp/xf/.beads/\n- Review recent commits: `cd /dp/xf && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Archive import mechanism\n- [ ] Index structure (FTS5 schema)\n- [ ] Search query syntax (operators, filters)\n- [ ] Content types (tweets, DMs, likes, bookmarks, Grok)\n- [ ] Robot output format\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] cass - similar search patterns\n- [ ] brenner - research integration\n- [ ] cm - memory of twitter insights\n\n### 2.3 Archive Verification\n```bash\n# Check archive contents\nls -la ~/.local/share/xf/ 2>/dev/null | head -10\n\n# Count indexed items\nxf stats 2>&1 | head -10 || echo \"No stats command\"\n\n# Check supported content types\nxf types 2>&1 | head -10 || echo \"No types command\"\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate xf entry with VERIFIED information:\n- `tagline`: X/Twitter archive search\n- `description`: Full-text search capabilities\n- `deepDescription`: How archive indexing works\n- `features`: Verified content types and search\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test xf entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst xf = flywheelTools.find(t => t.id === 'xf');\nconsole.log('Testing xf entry...');\nconsole.assert(xf, 'xf entry exists');\nconsole.assert(xf.features?.length > 0, 'has features');\nconsole.assert(xf.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Archive Search\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/xf-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== XF E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Basic search\necho \"Test 1: Basic search...\" | tee -a $LOG\nxf search \"test\" --limit 3 2>&1 | tee -a $LOG\n\n# Test 2: Robot mode\necho \"Test 2: Robot mode...\" | tee -a $LOG\nxf search \"code\" --robot --limit 3 2>&1 | tee -a $LOG\n\n# Test 3: Content type filter (if available)\necho \"Test 3: Filter test...\" | tee -a $LOG\nxf search \"agent\" --type tweets --limit 3 2>&1 | tee -a $LOG || echo \"No type filter\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-2h1y --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Content types documented correctly\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:02:52.507929572Z","created_by":"ubuntu","updated_at":"2026-01-27T05:34:21.798988659Z","closed_at":"2026-01-27T05:34:21.798964073Z","close_reason":"Deep exploration completed by EmeraldCrane. Verified Tantivy BM25, three search modes, SIMD-accelerated vectors.","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-2igt6","title":"Add zero-config agent notification system via ntfy.sh (GH#131)","description":"## Problem\n\nUsers running agent swarms on remote VPS have no way to know when agents complete tasks without manually checking tmux sessions. Need push notifications that are zero-config by default and trivially easy to opt into.\n\n## Existing Infrastructure\n\nACFS already has a comprehensive webhook library at `scripts/lib/webhook.sh` (340 lines) supporting Slack, Discord, and generic JSON webhooks. It's integrated into install.sh (success/failure notifications at lines 959 and 5585). The ACFS CLI dispatcher at `acfs/zsh/acfs.zshrc:249-444` is ready for a new `notifications` subcommand. A detailed bead bd-144la already exists with full specification.\n\n## Implementation Plan (following bd-144la spec)\n\n### 1. Create notification library (scripts/lib/notify.sh)\n\nCore function `acfs_notify()` that sends to ntfy.sh:\n```bash\nacfs_notify() {\n local title=\"$1\" body=\"${2:-}\" priority=\"${3:-default}\"\n local config_file=\"$HOME/.config/acfs/config.yaml\"\n\n # Check if notifications are enabled\n local enabled topic server\n enabled=$(grep 'enabled:' \"$config_file\" 2>/dev/null | awk '{print $2}')\n [[ \"$enabled\" != \"true\" ]] && return 0\n\n topic=$(grep 'topic:' \"$config_file\" | awk '{print $2}')\n server=$(grep 'server:' \"$config_file\" | awk '{print $2}' || echo \"https://ntfy.sh\")\n\n # Send via ntfy.sh HTTP API (non-blocking)\n curl -s -o /dev/null \\\n -H \"Title: $title\" \\\n -H \"Priority: $priority\" \\\n -d \"$body\" \\\n \"${server}/${topic}\" &\n disown\n}\n```\n\n### 2. Create notifications subcommand (scripts/lib/notifications.sh)\n\nImplement `acfs notifications` with subcommands:\n- `enable` -- Generate random topic, write config, show subscribe URL\n- `disable` -- Set enabled=false in config\n- `test` -- Send test notification\n- `status` -- Show current config and subscription URL\n- `topic` -- Print just the subscribe URL (for piping)\n- `set-server ` -- Switch to self-hosted ntfy instance\n\n### 3. Wire into ACFS CLI dispatcher (acfs/zsh/acfs.zshrc)\n\nAdd case at ~line 377:\n```bash\nnotifications|notify)\n shift\n source \"$ACFS_HOME/scripts/lib/notifications.sh\"\n acfs_notifications \"$@\"\n ;;\n```\n\n### 4. Create Claude Code Stop hook\n\nCreate `.claude/hooks/post-stop.sh` that calls acfs_notify on session end:\n```bash\n#!/usr/bin/env bash\nsource \"$HOME/.acfs/scripts/lib/notify.sh\" 2>/dev/null || exit 0\nacfs_notify \"Agent session ended\" \"Session completed in $(pwd)\" \"default\"\n```\n\n### 5. Config file format (~/.config/acfs/config.yaml)\n\n```yaml\nnotifications:\n enabled: false\n server: \"https://ntfy.sh\"\n topic: \"acfs-HOSTNAME-RANDOM8\"\n events:\n agent_session_end: true\n install_complete: true\n```\n\n## Files to Create\n\n- `scripts/lib/notify.sh` (notification send library)\n- `scripts/lib/notifications.sh` (subcommand handler)\n- Example Claude Code hook for agent session end\n\n## Files to Modify\n\n- `acfs/zsh/acfs.zshrc` lines 249-444 (add notifications case to dispatcher)\n- `acfs/zsh/acfs.zshrc` lines 406-432 (add to help text)\n- `install.sh` (add acfs_notify calls alongside existing webhook_notify calls)","acceptance_criteria":"1. `acfs notifications enable` generates topic, writes config, prints subscribe URL\n2. `acfs notifications test` sends visible push notification to subscribed device\n3. `acfs notifications disable` stops all notifications\n4. `acfs notifications status` shows current config (enabled/disabled, topic, server)\n5. Zero-config by default: fresh ACFS install has notifications disabled\n6. One-line setup: `acfs notifications enable` is all that's needed\n7. Cross-device: subscribe URL works in ntfy.sh app (iOS/Android) and web browser\n8. Claude Code Stop hook fires notification on session end when enabled\n9. Non-blocking: notification sending doesn't delay agent workflow\n10. Self-hosted support: `acfs notifications set-server https://my-ntfy.example.com` works\n11. Existing webhook system unaffected (parallel notification channels)","notes":"## Unit Tests (tests/unit/notifications.bats)\n\n1. **test_notify_disabled_noop**: Set enabled=false in config. Call acfs_notify. Assert no HTTP request made.\n2. **test_notify_enabled_sends**: Set enabled=true with mock server. Call acfs_notify. Assert curl called with correct topic and body.\n3. **test_notify_missing_config**: Remove config file. Call acfs_notify. Assert silent exit (no error).\n4. **test_enable_generates_topic**: Run `acfs notifications enable`. Assert config file created with random topic.\n5. **test_enable_idempotent**: Run `acfs notifications enable` twice. Assert topic doesn't change on second run.\n6. **test_disable_sets_flag**: Enable then disable. Assert config shows enabled=false.\n7. **test_status_output**: Enable notifications. Run `acfs notifications status`. Assert shows enabled, topic, subscribe URL.\n8. **test_test_sends_notification**: Enable with mock. Run `acfs notifications test`. Assert notification sent with test message.\n9. **test_set_server**: Run `acfs notifications set-server https://custom.ntfy.sh`. Assert config updated.\n10. **test_topic_format**: Enable. Assert topic matches pattern `acfs-HOSTNAME-[a-z0-9]{8}`.\n\n## E2E Test Script (scripts/e2e/test_notifications.sh)\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\necho \"=== ACFS Notifications E2E Test ===\"\nPASS=0; FAIL=0\n\n# Use a unique test topic to avoid interference\nTEST_TOPIC=\"acfs-test-$(date +%s)\"\n\n# Test 1: Enable notifications\nacfs notifications enable 2>&1 | grep -q \"Notifications enabled\" && ((PASS++)) || { echo \"FAIL: enable\"; ((FAIL++)); }\n\n# Test 2: Status shows enabled\nacfs notifications status 2>&1 | grep -q \"enabled: true\" && ((PASS++)) || { echo \"FAIL: status\"; ((FAIL++)); }\n\n# Test 3: Test notification (sends to ntfy.sh)\nacfs notifications test 2>&1 | grep -q \"Test notification sent\" && ((PASS++)) || { echo \"FAIL: test send\"; ((FAIL++)); }\n\n# Test 4: Disable\nacfs notifications disable 2>&1 | grep -q \"Notifications disabled\" && ((PASS++)) || { echo \"FAIL: disable\"; ((FAIL++)); }\n\n# Test 5: Status shows disabled\nacfs notifications status 2>&1 | grep -q \"enabled: false\" && ((PASS++)) || { echo \"FAIL: status after disable\"; ((FAIL++)); }\n\necho \"Results: $PASS passed, $FAIL failed\"\n[[ $FAIL -eq 0 ]] && echo \"ALL PASS\" || exit 1\n```","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-11T06:22:29.905627362Z","created_by":"ubuntu","updated_at":"2026-02-11T16:26:18.108143701Z","closed_at":"2026-02-11T16:26:18.108119366Z","close_reason":"implemented: notify.sh and notifications.sh created, wired into CLI and install.sh","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-2p56","title":"acfs changelog: Show recent changes command","description":"## Overview\nAdd `acfs changelog` command that shows recent changes, new tools, and breaking changes since last update.\n\n## Current Problem\n- Users don't know what changed after `acfs update`\n- No visibility into new features\n- Breaking changes can surprise users\n- Manual changelog reading is friction\n\n## Proposed Output\n```bash\n$ acfs changelog\nACFS Changelog (since your last update: 2026-01-20)\n\n## 2026-01-25\n- ✨ NEW: Added `caam` (Coding Agent Account Manager)\n- 🔧 FIX: Checksum verification for atuin installer\n- 📝 DOC: Improved SSH setup guide\n\n## 2026-01-22 \n- ⚠️ BREAKING: Renamed `acfs doctor --verbose` to `--json`\n- ✨ NEW: Added dark mode to web wizard\n```\n\n## Implementation Details\n1. Maintain CHANGELOG.md in repo root\n2. Parse changelog to extract entries\n3. Filter by date (since last update)\n4. Format for terminal output\n5. Support `--json` for machine parsing\n\n## Changelog Format (Keep-a-Changelog)\n```markdown\n## [2026-01-25]\n### Added\n- `caam` tool for account management\n\n### Fixed\n- Checksum verification for atuin\n\n### Changed\n- Doctor output format\n```\n\n## Commands\n```bash\nacfs changelog # Since last update\nacfs changelog --all # Full history\nacfs changelog --since 7d # Last 7 days\nacfs changelog --json # Machine-readable\n```\n\n## State Tracking\n- Store last_update_version in state.json\n- Compare against current version\n- Show \"You're up to date!\" if no new changes\n\n## Test Plan\n- [ ] Test changelog parsing\n- [ ] Test date filtering\n- [ ] Test --json output format\n- [ ] Test \"up to date\" message\n- [ ] Test with malformed changelog\n\n## Files to Create\n- scripts/commands/changelog.sh\n- CHANGELOG.md (if not exists)","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-25T23:02:27.680404290Z","created_by":"ubuntu","updated_at":"2026-01-27T02:21:58.330479944Z","closed_at":"2026-01-27T02:21:58.330459295Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2p56","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:04:35.745684631Z","created_by":"ubuntu"}]} {"id":"bd-2qgg","title":"Deep exploration: UBS (Ultimate Bug Scanner)","description":"## Goal\nPerform deep exploration of UBS (Ultimate Bug Scanner) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n#!/bin/bash\nLOG=/tmp/ubs-preflight.log\necho \"=== UBS Pre-flight Check: $(date) ===\" | tee $LOG\n\n[[ -d /dp/ultimate_bug_scanner ]] && echo \"PASS: Directory exists\" || { echo \"FAIL: Directory not found\"; exit 1; }\n[[ -f /dp/ultimate_bug_scanner/README.md ]] && echo \"PASS: README exists\" || echo \"WARN: No README\"\n\n# Check if ubs is installed\nif command -v ubs &>/dev/null; then\n echo \"PASS: ubs command available: $(which ubs)\" | tee -a $LOG\n ubs --version 2>&1 | tee -a $LOG\nelse\n echo \"WARN: ubs command not in PATH\" | tee -a $LOG\nfi\n```\n\n### 0.2 Content Snapshot\n```bash\nSNAPSHOT_DIR=/tmp/ubs-exploration-snapshots\nmkdir -p $SNAPSHOT_DIR\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\n```\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n```bash\ncat /dp/ultimate_bug_scanner/README.md\nls -la /dp/ultimate_bug_scanner/patterns/ 2>/dev/null # Check pattern files\nls -la /dp/ultimate_bug_scanner/rules/ 2>/dev/null\n```\n\n### 1.2 Code Investigation\n- Architecture: ast-grep patterns, how they're loaded\n- Language handlers: count actual supported languages\n- Detection categories: COUNT actual categories (verify 18 claim)\n- Output formats: JSON, JSONL, SARIF - verify each works\n- Pre-commit hook: how it integrates\n- Performance: measure actual scan time\n\n### 1.3 Verify Claims\n```bash\n#!/bin/bash\necho \"=== UBS Claim Verification ===\" | tee /tmp/ubs-claims.log\n\n# Count detection categories\nCATEGORY_COUNT=$(ls /dp/ultimate_bug_scanner/patterns/*.yaml 2>/dev/null | wc -l)\necho \"Detection categories found: $CATEGORY_COUNT (claimed: 18)\" | tee -a /tmp/ubs-claims.log\n\n# Count supported languages\nLANG_COUNT=$(grep -r \"language:\" /dp/ultimate_bug_scanner/patterns/ 2>/dev/null | sort -u | wc -l)\necho \"Languages found: $LANG_COUNT (claimed: 7+)\" | tee -a /tmp/ubs-claims.log\n\n# Test performance claim (sub-5-second)\nSTART=$(date +%s.%N)\nubs scan /data/projects/agentic_coding_flywheel_setup/apps/web/lib --json >/dev/null 2>&1\nEND=$(date +%s.%N)\nDURATION=$(echo \"$END - $START\" | bc)\necho \"Scan time: ${DURATION}s (claimed: <5s)\" | tee -a /tmp/ubs-claims.log\n```\n\n### 1.4 External Context\n```bash\n/xf search 'ubs OR \"ultimate bug scanner\" OR ast-grep' 2>&1 | head -50\ncass search 'ubs bug scanner' --robot --limit 10 2>&1\n```\n\n## Phase 2: Analysis (SYNTHESIZE)\n\nDocument with VERIFICATION:\n- [ ] Detection categories: ACTUAL count = ___ (vs 18 claimed)\n- [ ] Languages: ACTUAL list = ___ (vs 7+ claimed)\n- [ ] Performance: ACTUAL time = ___ (vs <5s claimed)\n- [ ] Output formats: JSON ✓/✗, JSONL ✓/✗, SARIF ✓/✗\n- [ ] Synergies VERIFIED:\n - [ ] bv: [actual integration?]\n - [ ] slb: [actual integration?]\n - [ ] dcg: [actual integration?]\n- [ ] Tech stack: ACTUAL language = ___\n\n## Phase 3: Revision (UPDATE)\n\n### 3.1 Update with VERIFIED claims only\n- If 18 categories cannot be verified, use actual count\n- If 7 languages cannot be verified, list actual languages\n- Only list synergies that are VERIFIED\n\n### 3.2 Cross-reference Validation\n```bash\nfor tool in bv slb dcg; do\n grep -A10 \"id: '$tool'\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts | grep -q \"ubs\" && echo \"PASS: $tool lists ubs\" || echo \"WARN: $tool missing ubs\"\ndone\n```\n\n## Phase 4: Testing\n\n### 4.1 Static Analysis\n```bash\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\nbun run type-check 2>&1 | tee /tmp/ubs-typecheck.log\nbun run lint 2>&1 | tee /tmp/ubs-lint.log\n```\n\n### 4.2 Build\n```bash\nbun run build 2>&1 | tee /tmp/ubs-build.log\n[[ $? -ne 0 ]] && cp /tmp/ubs-exploration-snapshots/*.before apps/web/lib/ && exit 1\n```\n\n### 4.3 E2E Visual\n```bash\nlsof -t -i :3000 | xargs kill 2>/dev/null; sleep 2\ncd /data/projects/agentic_coding_flywheel_setup/apps/web && bun run dev &\nsleep 10\ncurl -sL http://localhost:3000/flywheel | grep -qi 'ubs' && echo \"PASS\" || echo \"FAIL\"\ncurl -sL http://localhost:3000/tldr | grep -qi 'ubs' && echo \"PASS\" || echo \"FAIL\"\nkill %1\n```\n\n### 4.4 Unit Tests\n```bash\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\ngrep -q \"id: 'ubs'\" lib/flywheel.ts && echo \"PASS: ubs exists\" || echo \"FAIL\"\ndiff -q lib/flywheel.ts /tmp/ubs-exploration-snapshots/flywheel.ts.before && echo \"WARN: unchanged\" || echo \"PASS: changed\"\n```\n\n### 4.5 CLI Tests\n```bash\nubs --help 2>&1 | head -20 || echo \"FAIL: ubs --help failed\"\nubs scan --help 2>&1 | head -20 || echo \"FAIL: ubs scan --help failed\"\n```\n\n### 4.6 Results Log\n```bash\nRESULTS=/tmp/ubs-exploration-test-results.log\necho \"=== UBS RESULTS ===\" > $RESULTS\ncat /tmp/ubs-claims.log >> $RESULTS\ngrep -E \"PASS|FAIL|WARN\" /tmp/ubs-*.log >> $RESULTS\nFAIL_COUNT=$(grep -c \"FAIL\" $RESULTS); echo \"FAILURES: $FAIL_COUNT\" >> $RESULTS\ncat $RESULTS\n```\n\n## Phase 5: Commit\n\n```bash\n[[ $(grep -c \"FAIL\" /tmp/ubs-exploration-test-results.log) -gt 0 ]] && exit 1\n\ncd /data/projects/agentic_coding_flywheel_setup\ngit add apps/web/lib/flywheel.ts apps/web/lib/tldr-content.ts\ngit commit -m \"docs(flywheel): update UBS descriptions with VERIFIED claims\n\n- Verified detection category count: [actual]\n- Verified language support: [actual list]\n- Verified performance: [actual timing]\n- Synergies verified against implementations\n\nCo-Authored-By: Claude Opus 4.5 \"\n\nbr update bd-2qgg --status closed\nbr sync --flush-only && git add .beads/ && git push\n```\n\n## Acceptance Criteria\n- [ ] Pre-flight passed\n- [ ] All claims VERIFIED (not assumed)\n- [ ] Detection categories: verified count\n- [ ] Languages: verified list\n- [ ] Performance: verified timing\n- [ ] All tests PASS\n- [ ] Content de-slopified\n- [ ] Pushed\n","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:00:37.436446449Z","created_by":"ubuntu","updated_at":"2026-01-27T02:33:19.628115844Z","closed_at":"2026-01-27T02:33:19.628089544Z","close_reason":"Deep exploration complete: Updated UBS entries in flywheel.ts and tldr-content.ts with verified info. 8 languages (including Swift), 18 detection categories, 5 output formats, Shell tech stack (not Python). Added reciprocal ubs synergies to BR. Build passes.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-2vzi","title":"JFP: Update verify + update workflow","description":"Align verification and update commands with JFP CLI capabilities.\\n\\nScope:\\n- Update acfs.manifest.yaml verify commands for jfp (prefer jfp --version and jfp doctor, or jfp status if doctor not available).\\n- Update scripts/lib/update.sh to use 'jfp update' instead of git pull/build (if CLI supports it).\\n- Ensure doctor checks reflect new verify commands (via generator if needed).\\n\\nValidation:\\n- bun run generate:validate (packages/manifest) if convenient; otherwise note not run.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-21T09:45:51.908606424Z","created_by":"ubuntu","updated_at":"2026-01-21T09:53:55.464519013Z","closed_at":"2026-01-21T09:53:55.463750145Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} @@ -951,28 +956,57 @@ {"id":"bd-33vh.8","title":"End-to-end test: Fresh install + doctor confirms DCG, no git_safety_guard warnings","description":"## Task\n\nRun a complete end-to-end test to verify complete git_safety_guard removal with DETAILED LOGGING.\n\n## Test Script Location\n\nCreate: tests/e2e/test_git_safety_guard_removal.sh\n\n## Logging Requirements\n\nALL tests MUST:\n1. Log to timestamped file: /tmp/git_safety_guard_removal_$(date +%Y%m%d_%H%M%S).log\n2. Structured format: [TIMESTAMP] [LEVEL] [TEST_NAME] Message\n3. JSON results file\n\n## Test Environment\n\nUse Docker-based test harness: ./tests/vm/test_install_ubuntu.sh\n\n## Test Cases\n\n### Test Case 1: Fresh Install Verification\n```bash\n# After fresh install in Docker\nls -la ~/.acfs/claude/ # Should only show settings.json\nls -la ~/.acfs/claude/hooks/ 2>&1 # Should fail (directory doesn't exist)\nls -la ~/.claude/hooks/ 2>&1 # Should fail or be empty\n```\n\n### Test Case 2: Doctor Output Verification\n```bash\nacfs doctor 2>&1 | tee doctor_output.txt\n\n# Should NOT contain\n! grep -i 'git.safety.guard' doctor_output.txt\n\n# Should contain DCG check\ngrep -i 'DCG' doctor_output.txt\n```\n\n### Test Case 3: Install Log Verification\n- Should see: 'Installing DCG' or similar\n- Should NOT see: 'Git Safety Guard'\n\n### Test Case 4: Settings.json Verification\n```bash\ncat ~/.claude/settings.json 2>/dev/null\n# Should NOT contain 'git_safety_guard'\n```\n\n### Test Case 5: Codebase Audit\n```bash\n# No git_safety_guard in codebase\n! grep -ri 'git_safety_guard' --include='*.sh' --include='*.ts' --include='*.md' .\n```\n\n## Expected Results Table\n\n| Check | Expected |\n|-------|----------|\n| ~/.acfs/claude/hooks/ exists | NO |\n| ~/.claude/hooks/git_safety_guard.py exists | NO |\n| acfs doctor mentions 'Git safety guard' | NO |\n| acfs doctor mentions 'DCG' | YES |\n| install.sh log mentions 'Git Safety Guard' | NO |\n| install.sh log mentions 'DCG' | YES |\n\n## Acceptance Criteria\n\n1. Docker-based fresh install completes successfully\n2. No git_safety_guard warnings in doctor output\n3. DCG check present and passes\n4. Install logs use 'DCG' terminology\n5. No stale artifacts created\n6. Detailed timestamped log file generated\n7. JSON results file with pass/fail summary","status":"closed","priority":2,"issue_type":"task","estimated_minutes":45,"created_at":"2026-01-24T20:39:16.986042720Z","created_by":"ubuntu","updated_at":"2026-01-27T00:53:52.224631333Z","closed_at":"2026-01-27T00:53:52.224494725Z","close_reason":"Created E2E test for git_safety_guard removal with Docker integration","source_repo":".","compaction_level":0,"original_size":0,"labels":["e2e","qa","testing"],"dependencies":[{"issue_id":"bd-33vh.8","depends_on_id":"bd-33vh","type":"parent-child","created_at":"2026-01-24T20:39:16.986042720Z","created_by":"ubuntu"},{"issue_id":"bd-33vh.8","depends_on_id":"bd-33vh.1","type":"blocks","created_at":"2026-01-24T20:40:06.307562642Z","created_by":"ubuntu"},{"issue_id":"bd-33vh.8","depends_on_id":"bd-33vh.4","type":"blocks","created_at":"2026-01-24T20:40:09.087861241Z","created_by":"ubuntu"},{"issue_id":"bd-33vh.8","depends_on_id":"bd-33vh.6","type":"blocks","created_at":"2026-01-24T20:40:11.715678744Z","created_by":"ubuntu"}]} {"id":"bd-33vh.9","title":"Document migration path for existing users in CHANGELOG or release notes","description":"## Task Overview\n\nDocument the git_safety_guard → DCG migration for users who may be upgrading from older ACFS installations.\n\n## Target Audience\n\nUsers who installed ACFS before January 11, 2026 (commit f1fd501) and may:\n1. See 'Git safety guard' warnings in acfs doctor\n2. Have stale git_safety_guard.py files\n3. Be confused about the difference between the old and new systems\n\n## Documentation Locations\n\n### Option 1: Add to CHANGELOG.md (if exists)\n```markdown\n## [0.x.x] - 2026-01-xx\n\n### Changed\n- **BREAKING**: git_safety_guard replaced by DCG (Destructive Command Guard)\n\n### Migration\nIf you see 'Git safety guard' warnings after updating:\n1. Run `acfs update` to get the latest scripts\n2. Remove legacy files: `rm -f ~/.acfs/claude/hooks/git_safety_guard.py`\n3. Install DCG: `dcg install`\n4. Verify: `acfs doctor` should show DCG check, not git_safety_guard\n```\n\n### Option 2: Add migration section to README.md\n```markdown\n## Upgrading from Pre-January 2026 Installations\n\nIf you installed ACFS before January 11, 2026, you may have the old git_safety_guard system.\nThe replacement is DCG (Destructive Command Guard).\n\n### Symptoms of Old Installation\n- `acfs doctor` shows 'Git safety guard' warning\n- File exists: ~/.acfs/claude/hooks/git_safety_guard.py\n\n### Migration Steps\n1. Update ACFS: `acfs update --force`\n2. Install DCG: `dcg install`\n3. Verify: `acfs doctor` should show only DCG\n\nThe update process automatically cleans up legacy git_safety_guard files.\n```\n\n### Option 3: In-tool messaging (services-setup.sh)\nWhen user runs `acfs services-setup`, if legacy files detected:\n```\n⚠️ Legacy git_safety_guard detected\n This has been replaced by DCG (Destructive Command Guard).\n \n Cleaning up old files...\n ✓ Removed ~/.acfs/claude/hooks/git_safety_guard.py\n \n To configure DCG, select 'DCG' from the menu or run: dcg install\n```\n\n## Recommended Approach\n\nCombine Options 1 and 3:\n1. Document in CHANGELOG for release notes\n2. Add proactive cleanup messaging in services-setup.sh\n\n## Content Requirements\n\nDocumentation should explain:\n1. **What changed**: git_safety_guard.py removed, DCG introduced\n2. **Why**: Better performance (Rust vs Python), modular packs, dedicated maintenance\n3. **How to migrate**: Run acfs update, install dcg\n4. **How to verify**: acfs doctor shows DCG, no git_safety_guard warnings\n5. **Timeline**: When this changed (January 11, 2026)\n\n## Notes\n\n- Keep migration docs concise - most users won't need them\n- Focus on actionable steps, not history\n- Link to DCG section in README for full documentation","status":"closed","priority":3,"issue_type":"task","estimated_minutes":15,"created_at":"2026-01-24T20:39:38.244493576Z","created_by":"ubuntu","updated_at":"2026-01-27T00:33:17.644632289Z","closed_at":"2026-01-27T00:33:17.643021004Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["documentation","migration","release"],"dependencies":[{"issue_id":"bd-33vh.9","depends_on_id":"bd-33vh","type":"parent-child","created_at":"2026-01-24T20:39:38.244493576Z","created_by":"ubuntu"},{"issue_id":"bd-33vh.9","depends_on_id":"bd-33vh.4","type":"blocks","created_at":"2026-01-24T20:40:03.529936278Z","created_by":"ubuntu"}]} {"id":"bd-34mf","title":"Optimize plan/list/print/dry-run fast paths","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-21T19:00:29.646180400Z","created_by":"ubuntu","updated_at":"2026-01-21T19:21:27.575225697Z","closed_at":"2026-01-21T19:21:27.575181604Z","close_reason":"Fast path optimization already implemented: source_generated_installers is skipped when running --list-modules, --print-plan, --dry-run, or --print modes. This avoids unnecessary script sourcing and initialization for operations that only need to display information.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-34mf","depends_on_id":"bd-2z32","type":"discovered-from","created_at":"2026-01-21T19:00:29.676141538Z","created_by":"ubuntu"}]} +{"id":"bd-38l7j","title":"Regenerate manifest_index.sh to fix SHA256 drift","description":"scripts/generated/manifest_index.sh is out of sync with acfs.manifest.yaml. The manifest was modified in commits 44852195 (tru->toon rename) and 0a64aa63 (revert toon->tru) but manifest_index.sh was never regenerated.\n\nCurrent state:\n- manifest_index.sh line 9 has stale hash d7db51f0... (old)\n- manifest_index.sh line 10 has DUPLICATE ACFS_MANIFEST_SHA256 with hash 3f916ff3... (also stale)\n- Actual manifest SHA256 at HEAD is 3183c594... (or newer if working dir changes committed)\n- Bootstrap validation: grep ACFS_MANIFEST_SHA256 | head -n 1 picks stale value -> validation failure\n- Users pinning ACFS_REF get mismatched hashes between tag and HEAD\n\nREGENERATION PROCESS (well-documented, automated):\n cd packages/manifest && bun run generate\n This runs packages/manifest/src/generate.ts (1723 lines) which:\n - Reads acfs.manifest.yaml\n - Computes SHA256 via computeManifestSha256() (line 469)\n - Outputs to scripts/generated/ via generateManifestIndex() (line 855)\n - Deterministic output: same input always produces same output\n\nFIX:\n1. Run: cd packages/manifest && bun run generate\n2. Verify: scripts/generated/manifest_index.sh has exactly ONE ACFS_MANIFEST_SHA256 line\n3. Verify: SHA256 value matches sha256sum acfs.manifest.yaml\n4. Verify: bootstrap validation passes (test with curl | bash flow)\n\nPREVENTIVE MEASURES (add CI check to prevent future drift):\n5. Add to .github/workflows/installer.yml (or new manifest-check.yml):\n - name: Verify manifest_index.sh is up to date\n run: |\n cd packages/manifest && bun run generate --diff\n if \\! git diff --quiet scripts/generated/manifest_index.sh; then\n echo \"ERROR: manifest_index.sh is out of sync with acfs.manifest.yaml\"\n echo \"Run: cd packages/manifest && bun run generate\"\n exit 1\n fi\n\n6. Add duplicate-line check to scripts/hooks/pre-commit:\n DUPLICATE_SHA=$(grep -c \"^ACFS_MANIFEST_SHA256=\" scripts/generated/manifest_index.sh)\n if [[ \"$DUPLICATE_SHA\" -gt 1 ]]; then\n echo \"ERROR: manifest_index.sh has $DUPLICATE_SHA ACFS_MANIFEST_SHA256 lines (expected 1)\"\n exit 1\n fi\n\nTESTS:\n\nUnit Tests (tests/unit/test_manifest_sha256.sh):\n- test_single_sha256_line: grep -c ACFS_MANIFEST_SHA256 returns exactly 1\n- test_sha256_matches_file: SHA256 in manifest_index.sh matches sha256sum acfs.manifest.yaml\n- test_no_conflict_markers: no <<<< ==== >>>> markers in generated files\n- test_regeneration_idempotent: running bun run generate twice produces identical output\n\nIntegration Tests:\n- test_bootstrap_validation_passes: source manifest_index.sh then validate against actual file\n- test_pinned_ref_validation: simulate ACFS_REF=v0.6.0 install and verify SHA check\n\nE2E Tests (in CI canary or test_runner.sh):\n- test_curl_bash_install_validates_manifest: full curl | bash install validates manifest SHA256\n- test_offline_doctor_checks_manifest: acfs doctor verifies manifest integrity\n\nGitHub issue: #116","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-07T03:08:37.075159984Z","created_by":"ubuntu","updated_at":"2026-02-11T16:18:52.125074220Z","closed_at":"2026-02-11T16:18:52.125033213Z","close_reason":"no drift detected; automated systemd timer handles this every 2h","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":54,"issue_id":"bd-38l7j","author":"Dicklesworthstone","text":"ADDITIONAL TEST LOGGING REQUIREMENTS:\nAll test scripts should use structured output:\n\n log_check() {\n local test_name=$1 result=$2 detail=$3\n printf '[%s] %-40s %s %s\\n' \"$(date -Iseconds)\" \"$test_name\" \"$result\" \"$detail\"\n }\n \nExample output:\n [2026-02-07T12:00:00+00:00] test_single_sha256_line PASS count=1\n [2026-02-07T12:00:00+00:00] test_sha256_matches_file PASS expected=3183c594... actual=3183c594...\n [2026-02-07T12:00:00+00:00] test_no_conflict_markers PASS markers_found=0\n [2026-02-07T12:00:00+00:00] test_regeneration_idempotent PASS diff_lines=0\n\nEach test should log:\n - Expected vs actual values for SHA256 comparisons\n - File paths being checked\n - Count of ACFS_MANIFEST_SHA256 lines found\n - Diff output (if any) after regeneration\n - Bootstrap validation exit code and stderr\n\nFinal summary line: 'MANIFEST SHA256 TESTS: N/N passed, 0 failed'\n","created_at":"2026-02-07T21:11:14Z"}]} {"id":"bd-39ye","title":"NO_COLOR environment variable support","description":"## Overview\nRespect the NO_COLOR environment variable across all ACFS scripts per https://no-color.org/ standard.\n\n## Current Problem\n- Scripts use colors unconditionally\n- Users with accessibility needs can't disable colors\n- Piped output includes ANSI codes\n- Some terminals render colors poorly\n\n## NO_COLOR Standard\n- If NO_COLOR env var is set (any value), disable colors\n- Also disable for non-TTY output (pipes, redirects)\n- Simple, widely adopted convention\n\n## Implementation Details\n1. Create color helper functions in logging.sh\n2. Check NO_COLOR and TTY status once at startup\n3. All color output goes through these helpers\n\n## Color Helper Functions\n```bash\n# scripts/lib/colors.sh\n_init_colors() {\n if [[ -n \"${NO_COLOR:-}\" ]] || [[ ! -t 1 ]]; then\n RED='' GREEN='' YELLOW='' BLUE='' RESET=''\n else\n RED='\\033[0;31m' GREEN='\\033[0;32m'\n YELLOW='\\033[0;33m' BLUE='\\033[0;34m' RESET='\\033[0m'\n fi\n}\n\ncolor_print() {\n local color=\"$1\" msg=\"$2\"\n printf '%b%s%b\\n' \"${!color}\" \"$msg\" \"$RESET\"\n}\n```\n\n## Files to Audit\n- scripts/lib/logging.sh (main color usage)\n- scripts/lib/tui.sh (interactive elements)\n- scripts/install.sh (progress output)\n- All scripts that use printf with ANSI codes\n\n## Test Plan\n- [ ] Test NO_COLOR=1 disables all colors\n- [ ] Test piped output has no ANSI codes\n- [ ] Test colors work normally when NO_COLOR unset\n- [ ] Grep for raw ANSI codes to ensure none slip through\n\n## Files to Modify\n- scripts/lib/logging.sh (add color helpers)\n- All files using hardcoded ANSI codes","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-25T23:01:49.345301724Z","created_by":"ubuntu","updated_at":"2026-01-27T04:01:25.739562837Z","closed_at":"2026-01-27T04:01:25.739539473Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-39ye","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:04:01.194662910Z","created_by":"ubuntu"}]} {"id":"bd-3aa6","title":"Prevent gcloud 'bv' from ever shadowing beads_viewer","description":"Summary\n- gcloud SDK installs a `bv` binary in `/home/ubuntu/google-cloud-sdk/bin`.\n- The SDK’s `path.zsh.inc` is sourced in `~/.zshrc.local`, so PATH can include gcloud’s `bv`.\n- User requirement: **gcloud’s `bv` must never, under any circumstances, intercept/be invoked instead of beads_viewer `bv`.**\n\nImpact\n- High risk of running the wrong `bv` in interactive shells, non-interactive shells, CI, or cron jobs.\n- Mis-executed `bv` can break bead workflows and cause confusion during incident response.\n\nEvidence (current environment)\n- `which -a bv` shows multiple `bv` binaries including gcloud’s:\n - `/home/ubuntu/google-cloud-sdk/bin/bv`\n - `/home/ubuntu/.local/bin/bv`\n - `/home/ubuntu/.bun/bin/bv`\n - `/home/ubuntu/go/bin/bv`\n- PATH is mutated by gcloud SDK via `path.zsh.inc` (sourced in `~/.zshrc.local`).\n\nRoot Cause\n- gcloud SDK ships a `bv` command (BigQuery-related) that collides with beads_viewer’s `bv`.\n- PATH ordering is not explicitly pinned to prefer user `bv` binaries.\n\nProposed Remediation (must enforce precedence)\n1) Prepend user bins ahead of gcloud in shell init:\n - `~/bin`, `~/.local/bin`, `~/.bun/bin`, `~/go/bin` must come before the SDK.\n2) Add an explicit `bv` shim at `~/bin/bv` that delegates to the preferred beads_viewer binary.\n3) Add a hard alias in shell init to force `bv` -> `~/.local/bin/bv` (or preferred).\n4) Add a health check command (or script) that fails if `command -v bv` resolves to gcloud.\n\nAcceptance Criteria\n- `command -v bv` resolves to user beads_viewer (not gcloud) in:\n - interactive shells\n - non-interactive shells (`zsh -lc`, cron)\n- `which -a bv` shows gcloud’s `bv` only after user paths.\n- Running `bv --version` matches beads_viewer’s version, not gcloud’s.\n\nNotes\n- The collision warning appears after `gcloud components update`.\n- This is a safety/operations issue, not a GA4 issue.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-25T00:42:06.371685930Z","created_by":"ubuntu","updated_at":"2026-01-26T23:22:24.883957326Z","closed_at":"2026-01-26T23:22:24.883935966Z","close_reason":"Implemented bv() protection function in acfs.zshrc that bypasses gcloud bv, added ~/bin to PATH, and enhanced doctor.sh to detect gcloud shadowing. Shellcheck passes.","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-3co7k","title":"World-Class UI/UX Polish","description":"# World-Class UI/UX Polish Initiative\n\n## Overview\nElevate the ACFS web application from B+ (78/100) to A+ (95/100) grade UI/UX quality, matching Stripe-level polish and sophistication.\n\n## Background & Context\nThe web app (apps/web/) is a Next.js 16 application using:\n- Tailwind CSS for styling\n- Framer Motion for animations \n- shadcn/ui component patterns\n- OKLCH color space for perceptually uniform colors\n\n**Current State (After Initial Session):**\n- ✅ Touch targets meet Apple HIG (44px minimum)\n- ✅ Glassmorphism refined with hover states (blur 16px → 20px on hover)\n- ✅ Card shadows use layered elevation (Stripe-style)\n- ✅ Reduced motion support (useReducedMotion throughout)\n- ✅ EmptyState component created with staggered animations\n- ✅ 4-column grids on widescreen (xl:grid-cols-4)\n- ✅ CodeBlock with line hover highlighting\n- ✅ Gradient button variants added\n- ✅ Z-index normalized to standard scale\n\n**Remaining Work:**\n1. Design System Foundation (typography, animation variants)\n2. Core Components (BottomSheet, FormField, Alerts)\n3. Page Enhancements (scroll reveals, empty states, swipe)\n4. Mobile Experience (bottom sheets, navigation)\n\n## Goals\n1. **Desktop Excellence**: Micro-interactions that feel delightful\n2. **Mobile Excellence**: Native-feeling gestures and thumb-friendly layouts\n3. **Visual Sophistication**: Depth, layering, attention to detail\n4. **Performance**: Smooth 60fps animations, no jank\n5. **Accessibility**: WCAG AA compliance maintained\n\n## Success Criteria\n- Every interaction feels intentional and crafted\n- Mobile feels like a native app, not responsive website\n- Loading states are elegant, not just functional\n- Empty states are delightful, not disappointing\n\n## Key Files\n- apps/web/components/ui/ - Core UI components\n- apps/web/components/motion/ - Animation system\n- apps/web/lib/design-tokens.ts - Design tokens\n- apps/web/app/globals.css - Global styles\n- apps/web/lib/hooks/useScrollReveal.ts - Scroll animations\n\n## Reference Standards\n- Stripe Dashboard, Linear App, Vercel Dashboard\n- Apple Human Interface Guidelines","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-03T19:53:00.455976257Z","created_by":"ubuntu","updated_at":"2026-02-04T04:55:08.197078236Z","closed_at":"2026-02-04T04:55:08.197057658Z","close_reason":"All sub-tasks complete","source_repo":".","compaction_level":0,"original_size":0,"labels":["epic","polish","ui","ux"],"comments":[{"id":53,"issue_id":"bd-3co7k","author":"Dicklesworthstone","text":"## Testing Strategy Overview\n\n### Unit Testing Approach\n- Each new component gets `__tests__/.test.tsx`\n- Tests verify: render, props handling, state transitions, ARIA attributes\n- Use Jest + React Testing Library\n- Mock framer-motion for unit tests when needed\n\n### E2E Testing Approach\n- Each feature gets `e2e/.spec.ts`\n- Tests include detailed `console.log('[E2E]...')` logging\n- Test reduced motion with `page.emulateMedia({ reducedMotion: 'reduce' })`\n- Test mobile with `page.setViewportSize({ width: 375, height: 812 })`\n- Test touch gestures with `page.mouse` swipe simulation\n\n### Test File Locations\n```\napps/web/\n├── components/\n│ ├── ui/__tests__/\n│ │ ├── bottom-sheet.test.tsx\n│ │ ├── form-field.test.tsx\n│ │ └── typography.test.tsx\n│ └── motion/__tests__/\n│ └── variants.test.ts\n├── lib/hooks/__tests__/\n│ ├── useScrollReveal.test.ts\n│ └── useSwipeScroll.test.ts\n└── e2e/\n ├── typography.spec.ts\n ├── animations.spec.ts\n ├── bottom-sheet.spec.ts\n ├── form-field.spec.ts\n ├── alert-card.spec.ts\n ├── button-loading.spec.ts\n ├── scroll-reveal.spec.ts\n ├── empty-states.spec.ts\n ├── swipe-scroll.spec.ts\n ├── jargon-mobile.spec.ts\n └── mobile-navigation.spec.ts\n```\n\n### Verification Commands\n```bash\n# Run unit tests\npnpm --filter @acfs/web test\n\n# Run E2E tests\npnpm --filter @acfs/web e2e\n\n# Run specific E2E test with logging\npnpm --filter @acfs/web e2e -- --grep \"BottomSheet\"\n```\n","created_at":"2026-02-03T20:11:54Z"}]} +{"id":"bd-3co7k.1","title":"Design System Foundation","description":"# Design System Foundation\n\n## Purpose\nEstablish the foundational design system improvements that other UI work depends on.\nThese changes affect multiple components and pages, so they must be done first.\n\n## Why This Matters\n- Typography scale affects all text rendering site-wide\n- Animation variants are imported by every animated component\n- Getting these right first prevents inconsistencies later\n\n## Scope\n1. Typography scale enhancement (add 6xl tier)\n2. Animation variants expansion in motion module\n\n## Files Affected\n- apps/web/app/globals.css (typography variables)\n- apps/web/components/motion/index.tsx (animation presets)\n- apps/web/lib/design-tokens.ts (token definitions)\n\n## Dependencies\nNone - this is foundational work.\n\n## Acceptance Criteria\n- Typography scale has 6xl tier with proper tracking/leading\n- Motion module exports additional easing variants\n- All changes documented in code comments","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-03T19:53:09.969044715Z","created_by":"ubuntu","updated_at":"2026-02-03T20:20:58.006509187Z","closed_at":"2026-02-03T20:20:58.006489790Z","close_reason":"Both child tasks completed: Typography Scale (1.1) and Animation Variants (1.2)","source_repo":".","compaction_level":0,"original_size":0,"labels":["design-system","foundation"],"dependencies":[{"issue_id":"bd-3co7k.1","depends_on_id":"bd-3co7k","type":"parent-child","created_at":"2026-02-03T19:53:09.969044715Z","created_by":"ubuntu"}]} +{"id":"bd-3co7k.1.1","title":"Typography Scale Enhancement","description":"# Typography Scale Enhancement\n\n## Problem Statement\nCurrent typography scale tops out at 3xl (clamp to 3rem). For hero sections and \nlarge displays on widescreen, we need a 6xl tier that can scale up to 6rem while\nmaintaining proper letter-spacing and line-height.\n\n## Background\nStripe and Linear use dramatic typography contrast in hero sections. Our current\nscale doesn't allow for this level of visual impact on large screens.\n\n**Current Scale (from globals.css):**\n```css\n--text-3xl: clamp(1.875rem, 1.5rem + 1.5vw, 3rem);\n```\n\n**Desired Addition:**\n```css\n--text-6xl: clamp(3.5rem, 3rem + 3vw, 6rem);\n--tracking-6xl: -0.04em;\n--leading-6xl: 1;\n```\n\n## Implementation Details\n\n### Step 1: Add CSS Variables (globals.css)\nAdd to the fluid typography section (~lines 120-128):\n```css\n/* Extra large display text for hero sections */\n--text-5xl: clamp(2.5rem, 2rem + 2.5vw, 4.5rem);\n--text-6xl: clamp(3.5rem, 3rem + 3vw, 6rem);\n```\n\n### Step 2: Add Letter-Spacing Variables\nLarge text needs tighter tracking for visual balance:\n```css\n--tracking-5xl: -0.03em;\n--tracking-6xl: -0.04em;\n```\n\n### Step 3: Add Line-Height Variables\nDisplay text needs tighter leading:\n```css\n--leading-5xl: 1.1;\n--leading-6xl: 1;\n```\n\n### Step 4: Create Utility Classes\nAdd to Tailwind config or globals.css:\n```css\n.text-display-5xl {\n font-size: var(--text-5xl);\n letter-spacing: var(--tracking-5xl);\n line-height: var(--leading-5xl);\n}\n\n.text-display-6xl {\n font-size: var(--text-6xl);\n letter-spacing: var(--tracking-6xl);\n line-height: var(--leading-6xl);\n}\n```\n\n### Step 5: Update design-tokens.ts\nExport these values for use in JS if needed:\n```typescript\nexport const typography = {\n display: {\n '5xl': 'clamp(2.5rem, 2rem + 2.5vw, 4.5rem)',\n '6xl': 'clamp(3.5rem, 3rem + 3vw, 6rem)',\n },\n tracking: {\n '5xl': '-0.03em',\n '6xl': '-0.04em',\n },\n};\n```\n\n## Testing\n- View hero sections at 1920px, 2560px, and 3840px widths\n- Verify text scales smoothly without jumps\n- Check that tracking looks balanced at all sizes\n\n## Files to Modify\n- apps/web/app/globals.css (lines ~120-140)\n- apps/web/lib/design-tokens.ts (typography section)\n\n## Acceptance Criteria\n- [ ] 5xl and 6xl font size variables defined\n- [ ] Corresponding tracking variables defined\n- [ ] Corresponding leading variables defined\n- [ ] Utility classes created\n- [ ] Design tokens updated\n- [ ] No visual regressions on existing pages","status":"closed","priority":2,"issue_type":"task","assignee":"ubuntu","estimated_minutes":45,"created_at":"2026-02-03T19:53:29.124313366Z","created_by":"ubuntu","updated_at":"2026-02-03T20:19:29.223502664Z","closed_at":"2026-02-03T20:19:29.223484781Z","close_reason":"Added text-6xl, leading-6xl, tracking-6xl CSS variables and text-display-5xl/6xl utility classes in globals.css. Added displayTypography tokens to design-tokens.ts.","source_repo":".","compaction_level":0,"original_size":0,"labels":["css","design-tokens","typography"],"dependencies":[{"issue_id":"bd-3co7k.1.1","depends_on_id":"bd-3co7k.1","type":"parent-child","created_at":"2026-02-03T19:53:29.124313366Z","created_by":"ubuntu"}],"comments":[{"id":42,"issue_id":"bd-3co7k.1.1","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/ui/__tests__/typography.test.tsx`\n\n```typescript\nimport { render } from '@testing-library/react';\n\ndescribe('Typography CSS Variables', () => {\n test('5xl/6xl font sizes scale correctly at breakpoints', async () => {\n // Test that clamp() values work across viewport sizes\n });\n \n test('tracking values are negative for large text', () => {\n // Verify --tracking-5xl and --tracking-6xl are negative\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/typography.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Typography Scale', () => {\n test('hero text scales at breakpoints', async ({ page }) => {\n console.log('[E2E] Testing typography at 2560px');\n await page.setViewportSize({ width: 2560, height: 1440 });\n await page.goto('/');\n // Verify hero uses new 5xl/6xl classes\n });\n});\n```\n","created_at":"2026-02-03T20:04:11Z"}]} +{"id":"bd-3co7k.1.2","title":"Animation Variants Expansion","description":"# Animation Variants Expansion\n\n## Problem Statement\nThe current motion module (apps/web/components/motion/index.tsx) has good spring \npresets but lacks:\n1. Scroll-reveal specific variants\n2. Stagger container variants for different use cases\n3. Entrance animations for modals/sheets\n\n## Background\nStripe uses subtle, purposeful animations that feel expensive. Our current set\ncovers basics but we need more nuanced options for specific UI patterns.\n\n**Current Exports:**\n- springs: smooth, snappy, gentle, quick\n- easings: out, in, inOut\n- fadeUp, fadeScale, slideLeft, slideRight\n- staggerContainer, staggerFast, staggerSlow\n- buttonMotion, cardMotion, listItemMotion\n\n**Needed Additions:**\n- Modal/sheet entrance variants\n- Scroll reveal with blur effect\n- Scale-up entrance for badges/pills\n- Stagger variants with different delays\n\n## Implementation Details\n\n### Step 1: Add Modal Entrance Variants\n```typescript\n/** Modal entrance - scale and fade from center */\nexport const modalEntrance: Variants = {\n hidden: {\n opacity: 0,\n scale: 0.95,\n y: 10,\n },\n visible: {\n opacity: 1,\n scale: 1,\n y: 0,\n transition: springs.smooth,\n },\n exit: {\n opacity: 0,\n scale: 0.98,\n y: 5,\n transition: { duration: 0.15 },\n },\n};\n\n/** Bottom sheet entrance - slide from bottom */\nexport const sheetEntrance: Variants = {\n hidden: {\n y: \"100%\",\n opacity: 0.8,\n },\n visible: {\n y: 0,\n opacity: 1,\n transition: {\n type: \"spring\",\n stiffness: 300,\n damping: 30,\n },\n },\n exit: {\n y: \"100%\",\n opacity: 0.8,\n transition: { duration: 0.2 },\n },\n};\n```\n\n### Step 2: Add Scroll Reveal Variants\n```typescript\n/** Fade up with blur - premium reveal effect */\nexport const fadeUpBlur: Variants = {\n hidden: {\n opacity: 0,\n y: 30,\n filter: \"blur(10px)\",\n },\n visible: {\n opacity: 1,\n y: 0,\n filter: \"blur(0px)\",\n transition: springs.smooth,\n },\n};\n\n/** Scale up for badges/pills */\nexport const scaleUp: Variants = {\n hidden: {\n opacity: 0,\n scale: 0.8,\n },\n visible: {\n opacity: 1,\n scale: 1,\n transition: springs.snappy,\n },\n};\n```\n\n### Step 3: Add Micro-Stagger Variants\n```typescript\n/** Micro stagger for pill/tag lists */\nexport const staggerMicro: Variants = {\n hidden: {},\n visible: {\n transition: {\n staggerChildren: 0.02,\n delayChildren: 0,\n },\n },\n};\n\n/** Cascade stagger for dashboard cards */\nexport const staggerCascade: Variants = {\n hidden: {},\n visible: {\n transition: {\n staggerChildren: 0.08,\n delayChildren: 0.15,\n staggerDirection: 1,\n },\n },\n};\n```\n\n### Step 4: Add Presence Animation Helpers\n```typescript\n/** Get animation props that respect reduced motion */\nexport function getPresenceProps(\n variants: Variants,\n prefersReducedMotion: boolean\n): MotionProps {\n if (prefersReducedMotion) {\n return {\n initial: false,\n animate: \"visible\",\n };\n }\n return {\n initial: \"hidden\",\n animate: \"visible\",\n exit: \"exit\",\n variants,\n };\n}\n```\n\n## Testing\n- Test all new variants with reduced motion on/off\n- Verify no duplicate keyframe definitions\n- Check bundle size impact (should be minimal)\n\n## Files to Modify\n- apps/web/components/motion/index.tsx\n\n## Acceptance Criteria\n- [ ] Modal entrance variants (modalEntrance, sheetEntrance)\n- [ ] Scroll reveal variants (fadeUpBlur, scaleUp)\n- [ ] Stagger variants (staggerMicro, staggerCascade)\n- [ ] Helper function (getPresenceProps)\n- [ ] All variants respect reduced motion\n- [ ] TypeScript types exported","status":"closed","priority":1,"issue_type":"task","assignee":"ubuntu","estimated_minutes":60,"created_at":"2026-02-03T19:53:48.668949503Z","created_by":"ubuntu","updated_at":"2026-02-03T20:17:29.577011647Z","closed_at":"2026-02-03T20:17:29.576990727Z","close_reason":"Implemented modalEntrance, sheetEntrance, fadeUpBlur, scaleUp, staggerMicro, staggerCascade variants and getPresenceProps helper in motion/index.tsx","source_repo":".","compaction_level":0,"original_size":0,"labels":["animation","design-tokens","framer-motion"],"dependencies":[{"issue_id":"bd-3co7k.1.2","depends_on_id":"bd-3co7k.1","type":"parent-child","created_at":"2026-02-03T19:53:48.668949503Z","created_by":"ubuntu"}],"comments":[{"id":43,"issue_id":"bd-3co7k.1.2","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/motion/__tests__/variants.test.ts`\n\n```typescript\nimport { modalEntrance, sheetEntrance, fadeUpBlur, getPresenceProps } from '../index';\n\ndescribe('Animation Variants', () => {\n describe('modalEntrance', () => {\n test('hidden state has opacity 0 and scale 0.95', () => {\n expect(modalEntrance.hidden).toMatchObject({ opacity: 0, scale: 0.95 });\n });\n \n test('visible state restores opacity and scale', () => {\n expect(modalEntrance.visible).toMatchObject({ opacity: 1, scale: 1 });\n });\n });\n\n describe('getPresenceProps', () => {\n test('returns immediate animation when reduced motion preferred', () => {\n const props = getPresenceProps(modalEntrance, true);\n expect(props.initial).toBe(false);\n });\n \n test('returns full animation when reduced motion not preferred', () => {\n const props = getPresenceProps(modalEntrance, false);\n expect(props.initial).toBe('hidden');\n });\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/animations.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Animation Variants', () => {\n test('modal animations respect reduced motion', async ({ page }) => {\n console.log('[E2E] Testing with reduced motion emulation');\n await page.emulateMedia({ reducedMotion: 'reduce' });\n await page.goto('/glossary');\n // Trigger a modal and verify no animation\n });\n});\n```\n","created_at":"2026-02-03T20:04:20Z"}]} +{"id":"bd-3co7k.2","title":"Core Components Enhancement","description":"# Core Components Enhancement\n\n## Purpose\nCreate and enhance core UI components that will be used across multiple pages.\nThese components embody Stripe-level polish and serve as building blocks.\n\n## Why This Matters\n- BottomSheet enables native-feeling mobile modals\n- FormField with animations creates premium form experiences\n- Dismissible alerts with progress feel intentional, not bolted-on\n- Enhanced loading states make waiting feel shorter\n\n## Scope\n1. BottomSheet component for mobile modals\n2. FormField with animated floating labels\n3. Dismissible Alert with auto-dismiss progress\n4. Enhanced button loading states\n\n## Dependencies\n- Depends on: Design System Foundation (bd-3co7k.1)\n- Animation variants needed for smooth entrances\n\n## Files to Create/Modify\n- apps/web/components/ui/bottom-sheet.tsx (NEW)\n- apps/web/components/ui/form-field.tsx (NEW)\n- apps/web/components/alert-card.tsx (MODIFY)\n- apps/web/components/ui/button.tsx (MODIFY)\n\n## Acceptance Criteria\n- Components follow existing patterns in components/ui/\n- All components respect reduced motion preference\n- Touch targets meet Apple HIG (44px minimum)\n- Focus states visible for keyboard navigation\n- TypeScript types exported for consumers","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-03T19:53:58.973436183Z","created_by":"ubuntu","updated_at":"2026-02-04T04:41:48.184837256Z","closed_at":"2026-02-04T04:41:48.184818420Z","close_reason":"Core components (BottomSheet, FormField, Alert, Button) complete","source_repo":".","compaction_level":0,"original_size":0,"labels":["components","ui"],"dependencies":[{"issue_id":"bd-3co7k.2","depends_on_id":"bd-3co7k","type":"parent-child","created_at":"2026-02-03T19:53:58.973436183Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.2","depends_on_id":"bd-3co7k.1","type":"blocks","created_at":"2026-02-03T19:54:14.883349379Z","created_by":"ubuntu"}]} +{"id":"bd-3co7k.2.1","title":"BottomSheet Component","description":"# BottomSheet Component\n\n## Problem Statement\nMobile modals that use traditional center-screen dialogs feel \"web-like\" rather \nthan native. iOS and Android users expect bottom sheets for contextual actions\nand detail views. Our current jargon.tsx uses a custom bottom sheet pattern that\nshould be extracted into a reusable component.\n\n## Background\n**Why Bottom Sheets Matter:**\n- Thumb-reachable on large phones\n- Natural gesture interaction (swipe down to dismiss)\n- Maintains context (partial view of underlying content)\n- Feels native on both iOS and Android\n\n**Current Pattern (jargon.tsx lines 298-331):**\n```tsx\n\n```\n\n## Implementation Details\n\n### Step 1: Create Component File\nLocation: apps/web/components/ui/bottom-sheet.tsx\n\n### Step 2: Component Interface\n```typescript\ninterface BottomSheetProps {\n /** Whether the sheet is open */\n open: boolean;\n /** Callback when sheet should close */\n onClose: () => void;\n /** Title for accessibility (aria-label) */\n title: string;\n /** Content to render inside the sheet */\n children: React.ReactNode;\n /** Maximum height (default: 80vh) */\n maxHeight?: string;\n /** Whether to show the drag handle */\n showHandle?: boolean;\n /** Whether to close on backdrop click (default: true) */\n closeOnBackdrop?: boolean;\n /** Whether to enable swipe-to-close (default: true) */\n swipeable?: boolean;\n /** Additional className for the sheet container */\n className?: string;\n}\n```\n\n### Step 3: Core Implementation\n```typescript\n\"use client\";\n\nimport { useEffect, useCallback } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { motion, AnimatePresence, useDragControls } from \"framer-motion\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { sheetEntrance, springs } from \"@/components/motion\";\nimport { useReducedMotion } from \"@/lib/hooks/useReducedMotion\";\n\nexport function BottomSheet({\n open,\n onClose,\n title,\n children,\n maxHeight = \"80vh\",\n showHandle = true,\n closeOnBackdrop = true,\n swipeable = true,\n className,\n}: BottomSheetProps) {\n const prefersReducedMotion = useReducedMotion();\n const reducedMotion = prefersReducedMotion ?? false;\n const dragControls = useDragControls();\n\n // Escape key handling\n useEffect(() => {\n if (!open) return;\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handleEscape);\n return () => document.removeEventListener(\"keydown\", handleEscape);\n }, [open, onClose]);\n\n // Lock body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = \"\";\n };\n }\n }, [open]);\n\n // Swipe to close handler\n const handleDragEnd = useCallback(\n (_: unknown, info: { velocity: { y: number }; offset: { y: number } }) => {\n if (info.velocity.y > 500 || info.offset.y > 200) {\n onClose();\n }\n },\n [onClose]\n );\n\n if (typeof window === \"undefined\") return null;\n\n return createPortal(\n \n {open && (\n <>\n {/* Backdrop */}\n \n\n {/* Sheet */}\n \n {/* Drag handle */}\n {showHandle && (\n dragControls.start(e)}\n >\n
\n
\n )}\n\n {/* Close button */}\n \n \n \n\n {/* Content - scrollable with safe area padding */}\n \n {children}\n \n \n \n )}\n
,\n document.body\n );\n}\n```\n\n### Step 4: Export from ui/index.ts (if exists)\n\n### Step 5: Usage Example\n```tsx\nimport { BottomSheet } from \"@/components/ui/bottom-sheet\";\n\nfunction Example() {\n const [open, setOpen] = useState(false);\n \n return (\n setOpen(false)}\n title=\"Settings\"\n >\n

Settings

\n {/* Content */}\n \n );\n}\n```\n\n## Testing\n- Test on iOS Safari (swipe behavior, safe areas)\n- Test on Android Chrome (swipe behavior)\n- Test with VoiceOver/TalkBack\n- Test escape key dismissal\n- Test backdrop click dismissal\n- Test with reduced motion enabled\n- Verify body scroll lock works\n- Test nested scrollable content\n\n## Files to Create\n- apps/web/components/ui/bottom-sheet.tsx\n\n## Acceptance Criteria\n- [ ] Component renders via portal\n- [ ] Swipe-to-close gesture works (drag Y > 200px or velocity > 500)\n- [ ] Escape key dismisses\n- [ ] Backdrop click dismisses (configurable)\n- [ ] Body scroll locked when open\n- [ ] Safe area padding applied (pb-safe)\n- [ ] Reduced motion fallback (opacity instead of slide)\n- [ ] Close button meets 44px touch target\n- [ ] Drag handle is grabbable\n- [ ] ARIA attributes correct (role=dialog, aria-modal, aria-label)\n- [ ] Works on iOS Safari and Android Chrome","status":"closed","priority":1,"issue_type":"task","estimated_minutes":90,"created_at":"2026-02-03T19:54:49.371133469Z","created_by":"ubuntu","updated_at":"2026-02-04T04:41:40.883735525Z","closed_at":"2026-02-04T04:41:40.883709336Z","close_reason":"BottomSheet component already meets acceptance","source_repo":".","compaction_level":0,"original_size":0,"labels":["component","mobile","sheet"],"dependencies":[{"issue_id":"bd-3co7k.2.1","depends_on_id":"bd-3co7k.1.2","type":"blocks","created_at":"2026-02-03T20:10:29.474913004Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.2.1","depends_on_id":"bd-3co7k.2","type":"parent-child","created_at":"2026-02-03T19:54:49.371133469Z","created_by":"ubuntu"}],"comments":[{"id":44,"issue_id":"bd-3co7k.2.1","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/ui/__tests__/bottom-sheet.test.tsx`\n\n```typescript\nimport { render, screen, fireEvent, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { BottomSheet } from '../bottom-sheet';\n\ndescribe('BottomSheet', () => {\n const mockOnClose = jest.fn();\n \n beforeEach(() => {\n mockOnClose.mockClear();\n });\n\n test('renders content when open', () => {\n render(\n \n

Sheet content

\n
\n );\n expect(screen.getByText('Sheet content')).toBeInTheDocument();\n });\n\n test('does not render when closed', () => {\n render(\n \n

Sheet content

\n
\n );\n expect(screen.queryByText('Sheet content')).not.toBeInTheDocument();\n });\n\n test('calls onClose when backdrop clicked', async () => {\n render(\n \n

Content

\n
\n );\n const backdrop = screen.getByRole('presentation', { hidden: true });\n fireEvent.click(backdrop);\n expect(mockOnClose).toHaveBeenCalledTimes(1);\n });\n\n test('calls onClose on Escape key', async () => {\n render(\n \n

Content

\n
\n );\n fireEvent.keyDown(document, { key: 'Escape' });\n expect(mockOnClose).toHaveBeenCalledTimes(1);\n });\n\n test('close button meets 44px touch target', () => {\n render(\n \n

Content

\n
\n );\n const closeButton = screen.getByRole('button', { name: /close/i });\n const styles = getComputedStyle(closeButton);\n expect(parseInt(styles.minHeight) || parseInt(styles.height)).toBeGreaterThanOrEqual(44);\n });\n\n test('has correct ARIA attributes', () => {\n render(\n \n

Content

\n
\n );\n const dialog = screen.getByRole('dialog');\n expect(dialog).toHaveAttribute('aria-modal', 'true');\n expect(dialog).toHaveAttribute('aria-label', 'Test Sheet');\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/bottom-sheet.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('BottomSheet Component', () => {\n test.beforeEach(async ({ page }) => {\n await page.setViewportSize({ width: 375, height: 812 }); // iPhone X\n console.log('[E2E] Set mobile viewport for bottom sheet tests');\n });\n\n test('opens from bottom with animation', async ({ page }) => {\n await page.goto('/glossary');\n console.log('[E2E] Navigated to glossary');\n \n // Click a jargon term to open bottom sheet\n await page.getByText('API').first().click();\n console.log('[E2E] Clicked jargon term');\n \n // Verify sheet appears\n const sheet = page.getByRole('dialog');\n await expect(sheet).toBeVisible();\n console.log('[E2E] Bottom sheet visible');\n });\n\n test('swipe down closes sheet', async ({ page }) => {\n await page.goto('/glossary');\n await page.getByText('API').first().click();\n \n const sheet = page.getByRole('dialog');\n await expect(sheet).toBeVisible();\n \n // Simulate swipe down\n const box = await sheet.boundingBox();\n if (box) {\n await page.mouse.move(box.x + box.width / 2, box.y + 50);\n await page.mouse.down();\n await page.mouse.move(box.x + box.width / 2, box.y + 300, { steps: 10 });\n await page.mouse.up();\n console.log('[E2E] Performed swipe gesture');\n }\n \n await expect(sheet).not.toBeVisible({ timeout: 1000 });\n console.log('[E2E] Sheet dismissed via swipe');\n });\n\n test('escape key closes sheet', async ({ page }) => {\n await page.goto('/glossary');\n await page.getByText('API').first().click();\n \n await expect(page.getByRole('dialog')).toBeVisible();\n await page.keyboard.press('Escape');\n console.log('[E2E] Pressed Escape');\n \n await expect(page.getByRole('dialog')).not.toBeVisible();\n console.log('[E2E] Sheet dismissed via Escape');\n });\n});\n```\n","created_at":"2026-02-03T20:04:34Z"}]} +{"id":"bd-3co7k.2.2","title":"FormField with Animated Labels","description":"# FormField with Animated Labels\n\n## Problem Statement\nForm inputs in the app use standard labels positioned above inputs. For a premium\nfeel, we should support Material Design-style floating labels that animate from\nplaceholder position to label position when focused or filled.\n\n## Background\n**Why Animated Labels Matter:**\n- Saves vertical space (label shares space with placeholder)\n- Provides clear visual feedback on focus\n- Feels modern and polished\n- Common pattern users recognize from native apps\n\n**Current State:**\n- Basic input styling in globals.css\n- No animated label component exists\n- Checkbox component has aria-invalid support\n\n## Implementation Details\n\n### Step 1: Create Component File\nLocation: apps/web/components/ui/form-field.tsx\n\n### Step 2: Component Interface\n```typescript\ninterface FormFieldProps {\n /** Input name for form submission */\n name: string;\n /** Label text (becomes floating label) */\n label: string;\n /** Input type (text, email, password, etc.) */\n type?: \"text\" | \"email\" | \"password\" | \"url\" | \"tel\" | \"number\";\n /** Current value (controlled) */\n value: string;\n /** Change handler */\n onChange: (value: string) => void;\n /** Blur handler */\n onBlur?: () => void;\n /** Error message (shows error state when truthy) */\n error?: string;\n /** Helper text (shown below input when no error) */\n helperText?: string;\n /** Whether field is required */\n required?: boolean;\n /** Whether field is disabled */\n disabled?: boolean;\n /** Placeholder (optional, label acts as placeholder when empty) */\n placeholder?: string;\n /** Character count limit (shows counter when set) */\n maxLength?: number;\n /** Additional className */\n className?: string;\n /** Input ref for focus management */\n inputRef?: React.Ref;\n}\n```\n\n### Step 3: Core Implementation\n```typescript\n\"use client\";\n\nimport { useState, useId } from \"react\";\nimport { motion, AnimatePresence } from \"@/components/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/lib/hooks/useReducedMotion\";\n\nexport function FormField({\n name,\n label,\n type = \"text\",\n value,\n onChange,\n onBlur,\n error,\n helperText,\n required,\n disabled,\n placeholder,\n maxLength,\n className,\n inputRef,\n}: FormFieldProps) {\n const id = useId();\n const [isFocused, setIsFocused] = useState(false);\n const prefersReducedMotion = useReducedMotion();\n const reducedMotion = prefersReducedMotion ?? false;\n\n const hasValue = value.length > 0;\n const isFloating = isFocused || hasValue;\n const showError = !!error;\n\n const handleFocus = () => setIsFocused(true);\n const handleBlur = () => {\n setIsFocused(false);\n onBlur?.();\n };\n\n return (\n
\n {/* Input container with floating label */}\n \n {/* Floating label */}\n \n {label}\n {required && *}\n \n\n {/* Input */}\n onChange(e.target.value)}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled}\n required={required}\n maxLength={maxLength}\n placeholder={isFloating ? placeholder : undefined}\n aria-invalid={showError}\n aria-describedby={error ? `${id}-error` : helperText ? `${id}-helper` : undefined}\n className={cn(\n \"w-full bg-transparent px-4 pt-6 pb-2 text-base\",\n \"outline-none placeholder:text-muted-foreground/50\",\n \"rounded-xl\", // Match container border radius\n disabled && \"cursor-not-allowed\"\n )}\n />\n
\n\n {/* Helper/Error text and character counter */}\n
\n \n {showError ? (\n \n {error}\n \n ) : helperText ? (\n \n {helperText}\n \n ) : (\n \n )}\n \n\n {maxLength && (\n = maxLength ? \"text-destructive\" : \"text-muted-foreground\"\n )}\n >\n {value.length}/{maxLength}\n \n )}\n
\n \n );\n}\n```\n\n### Step 4: Create Textarea Variant (Optional)\nSimilar to FormField but for textarea elements with auto-resize.\n\n## Testing\n- Test focus/blur states\n- Test with value vs empty\n- Test error state transitions\n- Test character counter at limit\n- Test with disabled state\n- Test reduced motion (no transform animation)\n- Test with screen reader (VoiceOver)\n- Test keyboard navigation (Tab focus)\n\n## Files to Create\n- apps/web/components/ui/form-field.tsx\n\n## Acceptance Criteria\n- [ ] Label floats up when focused or has value\n- [ ] Smooth 150ms transition (respects reduced motion)\n- [ ] Error state shows red border and error message\n- [ ] Helper text shows when no error\n- [ ] Character counter shows when maxLength set\n- [ ] Counter turns red at limit\n- [ ] ARIA attributes correct (aria-invalid, aria-describedby)\n- [ ] Keyboard focusable\n- [ ] Required indicator shows asterisk\n- [ ] Disabled state has reduced opacity and cursor","status":"closed","priority":2,"issue_type":"task","estimated_minutes":75,"created_at":"2026-02-03T19:55:19.777223232Z","created_by":"ubuntu","updated_at":"2026-02-04T04:32:59.790178156Z","closed_at":"2026-02-04T04:32:59.790140665Z","close_reason":"Implemented FormField component","source_repo":".","compaction_level":0,"original_size":0,"labels":["animation","component","forms"],"dependencies":[{"issue_id":"bd-3co7k.2.2","depends_on_id":"bd-3co7k.2","type":"parent-child","created_at":"2026-02-03T19:55:19.777223232Z","created_by":"ubuntu"}],"comments":[{"id":45,"issue_id":"bd-3co7k.2.2","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/ui/__tests__/form-field.test.tsx`\n\n```typescript\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { FormField } from '../form-field';\n\ndescribe('FormField', () => {\n const defaultProps = {\n name: 'email',\n label: 'Email',\n value: '',\n onChange: jest.fn(),\n };\n\n test('renders with label', () => {\n render();\n expect(screen.getByLabelText('Email')).toBeInTheDocument();\n });\n\n test('label floats when focused', async () => {\n render();\n const input = screen.getByLabelText('Email');\n await userEvent.click(input);\n // Label should have different styling when floating\n const label = screen.getByText('Email');\n expect(label).toHaveClass('text-xs'); // or check computed style\n });\n\n test('label floats when has value', () => {\n render();\n const label = screen.getByText('Email');\n expect(label).toHaveClass('text-xs');\n });\n\n test('shows error state with message', () => {\n render();\n expect(screen.getByRole('alert')).toHaveTextContent('Invalid email');\n });\n\n test('shows character counter when maxLength set', () => {\n render();\n expect(screen.getByText('5/100')).toBeInTheDocument();\n });\n\n test('counter turns red at limit', () => {\n render();\n const counter = screen.getByText('5/5');\n expect(counter).toHaveClass('text-destructive');\n });\n\n test('has correct aria-invalid when error', () => {\n render();\n expect(screen.getByLabelText('Email')).toHaveAttribute('aria-invalid', 'true');\n });\n\n test('required indicator shows asterisk', () => {\n render();\n expect(screen.getByText('*')).toBeInTheDocument();\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/form-field.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('FormField Component', () => {\n test('animated label floats on focus', async ({ page }) => {\n await page.goto('/wizard/os-selection'); // Page with form fields\n console.log('[E2E] Navigated to wizard');\n \n const input = page.locator('input[type=\"text\"]').first();\n const label = input.locator('xpath=preceding-sibling::label');\n \n // Get initial position\n const initialPos = await label.boundingBox();\n console.log('[E2E] Initial label position:', initialPos?.y);\n \n // Focus input\n await input.focus();\n await page.waitForTimeout(200); // Wait for animation\n \n // Get floated position\n const floatedPos = await label.boundingBox();\n console.log('[E2E] Floated label position:', floatedPos?.y);\n \n // Label should have moved up\n expect(floatedPos!.y).toBeLessThan(initialPos!.y);\n });\n});\n```\n","created_at":"2026-02-03T20:04:48Z"}]} +{"id":"bd-3co7k.2.3","title":"Dismissible Alert with Progress","description":"# Dismissible Alert with Progress\n\n## Problem Statement\nCurrent AlertCard component (apps/web/components/alert-card.tsx) shows static \nalerts without:\n1. Dismiss button (manual close)\n2. Auto-dismiss with countdown progress\n3. Animated exit when dismissed\n\nStripe and Linear use dismissible alerts with progress indicators that feel \nintentional rather than intrusive.\n\n## Background\n**Current AlertCard Features:**\n- Multiple variants (info, success, warning, error)\n- Collapsible details section\n- Good visual design with gradients\n\n**Missing Features:**\n- No dismiss button\n- No auto-dismiss timer\n- No exit animation\n- No stacking/queue behavior\n\n## Implementation Details\n\n### Step 1: Extend AlertCard Interface\nAdd to existing AlertCard in apps/web/components/alert-card.tsx:\n\n```typescript\ninterface AlertCardProps {\n // ... existing props ...\n \n /** Whether the alert can be dismissed */\n dismissible?: boolean;\n /** Callback when dismissed */\n onDismiss?: () => void;\n /** Auto-dismiss after this many milliseconds (0 = no auto-dismiss) */\n autoDismissMs?: number;\n /** Whether to show countdown progress bar when auto-dismissing */\n showProgress?: boolean;\n}\n```\n\n### Step 2: Add Dismiss Button\n```tsx\n{dismissible && (\n \n \n \n)}\n```\n\n### Step 3: Add Auto-Dismiss Logic\n```tsx\nuseEffect(() => {\n if (!autoDismissMs || autoDismissMs <= 0) return;\n \n const timeout = setTimeout(() => {\n onDismiss?.();\n }, autoDismissMs);\n \n return () => clearTimeout(timeout);\n}, [autoDismissMs, onDismiss]);\n```\n\n### Step 4: Add Progress Bar\n```tsx\n{showProgress && autoDismissMs > 0 && (\n \n)}\n```\n\n### Step 5: Wrap with AnimatePresence for Exit\nConsumer wraps with AnimatePresence:\n```tsx\n\n {showAlert && (\n \n )}\n\n```\n\nOr integrate motion props into AlertCard:\n```tsx\nexport function AlertCard({\n // ... props\n}: AlertCardProps) {\n const prefersReducedMotion = useReducedMotion();\n \n return (\n \n );\n}\n```\n\n### Step 6: Add Pause-on-Hover (Optional Enhancement)\n```tsx\nconst [isPaused, setIsPaused] = useState(false);\n\n// Pause countdown on hover\n setIsPaused(true)}\n onMouseLeave={() => setIsPaused(false)}\n>\n {/* Progress bar uses isPaused to stop animation */}\n\n```\n\n## Testing\n- Test dismiss button click\n- Test auto-dismiss countdown\n- Test progress bar animation (smooth, linear)\n- Test pause on hover (if implemented)\n- Test exit animation\n- Test with reduced motion\n- Test keyboard accessibility (Escape to dismiss?)\n- Test screen reader announcement\n\n## Files to Modify\n- apps/web/components/alert-card.tsx\n\n## Acceptance Criteria\n- [ ] Dismiss button shown when dismissible=true\n- [ ] Dismiss button meets touch target (min 32px, recommend 44px)\n- [ ] onDismiss callback fires on dismiss\n- [ ] Auto-dismiss works with autoDismissMs prop\n- [ ] Progress bar shows countdown (when showProgress=true)\n- [ ] Progress bar is linear, not eased\n- [ ] Exit animation smooth (respects reduced motion)\n- [ ] Pause on hover (nice-to-have)\n- [ ] ARIA label on dismiss button","status":"closed","priority":2,"issue_type":"task","estimated_minutes":60,"created_at":"2026-02-03T19:55:43.252390805Z","created_by":"ubuntu","updated_at":"2026-02-04T04:33:02.857823904Z","closed_at":"2026-02-04T04:33:02.857800500Z","close_reason":"Added dismissible alerts with progress","source_repo":".","compaction_level":0,"original_size":0,"labels":["alerts","animation","component"],"dependencies":[{"issue_id":"bd-3co7k.2.3","depends_on_id":"bd-3co7k.2","type":"parent-child","created_at":"2026-02-03T19:55:43.252390805Z","created_by":"ubuntu"}],"comments":[{"id":46,"issue_id":"bd-3co7k.2.3","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/__tests__/alert-card.test.tsx`\n\n```typescript\nimport { render, screen, fireEvent, act } from '@testing-library/react';\nimport { AlertCard } from '../alert-card';\n\ndescribe('AlertCard Dismissible Features', () => {\n const defaultProps = {\n title: 'Test Alert',\n message: 'This is a test',\n };\n\n jest.useFakeTimers();\n\n test('shows dismiss button when dismissible', () => {\n render( {}} />);\n expect(screen.getByRole('button', { name: /dismiss/i })).toBeInTheDocument();\n });\n\n test('calls onDismiss when button clicked', () => {\n const onDismiss = jest.fn();\n render();\n fireEvent.click(screen.getByRole('button', { name: /dismiss/i }));\n expect(onDismiss).toHaveBeenCalledTimes(1);\n });\n\n test('auto-dismisses after specified time', () => {\n const onDismiss = jest.fn();\n render();\n \n act(() => {\n jest.advanceTimersByTime(4999);\n });\n expect(onDismiss).not.toHaveBeenCalled();\n \n act(() => {\n jest.advanceTimersByTime(1);\n });\n expect(onDismiss).toHaveBeenCalledTimes(1);\n });\n\n test('progress bar animates during countdown', () => {\n render();\n const progressBar = screen.getByTestId('progress-bar');\n expect(progressBar).toBeInTheDocument();\n });\n\n test('dismiss button meets touch target size', () => {\n render( {}} />);\n const button = screen.getByRole('button', { name: /dismiss/i });\n const styles = getComputedStyle(button);\n expect(parseInt(styles.minWidth) || parseInt(styles.width)).toBeGreaterThanOrEqual(32);\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/alert-card.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('AlertCard Dismissible', () => {\n test('dismiss button closes alert', async ({ page }) => {\n // Navigate to a page with dismissible alerts\n await page.goto('/wizard/os-selection');\n console.log('[E2E] Looking for dismissible alert');\n \n const alert = page.locator('[role=\"alert\"]').first();\n if (await alert.isVisible()) {\n const dismissBtn = alert.getByRole('button', { name: /dismiss/i });\n await dismissBtn.click();\n console.log('[E2E] Clicked dismiss button');\n await expect(alert).not.toBeVisible({ timeout: 1000 });\n }\n });\n\n test('auto-dismiss countdown shows progress', async ({ page }) => {\n // This test requires triggering an auto-dismissing alert\n await page.goto('/');\n console.log('[E2E] Checking for progress bar on auto-dismiss alerts');\n // Implementation depends on where auto-dismiss alerts appear\n });\n});\n```\n","created_at":"2026-02-03T20:04:59Z"}]} +{"id":"bd-3co7k.2.4","title":"Enhanced Button Loading States","description":"# Enhanced Button Loading States\n\n## Problem Statement\nCurrent button loading state (apps/web/components/ui/button.tsx) shows a simple\nspinning Loader2 icon. For world-class polish, the loading state should:\n1. Have a shimmer/pulse effect on the button itself\n2. Optionally show progress percentage\n3. Animate between states smoothly\n\n## Background\n**Current Implementation (lines 99-114):**\n```tsx\nconst LoadingSpinner = () => (\n \n);\n\nif (loading) {\n return (\n <>\n \n {loadingText && {loadingText}}\n \n );\n}\n```\n\n**Issues:**\n- No visual shimmer on button background\n- Spinner is basic rotate animation\n- No progress indicator option\n- No smooth transition between loading/not-loading\n\n## Implementation Details\n\n### Step 1: Add Loading Progress Prop\n```typescript\ninterface ButtonProps {\n // ... existing props ...\n \n /** Progress percentage (0-100) when loading - shows determinate progress */\n loadingProgress?: number;\n}\n```\n\n### Step 2: Enhance Button Background During Loading\nAdd a shimmer overlay when loading:\n```tsx\n{loading && (\n \n {/* Shimmer effect */}\n
\n \n)}\n```\n\n### Step 3: Enhance Spinner Animation\nReplace basic spin with a more refined animation:\n```tsx\nconst LoadingSpinner = ({ className }: { className?: string }) => (\n \n \n \n);\n```\n\nOr use a custom spinner SVG with gradient:\n```tsx\nconst LoadingSpinner = () => (\n \n \n \n \n);\n```\n\n### Step 4: Add Progress Bar Option\nWhen loadingProgress is provided:\n```tsx\n{loading && typeof loadingProgress === \"number\" && (\n
\n \n
\n)}\n```\n\n### Step 5: Smooth State Transition\nWrap content with AnimatePresence for smooth swap:\n```tsx\n\n {loading ? (\n \n \n {loadingText && {loadingText}}\n \n ) : (\n \n {children}\n \n )}\n\n```\n\n### Step 6: Add Pulse Effect to Disabled Loading Button\n```tsx\nclassName={cn(\n buttonVariants({ variant, size }),\n loading && \"animate-pulse opacity-90\",\n className\n)}\n```\n\n## Testing\n- Test loading state transition (smooth, no jump)\n- Test with loadingText\n- Test with loadingProgress (0-100)\n- Test shimmer effect visibility\n- Test with reduced motion (no shimmer, simple fade)\n- Test all button variants in loading state\n- Test disabled + loading combination\n\n## Files to Modify\n- apps/web/components/ui/button.tsx\n\n## Acceptance Criteria\n- [ ] Loading state has subtle shimmer effect on background\n- [ ] Transition between loading/not-loading is animated (fade + scale)\n- [ ] loadingProgress prop shows determinate progress bar\n- [ ] Progress bar animates smoothly\n- [ ] Spinner rotation is smooth (not janky)\n- [ ] Reduced motion: no shimmer, simple opacity transition\n- [ ] All button variants look good in loading state\n- [ ] Button remains same size during loading (no layout shift)","status":"closed","priority":2,"issue_type":"task","estimated_minutes":45,"created_at":"2026-02-03T19:56:06.111850561Z","created_by":"ubuntu","updated_at":"2026-02-04T04:33:07.733457511Z","closed_at":"2026-02-04T04:33:07.733428787Z","close_reason":"Enhanced button loading states","source_repo":".","compaction_level":0,"original_size":0,"labels":["animation","button","component","loading"],"dependencies":[{"issue_id":"bd-3co7k.2.4","depends_on_id":"bd-3co7k.2","type":"parent-child","created_at":"2026-02-03T19:56:06.111850561Z","created_by":"ubuntu"}],"comments":[{"id":47,"issue_id":"bd-3co7k.2.4","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/components/ui/__tests__/button-loading.test.tsx`\n\n```typescript\nimport { render, screen } from '@testing-library/react';\nimport { Button } from '../button';\n\ndescribe('Button Loading States', () => {\n test('shows spinner when loading', () => {\n render();\n expect(screen.getByRole('button')).toContainElement(screen.getByTestId('loading-spinner'));\n });\n\n test('shows loadingText when provided', () => {\n render();\n expect(screen.getByText('Saving...')).toBeInTheDocument();\n });\n\n test('maintains button size during loading (no layout shift)', () => {\n const { rerender, container } = render();\n const initialWidth = container.firstChild?.clientWidth;\n \n rerender();\n const loadingWidth = container.firstChild?.clientWidth;\n \n expect(loadingWidth).toBe(initialWidth);\n });\n\n test('shows progress bar when loadingProgress provided', () => {\n render();\n const progressBar = screen.getByTestId('progress-bar');\n expect(progressBar).toHaveStyle({ width: '50%' });\n });\n\n test('button is disabled during loading', () => {\n render();\n expect(screen.getByRole('button')).toBeDisabled();\n });\n\n test('shimmer effect present during loading', () => {\n render();\n const button = screen.getByRole('button');\n // Check for shimmer class or animation\n expect(button.querySelector('[class*=\"shimmer\"]')).toBeInTheDocument();\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/button-loading.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Button Loading States', () => {\n test('loading transition is smooth', async ({ page }) => {\n await page.goto('/wizard/result');\n console.log('[E2E] Navigated to wizard result page');\n \n // Find a button that triggers loading\n const button = page.getByRole('button', { name: /download/i });\n const initialBox = await button.boundingBox();\n console.log('[E2E] Initial button dimensions:', initialBox);\n \n await button.click();\n await page.waitForTimeout(100); // Wait for loading state\n \n const loadingBox = await button.boundingBox();\n console.log('[E2E] Loading button dimensions:', loadingBox);\n \n // Size should be the same (no layout shift)\n expect(loadingBox?.width).toBeCloseTo(initialBox!.width, 0);\n });\n\n test('loading respects reduced motion', async ({ page }) => {\n await page.emulateMedia({ reducedMotion: 'reduce' });\n await page.goto('/wizard/result');\n console.log('[E2E] Testing with reduced motion');\n \n // Loading should still work but without shimmer animation\n const button = page.getByRole('button', { name: /download/i });\n await button.click();\n \n // Verify button shows loading state\n await expect(button).toBeDisabled();\n });\n});\n```\n","created_at":"2026-02-03T20:05:11Z"}]} +{"id":"bd-3co7k.3","title":"Page-Level Enhancements","description":"# Page-Level Enhancements\n\n## Purpose\nApply the enhanced components and design system improvements to actual pages.\nThis is where the polish becomes visible to users.\n\n## Why This Matters\n- Components alone don't create great UX - their application does\n- Scroll-triggered reveals create dynamic, engaging pages\n- Consistent empty states across pages feel professional\n- Swipe gestures make content browsing feel native\n\n## Scope\n1. Scroll reveal animations on landing page sections\n2. Empty states for glossary and learn pages\n3. Swipe gestures for carousels/horizontal scrolling\n\n## Dependencies\n- Depends on: Core Components Enhancement (bd-3co7k.2)\n- Needs EmptyState component (already done)\n- Needs animation variants (from Design System)\n\n## Pages to Enhance\n- apps/web/app/page.tsx (landing page)\n- apps/web/app/glossary/page.tsx\n- apps/web/app/learn/glossary/page.tsx\n- apps/web/app/learn/page.tsx\n\n## Acceptance Criteria\n- Landing page sections animate on scroll\n- All empty search states use EmptyState component\n- Horizontal scrollable areas support swipe gestures\n- No jank or performance issues on scroll","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-03T19:56:17.764302018Z","created_by":"ubuntu","updated_at":"2026-02-04T04:50:12.025628574Z","closed_at":"2026-02-04T04:50:12.025610150Z","close_reason":"Page-level enhancements complete","source_repo":".","compaction_level":0,"original_size":0,"labels":["enhancements","pages"],"dependencies":[{"issue_id":"bd-3co7k.3","depends_on_id":"bd-3co7k","type":"parent-child","created_at":"2026-02-03T19:56:17.764302018Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.3","depends_on_id":"bd-3co7k.2","type":"blocks","created_at":"2026-02-03T19:56:23.839030770Z","created_by":"ubuntu"}]} +{"id":"bd-3co7k.3.1","title":"Landing Page Scroll Reveal Animations","description":"# Landing Page Scroll Reveal Animations\n\n## Problem Statement\nThe landing page (apps/web/app/page.tsx) has good static design but sections \nappear instantly rather than revealing as the user scrolls. Scroll-triggered\nreveals create a dynamic, engaging experience that feels premium.\n\n## Background\n**Current State:**\n- useScrollReveal hook exists but is underutilized\n- Some sections use basic fade-in on mount\n- No staggered reveals within sections\n\n**Reference:**\n- Stripe.com: Sections fade and slide up as they enter viewport\n- Linear.app: Features cascade in with staggered timing\n- Vercel.com: Smooth parallax effects on scroll\n\n## Implementation Details\n\n### Step 1: Identify Sections to Animate\nReview page.tsx and identify major sections:\n1. Hero section (~line 100-200)\n2. Features grid (~line 300-400)\n3. Tool showcase section\n4. Testimonials/social proof\n5. CTA section\n6. Footer\n\n### Step 2: Apply useScrollReveal to Each Section\n```tsx\nimport { useScrollReveal, staggerDelay } from \"@/lib/hooks/useScrollReveal\";\n\nfunction FeaturesSection() {\n const { ref, isInView } = useScrollReveal({ threshold: 0.1 });\n\n return (\n
\n \n

Features

\n \n \n
\n {features.map((feature, i) => (\n \n \n \n ))}\n
\n
\n );\n}\n```\n\n### Step 3: Use staggerContainer for Grid Items\n```tsx\n\n {features.map((feature) => (\n \n \n \n ))}\n\n```\n\n### Step 4: Add Blur Effect for Premium Reveals\nUsing the new fadeUpBlur variant:\n```tsx\n\n```\n\n### Step 5: Different Effects for Different Sections\n- Hero: Immediate (no scroll trigger, already visible)\n- Features: Fade up with stagger\n- Tool showcase: Slide in from sides alternating\n- Testimonials: Scale up with blur\n- CTA: Fade up (simple)\n\n### Step 6: Performance Optimization\n- Use CSS will-change sparingly\n- Ensure animations use transform/opacity only (GPU accelerated)\n- Don't animate too many elements simultaneously\n- Use triggerOnce: true to avoid re-triggering\n\n### Step 7: Reduced Motion Support\nThe useScrollReveal hook already handles this:\n```typescript\nconst shouldShowImmediately =\n disabled || prefersReducedMotion || typeof IntersectionObserver === \"undefined\";\n\nreturn {\n isInView: shouldShowImmediately || isInViewState,\n};\n```\n\n## Testing\n- Test scroll down through all sections\n- Test quick scroll (animations should complete, not stack)\n- Test slow scroll (animations trigger at right time)\n- Test with reduced motion (all content visible immediately)\n- Test on mobile (touch scroll)\n- Test performance (no jank, maintain 60fps)\n- Test in Safari (IntersectionObserver support)\n\n## Files to Modify\n- apps/web/app/page.tsx\n\n## Acceptance Criteria\n- [ ] Each major section has scroll-triggered reveal\n- [ ] Grid items stagger their entrance (0.1s between items)\n- [ ] At least one section uses fadeUpBlur for premium feel\n- [ ] Animations only trigger once (don't replay on scroll up)\n- [ ] Reduced motion users see all content immediately\n- [ ] No layout shift as content animates in\n- [ ] Performance stays at 60fps during scroll\n- [ ] Works on iOS Safari and Android Chrome","status":"closed","priority":1,"issue_type":"task","estimated_minutes":90,"created_at":"2026-02-03T19:56:49.076279621Z","created_by":"ubuntu","updated_at":"2026-02-04T04:50:01.528146972Z","closed_at":"2026-02-04T04:50:01.528124409Z","close_reason":"Landing page sections already use scroll reveals","source_repo":".","compaction_level":0,"original_size":0,"labels":["animation","landing-page","scroll"],"dependencies":[{"issue_id":"bd-3co7k.3.1","depends_on_id":"bd-3co7k.1.2","type":"blocks","created_at":"2026-02-03T20:10:30.513348480Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.3.1","depends_on_id":"bd-3co7k.3","type":"parent-child","created_at":"2026-02-03T19:56:49.076279621Z","created_by":"ubuntu"}],"comments":[{"id":48,"issue_id":"bd-3co7k.3.1","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/lib/hooks/__tests__/useScrollReveal.test.ts`\n\n```typescript\nimport { renderHook } from '@testing-library/react';\nimport { useScrollReveal } from '../useScrollReveal';\n\n// Mock IntersectionObserver\nconst mockIntersectionObserver = jest.fn();\nmockIntersectionObserver.mockReturnValue({\n observe: () => null,\n unobserve: () => null,\n disconnect: () => null,\n});\nwindow.IntersectionObserver = mockIntersectionObserver;\n\ndescribe('useScrollReveal', () => {\n test('returns ref and isInView state', () => {\n const { result } = renderHook(() => useScrollReveal());\n expect(result.current.ref).toBeDefined();\n expect(typeof result.current.isInView).toBe('boolean');\n });\n\n test('creates IntersectionObserver with correct options', () => {\n renderHook(() => useScrollReveal({ threshold: 0.2, rootMargin: '-50px' }));\n expect(mockIntersectionObserver).toHaveBeenCalledWith(\n expect.any(Function),\n expect.objectContaining({ threshold: 0.2, rootMargin: '-50px' })\n );\n });\n\n test('returns true immediately when reduced motion preferred', () => {\n // Mock matchMedia for reduced motion\n window.matchMedia = jest.fn().mockImplementation((query) => ({\n matches: query === '(prefers-reduced-motion: reduce)',\n addEventListener: jest.fn(),\n removeEventListener: jest.fn(),\n }));\n\n const { result } = renderHook(() => useScrollReveal());\n expect(result.current.isInView).toBe(true);\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/scroll-reveal.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Scroll Reveal Animations', () => {\n test('sections animate as they enter viewport', async ({ page }) => {\n await page.goto('/');\n console.log('[E2E] Loaded landing page');\n \n // Scroll to features section\n const featuresSection = page.locator('section').filter({ hasText: 'Features' });\n \n // Check initial state (should be hidden/transparent)\n const initialOpacity = await featuresSection.evaluate(el => \n getComputedStyle(el).opacity\n );\n console.log('[E2E] Initial opacity:', initialOpacity);\n \n // Scroll into view\n await featuresSection.scrollIntoViewIfNeeded();\n await page.waitForTimeout(500); // Wait for animation\n \n // Check animated state\n const animatedOpacity = await featuresSection.evaluate(el => \n getComputedStyle(el).opacity\n );\n console.log('[E2E] Animated opacity:', animatedOpacity);\n \n expect(parseFloat(animatedOpacity)).toBeGreaterThan(parseFloat(initialOpacity));\n });\n\n test('grid items stagger their entrance', async ({ page }) => {\n await page.goto('/');\n \n // Get all feature cards\n const cards = page.locator('[data-testid=\"feature-card\"]');\n const count = await cards.count();\n console.log('[E2E] Found', count, 'feature cards');\n \n // Scroll to feature section\n await cards.first().scrollIntoViewIfNeeded();\n \n // Check cards animate in sequence (staggered)\n for (let i = 0; i < Math.min(count, 3); i++) {\n await page.waitForTimeout(100 * i);\n const opacity = await cards.nth(i).evaluate(el => \n getComputedStyle(el).opacity\n );\n console.log(\\`[E2E] Card \\${i} opacity after \\${100 * i}ms: \\${opacity}\\`);\n }\n });\n\n test('animations skip with reduced motion', async ({ page }) => {\n await page.emulateMedia({ reducedMotion: 'reduce' });\n await page.goto('/');\n console.log('[E2E] Testing with reduced motion');\n \n // All content should be immediately visible\n const section = page.locator('section').first();\n const opacity = await section.evaluate(el => \n getComputedStyle(el).opacity\n );\n expect(opacity).toBe('1');\n });\n});\n```\n","created_at":"2026-02-03T20:05:24Z"}]} +{"id":"bd-3co7k.3.2","title":"Empty States for Glossary and Learn Pages","description":"# Empty States for Glossary and Learn Pages\n\n## Problem Statement\nThe glossary pages (apps/web/app/glossary/page.tsx and apps/web/app/learn/glossary/page.tsx)\nshow basic \"No terms found\" text when search returns no results. We should use the new\nEmptyState component for a consistent, polished experience.\n\n## Background\n**Current State (glossary/page.tsx ~line 270-276):**\n```tsx\n{filtered.length === 0 ? (\n \n

\n No matches. Try a different search or switch back to{\" \"}\n All.\n

\n
\n) : (\n```\n\n**Current State (learn/glossary/page.tsx ~line 450):**\n```\nNo terms found\n```\n\n**Tools Page (already updated):**\nUses EmptyState component with Search icon, title, description, and action button.\n\n## Implementation Details\n\n### Step 1: Update apps/web/app/glossary/page.tsx\n\nAdd import:\n```tsx\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Button } from \"@/components/ui/button\";\nimport { BookOpen } from \"lucide-react\"; // or Search\n```\n\nReplace the \"No matches\" section:\n```tsx\n{filtered.length === 0 ? (\n {\n setQuery(\"\");\n setCategory(\"all\");\n }}\n >\n Clear filters\n \n }\n />\n) : (\n```\n\n### Step 2: Update apps/web/app/learn/glossary/page.tsx\n\nSimilar update - find the \"No terms found\" text and replace:\n```tsx\n{filteredTerms.length === 0 ? (\n setSearchQuery(\"\")}>\n Clear search\n \n }\n />\n) : (\n```\n\n### Step 3: Review Other Pages for Empty States\nCheck if any other pages need EmptyState:\n- apps/web/app/troubleshooting/page.tsx (search results)\n- apps/web/app/learn/page.tsx (if filtering lessons)\n\n### Step 4: Ensure Consistent Styling\nAll empty states should:\n- Use appropriate icon for context (BookOpen for glossary, Search for generic)\n- Have clear, helpful title\n- Have actionable description\n- Provide a way to reset/clear filters\n\n## Testing\n- Test search with no results on each page\n- Test category filter with no results (glossary)\n- Test \"Clear filters\" button functionality\n- Test reduced motion (EmptyState respects it)\n- Test on mobile (should look good)\n\n## Files to Modify\n- apps/web/app/glossary/page.tsx\n- apps/web/app/learn/glossary/page.tsx\n- Possibly: apps/web/app/troubleshooting/page.tsx\n\n## Acceptance Criteria\n- [ ] glossary/page.tsx uses EmptyState component\n- [ ] learn/glossary/page.tsx uses EmptyState component\n- [ ] All empty states have appropriate icons\n- [ ] All empty states have clear filter/reset buttons\n- [ ] Animations are smooth (or instant with reduced motion)\n- [ ] Mobile layout looks good","status":"closed","priority":2,"issue_type":"task","estimated_minutes":45,"created_at":"2026-02-03T19:57:07.454473104Z","created_by":"ubuntu","updated_at":"2026-02-04T04:50:04.858943686Z","closed_at":"2026-02-04T04:50:04.858922676Z","close_reason":"Replaced glossary empty states with EmptyState component","source_repo":".","compaction_level":0,"original_size":0,"labels":["empty-state","glossary","learn"],"dependencies":[{"issue_id":"bd-3co7k.3.2","depends_on_id":"bd-3co7k.3","type":"parent-child","created_at":"2026-02-03T19:57:07.454473104Z","created_by":"ubuntu"}],"comments":[{"id":49,"issue_id":"bd-3co7k.3.2","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nAlready exists: `apps/web/components/ui/__tests__/empty-state.test.tsx`\n\nVerify coverage for:\n```typescript\ndescribe('EmptyState', () => {\n test('renders icon, title, and description', () => {\n render();\n expect(screen.getByRole('heading', { name: 'No results' })).toBeInTheDocument();\n });\n\n test('renders action button when provided', () => {\n render(\n Clear}\n />\n );\n expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument();\n });\n\n test('variant=\"compact\" applies smaller sizing', () => {\n const { container } = render(\n \n );\n expect(container.firstChild).toHaveClass('py-10');\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/empty-states.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Empty States', () => {\n test('glossary shows empty state for no results', async ({ page }) => {\n await page.goto('/glossary');\n console.log('[E2E] Navigated to glossary');\n \n // Search for something that won't match\n const searchInput = page.getByPlaceholder(/search/i);\n await searchInput.fill('xyzabc123nonexistent');\n console.log('[E2E] Searched for nonexistent term');\n \n // Verify empty state appears\n await expect(page.getByText('No terms found')).toBeVisible();\n console.log('[E2E] Empty state displayed');\n \n // Verify clear button works\n const clearBtn = page.getByRole('button', { name: /clear/i });\n await clearBtn.click();\n console.log('[E2E] Clicked clear button');\n \n // Results should appear again\n await expect(page.locator('[data-testid=\"glossary-term\"]').first()).toBeVisible();\n });\n\n test('tools page shows empty state for no results', async ({ page }) => {\n await page.goto('/tools');\n console.log('[E2E] Navigated to tools');\n \n const searchInput = page.getByPlaceholder(/search/i);\n await searchInput.fill('xyznonexistent');\n \n await expect(page.getByText('No tools found')).toBeVisible();\n console.log('[E2E] Empty state displayed on tools page');\n });\n\n test('empty state respects reduced motion', async ({ page }) => {\n await page.emulateMedia({ reducedMotion: 'reduce' });\n await page.goto('/glossary');\n \n const searchInput = page.getByPlaceholder(/search/i);\n await searchInput.fill('xyznonexistent');\n \n // Should be immediately visible (no fade-in delay)\n const emptyState = page.getByText('No terms found');\n await expect(emptyState).toBeVisible();\n console.log('[E2E] Empty state visible immediately with reduced motion');\n });\n});\n```\n","created_at":"2026-02-03T20:05:38Z"}]} +{"id":"bd-3co7k.3.3","title":"Swipe Gestures for Horizontal Scrolling","description":"# Swipe Gestures for Horizontal Scrolling\n\n## Problem Statement\nHorizontal scrollable areas (carousels, feature grids on mobile) rely on native \nscroll behavior. Adding explicit swipe gesture support with snap points and \nvisual feedback creates a more native-feeling experience.\n\n## Background\n**Current State:**\n- Stepper component has swipe support via @use-gesture/react\n- Other horizontal scrollable areas use CSS scroll-snap\n- No visual indicator of swipe progress or direction\n\n**@use-gesture/react Usage (stepper.tsx ~line 169-193):**\n```tsx\nconst bind = useDrag(\n ({ movement: [mx], velocity: [vx], direction: [dx], cancel }) => {\n // Handle swipe logic\n },\n { axis: \"x\", filterTaps: true }\n);\n```\n\n## Implementation Details\n\n### Step 1: Identify Swipeable Areas\nReview the app for horizontal scrollable content:\n1. Mobile feature cards on landing page\n2. Tool categories on /tldr or /tools (if horizontal)\n3. Lesson cards on /learn (mobile)\n4. Any carousel components\n\n### Step 2: Create useSwipeScroll Hook\n```typescript\n// apps/web/lib/hooks/useSwipeScroll.ts\n\nimport { useRef, useCallback } from \"react\";\nimport { useDrag } from \"@use-gesture/react\";\nimport { useReducedMotion } from \"./useReducedMotion\";\n\ninterface UseSwipeScrollOptions {\n /** Minimum swipe distance to trigger navigation */\n threshold?: number;\n /** Items per \"page\" for snap calculation */\n itemsPerPage?: number;\n /** Callback when page changes */\n onPageChange?: (page: number) => void;\n}\n\nexport function useSwipeScroll(options: UseSwipeScrollOptions = {}) {\n const {\n threshold = 50,\n itemsPerPage = 1,\n onPageChange,\n } = options;\n\n const containerRef = useRef(null);\n const prefersReducedMotion = useReducedMotion();\n const [currentPage, setCurrentPage] = useState(0);\n\n const scrollToPage = useCallback((page: number) => {\n if (!containerRef.current) return;\n \n const container = containerRef.current;\n const itemWidth = container.scrollWidth / totalItems;\n const targetScroll = page * itemWidth * itemsPerPage;\n \n container.scrollTo({\n left: targetScroll,\n behavior: prefersReducedMotion ? \"auto\" : \"smooth\",\n });\n \n setCurrentPage(page);\n onPageChange?.(page);\n }, [itemsPerPage, prefersReducedMotion, onPageChange]);\n\n const bind = useDrag(\n ({ movement: [mx], velocity: [vx], direction: [dx], last }) => {\n if (!last) return;\n \n const shouldNavigate = Math.abs(mx) > threshold || Math.abs(vx) > 0.5;\n if (!shouldNavigate) return;\n \n const nextPage = dx < 0 ? currentPage + 1 : currentPage - 1;\n scrollToPage(Math.max(0, nextPage));\n },\n { axis: \"x\", filterTaps: true, pointer: { touch: true } }\n );\n\n return {\n containerRef,\n bind,\n currentPage,\n scrollToPage,\n };\n}\n```\n\n### Step 3: Add Visual Swipe Indicator\nShow dots or line indicator for current position:\n```tsx\nfunction SwipeIndicator({ current, total }: { current: number; total: number }) {\n return (\n
\n {Array.from({ length: total }).map((_, i) => (\n \n ))}\n
\n );\n}\n```\n\n### Step 4: Apply to Mobile Feature Cards\n```tsx\nfunction MobileFeatureCarousel({ features }) {\n const { containerRef, bind, currentPage } = useSwipeScroll({\n itemsPerPage: 1,\n });\n\n return (\n
\n \n {features.map((feature) => (\n
\n \n
\n ))}\n
\n \n
\n );\n}\n```\n\n### Step 5: Add CSS for Smooth Scrolling\n```css\n/* In globals.css */\n.scrollbar-hide {\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.scrollbar-hide::-webkit-scrollbar {\n display: none;\n}\n\n.snap-x {\n scroll-snap-type: x mandatory;\n}\n\n.snap-center {\n scroll-snap-align: center;\n}\n\n.snap-start {\n scroll-snap-align: start;\n}\n```\n\n## Testing\n- Test swipe left/right on mobile\n- Test swipe velocity (quick flick should navigate)\n- Test swipe threshold (small swipe should not navigate)\n- Test indicator updates correctly\n- Test with reduced motion (instant snap, no animation)\n- Test on iOS Safari (touch-action compatibility)\n- Test on Android Chrome\n- Test with mouse drag on desktop (should work)\n\n## Files to Create/Modify\n- apps/web/lib/hooks/useSwipeScroll.ts (NEW)\n- apps/web/app/page.tsx (apply to mobile sections)\n- apps/web/app/globals.css (snap utilities if not present)\n\n## Acceptance Criteria\n- [ ] useSwipeScroll hook created\n- [ ] Swipe left/right navigates between items\n- [ ] Velocity-based navigation (quick flick)\n- [ ] Visual indicator shows current position\n- [ ] Indicator animates smoothly\n- [ ] Reduced motion: instant navigation\n- [ ] Works on iOS Safari and Android Chrome\n- [ ] Falls back gracefully on desktop (native scroll or mouse drag)","status":"closed","priority":3,"issue_type":"task","estimated_minutes":60,"created_at":"2026-02-03T19:57:34.493424120Z","created_by":"ubuntu","updated_at":"2026-02-04T04:50:07.956223155Z","closed_at":"2026-02-04T04:50:07.956197046Z","close_reason":"Added swipe drag support to landing page horizontal scroller","source_repo":".","compaction_level":0,"original_size":0,"labels":["gestures","mobile","swipe"],"dependencies":[{"issue_id":"bd-3co7k.3.3","depends_on_id":"bd-3co7k.3","type":"parent-child","created_at":"2026-02-03T19:57:34.493424120Z","created_by":"ubuntu"}],"comments":[{"id":50,"issue_id":"bd-3co7k.3.3","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nCreate: `apps/web/lib/hooks/__tests__/useSwipeScroll.test.ts`\n\n```typescript\nimport { renderHook, act } from '@testing-library/react';\nimport { useSwipeScroll } from '../useSwipeScroll';\n\ndescribe('useSwipeScroll', () => {\n test('returns containerRef and currentPage', () => {\n const { result } = renderHook(() => useSwipeScroll());\n expect(result.current.containerRef).toBeDefined();\n expect(result.current.currentPage).toBe(0);\n });\n\n test('scrollToPage updates currentPage', () => {\n const { result } = renderHook(() => useSwipeScroll());\n act(() => {\n result.current.scrollToPage(2);\n });\n expect(result.current.currentPage).toBe(2);\n });\n\n test('calls onPageChange callback', () => {\n const onPageChange = jest.fn();\n const { result } = renderHook(() => useSwipeScroll({ onPageChange }));\n act(() => {\n result.current.scrollToPage(1);\n });\n expect(onPageChange).toHaveBeenCalledWith(1);\n });\n\n test('respects threshold for swipe navigation', () => {\n const { result } = renderHook(() => useSwipeScroll({ threshold: 100 }));\n // Simulate drag that doesn't meet threshold\n // Page should not change\n expect(result.current.currentPage).toBe(0);\n });\n});\n```\n\n### E2E Tests\nCreate: `apps/web/e2e/swipe-scroll.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Swipe Scroll', () => {\n test.beforeEach(async ({ page }) => {\n await page.setViewportSize({ width: 375, height: 812 });\n console.log('[E2E] Set mobile viewport');\n });\n\n test('swipe navigates between carousel items', async ({ page }) => {\n await page.goto('/');\n console.log('[E2E] Loaded landing page');\n \n // Find swipeable carousel\n const carousel = page.locator('[data-testid=\"feature-carousel\"]');\n if (await carousel.isVisible()) {\n const box = await carousel.boundingBox();\n \n // Check initial indicator\n const indicator = page.locator('[data-testid=\"swipe-indicator\"]');\n const initialActive = await indicator.locator('.bg-primary').count();\n console.log('[E2E] Initial active indicators:', initialActive);\n \n // Perform swipe left\n if (box) {\n await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);\n await page.mouse.down();\n await page.mouse.move(box.x + box.width * 0.2, box.y + box.height / 2, { steps: 10 });\n await page.mouse.up();\n console.log('[E2E] Performed swipe left');\n }\n \n await page.waitForTimeout(300); // Wait for animation\n \n // Check indicator updated\n // (Implementation-specific verification)\n }\n });\n\n test('quick flick navigates via velocity', async ({ page }) => {\n await page.goto('/');\n \n const carousel = page.locator('[data-testid=\"feature-carousel\"]');\n if (await carousel.isVisible()) {\n const box = await carousel.boundingBox();\n \n // Quick flick (fast movement, short distance)\n if (box) {\n await page.mouse.move(box.x + box.width * 0.6, box.y + box.height / 2);\n await page.mouse.down();\n await page.mouse.move(box.x + box.width * 0.4, box.y + box.height / 2, { steps: 2 });\n await page.mouse.up();\n console.log('[E2E] Performed quick flick');\n }\n \n // Should still navigate due to velocity\n }\n });\n});\n```\n","created_at":"2026-02-03T20:05:49Z"}]} +{"id":"bd-3co7k.4","title":"Mobile Experience Optimization","description":"# Mobile Experience Optimization\n\n## Purpose\nMake the mobile experience feel like a native app rather than a responsive website.\nThis goes beyond just \"working on mobile\" to \"delighting on mobile.\"\n\n## Why This Matters\n- Over 50% of web traffic is mobile\n- Mobile users have different expectations (gestures, bottom navigation)\n- Native-feeling apps build trust and engagement\n- Poor mobile experience reflects poorly on the overall product\n\n## Scope\n1. Convert jargon modal to BottomSheet on mobile\n2. Mobile navigation improvements (thumb-friendly layout)\n3. Pull-to-refresh patterns (if applicable)\n\n## Dependencies\n- Depends on: Page-Level Enhancements (bd-3co7k.3)\n- Needs BottomSheet component from Core Components\n\n## Key Considerations\n- Safe areas (notch, home indicator) must be handled\n- Touch targets must be minimum 44px\n- Primary actions should be in thumb zone (bottom 1/3)\n- Gestures should match platform conventions\n\n## Reference\n- Apple Human Interface Guidelines (iOS)\n- Material Design Guidelines (Android)\n- Both platforms prefer bottom sheets for contextual content\n\n## Acceptance Criteria\n- Mobile experience feels native\n- All modals use BottomSheet on mobile viewports\n- Primary CTAs are in thumb-friendly positions\n- Safe areas properly handled on all fixed elements","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-03T19:57:48.037864500Z","created_by":"ubuntu","updated_at":"2026-02-04T04:54:56.926556941Z","closed_at":"2026-02-04T04:54:56.926539007Z","close_reason":"Mobile experience updates complete","source_repo":".","compaction_level":0,"original_size":0,"labels":["mobile","optimization","ux"],"dependencies":[{"issue_id":"bd-3co7k.4","depends_on_id":"bd-3co7k","type":"parent-child","created_at":"2026-02-03T19:57:48.037864500Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.4","depends_on_id":"bd-3co7k.3","type":"blocks","created_at":"2026-02-03T19:57:52.847112217Z","created_by":"ubuntu"}]} +{"id":"bd-3co7k.4.1","title":"Convert Jargon Modal to BottomSheet","description":"# Convert Jargon Modal to BottomSheet\n\n## Problem Statement\nThe jargon component (apps/web/components/jargon.tsx) currently has a custom \nbottom sheet implementation for mobile. Once the BottomSheet component is \ncreated, jargon.tsx should use it for consistency and reduced code duplication.\n\n## Background\n**Current Implementation (jargon.tsx ~lines 282-336):**\nThe component already shows a bottom sheet on mobile, but it's custom-built:\n- Has escape key handling\n- Has swipe-to-close (sort of)\n- Has backdrop\n- Has drag handle\n\nThe new BottomSheet component will provide:\n- Better swipe gesture handling via useDrag\n- Consistent animation timing\n- Proper focus management\n- Accessibility improvements\n\n## Implementation Details\n\n### Step 1: Import BottomSheet\n```tsx\nimport { BottomSheet } from \"@/components/ui/bottom-sheet\";\n```\n\n### Step 2: Replace Custom Mobile Sheet\nFind the mobile sheet section (~lines 282-336) and replace with:\n\n**Before:**\n```tsx\n{/* Mobile Bottom Sheet - rendered via portal to escape stacking contexts */}\n{canUsePortal && createPortal(\n \n {isOpen && isMobile && (\n <>\n {/* Backdrop */}\n \n {/* Sheet */}\n \n {/* Handle */}\n {/* Close button */}\n {/* Content */}\n \n \n )}\n ,\n document.body\n)}\n```\n\n**After:**\n```tsx\n{/* Mobile Bottom Sheet */}\n setIsOpen(false)}\n title={`${jargonData.term} definition`}\n>\n \n\n```\n\n### Step 3: Remove Redundant Code\n- Remove custom backdrop rendering for mobile\n- Remove custom drag handle for mobile\n- Remove custom close button for mobile (BottomSheet provides it)\n- Keep desktop tooltip logic unchanged\n\n### Step 4: Ensure Desktop Tooltip Unchanged\nThe desktop tooltip (lines 232-280) should remain as-is since it's a hover popup,\nnot a modal. Only the mobile experience changes.\n\n### Step 5: Update State Management\nThe isOpen state now only controls mobile BottomSheet when isMobile is true.\nDesktop tooltip uses separate hover logic.\n\n## Testing\n- Test jargon term click on mobile (opens BottomSheet)\n- Test swipe down to close\n- Test escape key to close\n- Test backdrop click to close\n- Test close button\n- Test scroll within sheet content\n- Test desktop tooltip (unchanged)\n- Test reduced motion\n\n## Files to Modify\n- apps/web/components/jargon.tsx\n\n## Acceptance Criteria\n- [ ] Mobile jargon uses BottomSheet component\n- [ ] All previous functionality preserved (escape, backdrop, close)\n- [ ] Swipe-to-close works (via BottomSheet)\n- [ ] Desktop tooltip unchanged\n- [ ] Content scrollable within sheet\n- [ ] Safe area padding applied (via BottomSheet)\n- [ ] No visual regressions\n- [ ] Code is significantly simpler (DRY)","status":"closed","priority":2,"issue_type":"task","estimated_minutes":45,"created_at":"2026-02-03T19:58:12.921817316Z","created_by":"ubuntu","updated_at":"2026-02-04T04:54:47.225540804Z","closed_at":"2026-02-04T04:54:47.225522790Z","close_reason":"Jargon mobile uses shared BottomSheet","source_repo":".","compaction_level":0,"original_size":0,"labels":["bottom-sheet","jargon","mobile"],"dependencies":[{"issue_id":"bd-3co7k.4.1","depends_on_id":"bd-3co7k.2.1","type":"blocks","created_at":"2026-02-03T20:03:28.889039582Z","created_by":"ubuntu"},{"issue_id":"bd-3co7k.4.1","depends_on_id":"bd-3co7k.4","type":"parent-child","created_at":"2026-02-03T19:58:12.921817316Z","created_by":"ubuntu"}],"comments":[{"id":51,"issue_id":"bd-3co7k.4.1","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nNo new unit tests needed - this task refactors existing code to use the BottomSheet component.\nVerify existing jargon tests still pass.\n\n### E2E Tests\nCreate: `apps/web/e2e/jargon-mobile.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Jargon BottomSheet on Mobile', () => {\n test.beforeEach(async ({ page }) => {\n await page.setViewportSize({ width: 375, height: 812 });\n await page.goto('/glossary');\n console.log('[E2E] Set mobile viewport and navigated to glossary');\n });\n\n test('clicking jargon term opens BottomSheet', async ({ page }) => {\n // Find a jargon-wrapped term\n const jargonTerm = page.locator('[data-jargon]').first();\n await jargonTerm.click();\n console.log('[E2E] Clicked jargon term');\n \n // Verify BottomSheet appears (not a centered modal)\n const sheet = page.getByRole('dialog');\n await expect(sheet).toBeVisible();\n \n // Verify it's positioned at bottom\n const box = await sheet.boundingBox();\n const viewport = page.viewportSize();\n console.log('[E2E] Sheet bottom:', box!.y + box!.height, 'Viewport height:', viewport!.height);\n \n // Sheet bottom should be near viewport bottom\n expect(box!.y + box!.height).toBeGreaterThan(viewport!.height - 50);\n });\n\n test('swipe down closes BottomSheet', async ({ page }) => {\n const jargonTerm = page.locator('[data-jargon]').first();\n await jargonTerm.click();\n \n const sheet = page.getByRole('dialog');\n await expect(sheet).toBeVisible();\n \n // Swipe down\n const box = await sheet.boundingBox();\n if (box) {\n await page.mouse.move(box.x + box.width / 2, box.y + 50);\n await page.mouse.down();\n await page.mouse.move(box.x + box.width / 2, box.y + 300, { steps: 10 });\n await page.mouse.up();\n console.log('[E2E] Performed swipe down');\n }\n \n await expect(sheet).not.toBeVisible({ timeout: 1000 });\n console.log('[E2E] Sheet dismissed');\n });\n\n test('desktop still uses tooltip (not BottomSheet)', async ({ page }) => {\n await page.setViewportSize({ width: 1440, height: 900 });\n await page.goto('/glossary');\n console.log('[E2E] Set desktop viewport');\n \n const jargonTerm = page.locator('[data-jargon]').first();\n await jargonTerm.hover();\n \n // Should see tooltip, not dialog\n const tooltip = page.locator('[role=\"tooltip\"]');\n await expect(tooltip).toBeVisible({ timeout: 500 });\n console.log('[E2E] Tooltip visible on desktop');\n \n // Dialog should NOT appear\n const dialog = page.getByRole('dialog');\n await expect(dialog).not.toBeVisible();\n });\n\n test('content scrollable within sheet', async ({ page }) => {\n const jargonTerm = page.locator('[data-jargon]').first();\n await jargonTerm.click();\n \n const sheet = page.getByRole('dialog');\n await expect(sheet).toBeVisible();\n \n // Try scrolling within the sheet content\n const scrollable = sheet.locator('[class*=\"overflow-y-auto\"]');\n if (await scrollable.isVisible()) {\n await scrollable.evaluate(el => el.scrollTop = 100);\n const scrollTop = await scrollable.evaluate(el => el.scrollTop);\n console.log('[E2E] Sheet content scrollTop:', scrollTop);\n // Should be scrollable if content is long enough\n }\n });\n});\n```\n","created_at":"2026-02-03T20:06:04Z"}]} +{"id":"bd-3co7k.4.2","title":"Mobile Navigation Thumb-Zone Optimization","description":"# Mobile Navigation Thumb-Zone Optimization\n\n## Problem Statement\nPrimary navigation and CTAs should be in the \"thumb zone\" - the bottom third of\nthe screen where users can easily reach with one hand. Currently, some navigation\nelements are at the top of the screen, requiring uncomfortable reaches on large phones.\n\n## Background\n**Thumb Zone Studies:**\n- 75% of users operate phones with one hand\n- Bottom of screen is most comfortable to reach\n- Top corners are \"danger zones\" (hard to reach)\n- iOS tab bar and Android navigation bar are both at bottom\n\n**Current State:**\n- Wizard has bottom stepper (good!)\n- Main navigation header is at top (standard but not optimal for mobile)\n- Some inline CTAs are mid-page (OK for content flow)\n- Fixed bottom actions exist in some places\n\n**Goal:**\n- Ensure primary actions are reachable\n- Consider sticky bottom CTA on key conversion pages\n- Audit all fixed elements for safe area handling\n\n## Implementation Details\n\n### Step 1: Audit Fixed Bottom Elements\nCheck all pages for fixed/sticky bottom elements:\n```bash\ngrep -r \"fixed.*bottom\\|sticky.*bottom\" apps/web/\n```\n\nEnsure all have:\n- `pb-safe` or `pb-[env(safe-area-inset-bottom)]`\n- Minimum 44px touch targets\n- Proper z-index layering\n\n### Step 2: Add Sticky Bottom CTA to Key Pages\nFor high-conversion pages (wizard completion, learn start), consider:\n```tsx\n{/* Sticky bottom CTA - mobile only */}\n
\n
\n \n
\n
\n\n{/* Spacer to prevent content overlap */}\n
\n```\n\n### Step 3: Evaluate Bottom Tab Navigation (Optional)\nFor the learning hub (/learn), consider bottom tab navigation on mobile:\n```tsx\n// Mobile only - shows at bottom\n\n```\n\nNote: This is a significant UX change and may be out of scope.\n\n### Step 4: Mobile Header Simplification\nOn mobile, the header could be simplified:\n- Logo/title\n- Hamburger menu (opens full-screen nav)\n- Remove secondary links (move to menu)\n\nThis reduces visual clutter and keeps focus on content.\n\n### Step 5: Safe Area Audit\nCheck all fixed elements have proper safe area handling:\n\n**Files to check:**\n- apps/web/app/wizard/layout.tsx (stepper)\n- apps/web/components/jargon.tsx (bottom sheet)\n- apps/web/app/layout.tsx (header?)\n- Any page with fixed CTAs\n\n**Pattern:**\n```tsx\nclassName=\"pb-safe\" // or\nclassName=\"pb-[calc(1rem+env(safe-area-inset-bottom))]\"\n```\n\n### Step 6: Touch Target Audit\nFind any touch targets under 44px on mobile:\n```bash\ngrep -r \"h-8\\|w-8\\|h-6\\|w-6\" apps/web/components/\n```\n\nIncrease to h-10/w-10 minimum or add padding.\n\n## Testing\n- Test on iPhone with notch (safe area bottom)\n- Test on iPhone without notch\n- Test on Android with gesture navigation\n- Test reach-ability of primary CTAs\n- Test sticky bottom CTA doesn't overlap content\n- Test bottom navigation (if implemented)\n\n## Files to Modify\n- apps/web/app/wizard/layout.tsx (audit)\n- apps/web/app/layout.tsx (possible mobile header changes)\n- Various pages (add sticky CTAs if needed)\n\n## Acceptance Criteria\n- [ ] All fixed bottom elements have safe area padding\n- [ ] All touch targets are minimum 44px\n- [ ] Primary CTAs are reachable on large phones\n- [ ] No content hidden behind fixed elements\n- [ ] Sticky bottom CTAs (if added) are useful, not annoying\n- [ ] Header works well on mobile (simplified if needed)","status":"closed","priority":2,"issue_type":"task","estimated_minutes":60,"created_at":"2026-02-03T19:58:37.718786555Z","created_by":"ubuntu","updated_at":"2026-02-04T04:54:53.196270734Z","closed_at":"2026-02-04T04:54:53.196247600Z","close_reason":"Added mobile thumb-zone nav to glossary","source_repo":".","compaction_level":0,"original_size":0,"labels":["mobile","navigation","ux"],"dependencies":[{"issue_id":"bd-3co7k.4.2","depends_on_id":"bd-3co7k.4","type":"parent-child","created_at":"2026-02-03T19:58:37.718786555Z","created_by":"ubuntu"}],"comments":[{"id":52,"issue_id":"bd-3co7k.4.2","author":"Dicklesworthstone","text":"## Testing Requirements\n\n### Unit Tests\nNo specific unit tests - this is primarily an audit and CSS adjustments task.\n\n### E2E Tests\nCreate: `apps/web/e2e/mobile-navigation.spec.ts`\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Mobile Navigation & Thumb Zone', () => {\n test.beforeEach(async ({ page }) => {\n // iPhone 14 Pro Max viewport (largest common phone)\n await page.setViewportSize({ width: 430, height: 932 });\n console.log('[E2E] Set large phone viewport');\n });\n\n test('all fixed bottom elements have safe area padding', async ({ page }) => {\n await page.goto('/wizard/os-selection');\n console.log('[E2E] Navigated to wizard');\n \n // Find fixed bottom elements\n const fixedBottom = page.locator('[class*=\"fixed\"][class*=\"bottom\"]');\n const count = await fixedBottom.count();\n console.log('[E2E] Found', count, 'fixed bottom elements');\n \n for (let i = 0; i < count; i++) {\n const el = fixedBottom.nth(i);\n const paddingBottom = await el.evaluate(el => \n getComputedStyle(el).paddingBottom\n );\n console.log(\\`[E2E] Element \\${i} paddingBottom: \\${paddingBottom}\\`);\n // Should have some padding for safe area\n expect(parseInt(paddingBottom)).toBeGreaterThan(0);\n }\n });\n\n test('all touch targets meet 44px minimum', async ({ page }) => {\n await page.goto('/');\n \n // Find all buttons and links\n const interactives = page.locator('button, a[href], [role=\"button\"]');\n const count = await interactives.count();\n console.log('[E2E] Checking', count, 'interactive elements');\n \n let violations = 0;\n for (let i = 0; i < Math.min(count, 20); i++) { // Check first 20\n const el = interactives.nth(i);\n const box = await el.boundingBox();\n if (box && (box.width < 44 || box.height < 44)) {\n const text = await el.textContent();\n console.log(\\`[E2E] Touch target violation: \"\\${text?.slice(0, 20)}\" is \\${box.width}x\\${box.height}\\`);\n violations++;\n }\n }\n \n console.log('[E2E] Total touch target violations:', violations);\n // Allow some violations for inline links, but flag major issues\n expect(violations).toBeLessThan(5);\n });\n\n test('primary CTAs are in thumb zone', async ({ page }) => {\n await page.goto('/');\n \n // Find primary CTA buttons\n const ctaButtons = page.locator('button[class*=\"primary\"], a[class*=\"primary\"]');\n const viewport = page.viewportSize()!;\n const thumbZoneY = viewport.height * 0.67; // Bottom third\n \n const count = await ctaButtons.count();\n console.log('[E2E] Found', count, 'primary CTAs');\n \n for (let i = 0; i < count; i++) {\n const box = await ctaButtons.nth(i).boundingBox();\n if (box) {\n const isInThumbZone = box.y > thumbZoneY;\n console.log(\\`[E2E] CTA \\${i} at y=\\${box.y}, in thumb zone: \\${isInThumbZone}\\`);\n }\n }\n });\n\n test('no content hidden behind fixed elements', async ({ page }) => {\n await page.goto('/wizard/os-selection');\n \n // Scroll to bottom\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));\n await page.waitForTimeout(200);\n \n // Check last content element is visible above fixed footer\n const lastContent = page.locator('main > *:last-child');\n const contentBox = await lastContent.boundingBox();\n \n const fixedBottom = page.locator('[class*=\"fixed\"][class*=\"bottom\"]').first();\n const fixedBox = await fixedBottom.boundingBox();\n \n if (contentBox && fixedBox) {\n console.log('[E2E] Content ends at:', contentBox.y + contentBox.height);\n console.log('[E2E] Fixed element starts at:', fixedBox.y);\n \n // Content should end before fixed element starts\n expect(contentBox.y + contentBox.height).toBeLessThanOrEqual(fixedBox.y + 10);\n }\n });\n});\n```\n","created_at":"2026-02-03T20:06:20Z"}]} {"id":"bd-3fd3","title":"JFP: Switch installer to official CLI + checksums","description":"Align JFP install with official CLI script and checksum verification.\\n\\nScope:\\n- Update acfs.manifest.yaml stack.jeffreysprompts to use verified_installer with official install CLI (https://jeffreysprompts.com/install-cli.sh).\\n- Add checksums.yaml entry for jfp installer (compute sha256 via scripts/lib/security.sh --checksum).\\n- Regenerate scripts if needed (packages/manifest).\\n\\nValidation:\\n- bun run generate:validate (packages/manifest) if convenient; otherwise note not run.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-21T09:45:44.088710307Z","created_by":"ubuntu","updated_at":"2026-01-21T09:52:51.548924906Z","closed_at":"2026-01-21T09:52:51.548479497Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3hg8","title":"Fix Codex CLI install (npm 404 @openai/codex version)","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-21T19:25:19.742598724Z","created_by":"ubuntu","updated_at":"2026-01-21T21:59:54.187519815Z","closed_at":"2026-01-21T21:59:54.187473077Z","close_reason":"Added pinned version fallback (0.87.0) to both install.sh and update.sh when @latest and unversioned npm installs fail due to transient 404s","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3hg8","depends_on_id":"bd-pkta","type":"discovered-from","created_at":"2026-01-21T19:25:19.772924267Z","created_by":"ubuntu"}]} {"id":"bd-3jd9","title":"acfs export-config: Export current config command","description":"## Overview\nAdd `acfs export-config` command to export current ACFS configuration for backup or migration to another machine.\n\n## Use Cases\n- Backup before reinstall\n- Share config with teammates\n- Migrate to new VPS\n- Version control your setup\n\n## Export Format\n```yaml\n# acfs-config-export.yaml\n# Generated: 2026-01-25T23:00:00Z\n# Hostname: my-vps\n# ACFS Version: 1.0.0\n\nmodules:\n - core\n - dev\n - shell\n\ntools:\n rust: { version: \"1.79.0\", installed: true }\n bun: { version: \"1.1.38\", installed: true }\n zoxide: { version: \"0.9.4\", installed: true }\n\nsettings:\n shell: zsh\n editor: nvim\n theme: dark\n```\n\n## Commands\n```bash\nacfs export-config # Print to stdout\nacfs export-config > my-config.yaml # Save to file\nacfs export-config --json # JSON format\nacfs export-config --minimal # Just module list\n```\n\n## Import (Future)\n```bash\n# Future enhancement (not in this bead)\nacfs import-config my-config.yaml\n```\n\n## Implementation Details\n1. Read current state.json\n2. Query installed tool versions\n3. Format as YAML or JSON\n4. Add metadata (timestamp, hostname, version)\n5. Exclude sensitive data (tokens, keys)\n\n## Sensitive Data Handling\n- NEVER export: SSH keys, API tokens, passwords\n- NEVER export: Full paths containing username\n- DO export: Tool selections, versions, preferences\n\n## Test Plan\n- [ ] Test YAML output is valid\n- [ ] Test JSON output is valid\n- [ ] Test no sensitive data in output\n- [ ] Test --minimal output\n- [ ] Test output on fresh install\n\n## Files to Create\n- scripts/commands/export-config.sh","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-25T23:02:44.767369417Z","created_by":"ubuntu","updated_at":"2026-01-27T03:42:00.984354669Z","closed_at":"2026-01-27T03:42:00.984336184Z","close_reason":"Implemented acfs export-config command: YAML/JSON/minimal output, tool version detection for 30+ tools, secure (no sensitive data). Created export-config.sh and updated acfs.zshrc.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3jd9","depends_on_id":"bd-3y1n","type":"blocks","created_at":"2026-01-25T23:04:58.683105133Z","created_by":"ubuntu"}]} {"id":"bd-3k2f","title":"Subtask: Add remediation logging to doctor","status":"closed","priority":2,"issue_type":"subtask","created_at":"2026-01-25T23:21:16.780150760Z","created_by":"ubuntu","updated_at":"2026-01-27T02:13:26.380121120Z","closed_at":"2026-01-27T02:13:26.380102184Z","close_reason":"Already implemented in doctor_fix.sh - DOCTOR_FIX_LOG with doctor_fix_log() function provides comprehensive remediation logging","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-3kng","title":"Fix zoxide installation to avoid GitHub API rate limits in CI","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-03T06:03:35.130030786Z","created_by":"ubuntu","updated_at":"2026-02-03T06:04:43.963205020Z","closed_at":"2026-02-03T06:04:43.963186034Z","close_reason":"Fixed by preferring apt install for zoxide (commit 7b3b5357)","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3kug","title":"Deep exploration: BV (Beads Viewer)","description":"## Goal\nPerform deep exploration of BV (Beads Viewer) and revise its description with comprehensive testing.\n\n## Phase 0: Pre-flight Verification\n\n```bash\n#!/bin/bash\nLOG=/tmp/bv-preflight.log\necho \"=== BV Pre-flight ===\" | tee $LOG\n\n[[ -d /dp/beads_viewer ]] && echo \"PASS: Directory exists\" || exit 1\ncommand -v bv &>/dev/null && echo \"PASS: bv installed\" || echo \"WARN: bv not in PATH\"\nbv --version 2>&1 | tee -a $LOG\n\nSNAPSHOT_DIR=/tmp/bv-exploration-snapshots\nmkdir -p $SNAPSHOT_DIR\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\n```\n\n## Phase 1: Research\n\n### 1.1 Documentation\n- cat /dp/beads_viewer/README.md\n- Check AGENTS.md if exists\n\n### 1.2 Code Investigation\n- Graph algorithms: PageRank, Betweenness, HITS, Eigenvector\n- Robot modes: --robot-triage, --robot-suggest, --robot-insights, --robot-next\n- How it reads br/bd JSONL data\n- Output JSON schemas\n- Critical path computation\n\n### 1.3 Verify Algorithms\n```bash\n#!/bin/bash\necho \"=== BV Algorithm Verification ===\"\nbv --robot-triage 2>&1 | jq '.triage.recommendations[0].breakdown.pagerank' && echo \"PASS: PageRank\" || echo \"FAIL\"\nbv --robot-triage 2>&1 | jq '.triage.recommendations[0].breakdown.betweenness' && echo \"PASS: Betweenness\" || echo \"FAIL\"\nbv --robot-triage >/dev/null 2>&1 && echo \"PASS: --robot-triage\" || echo \"FAIL\"\nbv --robot-suggest >/dev/null 2>&1 && echo \"PASS: --robot-suggest\" || echo \"FAIL\"\nbv --robot-insights >/dev/null 2>&1 && echo \"PASS: --robot-insights\" || echo \"FAIL\"\n```\n\n### 1.4 External Context\n- /xf search 'bv OR beads_viewer'\n- cass search 'bv triage pagerank' --robot --limit 10\n\n## Phase 2: Analysis\n\nDocument with VERIFICATION:\n- [ ] PageRank: VERIFIED working\n- [ ] Betweenness: VERIFIED working\n- [ ] HITS: VERIFIED working\n- [ ] Eigenvector: VERIFIED working\n- [ ] Robot modes: list VERIFIED working modes\n- [ ] Tech stack: ACTUAL language (Rust)\n- [ ] Synergies VERIFIED:\n - [ ] br: reads JSONL from br\n - [ ] mail: any integration?\n - [ ] ntm: any integration?\n\n## Phase 3: Revision\n\nUpdate with VERIFIED capabilities only:\n- Only list algorithms that actually work\n- Only list robot modes that are implemented\n- Only list synergies that exist\n\n## Phase 4: Testing\n\n```bash\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\nbun run type-check 2>&1 | tee /tmp/bv-typecheck.log\nbun run lint 2>&1 | tee /tmp/bv-lint.log\nbun run build 2>&1 | tee /tmp/bv-build.log\n\n# E2E\nlsof -t -i :3000 | xargs kill 2>/dev/null; sleep 2\nbun run dev &\nsleep 10\ncurl -sL http://localhost:3000/flywheel | grep -qi 'bv' && echo \"PASS\" || echo \"FAIL\"\ncurl -sL http://localhost:3000/tldr | grep -qi 'bv' && echo \"PASS\" || echo \"FAIL\"\nkill %1\n```\n\n## Phase 5: Commit\n\n```bash\ngit add apps/web/lib/flywheel.ts apps/web/lib/tldr-content.ts\ngit commit -m \"docs(flywheel): update BV with verified graph algorithms\n\n- Verified PageRank, Betweenness, HITS implementations\n- Verified robot mode commands\n- Tested against actual bv output\n\nCo-Authored-By: Claude Opus 4.5 \"\n\nbr update bd-3kug --status closed\nbr sync --flush-only && git add .beads/ && git push\n```\n\n## Acceptance Criteria\n- [ ] All graph algorithms VERIFIED\n- [ ] All robot modes VERIFIED\n- [ ] Tech stack VERIFIED\n- [ ] All tests PASS\n- [ ] Pushed","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:00:41.614753229Z","created_by":"ubuntu","updated_at":"2026-01-27T02:42:21.606122487Z","closed_at":"2026-01-27T02:42:21.606099433Z","close_reason":"Verified and updated BV entry in flywheel.ts and tldr-content.ts","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3ljs","title":"Deep exploration: NTM (Named Tmux Manager)","description":"## Goal\nPerform deep exploration of NTM (Named Tmux Manager) and revise its description on the flywheel/TLDR pages with comprehensive testing and validation.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-preflight.log\necho \"=== NTM Pre-flight Check: $(date) ===\" | tee $LOG\n\n# Verify tool directory exists\nif [[ -d /dp/ntm ]]; then\n echo \"PASS: /dp/ntm directory exists\" | tee -a $LOG\nelse\n echo \"FAIL: /dp/ntm directory NOT FOUND - aborting\" | tee -a $LOG\n exit 1\nfi\n\n# Verify README exists\nif [[ -f /dp/ntm/README.md ]]; then\n echo \"PASS: README.md exists\" | tee -a $LOG\nelse\n echo \"WARN: README.md not found\" | tee -a $LOG\nfi\n\n# Verify tool is installed and executable\nif command -v ntm &>/dev/null; then\n echo \"PASS: ntm command available: $(which ntm)\" | tee -a $LOG\n ntm --version 2>&1 | tee -a $LOG || echo \"No --version flag\" | tee -a $LOG\nelse\n echo \"WARN: ntm command not in PATH\" | tee -a $LOG\nfi\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\n#!/bin/bash\nSNAPSHOT_DIR=/tmp/ntm-exploration-snapshots\nmkdir -p $SNAPSHOT_DIR\n\n# Capture current state for diff comparison later\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\n\n# Extract current NTM entry from flywheel.ts for reference\ngrep -A 100 \"id: 'ntm'\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts | head -100 > $SNAPSHOT_DIR/ntm-flywheel-entry.before.txt\n\necho \"Snapshots saved to $SNAPSHOT_DIR\"\nls -la $SNAPSHOT_DIR\n```\n\n### 0.3 Verify TypeScript Interfaces\n```bash\n# Check the FlywheelTool interface we must conform to\ngrep -A 30 \"export interface FlywheelTool\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts\ngrep -A 30 \"export interface TldrFlywheelTool\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/tldr-content.ts\n```\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n```bash\n# Read full README with line count for reference\nwc -l /dp/ntm/README.md\ncat /dp/ntm/README.md\n\n# Check for additional docs\nls -la /dp/ntm/docs/ 2>/dev/null || echo \"No docs/ directory\"\ncat /dp/ntm/AGENTS.md 2>/dev/null || echo \"No AGENTS.md\"\ncat /dp/ntm/CONTRIBUTING.md 2>/dev/null || echo \"No CONTRIBUTING.md\"\n```\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Architecture and key files (main entry point, core modules)\n - Tmux session naming and management logic\n - How it spawns and tracks agent sessions\n - Integration points with other flywheel tools\n - Key CLI commands: `ntm spawn`, `ntm list`, `ntm kill`, `ntm attach`\n - Configuration files and defaults\n\n### 1.3 External Context Search\n```bash\n# Twitter archive search\n/xf search 'ntm OR \"named tmux manager\" OR \"tmux session\"' 2>&1 | head -50\n\n# Past agent session insights \ncass search 'ntm tmux spawn' --robot --limit 10 2>&1\n\n# Look for practical use cases\ncass search 'ntm workflow' --robot --limit 5 2>&1\n```\n\n### 1.4 Project State Review\n```bash\n# Check beads\nls -la /dp/ntm/.beads/ 2>/dev/null || echo \"No .beads directory\"\nbr list 2>/dev/null | grep -i ntm || echo \"No ntm-related beads found\"\n\n# Recent commits\ncd /dp/ntm && git log --oneline -20 && cd -\n\n# Any plan documents\nfind /dp/ntm -name \"*.md\" -type f 2>/dev/null | head -10\n```\n\n### 1.5 CLI Command Inventory\n```bash\n# Get actual CLI help\nntm --help 2>&1 || echo \"ntm --help failed\"\nntm help 2>&1 || echo \"ntm help failed\"\n\n# List actual subcommands available\nntm 2>&1 | head -30\n```\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\nDocument findings for each area (fill in during research):\n- [ ] Core functionality: [describe tmux session management]\n- [ ] Key commands verified working: [list commands tested]\n- [ ] Synergies VERIFIED (not assumed):\n - [ ] mail: [how does ntm use mail? does it?]\n - [ ] caam: [how does ntm use caam for account switching?] \n - [ ] slb: [any slb integration?]\n - [ ] srps: [resource protection integration?]\n - [ ] ru: [repo update integration?]\n- [ ] Tech stack verified: [language, dependencies]\n- [ ] Performance characteristics: [startup time, memory usage]\n- [ ] Known limitations discovered: [list any]\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate the ntm entry ensuring TypeScript interface compliance:\n```typescript\n// Required fields per FlywheelTool interface:\n{\n id: 'ntm', // must be lowercase, unique\n name: string, // full name\n shortName: string, // abbreviated\n href: string, // valid URL or path\n icon: LucideIcon, // must be valid Lucide icon\n color: string, // valid Tailwind color\n tagline: string, // concise (<80 chars)\n description: string, // 1-2 sentences\n deepDescription: string, // detailed explanation\n connectsTo: string[], // VERIFIED synergy IDs only\n connectionDescriptions: Record, // must match connectsTo\n stars: number, // GitHub stars\n features: string[], // VERIFIED features only\n cliCommands: { command: string; description: string }[], // TESTED commands\n installCommand: string, // working install command\n language: string, // actual language\n}\n```\n\n### 3.2 Update apps/web/lib/tldr-content.ts \nUpdate ensuring TldrFlywheelTool interface compliance:\n```typescript\n{\n id: 'ntm',\n name: string,\n shortName: string,\n href: string,\n icon: LucideIcon,\n color: string,\n category: 'core' | 'supporting',\n stars: number,\n whatItDoes: string, // clear, accurate\n whyItsUseful: string, // real value proposition\n implementationHighlights: string[], // verified technical details\n synergies: string[], // VERIFIED connections only\n techStack: string[], // actual technologies\n keyFeatures: string[], // verified features\n useCases: string[], // realistic scenarios\n}\n```\n\n### 3.3 Cross-reference Validation\n```bash\n# After updating NTM synergies, verify reciprocal connections exist\n# If NTM lists \"caam\" in connectsTo, then caam entry must list \"ntm\"\nSYNERGIES=$(grep -A5 \"id: 'ntm'\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts | grep -o \"connectsTo.*\" | head -1)\necho \"NTM synergies: $SYNERGIES\"\n\n# Check each synergy has reciprocal\nfor tool in caam mail slb; do\n if grep -A10 \"id: '$tool'\" /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts | grep -q \"ntm\"; then\n echo \"PASS: $tool lists ntm as synergy\"\n else\n echo \"WARN: $tool does NOT list ntm - may need update\"\n fi\ndone\n```\n\n### 3.4 De-slopify\n- Run `/de-slopify` on all revised descriptions\n- Remove: \"harness\", \"empower\", \"leverage\", \"robust\", \"seamless\", \"cutting-edge\"\n- Ensure authentic, technically accurate voice\n- Verify no placeholder text remains\n\n## Phase 4: Testing (VERIFY QUALITY)\n\n### 4.1 Static Analysis\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-static-analysis.log\necho \"=== NTM Static Analysis: $(date) ===\" | tee $LOG\n\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\n\necho \"--- Type Check ---\" | tee -a $LOG\nbun run type-check 2>&1 | tee -a $LOG\nTYPE_EXIT=$?\necho \"Type Check Exit Code: $TYPE_EXIT\" | tee -a $LOG\n\necho \"--- Lint Check ---\" | tee -a $LOG \nbun run lint 2>&1 | tee -a $LOG\nLINT_EXIT=$?\necho \"Lint Exit Code: $LINT_EXIT\" | tee -a $LOG\n\nif [[ $TYPE_EXIT -ne 0 ]] || [[ $LINT_EXIT -ne 0 ]]; then\n echo \"FAIL: Static analysis failed - DO NOT PROCEED\" | tee -a $LOG\n exit 1\nfi\necho \"PASS: Static analysis succeeded\" | tee -a $LOG\n```\n\n### 4.2 Build Verification\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-build.log\necho \"=== NTM Build Test: $(date) ===\" | tee $LOG\n\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\nbun run build 2>&1 | tee -a $LOG\nBUILD_EXIT=$?\necho \"Build Exit Code: $BUILD_EXIT\" | tee -a $LOG\n\nif [[ $BUILD_EXIT -ne 0 ]]; then\n echo \"FAIL: Build failed - ROLLBACK REQUIRED\" | tee -a $LOG\n echo \"Restoring from snapshot...\" | tee -a $LOG\n cp /tmp/ntm-exploration-snapshots/flywheel.ts.before lib/flywheel.ts\n cp /tmp/ntm-exploration-snapshots/tldr-content.ts.before lib/tldr-content.ts\n exit 1\nfi\necho \"PASS: Build succeeded\" | tee -a $LOG\n```\n\n### 4.3 E2E Visual Verification\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-e2e.log\necho \"=== NTM E2E Test: $(date) ===\" | tee $LOG\n\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\n\n# Check if port 3000 is already in use\nif lsof -i :3000 &>/dev/null; then\n echo \"WARN: Port 3000 already in use, killing existing process\" | tee -a $LOG\n kill $(lsof -t -i :3000) 2>/dev/null\n sleep 2\nfi\n\n# Start dev server\nbun run dev &\nDEV_PID=$!\necho \"Dev server PID: $DEV_PID\" | tee -a $LOG\n\n# Wait for server to be ready (with timeout)\necho \"Waiting for server...\" | tee -a $LOG\nfor i in {1..30}; do\n if curl -s --max-time 2 http://localhost:3000 &>/dev/null; then\n echo \"Server ready after ${i}s\" | tee -a $LOG\n break\n fi\n sleep 1\ndone\n\n# Test flywheel page\necho \"--- Testing /flywheel ---\" | tee -a $LOG\nFLYWHEEL_RESP=$(curl -sL --max-time 10 http://localhost:3000/flywheel 2>&1)\nif echo \"$FLYWHEEL_RESP\" | grep -qi 'ntm'; then\n echo \"PASS: NTM found on /flywheel\" | tee -a $LOG\nelse\n echo \"FAIL: NTM not found on /flywheel\" | tee -a $LOG\nfi\n\n# Test tldr page\necho \"--- Testing /tldr ---\" | tee -a $LOG\nTLDR_RESP=$(curl -sL --max-time 10 http://localhost:3000/tldr 2>&1)\nif echo \"$TLDR_RESP\" | grep -qi 'ntm'; then\n echo \"PASS: NTM found on /tldr\" | tee -a $LOG\nelse\n echo \"FAIL: NTM not found on /tldr\" | tee -a $LOG\nfi\n\n# Check for console errors (look in dev server output)\necho \"--- Checking for errors ---\" | tee -a $LOG\nif echo \"$FLYWHEEL_RESP\" | grep -qi 'error\\|exception'; then\n echo \"WARN: Possible errors detected in response\" | tee -a $LOG\nfi\n\n# Cleanup\nkill $DEV_PID 2>/dev/null\necho \"=== E2E Complete ===\" | tee -a $LOG\n```\n\n### 4.4 Unit Tests: Content Integrity\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-unit-tests.log\necho \"=== NTM Content Unit Tests: $(date) ===\" | tee $LOG\n\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\n\n# Test 1: NTM entry exists in flywheel.ts\necho \"Test 1: NTM entry exists in flywheel.ts\" | tee -a $LOG\nif grep -q \"id: 'ntm'\" lib/flywheel.ts; then\n echo \" PASS\" | tee -a $LOG\nelse\n echo \" FAIL: ntm entry not found\" | tee -a $LOG\nfi\n\n# Test 2: Required fields are non-empty\necho \"Test 2: Required fields non-empty\" | tee -a $LOG\nNTM_ENTRY=$(grep -A 50 \"id: 'ntm'\" lib/flywheel.ts | head -50)\nfor field in tagline description deepDescription; do\n if echo \"$NTM_ENTRY\" | grep -q \"$field: '[^']\\+'\"; then\n echo \" PASS: $field is non-empty\" | tee -a $LOG\n else\n echo \" WARN: $field may be empty\" | tee -a $LOG\n fi\ndone\n\n# Test 3: connectsTo and connectionDescriptions match\necho \"Test 3: Synergy consistency\" | tee -a $LOG\nCONNECTS=$(echo \"$NTM_ENTRY\" | grep -o \"connectsTo: \\[.*\\]\" | head -1)\necho \" connectsTo: $CONNECTS\" | tee -a $LOG\n\n# Test 4: cliCommands have both command and description\necho \"Test 4: CLI commands complete\" | tee -a $LOG\nif echo \"$NTM_ENTRY\" | grep -q \"cliCommands:\"; then\n echo \" PASS: cliCommands field exists\" | tee -a $LOG\nelse\n echo \" FAIL: cliCommands missing\" | tee -a $LOG\nfi\n\n# Test 5: NTM in tldr-content.ts\necho \"Test 5: NTM in tldr-content.ts\" | tee -a $LOG\nif grep -q \"id: 'ntm'\" lib/tldr-content.ts; then\n echo \" PASS: NTM in tldr-content.ts\" | tee -a $LOG\nelse\n echo \" FAIL: NTM not in tldr-content.ts\" | tee -a $LOG\nfi\n\n# Test 6: Diff from original (should have changes)\necho \"Test 6: Content actually changed\" | tee -a $LOG\nif diff -q lib/flywheel.ts /tmp/ntm-exploration-snapshots/flywheel.ts.before &>/dev/null; then\n echo \" WARN: flywheel.ts unchanged - was update applied?\" | tee -a $LOG\nelse\n echo \" PASS: flywheel.ts has changes\" | tee -a $LOG\n diff --brief lib/flywheel.ts /tmp/ntm-exploration-snapshots/flywheel.ts.before | tee -a $LOG\nfi\n\necho \"=== Unit Tests Complete ===\" | tee -a $LOG\n```\n\n### 4.5 CLI Command Verification\n```bash\n#!/bin/bash\nLOG=/tmp/ntm-cli-tests.log\necho \"=== NTM CLI Command Tests: $(date) ===\" | tee $LOG\n\n# Test each CLI command mentioned in the updated content\n# Extract commands from flywheel.ts and verify they work\n\necho \"Testing ntm commands...\" | tee -a $LOG\n\n# Test: ntm list (should work without error)\necho \"--- ntm list ---\" | tee -a $LOG\nntm list 2>&1 | tee -a $LOG\necho \"Exit code: $?\" | tee -a $LOG\n\n# Test: ntm --help\necho \"--- ntm --help ---\" | tee -a $LOG\nntm --help 2>&1 | tee -a $LOG\necho \"Exit code: $?\" | tee -a $LOG\n\n# Add more command tests based on documented commands\necho \"=== CLI Tests Complete ===\" | tee -a $LOG\n```\n\n### 4.6 Consolidated Test Results Log\n```bash\n#!/bin/bash\nRESULTS=/tmp/ntm-exploration-test-results.log\necho \"=============================================\" > $RESULTS\necho \" NTM DEEP EXPLORATION TEST RESULTS\" >> $RESULTS \necho \"=============================================\" >> $RESULTS\necho \"Date: $(date)\" >> $RESULTS\necho \"Agent: $(whoami)\" >> $RESULTS\necho \"\" >> $RESULTS\n\necho \"=== PRE-FLIGHT ===\" >> $RESULTS\ncat /tmp/ntm-preflight.log >> $RESULTS 2>/dev/null || echo \"No preflight log\" >> $RESULTS\necho \"\" >> $RESULTS\n\necho \"=== STATIC ANALYSIS ===\" >> $RESULTS\ngrep -E \"PASS|FAIL|Exit Code\" /tmp/ntm-static-analysis.log >> $RESULTS 2>/dev/null\necho \"\" >> $RESULTS\n\necho \"=== BUILD ===\" >> $RESULTS\ngrep -E \"PASS|FAIL|Exit Code\" /tmp/ntm-build.log >> $RESULTS 2>/dev/null\necho \"\" >> $RESULTS\n\necho \"=== E2E ===\" >> $RESULTS\ngrep -E \"PASS|FAIL|WARN\" /tmp/ntm-e2e.log >> $RESULTS 2>/dev/null\necho \"\" >> $RESULTS\n\necho \"=== UNIT TESTS ===\" >> $RESULTS\ngrep -E \"PASS|FAIL|WARN\" /tmp/ntm-unit-tests.log >> $RESULTS 2>/dev/null\necho \"\" >> $RESULTS\n\necho \"=== CLI TESTS ===\" >> $RESULTS\ngrep -E \"Exit code\" /tmp/ntm-cli-tests.log >> $RESULTS 2>/dev/null\necho \"\" >> $RESULTS\n\necho \"=== CONTENT DIFF ===\" >> $RESULTS\ndiff /tmp/ntm-exploration-snapshots/flywheel.ts.before /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts >> $RESULTS 2>/dev/null | head -50\necho \"\" >> $RESULTS\n\necho \"=============================================\" >> $RESULTS\necho \" SUMMARY\" >> $RESULTS\necho \"=============================================\" >> $RESULTS\nPASS_COUNT=$(grep -c \"PASS\" $RESULTS)\nFAIL_COUNT=$(grep -c \"FAIL\" $RESULTS)\nWARN_COUNT=$(grep -c \"WARN\" $RESULTS)\necho \"PASS: $PASS_COUNT\" >> $RESULTS\necho \"FAIL: $FAIL_COUNT\" >> $RESULTS\necho \"WARN: $WARN_COUNT\" >> $RESULTS\n\nif [[ $FAIL_COUNT -gt 0 ]]; then\n echo \"\" >> $RESULTS\n echo \"!!! FAILURES DETECTED - DO NOT COMMIT !!!\" >> $RESULTS\nfi\n\ncat $RESULTS\n```\n\n## Phase 5: Commit (FINALIZE)\n\n### 5.1 Pre-commit Validation\n```bash\n# Only proceed if all tests pass\nFAIL_COUNT=$(grep -c \"FAIL\" /tmp/ntm-exploration-test-results.log)\nif [[ $FAIL_COUNT -gt 0 ]]; then\n echo \"ABORT: $FAIL_COUNT failures detected\"\n echo \"Review /tmp/ntm-exploration-test-results.log\"\n exit 1\nfi\n```\n\n### 5.2 Stage and Commit\n```bash\ncd /data/projects/agentic_coding_flywheel_setup\ngit add apps/web/lib/flywheel.ts apps/web/lib/tldr-content.ts\ngit diff --cached --stat # Review what is being committed\n\ngit commit -m \"docs(flywheel): update NTM descriptions with verified, accurate content\n\nResearch completed:\n- Read /dp/ntm/README.md and source code\n- Verified CLI commands work: ntm list, ntm spawn, etc.\n- Searched cass and xf for usage patterns\n- Reviewed project beads and recent commits\n\nContent updates:\n- Updated tagline, description, and deepDescription\n- Verified all CLI commands work correctly \n- Updated synergies based on ACTUAL integrations (not assumed)\n- Verified reciprocal synergies exist\n- Passed type-check, lint, and build verification\n\nTest results: /tmp/ntm-exploration-test-results.log\n\nCo-Authored-By: Claude Opus 4.5 \"\n```\n\n### 5.3 Update Beads and Push\n```bash\nbr update bd-3ljs --status closed\nbr sync --flush-only\ngit add .beads/\ngit commit -m \"chore(beads): mark NTM exploration complete\"\ngit push\n```\n\n### 5.4 Rollback Procedure (if needed)\n```bash\n# If issues discovered after commit:\ngit revert HEAD~2 # Revert both commits\n# OR restore from snapshot:\ncp /tmp/ntm-exploration-snapshots/flywheel.ts.before apps/web/lib/flywheel.ts\ncp /tmp/ntm-exploration-snapshots/tldr-content.ts.before apps/web/lib/tldr-content.ts\ngit add . && git commit -m \"revert: rollback NTM exploration changes\"\n```\n\n## Acceptance Criteria\n- [ ] Pre-flight verification passed (tool exists, is installed)\n- [ ] Content snapshot captured for rollback capability \n- [ ] All Phase 4 tests pass (type-check, lint, build, E2E, unit tests, CLI)\n- [ ] ZERO failures in test results log\n- [ ] No type errors or lint warnings introduced\n- [ ] Build succeeds without errors\n- [ ] Both /flywheel and /tldr pages render correctly with NTM entry\n- [ ] All CLI commands listed are VERIFIED working\n- [ ] All synergies are VERIFIED (not assumed)\n- [ ] Reciprocal synergies confirmed in connected tools\n- [ ] Content de-slopified\n- [ ] Changes committed with detailed message\n- [ ] Changes pushed to remote\n- [ ] Bead marked closed\n","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:00:27.642077588Z","created_by":"ubuntu","updated_at":"2026-01-27T02:28:51.058346825Z","closed_at":"2026-01-27T02:28:51.058307801Z","close_reason":"Deep exploration complete: Updated NTM entries in flywheel.ts and tldr-content.ts with verified info from README.md (80+ commands, robot mode integrations, synergies with bv/br/dcg). Added reciprocal ntm synergy to bv entry. Build passes.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3lzu","title":"JFP: Add flywheel tool entry + metrics","description":"Add JeffreysPrompts (jfp) to the Flywheel page tool data.\\n\\nScope:\\n- Update apps/web/lib/flywheel.ts: add a FlywheelTool entry for jfp with description, connections (ms, apr, cm), CLI commands, installCommand, and metadata.\\n- Adjust flywheelDescription.subtitle/toolCount to match the new tool count.\\n\\nValidation:\\n- bun run build (apps/web) if convenient; otherwise note not run.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-21T09:45:20.274651468Z","created_by":"ubuntu","updated_at":"2026-01-21T09:50:01.878875498Z","closed_at":"2026-01-21T09:50:01.878824732Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3moq","title":"Subtask: Add remediate() functions to doctor checks","status":"closed","priority":1,"issue_type":"subtask","created_at":"2026-01-25T23:19:52.953602121Z","created_by":"ubuntu","updated_at":"2026-01-25T23:44:38.331339230Z","closed_at":"2026-01-25T23:44:38.331179739Z","close_reason":"Duplicate of bd-31ps.6.2","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3nbx","title":"acfs doctor --fix: Auto-remediation mode","description":"## Overview\nAdd `--fix` flag to `acfs doctor` that automatically remediates detected issues instead of just reporting them.\n\n## Current Behavior\n- `acfs doctor` runs health checks and reports problems\n- User must manually fix each issue based on suggestions\n- High friction for beginners who don't know HOW to fix things\n\n## Proposed Behavior\n- `acfs doctor --fix` attempts automatic remediation\n- Safe fixes only: reinstall tools, fix permissions, regenerate configs\n- Never destructive: won't delete user data or change system settings\n- Falls back to manual instructions for unsafe operations\n\n## Implementation Details\n1. Modify `scripts/lib/doctor.sh` check functions to return remediation commands\n2. Add `--fix` flag parsing to `scripts/commands/doctor.sh`\n3. Each check needs: detect(), describe(), remediate()\n4. Remediation functions wrapped in confirmation prompts\n5. Dry-run mode: `--fix --dry-run` shows what would be done\n\n## Safety Constraints\n- NEVER delete user files\n- NEVER modify ~/.bashrc or ~/.zshrc without backup\n- Always confirm before system-level changes\n- Log all remediation actions to ~/.local/share/acfs/doctor.log\n\n## Test Plan\n- [ ] Unit tests for each remediation function\n- [ ] E2E test: break tool, run doctor --fix, verify fixed\n- [ ] Test --dry-run shows correct actions\n- [ ] Test unsafe operations fall back to manual\n\n## Files to Modify\n- scripts/lib/doctor.sh (add remediate functions)\n- scripts/commands/doctor.sh (add --fix flag)\n- New: scripts/lib/remediation.sh (shared remediation helpers)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-25T23:01:11.394425192Z","created_by":"ubuntu","updated_at":"2026-01-26T23:31:50.786428028Z","closed_at":"2026-01-26T23:31:50.786408852Z","close_reason":"acfs doctor --fix already fully implemented: doctor_fix.sh with 6 fixers (path ordering, config copy, DCG hook, symlink create, plugin clone, ACFS sourcing), dry-run mode, undo support via autofix.sh, 25 passing unit tests","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3opa","title":"Deep exploration: Brenner Bot","description":"## Goal\nPerform deep exploration of Brenner Bot and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify Brenner installation\n[[ -d /dp/brenner_bot ]] && echo \"PASS: brenner repo exists\" || { echo \"FAIL: brenner repo missing\"; exit 1; }\ncommand -v brenner &>/dev/null && echo \"PASS: brenner command available\" || { echo \"FAIL: brenner not in PATH\"; exit 1; }\n\n# Check corpus location\n[[ -d ~/.local/share/brenner/corpus ]] && echo \"INFO: Corpus exists\" || echo \"INFO: No corpus yet\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/brenner-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/brenner_bot/README.md` - Read full README\n- Check for research methodology docs, corpus format\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Research session management\n - Hypothesis tracking system\n - Corpus organization and search\n - Multi-agent research swarms\n - Integration with Claude Code\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nbrenner --help 2>&1 | head -20\nbrenner search --help 2>&1 | head -10\nbrenner hypothesis --help 2>&1 | head -10\n\n# Test actual functionality\nbrenner list 2>&1 | head -10\n```\n\n### 1.4 External Context Search\n- `/xf search 'brenner bot OR research session OR hypothesis'` - Twitter archive\n- `cass search 'brenner research' --robot --limit 10` - Past sessions\n\n### 1.5 Project State Review\n- Check beads in /dp/brenner_bot/.beads/\n- Review recent commits: `cd /dp/brenner_bot && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Research session structure\n- [ ] Hypothesis tracking format\n- [ ] Corpus organization\n- [ ] Search capabilities\n- [ ] Multi-agent coordination\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] cass - session mining\n- [ ] xf - external research\n- [ ] mail - research coordination\n- [ ] ntm - multi-agent research\n\n### 2.3 Corpus Verification\n```bash\n# Check corpus structure\nls -la ~/.local/share/brenner/corpus/ 2>/dev/null | head -10 || echo \"No corpus yet\"\n\n# Check hypothesis tracking\nbrenner hypotheses 2>&1 | head -10 || echo \"No hypotheses command\"\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate brenner entry with VERIFIED information:\n- `tagline`: Research session management\n- `description`: Hypothesis-driven research\n- `deepDescription`: How research workflow works\n- `features`: Verified capabilities\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test brenner entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst brenner = flywheelTools.find(t => t.id === 'brenner');\nconsole.log('Testing brenner entry...');\nconsole.assert(brenner, 'brenner entry exists');\nconsole.assert(brenner.features?.length > 0, 'has features');\nconsole.assert(brenner.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Research Workflow\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/brenner-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== Brenner E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: List research sessions\necho \"Test 1: List sessions...\" | tee -a $LOG\nbrenner list 2>&1 | head -10 | tee -a $LOG\n\n# Test 2: Search corpus\necho \"Test 2: Search...\" | tee -a $LOG\nbrenner search \"test\" --limit 3 2>&1 | tee -a $LOG || echo \"No search results\"\n\n# Test 3: Hypotheses\necho \"Test 3: Hypotheses...\" | tee -a $LOG\nbrenner hypothesis list 2>&1 | head -5 | tee -a $LOG || echo \"No hypothesis command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-3opa --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Research workflow documented correctly\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:02:10.138573259Z","created_by":"ubuntu","updated_at":"2026-01-27T03:39:55.530222451Z","closed_at":"2026-01-27T03:39:55.530196993Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-3te83","title":"Add ARM64 Linux binary to meta_skill releases","description":"meta_skill v0.1.0 release is missing aarch64-unknown-linux-gnu binary. When acfs doctor runs on ARM64 Linux (AWS Graviton, Raspberry Pi), the install script tries to download ms-0.1.0-aarch64-unknown-linux-gnu.tar.gz which 404s.\n\nCurrent release binaries:\n- aarch64-apple-darwin (ARM64 macOS) present\n- x86_64-unknown-linux-gnu (x86_64 Linux) present\n- x86_64-pc-windows-msvc (Windows) present\n- aarch64-unknown-linux-gnu (ARM64 Linux) MISSING\n\nROOT CAUSE: In /data/projects/meta_skill/.github/workflows/release.yml (lines 91-95), the ARM64 Linux target is commented out with note: \"ARM Linux cross-compile requires vendored OpenSSL - see Cargo.toml\"\n\nFIX (in meta_skill repo at /data/projects/meta_skill):\n1. Uncomment the aarch64-unknown-linux-gnu target in .github/workflows/release.yml:\n - target: aarch64-unknown-linux-gnu\n os: ubuntu-latest\n archive: tar.gz\n cross: true\n2. Add cross-compilation support:\n - Install cross tool: cargo install cross --locked\n - Build with: cross build --release --target aarch64-unknown-linux-gnu --locked\n - OR: add openssl = { version = \"0.10\", features = [\"vendored\"] } to Cargo.toml\n3. Cut new meta_skill release (v0.1.2+) that includes ARM64 Linux binary\n\nFIX (in ACFS repo):\n4. Update acfs doctor to show clear message when ARM64 Linux binary unavailable:\n \"meta_skill ARM64 Linux binary not yet available. Install manually from https://github.com/.../releases or build from source: cargo install meta_skill\"\n5. Update acfs.manifest.yaml meta_skill entry to note ARM64 Linux support status\n\nTESTS:\n\nBuild Tests (in meta_skill CI):\n- test_arm64_linux_build: cross build --target aarch64-unknown-linux-gnu succeeds\n- test_arm64_binary_runs: run binary in ARM64 Docker container (docker run --platform linux/arm64)\n- test_release_includes_arm64: after release, verify aarch64-unknown-linux-gnu.tar.gz asset exists\n\nIntegration Tests (in ACFS):\n- test_doctor_arm64_graceful: on ARM64 Linux, acfs doctor shows helpful message (not crash)\n- test_install_arm64: full install script on ARM64 container successfully installs meta_skill\n\nE2E Tests:\n- test_arm64_graviton: run full ACFS install on AWS Graviton instance (or GitHub ARM runner)\n\nGitHub issue: #115","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-07T03:08:40.264250256Z","created_by":"ubuntu","updated_at":"2026-02-11T17:16:33.618142200Z","closed_at":"2026-02-11T17:16:33.618121230Z","close_reason":"Fixed: ARM64 Linux target already uncommented (ac32c27), added AR env var for cross-compilation (ec2912f). Both commits pushed to meta_skill. Next release will include aarch64-unknown-linux-gnu binary.","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":55,"issue_id":"bd-3te83","author":"Dicklesworthstone","text":"ADDITIONAL TEST LOGGING REQUIREMENTS:\nBuild tests should log:\n - Target triple being built: 'Building for aarch64-unknown-linux-gnu'\n - Cross-compilation tool: 'Using cross v0.2.x' or 'Using cargo with vendored OpenSSL'\n - Binary size and architecture: 'Binary: 4.2MB, ELF 64-bit LSB, ARM aarch64'\n - file(1) output on the produced binary to verify architecture\n - Docker container platform: 'Testing in linux/arm64 container'\n - Binary execution result: './ms --version -> meta_skill 0.1.2'\n\nIntegration tests should log:\n - Detected architecture: 'uname -m -> aarch64'\n - Download URL attempted: 'https://github.com/.../ms-0.1.2-aarch64-unknown-linux-gnu.tar.gz'\n - HTTP response code: '200 OK' or '404 Not Found'\n - acfs doctor output for meta_skill check\n\nFinal summary: 'ARM64 BUILD TESTS: N/N passed'\n","created_at":"2026-02-07T21:11:15Z"}]} {"id":"bd-3ucd","title":"state.sh: remove duplicate helper block + add guard","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-21T07:11:24.447472677Z","created_by":"ubuntu","updated_at":"2026-01-21T07:21:24.823507314Z","closed_at":"2026-01-21T07:21:24.823357592Z","close_reason":"Removed duplicate helper block in state.sh and added guard comment; shellcheck run","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3ukl","title":"Deep exploration: DCG (Destructive Command Guard)","description":"## Goal\nPerform deep exploration of DCG (Destructive Command Guard) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify DCG installation\n[[ -d /dp/destructive_command_guard ]] && echo \"PASS: dcg repo exists\" || { echo \"FAIL: dcg repo missing\"; exit 1; }\ncommand -v dcg &>/dev/null && echo \"PASS: dcg command available\" || { echo \"FAIL: dcg not in PATH\"; exit 1; }\n\n# Check if dcg is integrated with shell\ngrep -q dcg ~/.bashrc ~/.zshrc 2>/dev/null && echo \"INFO: dcg in shell config\" || echo \"INFO: dcg shell integration unclear\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/dcg-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/destructive_command_guard/README.md` - Read full README\n- Check for pattern docs, whitelist configuration\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Command interception mechanism (preexec hook? wrapper?)\n - Dangerous command patterns (rm -rf, git reset --hard, etc.)\n - Whitelist/blacklist configuration\n - Integration with Claude Code hooks\n - Escape hatch for legitimate use\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\ndcg --help 2>&1 | head -20\ndcg status --help 2>&1 | head -10\ndcg whitelist --help 2>&1 | head -10\n\n# Test actual functionality (safe test)\ndcg status 2>&1 | head -10\n```\n\n### 1.4 Pattern Verification\n```bash\n# Check what patterns are blocked\ncat /dp/destructive_command_guard/patterns/*.yaml 2>/dev/null | head -30\n# Or check code for pattern definitions\ngrep -r \"rm -rf\\|git reset\\|DROP TABLE\" /dp/destructive_command_guard/ 2>/dev/null | head -10\n```\n\n### 1.5 External Context Search\n- `/xf search 'dcg OR destructive command OR guard'` - Twitter archive\n- `cass search 'dcg blocked command' --robot --limit 10` - Past sessions\n\n### 1.6 Project State Review\n- Check beads in /dp/destructive_command_guard/.beads/\n- Review recent commits: `cd /dp/destructive_command_guard && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Command interception mechanism\n- [ ] Complete list of blocked patterns\n- [ ] Whitelist configuration format\n- [ ] Claude Code hooks integration\n- [ ] Override/escape mechanism\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] slb - two-person rule escalation\n- [ ] mail - alert messaging\n- [ ] ntm - agent coordination\n\n### 2.3 Claim Verification\n```bash\n# Count blocked patterns\ngrep -c \"pattern\\|regex\" /dp/destructive_command_guard/patterns/*.yaml 2>/dev/null | awk -F: '{sum+=} END {print \"Total patterns: \" sum}'\n\n# Verify hook mechanism\ngrep -l \"preexec\\|precmd\" /dp/destructive_command_guard/**/*.{sh,zsh} 2>/dev/null | head -3\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate dcg entry with VERIFIED information:\n- `tagline`: Accurate one-liner\n- `description`: Command interception mechanism\n- `deepDescription`: How blocking works\n- `features`: Verified blocked commands list\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test dcg entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst dcg = flywheelTools.find(t => t.id === 'dcg');\nconsole.log('Testing dcg entry...');\nconsole.assert(dcg, 'dcg entry exists');\nconsole.assert(dcg.features?.length > 0, 'has features');\nconsole.assert(dcg.connectsTo?.includes('slb'), 'connects to slb');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Guard Verification (Safe Mode)\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/dcg-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== DCG E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Status check\necho \"Test 1: Status check...\" | tee -a $LOG\ndcg status 2>&1 | tee -a $LOG\n\n# Test 2: List blocked patterns\necho \"Test 2: List patterns...\" | tee -a $LOG\ndcg list 2>&1 | head -10 | tee -a $LOG || echo \"No list command\"\n\n# Test 3: Whitelist view\necho \"Test 3: Whitelist...\" | tee -a $LOG\ndcg whitelist --show 2>&1 | head -5 | tee -a $LOG || echo \"No whitelist command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-3ukl --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Blocked patterns documented completely\n- [ ] Synergies are reciprocal (dcg->slb and slb->dcg)\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:01:27.806155248Z","created_by":"ubuntu","updated_at":"2026-01-27T03:29:33.713449879Z","closed_at":"2026-01-27T03:29:33.713422467Z","close_reason":"DCG deep exploration complete: added heredoc/inline script scanning, smart context detection, agent-specific trust profiles, MCP server mode, dcg scan for CI","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-3vx8","title":"Optimize runtime install phases (apt/bun/cargo/etc) based on profiling","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-21T19:00:39.996743538Z","created_by":"ubuntu","updated_at":"2026-01-22T01:16:40.415209476Z","closed_at":"2026-01-22T01:16:40.415131199Z","close_reason":"Verified: Batched cargo install optimization is implemented in install.sh at line 3009-3038. Tools (du-dust, lsd, bat, fd-find, ripgrep) are now batch-installed with a single cargo command, reducing index downloads and enabling parallel compilation.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3vx8","depends_on_id":"bd-2z32","type":"discovered-from","created_at":"2026-01-21T19:00:40.029703844Z","created_by":"ubuntu"}]} {"id":"bd-3y1n","title":"EPIC: No-Brainer ACFS Improvements (Jan 2026)","description":"## Epic: No-Brainer ACFS Improvements (Jan 2026)\n\nThis epic contains 15 high-impact, low-complexity improvements to the ACFS project that will obviously benefit users with minimal risk. Each improvement was evaluated against these criteria:\n\n**Evaluation Criteria:**\n- Robust: Does it make the system more resilient?\n- Reliable: Does it reduce failure modes?\n- Performant: Does it make things faster?\n- Intuitive: Is it discoverable and obvious?\n- User-friendly: Does it reduce friction?\n- Ergonomic: Does it fit natural workflows?\n- Useful: Does it solve real problems?\n- Compelling: Will users be glad it exists?\n- Accretive: Does it build on existing strengths?\n- Pragmatic: Is the effort justified by the benefit?\n\n## Top 5 (Highest Impact)\n1. **bd-3nbx**: `acfs doctor --fix` - Auto-remediation mode\n2. **bd-2dkb**: Copy-to-clipboard for web wizard code blocks\n3. **bd-29fl**: `acfs status` - One-line health summary\n4. **bd-zhdi**: Shell tab completion for acfs command\n5. **bd-2gys**: Step validation in web wizard\n\n## Next 10 (High Value, Complementary)\n6. **bd-21kh**: Progress bar during tool installation\n7. **bd-39ye**: NO_COLOR environment variable support\n8. **bd-1lug**: Rate limit backoff for GitHub API calls\n9. **bd-1eop**: Skip already-installed tools during install\n10. **bd-w8fx**: VPS provider comparison table\n11. **bd-1yfv**: \"I'm stuck\" help button in web wizard\n12. **bd-2p56**: `acfs changelog` command\n13. **bd-331g**: Dark mode toggle in web wizard\n14. **bd-2zqr**: Webhook notification on install completion\n15. **bd-3jd9**: `acfs export-config` command\n\n## Implementation Notes\n- Each bead is self-contained with full implementation details\n- Start with top 5 for maximum early impact\n- Beads 6-10 are quick wins that complement the top 5\n- Beads 11-15 are nice-to-haves that round out the experience\n\n## Success Criteria\n- All 15 beads implemented and tested\n- No regressions in existing functionality\n- Documentation updated for new features\n- User feedback collected on improvements","status":"closed","priority":2,"issue_type":"epic","created_at":"2026-01-25T23:00:18.624398983Z","created_by":"ubuntu","updated_at":"2026-01-26T22:33:42.559881815Z","closed_at":"2026-01-26T22:33:39.119946333Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":37,"issue_id":"bd-3y1n","author":"Dicklesworthstone","text":"Closing EPIC to unblock 13 child tasks. bd-2dkb (copy-to-clipboard) completed. Remaining children are now actionable for individual implementation.","created_at":"2026-01-26T22:33:42Z"}]} {"id":"bd-47sq","title":"Deep exploration: MS (Meta Skill)","description":"## Goal\nPerform deep exploration of MS (Meta Skill) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify MS installation\n[[ -d /dp/meta_skill ]] && echo \"PASS: ms repo exists\" || { echo \"FAIL: ms repo missing\"; exit 1; }\ncommand -v ms &>/dev/null && echo \"PASS: ms command available\" || { echo \"FAIL: ms not in PATH\"; exit 1; }\n\n# Check Claude Code skill integration\n[[ -d ~/.claude/skills ]] && echo \"INFO: Claude skills directory exists\" || echo \"INFO: No skills directory yet\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/ms-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/meta_skill/README.md` - Read full README\n- Check for skill authoring docs, SKILL.md format\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Skill creation workflow\n - SKILL.md format and requirements\n - Skill validation mechanism\n - Installation into Claude Code\n - Skill discovery and loading\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nms --help 2>&1 | head -20\nms create --help 2>&1 | head -10\nms validate --help 2>&1 | head -10\n\n# Test actual functionality\nms list 2>&1 | head -10\n```\n\n### 1.4 External Context Search\n- `/xf search 'meta skill OR ms OR skill authoring'` - Twitter archive\n- `cass search 'ms skill create' --robot --limit 10` - Past sessions\n\n### 1.5 Project State Review\n- Check beads in /dp/meta_skill/.beads/\n- Review recent commits: `cd /dp/meta_skill && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Skill creation workflow\n- [ ] SKILL.md format specification\n- [ ] Validation rules and errors\n- [ ] Installation process\n- [ ] Skill discovery mechanism\n\n### 2.2 Synergy Verification\nCross-reference these tools actually integrate:\n- [ ] Claude Code - primary integration target\n- [ ] sc (skill) - skill creation helper\n- [ ] sw (skill writer) - advanced skill authoring\n\n### 2.3 Format Verification\n```bash\n# Check SKILL.md template\ncat /dp/meta_skill/templates/SKILL.md 2>/dev/null | head -30\n# Or find example skills\nfind /dp -name \"SKILL.md\" 2>/dev/null | head -5 | xargs head -20\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate ms entry with VERIFIED information:\n- `tagline`: Skill authoring for Claude Code\n- `description`: Creation and validation\n- `deepDescription`: How skill system works\n- `features`: Verified capabilities\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: Only verified integrations\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Working integration examples\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test ms entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst ms = flywheelTools.find(t => t.id === 'ms');\nconsole.log('Testing ms entry...');\nconsole.assert(ms, 'ms entry exists');\nconsole.assert(ms.features?.length > 0, 'has features');\nconsole.assert(ms.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Skill Operations\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/ms-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== MS E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: List skills\necho \"Test 1: List skills...\" | tee -a $LOG\nms list 2>&1 | head -10 | tee -a $LOG\n\n# Test 2: Validate a skill\necho \"Test 2: Validate...\" | tee -a $LOG\nms validate /dp/meta_skill 2>&1 | head -10 | tee -a $LOG || echo \"Validation done\"\n\n# Test 3: Create help\necho \"Test 3: Create help...\" | tee -a $LOG\nms create --help 2>&1 | head -5 | tee -a $LOG\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-47sq --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Skill format documented correctly\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:01:36.485651577Z","created_by":"ubuntu","updated_at":"2026-01-27T03:32:05.943130894Z","closed_at":"2026-01-27T03:32:05.943105255Z","close_reason":"MS deep exploration complete: verified v0.1.0, 60+ commands, 12 MCP tools (corrected from 6). Updated flywheel.ts and tldr-content.ts.","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-53zvp","title":"Test: verify claude update --channel latest works correctly","description":"## What\nVerify that the --channel latest fix actually works end-to-end.\n\n## Test Plan\n\n### Pre-flight check\n1. Run `claude --version` to record current version and channel\n2. Run `claude update --help` to confirm --channel flag exists\n\n### Test 1: Dry-run validation of update.sh\n1. Run `bash scripts/lib/update.sh --dry-run --verbose 2>&1 | grep -i \"claude update\"` \n2. Verify: ALL logged claude update commands include \"--channel latest\"\n3. Verify: No bare \"claude update\" appears in the dry-run output\n4. Log: All claude update commands found in dry-run output\n\n### Test 2: uca alias validation\n1. Source acfs/zsh/acfs.zshrc\n2. Run `alias uca` or `type uca`\n3. Verify: alias contains \"--channel latest\"\n4. Log: Full alias definition\n\n### Test 3: Live update test (if safe)\n1. Run `claude update --channel latest 2>&1`\n2. Verify: exit code 0\n3. Verify: version output shows \"latest\" channel or version >= current\n4. Log: Full update output, before/after versions\n\n### Test 4: Grep-based completeness check\n1. Run `grep -n \"claude update\" scripts/lib/update.sh | grep -v \"channel\"` \n2. Verify: NO matches (every occurrence has --channel)\n3. Also check: `grep -n \"claude update\" acfs/zsh/acfs.zshrc | grep -v \"channel\"`\n4. Verify: NO matches\n5. Log: Any remaining bare \"claude update\" occurrences\n\n## Output Format\n```\n[TEST_1] PASS: All dry-run claude update commands include --channel latest (5/5)\n[TEST_2] PASS: uca alias = \"~/.local/bin/claude update --channel latest\"\n[TEST_3] PASS: Live update succeeded, channel=latest version=2.1.37\n[TEST_4] PASS: No bare \"claude update\" found in update.sh or acfs.zshrc\n```\n\n## Acceptance Criteria\n- All 4 tests pass\n- Zero bare \"claude update\" anywhere in the codebase","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T18:35:47.930709260Z","created_by":"ubuntu","updated_at":"2026-02-11T16:41:41.035926441Z","closed_at":"2026-02-11T16:41:41.035906184Z","close_reason":"Superseded by bd-gsjqf.4. Original test plan was invalid (--channel flag doesn't exist). Comprehensive tests in bd-gsjqf.4 cover the actual verified installer approach.","external_ref":"gh:Dicklesworthstone/agentic_coding_flywheel_setup#125/test","source_repo":".","compaction_level":0,"original_size":0,"labels":["claude-code","testing","update"],"dependencies":[{"issue_id":"bd-53zvp","depends_on_id":"bd-gsjqf","type":"blocks","created_at":"2026-02-08T18:35:52.695521713Z","created_by":"ubuntu"},{"issue_id":"bd-53zvp","depends_on_id":"bd-gsjqf.4","type":"blocks","created_at":"2026-02-08T21:29:24.109266113Z","created_by":"ubuntu"}],"comments":[{"id":60,"issue_id":"bd-53zvp","author":"Dicklesworthstone","text":"TEST PLAN INVALIDATED: Tests 1-4 all assume the fix is claude update --channel latest. Since --channel does NOT exist as a CLI flag (verified by running claude update --help), the test criteria must change. If the fix switches to update_run_verified_installer, Test 1 should grep for update_run_verified_installer calls, Test 2 should check the uca alias for the new approach, Test 3 should verify the installer works, and Test 4 should grep for bare claude update occurrences. The specific test criteria depend on which fix approach is chosen (see bd-gsjqf comments for alternatives).","created_at":"2026-02-08T21:18:48Z"},{"id":61,"issue_id":"bd-53zvp","author":"Dicklesworthstone","text":"SUPERSEDED: This test plan is invalid because `claude update --channel latest` does not exist as a CLI flag (verified by running `claude update --help`). All 4 tests in this bead assume --channel latest works. Replaced by bd-gsjqf.4 which tests the actual fix approach (verified installer). This bead should be closed when bd-gsjqf.4 is completed.","created_at":"2026-02-08T21:29:22Z"}]} +{"id":"bd-5mes9","title":"Update all dependencies to latest stable versions","description":"## Dependency Update for agentic_coding_flywheel_setup\n\n### Context\nACFS is primarily Bash scripts + a Bun/TypeScript manifest generator. Dependencies are:\n- Bun packages in `packages/manifest/package.json`\n- System tools installed by the setup scripts\n- Shell libraries in `scripts/lib/`\n\n### Steps\n1. `cd /data/projects/agentic_coding_flywheel_setup`\n2. Update Bun packages: `cd packages/manifest && bun update && cd ../..`\n3. Regenerate manifest index: `cd packages/manifest && bun run generate && cd ../..`\n4. Verify manifest SHA256 matches: `scripts/check-manifest-drift.sh --json`\n5. Test installer: run a dry-run or check syntax with `bash -n install.sh`\n6. Verify acfs CLI works: `source acfs/zsh/acfs.zshrc && acfs doctor`\n\n### Key Dependencies to Watch\n- `@types/node` -- TypeScript definitions\n- `yaml` -- YAML parsing in manifest generator\n- `zod` -- schema validation (if used)\n- System tools: claude, codex, gemini CLI tools\n\n### Verification\n- Manifest regeneration produces valid output\n- `install.sh` syntax check passes\n- `acfs doctor` passes all checks\n- SHA256 hash in manifest_index.sh matches acfs.manifest.yaml","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-11T06:19:53.136401139Z","created_by":"ubuntu","updated_at":"2026-02-11T17:11:21.158286117Z","closed_at":"2026-02-11T17:11:21.158262793Z","close_reason":"Updated Bun packages to latest stable versions, regenerated manifest, verified SHA256 integrity","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-6h7h","title":"Add all utility tools to flywheel.ts for website visibility","description":"## Task\n\nAdd all 9 utility tools to apps/web/lib/flywheel.ts so they appear on the ACFS website.\n\n## Utility Tools to Add\n\n| Tool | Binary | Description |\n|------|--------|-------------|\n| toon_rust | tru | Token-optimized notation format |\n| rust_proxy | rust_proxy | Transparent proxy routing |\n| rano | rano | Network observer for AI CLIs |\n| xf | xf | X (Twitter) archive search |\n| markdown_web_browser | mdwb | Website to Markdown converter |\n| process_triage | pt | Zombie process detector |\n| aadc | aadc | ASCII diagram corrector |\n| source_to_prompt_tui | s2p | Code to LLM prompt generator |\n| coding_agent_usage_tracker | caut | LLM provider usage tracker |\n\n## Implementation\n\n1. Add entries to the tools array in flywheel.ts\n2. Use category: \"utilities\" to distinguish from first-class flywheel tools\n3. Include appropriate synergies where applicable\n\n## Acceptance Criteria\n\n1. All 9 utilities visible on website\n2. Proper categorization as utilities\n3. Website builds successfully","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T00:46:44.593801711Z","created_by":"ubuntu","updated_at":"2026-01-27T02:01:14.680436702Z","closed_at":"2026-01-27T02:01:14.680364496Z","close_reason":"All 9 utility tools (tru, rano, mdwb, s2p, rust_proxy, aadc, caut, pt, xf) already present in flywheel.ts with full entries","source_repo":".","compaction_level":0,"original_size":0,"labels":["flywheel","utilities","website"],"dependencies":[{"issue_id":"bd-6h7h","depends_on_id":"bd-1ega","type":"parent-child","created_at":"2026-01-25T01:10:04.019594635Z","created_by":"ubuntu"},{"issue_id":"bd-6h7h","depends_on_id":"bd-1ega.6","type":"blocks","created_at":"2026-01-25T00:47:15.824609584Z","created_by":"ubuntu"}]} +{"id":"bd-eh383","title":"meta_skill ARM64 Linux binary missing — blocked upstream (GH #115)","description":"## Context\nGitHub issue #115 filed by tayiorbeii (2026-02-06). On ARM64 Linux (aarch64), meta_skill install 404s because v0.1.0 has no aarch64-unknown-linux-gnu binary.\n\n## Available binaries in v0.1.0:\n- ms-0.1.0-aarch64-apple-darwin.tar.gz (ARM64 macOS - exists)\n- ms-0.1.0-x86_64-unknown-linux-gnu.tar.gz (x86_64 Linux - exists)\n- ms-0.1.0-x86_64-pc-windows-msvc.zip (Windows - exists)\n- aarch64-unknown-linux-gnu — MISSING\n\n## Two-Part Fix\n\n### Part 1: ACFS workaround (THIS repo, can do now)\nUpdate doctor.sh check at lines 1115-1116 to detect aarch64 on Linux and show:\n \"meta_skill: ARM64 Linux binary not yet available (see https://github.com/Dicklesworthstone/meta_skill/issues/1)\"\ninstead of the generic install suggestion that will 404.\n\nImplementation:\n```bash\n# In doctor.sh, before the meta_skill install suggestion:\nif [[ \"$(uname -m)\" == \"aarch64\" ]] && [[ \"$(uname -s)\" == \"Linux\" ]]; then\n log_item \"warn\" \"meta_skill (ms)\" \"ARM64 Linux binary not yet available\"\n return 0 # Skip install attempt\nfi\n```\n\n### Part 2: Upstream (meta_skill repo, blocked)\nAdd aarch64-unknown-linux-gnu to the GoReleaser or release CI matrix in Dicklesworthstone/meta_skill. Requires:\n- Cross-compilation target or dedicated ARM64 runner\n- New release (v0.1.1 or v0.2.0)\nThis is blocked on upstream release work.\n\n## Testing\nAfter Part 1:\n1. On this VPS (x86_64): `acfs doctor` should still show meta_skill normally\n2. Simulate ARM64: mock uname output or grep the code to verify the aarch64 check exists\n3. Verify the issue URL is correct and accessible\n\n## Key Files\n- scripts/lib/doctor.sh (lines 1109-1117, meta_skill check)\n- Upstream: Dicklesworthstone/meta_skill release CI matrix\n\n## Status\nPart 1 is actionable now. Part 2 is blocked upstream. Keep this bead open until both parts are done.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-02-08T18:16:39.015874050Z","created_by":"ubuntu","updated_at":"2026-02-11T17:11:27.194753691Z","external_ref":"gh:Dicklesworthstone/agentic_coding_flywheel_setup#115","source_repo":".","compaction_level":0,"original_size":0,"labels":["arm64","bug","meta_skill","upstream-blocked"],"comments":[{"id":83,"issue_id":"bd-eh383","author":"Dicklesworthstone","text":"Part 1 complete: doctor.sh updated to detect ARM64 Linux and show a helpful warning + build-from-source suggestion instead of the install script that would 404. Part 2 (upstream meta_skill binary publish) still pending.","created_at":"2026-02-11T17:11:27Z"}]} {"id":"bd-flfe","title":"Deep exploration: ACFS (Agentic Coding Flywheel Setup)","description":"## Goal\nPerform deep exploration of ACFS (Agentic Coding Flywheel Setup) and revise its description on the flywheel/TLDR pages with comprehensive testing.\n\n## Phase 0: Pre-flight Verification (CRITICAL)\n\n### 0.1 Tool Existence Check\n```bash\n# Verify ACFS installation\n[[ -d /dp/agentic_coding_flywheel_setup ]] && echo \"PASS: acfs repo exists\" || { echo \"FAIL: acfs repo missing\"; exit 1; }\ncommand -v acfs &>/dev/null && echo \"PASS: acfs command available\" || { echo \"FAIL: acfs not in PATH\"; exit 1; }\n\n# Check for manifest\n[[ -f /dp/agentic_coding_flywheel_setup/acfs.manifest.yaml ]] && echo \"INFO: Manifest exists\" || echo \"WARN: No manifest\"\n```\n\n### 0.2 Content Snapshot (BEFORE State)\n```bash\nSNAPSHOT_DIR=/tmp/acfs-exploration-snapshots-$(date +%Y%m%d-%H%M%S)\nmkdir -p $SNAPSHOT_DIR\ncp apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\necho \"Snapshots saved to $SNAPSHOT_DIR\"\n```\n\n### 0.3 TypeScript Interface Reference\nContent must match FlywheelTool interface:\n- id, name, tagline, description, deepDescription\n- features[], cliCommands[], connectsTo[]\n- integrationLevel, category, status\n\n## Phase 1: Research (GATHER CONTEXT)\n\n### 1.1 Primary Documentation\n- `cat /dp/agentic_coding_flywheel_setup/README.md` - Read full README\n- `cat /dp/agentic_coding_flywheel_setup/acfs.manifest.yaml` - Check manifest structure\n\n### 1.2 Code Investigation\n- Launch code investigation agent to understand:\n - Tool installation mechanism\n - Checksum verification (checksums.yaml)\n - Manifest structure and tool definitions\n - Dependency ordering\n - AGENTS.md generation\n\n### 1.3 CLI Command Verification\n```bash\n# Verify each documented command works\nacfs --help 2>&1 | head -20\nacfs install --help 2>&1 | head -10\nacfs verify --help 2>&1 | head -10\n\n# Test actual functionality\nacfs status 2>&1 | head -10\n```\n\n### 1.4 Security Verification\n```bash\n# Check checksums file\ncat /dp/agentic_coding_flywheel_setup/checksums.yaml | head -20\n\n# Verify checksum mechanism\nacfs verify --checksums 2>&1 | head -10 || echo \"No verify command\"\n```\n\n### 1.5 Project State Review\n- Check beads in /dp/agentic_coding_flywheel_setup/.beads/\n- Review recent commits: `cd /dp/agentic_coding_flywheel_setup && git log --oneline -20`\n\n## Phase 2: Analysis (SYNTHESIZE UNDERSTANDING)\n\n### 2.1 Core Capabilities Verification\nDocument findings for each area:\n- [ ] Tool installation mechanism\n- [ ] Checksum verification process\n- [ ] Manifest structure (tools, dependencies)\n- [ ] AGENTS.md generation\n- [ ] Environment setup\n\n### 2.2 Synergy Verification\nACFS is the bootstrap tool - verify it installs these:\n- [ ] All flywheel tools (ntm, mail, br, etc.)\n- [ ] Dependencies (rust, bun, uv, etc.)\n- [ ] Skills and integrations\n\n### 2.3 Manifest Verification\n```bash\n# Count tools in manifest\ngrep -c \"^ [a-z]\" /dp/agentic_coding_flywheel_setup/acfs.manifest.yaml 2>/dev/null || echo \"Count manually\"\n\n# Check dependencies\ngrep -A5 \"depends_on\" /dp/agentic_coding_flywheel_setup/acfs.manifest.yaml | head -20\n```\n\n## Phase 3: Revision (UPDATE DESCRIPTIONS)\n\n### 3.1 Update apps/web/lib/flywheel.ts\nUpdate acfs entry with VERIFIED information:\n- `tagline`: Bootstrap installer for flywheel\n- `description`: Tool installation and verification\n- `deepDescription`: How manifest-based installation works\n- `features`: Verified capabilities (checksums, AGENTS.md)\n- `cliCommands`: Only commands that actually work\n- `connectsTo`: All tools it installs\n\n### 3.2 Update apps/web/lib/tldr-content.ts\nUpdate TldrFlywheelTool entry with:\n- `briefDescription`: Technical summary\n- `bulletPoints`: Verified capabilities\n- `synergyExamples`: Bootstrap workflow\n\n## Phase 4: Testing (VERIFY CHANGES)\n\n### 4.1 TypeScript Compilation\n```bash\ncd apps/web && npx tsc --noEmit 2>&1 | head -20\n```\n\n### 4.2 Unit Tests\n```bash\n# Test acfs entry structure\nnode -e \"\nconst { flywheelTools } = require('./lib/flywheel');\nconst acfs = flywheelTools.find(t => t.id === 'acfs');\nconsole.log('Testing acfs entry...');\nconsole.assert(acfs, 'acfs entry exists');\nconsole.assert(acfs.features?.length > 0, 'has features');\nconsole.assert(acfs.cliCommands?.length > 0, 'has commands');\nconsole.log('All assertions passed');\n\"\n```\n\n### 4.3 E2E Test: Installation Verification\n```bash\n#\\!/bin/bash\nset -euo pipefail\nLOG=/tmp/acfs-e2e-$(date +%Y%m%d-%H%M%S).log\n\necho \"=== ACFS E2E Test ===\" | tee $LOG\necho \"Started: $(date)\" | tee -a $LOG\n\n# Test 1: Status\necho \"Test 1: Status...\" | tee -a $LOG\nacfs status 2>&1 | head -15 | tee -a $LOG\n\n# Test 2: Verify checksums\necho \"Test 2: Verify...\" | tee -a $LOG\nacfs verify 2>&1 | head -10 | tee -a $LOG || echo \"No verify command\"\n\n# Test 3: List tools\necho \"Test 3: List...\" | tee -a $LOG\nacfs list 2>&1 | head -10 | tee -a $LOG || echo \"No list command\"\n\necho \"=== All Tests Passed ===\" | tee -a $LOG\necho \"Log: $LOG\"\n```\n\n### 4.4 Content Diff Verification\n```bash\necho \"=== Changes Made ===\"\ndiff $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts || true\ndiff $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts || true\n```\n\n## Phase 5: Completion (FINALIZE)\n\n### 5.1 Rollback Procedure (if tests fail)\n```bash\ncp $SNAPSHOT_DIR/flywheel.ts.before apps/web/lib/flywheel.ts\ncp $SNAPSHOT_DIR/tldr-content.ts.before apps/web/lib/tldr-content.ts\necho \"Rolled back to pre-exploration state\"\n```\n\n### 5.2 Sync Changes\n```bash\nbr update bd-flfe --status done\nbr sync --flush-only\n```\n\n### 5.3 Final Verification\n- [ ] All tests pass\n- [ ] TypeScript compiles without errors\n- [ ] Manifest structure documented correctly\n- [ ] Checksum mechanism documented\n- [ ] No broken links or references","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:02:56.720155271Z","created_by":"ubuntu","updated_at":"2026-01-27T03:35:19.069946681Z","closed_at":"2026-01-27T03:35:19.069916263Z","close_reason":"ACFS deep exploration complete: verified v0.5.0, acfs doctor (47+ checks), acfs update (category-specific), acfs cheatsheet (50+ aliases), acfs session. Updated tldr-content.ts.","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-gsjqf","title":"Fix update.sh Claude Code channel: bare 'claude update' downgrades to stable (GH #125)","description":"## Context\nGitHub issue #125 filed by joyshmitz (2026-02-08). The install path was fixed (commit 4d0174f0 — agents.sh now uses @latest), but update.sh still calls bare \"claude update\" which forces the stable channel.\n\n## Problem\nrun_cmd_claude_update() in scripts/lib/update.sh (line 1230) calls bare \"claude update\" at lines 1254, 1260, 1267, 1269, 1274. This always uses the stable channel. On a SUCCESSFUL update, users get silently downgraded from latest (e.g., 2.1.37) to stable (e.g., 2.1.25). The fallback path (update_run_verified_installer claude latest at line 1157) correctly uses latest, but only fires when \"claude update\" FAILS.\n\nAdditionally, the uca alias in acfs/zsh/acfs.zshrc line 158 uses bare \"~/.local/bin/claude update\" (also stable channel). The full alias chains claude update with codex and gemini updates: alias uca='~/.local/bin/claude update && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n\n## CRITICAL: --channel flag DOES NOT EXIST\nVerified by running: claude update --help => only -h/--help available. The originally proposed fix (claude update --channel latest) is INVALID. This was confirmed by running the actual command on this VPS.\n\n## Chosen Fix: Replace run_cmd_claude_update() with verified installer\nThe update_run_verified_installer function already exists at update.sh:715-737 and correctly handles the latest channel. It:\n1. Looks up the installer URL from KNOWN_INSTALLERS[claude] = \"https://claude.ai/install.sh\" (security.sh:183)\n2. Fetches the installer script\n3. Verifies its SHA-256 checksum against checksums.yaml\n4. Pipes to bash with args (e.g., \"latest\")\nThe fallback path at line 1157 already uses this: run_cmd \"Claude Code (reinstall)\" update_run_verified_installer claude latest\n\nThe fix replaces the entire run_cmd_claude_update() function body with a call to update_run_verified_installer, preserving the logging/error-handling wrapper while switching the underlying mechanism from bare \"claude update\" to the verified installer with the \"latest\" argument.\n\nFor the uca alias, replace the \"~/.local/bin/claude update\" portion with an inline verified-installer invocation, OR redirect through a wrapper script, OR call the update function. The alias must preserve the codex and gemini update chain.\n\n## Affected Files\n- scripts/lib/update.sh (lines 1229-1296, run_cmd_claude_update(); lines 1150-1161, caller)\n- acfs/zsh/acfs.zshrc (line 158, uca alias)\n- scripts/lib/security.sh (line 183, KNOWN_INSTALLERS[claude] — reference only)\n- scripts/lib/agents.sh (line 21, CLAUDE_PACKAGE — reference only; line 148, verified installer call — reference for pattern)\n\n## Sub-tasks (see child beads)\n1. Audit: scan for ALL bare \"claude update\" occurrences (bd-gsjqf.1)\n2. Rewrite run_cmd_claude_update() to use verified installer (bd-gsjqf.2)\n3. Fix uca alias and any other occurrences found in audit (bd-gsjqf.3)\n4. Comprehensive testing (bd-gsjqf.4)\n5. End-to-end verification and GH #125 close (bd-gsjqf.5)","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-02-08T18:16:19.008573344Z","created_by":"ubuntu","updated_at":"2026-02-11T16:41:34.439322663Z","closed_at":"2026-02-11T16:41:34.439303427Z","close_reason":"All sub-tasks complete (.1-.5). Replaced bare 'claude update' with verified installer (update_run_verified_installer claude latest) in update.sh and uca alias. 9/9 tests pass, live system on latest channel (2.1.39). GH#125 closed.","external_ref":"gh:Dicklesworthstone/agentic_coding_flywheel_setup#125","source_repo":".","compaction_level":0,"original_size":0,"labels":["bug","claude-code","update"],"comments":[{"id":56,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"IMPORTANT: Before implementing, verify that 'claude update --channel latest' is a valid CLI flag. Run 'claude update --help' to check. If --channel does not exist, the fallback approach is: replace the native claude update call with update_run_verified_installer() using 'latest' as the primary path (this function already exists in update.sh and correctly handles the latest channel). Also search for any other aliases that might call bare 'claude update' besides the uca alias (e.g., grep for 'claude update' across all acfs shell configs).","created_at":"2026-02-08T18:59:20Z"},{"id":57,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"BUG IN PROPOSED FIX: The uca alias change oversimplifies. Bead says change alias uca from just claude update to claude update --channel latest. But the ACTUAL alias at line 158 is: alias uca='~/.local/bin/claude update && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'. Following the bead instructions literally would DESTROY the codex and gemini update steps. CORRECT FIX: Change only the claude update portion to claude update --channel latest, preserving the full chain: alias uca='~/.local/bin/claude update --channel latest && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'","created_at":"2026-02-08T20:07:41Z"},{"id":58,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"CRITICAL: --channel flag DOES NOT EXIST. Running 'claude update --help' shows only -h/--help as options. The proposed fix (claude update --channel latest) will either fail with unknown option or be silently ignored. VERIFIED BY RUNNING: claude update --help => 'Usage: claude update|upgrade [options] Options: -h, --help Display help for command'. THE FALLBACK APPROACH FROM COMMENT 1 IS NOW THE PRIMARY FIX: Replace all 5 bare 'claude update' calls in run_cmd_claude_update() with update_run_verified_installer claude latest (which already exists in update.sh and correctly handles the latest channel at line 1157). For the uca alias, replace '~/.local/bin/claude update' with a function call or inline the verified installer logic. This is a larger change than originally scoped — the function run_cmd_claude_update() essentially becomes a wrapper around update_run_verified_installer. ALTERNATIVE: Check if claude stores channel info in ~/.claude/ config files and whether the update mechanism reads it. If so, we could set the channel config once rather than changing every update call.","created_at":"2026-02-08T21:18:19Z"},{"id":59,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"FOLLOW-UP: The round 4 uca alias fix (comment 2) also used --channel latest which does not exist. The corrected alias suggestion (preserving codex/gemini chain) is also wrong in its claude update portion. The uca alias fix depends on the chosen approach for run_cmd_claude_update. If we switch to update_run_verified_installer, the alias would need to call a function or script that wraps update_run_verified_installer, since aliases cannot easily call bash functions defined in update.sh. Alternatively, the uca alias could be changed to invoke the acfs-update command directly (e.g., 'acfs-update --only=claude') if such a flag exists, or to call the official installer inline: alias uca='curl -fsSL https://claude.ai/install.sh | sh -s -- --channel latest && ...' (need to verify the exact installer URL and flags).","created_at":"2026-02-08T21:18:40Z"},{"id":69,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"REVIEW FIX — Version/Channel Verification Method: `claude --version` outputs \"2.1.37 (Claude Code)\" — it does NOT show the channel name. To verify channel programmatically, use:\n```\nnpm view @anthropic-ai/claude-code dist-tags\n=> { stable: \"2.1.25\", latest: \"2.1.37\", next: \"2.1.37\" }\n```\nCompare installed version against dist-tags. If installed matches `latest` dist-tag, we are on the latest channel. If it matches `stable` dist-tag, we got downgraded. This verification method is used in bd-gsjqf.4 (Test 8) and bd-gsjqf.5 (E2E Steps 1, 5, 7).","created_at":"2026-02-08T21:39:22Z"},{"id":75,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"HISTORICAL NOTE for future readers: Comments 1-4 on this epic document the DISCOVERY PROCESS of the bug. Comments 1 and 2 suggested --channel latest as a fix. Comment 3 discovered --channel does not exist. Comment 4 explored alternatives. The FINAL APPROACH is documented in the epic description body and in the sub-task beads (.1-.5). When implementing, IGNORE comments 1-2 fix suggestions and follow the sub-task descriptions instead.","created_at":"2026-02-08T21:40:26Z"},{"id":76,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"CONFIRMED: install.sh DOES accept \"latest\" as $1. Downloaded and inspected https://claude.ai/install.sh:\n- Line 6: TARGET=\"$1\"\n- Line 9-10: Validates TARGET against ^(stable|latest|VERSION_REGEX)$\n- Line 98-99: Always downloads latest binary from GCS bucket\n- Line 142: \"$binary_path\" install ${TARGET:+\"$TARGET\"} — passes TARGET to binary self-install\nSo `bash -s -- latest` correctly installs the latest-channel version. The install.sh also has its OWN checksum verification (downloads manifest.json, verifies binary hash). This means even the uca alias Option A (curl|bash) gets checksumming from install.sh itself, PLUS ACFS adds an outer SHA-256 check on install.sh via security.sh. Two layers of verification.","created_at":"2026-02-08T22:04:17Z"},{"id":82,"issue_id":"bd-gsjqf","author":"Dicklesworthstone","text":"FRESH-EYES REVIEW COMPLETE (2026-02-09). Summary of findings applied to sub-tasks:\n\nbd-gsjqf.1: Added 9 missing documentation/comment occurrences (total scope: 15 items, not 6)\nbd-gsjqf.2: (1) INTENTIONAL marker needed on security fallback line for Test 7 compat, (2) lines 2151/2187 need to be in this bead's scope, (3) VERBOSE streaming note, (4) \\!= display artifact note\nbd-gsjqf.3: (1) Explicit 7-file documentation list added, (2) CRITICAL: must run manifest generate after acfs.manifest.yaml edit, (3) Final uca alias with pipefail confirmed\nbd-gsjqf.4: (1) CRITICAL BUG: ((PASS++)) crashes under set -e when PASS=0 — must use PASS=$((PASS + 1)), (2) Test 7 needs INTENTIONAL exclusion, (3) Confirmed test stubbing works without extras\nbd-gsjqf.5: (1) Step 6 requires zsh shell, (2) Add manifest SHA256 verification step\n\nAll findings are documented as comments on the respective sub-tasks. Dependency graph: .1 -> .2 -> .3 -> .4 -> .5 (with .3 also depending on .1). No cycles. bv diagnostics clean.\n","created_at":"2026-02-09T00:36:10Z"}]} +{"id":"bd-gsjqf.1","title":"Audit: scan all files for bare \"claude update\" occurrences","description":"## What\nScan the entire ACFS codebase for every occurrence of bare \"claude update\" (without --channel or equivalent). This determines the full scope of the fix before any code changes.\n\n## Why (P1 — scope discovery)\nWe know about 5 occurrences in update.sh and 1 in acfs.zshrc, but there could be others in documentation, other shell configs, helper scripts, or CI workflows. Missing even one occurrence means users can still get silently downgraded.\n\n## Scan Commands\n```bash\n# Primary search: all shell scripts\ngrep -rn \"claude update\" scripts/ acfs/ --include=\"*.sh\" --include=\"*.zsh\" --include=\"*.zshrc\" --include=\"*.bashrc\"\n\n# Broader search: all files (might find docs, CI, configs)\ngrep -rn \"claude update\" . --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.beads\n\n# Specifically check for the alias pattern\ngrep -rn \"uca\\|claude update\" acfs/\n```\n\n## Expected Findings (from prior analysis)\n1. scripts/lib/update.sh:1254 — `claude update 2>&1 | tee -a ...`\n2. scripts/lib/update.sh:1260 — `claude update >> ...`\n3. scripts/lib/update.sh:1267 — `claude update || exit_code=$?`\n4. scripts/lib/update.sh:1269 — `claude update >/dev/null 2>&1 || ...`\n5. scripts/lib/update.sh:1274 — `output=$(claude update 2>&1) || ...`\n6. acfs/zsh/acfs.zshrc:158 — uca alias with `~/.local/bin/claude update`\n\n## Deliverable\nA comment on this bead listing EVERY occurrence with file:line, plus whether each is:\n- (a) bare \"claude update\" that needs fixing\n- (b) already using verified installer (reference only)\n- (c) documentation/comment (may need updating)\n- (d) test/CI (may need updating)\n\n## Acceptance Criteria\n- Zero occurrences of bare \"claude update\" remain unaccounted for\n- Each finding classified as (a)/(b)/(c)/(d)\n- Full scope documented for bd-gsjqf.2 and bd-gsjqf.3 to consume","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T21:25:03.950395729Z","created_by":"ubuntu","updated_at":"2026-02-11T16:19:10.295561371Z","closed_at":"2026-02-11T16:19:10.295538899Z","close_reason":"Audit complete: found 9 occurrences needing fix across update.sh (6 exec + 2 doc), acfs.zshrc (1 alias), and acfs.manifest.yaml (1 doc). Security fallback at ~line 1207 already correct.","source_repo":".","compaction_level":0,"original_size":0,"labels":["audit","claude-code","update"],"dependencies":[{"issue_id":"bd-gsjqf.1","depends_on_id":"bd-gsjqf","type":"parent-child","created_at":"2026-02-08T21:25:03.950395729Z","created_by":"ubuntu"}],"comments":[{"id":62,"issue_id":"bd-gsjqf.1","author":"Dicklesworthstone","text":"BACKGROUND: This audit is the first step because we discovered in review round 6 that the scope was larger than initially thought. We knew about 5 occurrences in run_cmd_claude_update() and 1 in the uca alias, but there may be more in docs, CI, other shell configs, or helper scripts. The audit prevents us from fixing 6/N occurrences and thinking we are done. PRIORITY: Run this BEFORE any code changes so the scope document is complete and unchanging while .2 and .3 are implemented.","created_at":"2026-02-08T21:30:19Z"},{"id":77,"issue_id":"bd-gsjqf.1","author":"Dicklesworthstone","text":"FRESH-EYES: MISSING DOCUMENTATION OCCURRENCES.\n\nThe audit lists only 6 expected findings (all code invocations). Prior analysis found 9 MORE in documentation/comments/help-text that must also be tracked:\n\n7. scripts/lib/update.sh:1232 — local cmd_display=\"claude update\" (string literal — update display string)\n8. scripts/lib/update.sh:2151 — \"agents: Claude Code (claude update)\" (help text)\n9. scripts/lib/update.sh:2187 — \"claude update\" (troubleshooting text)\n10. acfs.manifest.yaml:652 — documentation note referencing \"built-in claude update command\"\n11. README.md:620 — update table showing \"claude update\"\n12. README.md:1242 — update instruction text\n13. acfs/onboard/lessons/08_keeping_updated.md:131 — troubleshooting section\n14. apps/web/components/lessons/keeping-updated-lesson.tsx:218 — web lesson component\n15. PLAN_TO_CREATE_ACFS.md:190 — old alias definition (leave as historical)\n\nClassification:\n- Items 1-5: Code invocations → fix in bd-gsjqf.2\n- Item 6: Alias → fix in bd-gsjqf.3\n- Item 7: String literal → fix in bd-gsjqf.2 (same file)\n- Items 8-9: Help text → fix in bd-gsjqf.2 (same file) or bd-gsjqf.3\n- Items 10-14: Documentation → fix in bd-gsjqf.3\n- Item 15: Planning doc → leave as historical record\n\nThe full 15-item list must be the scope document that bd-gsjqf.2 and bd-gsjqf.3 consume.\n","created_at":"2026-02-09T00:35:16Z"}]} +{"id":"bd-gsjqf.2","title":"Rewrite run_cmd_claude_update() to use verified installer","description":"## What\nReplace the body of `run_cmd_claude_update()` (update.sh lines 1230-1296) so it calls `update_run_verified_installer claude latest` instead of bare `claude update`. This is the core fix for GH #125.\n\n## Why\n`claude update` (bare) always uses the stable channel. There is NO `--channel` flag (verified: `claude update --help` shows only `-h/--help`). The function currently calls bare `claude update` at 5 separate locations (lines 1254, 1260, 1267, 1269, 1274), all of which silently downgrade users from the latest channel (e.g., 2.1.37) to stable (e.g., 2.1.25).\n\nThe `update_run_verified_installer` function (update.sh lines 715-737) already exists and correctly handles the latest channel by:\n1. Looking up installer URL from `KNOWN_INSTALLERS[claude]` = `https://claude.ai/install.sh` (security.sh:183)\n2. Fetching the installer script\n3. Verifying its SHA-256 checksum against checksums.yaml\n4. Piping to `bash -s -- latest` (the \"latest\" arg selects the channel)\n\nThis function is already used as the fallback at line 1157: `run_cmd \"Claude Code (reinstall)\" update_run_verified_installer claude latest`\n\n## Current Code (lines 1230-1296)\n```bash\nrun_cmd_claude_update() {\n local desc=\"Claude Code (native update)\"\n local cmd_display=\"claude update\"\n log_to_file \"Running: $cmd_display\"\n if [[ \"$DRY_RUN\" == \"true\" ]]; then\n log_item \"skip\" \"$desc\" \"dry-run: $cmd_display\"\n return 0\n fi\n log_item \"run\" \"$desc\"\n local exit_code=0\n # ... 5 branches each calling bare \"claude update\" ...\n # Lines 1254, 1260, 1267, 1269, 1274\n}\n```\n\n## Proposed Fix\nReplace the 5-branch if/elif structure with a single call to `update_run_verified_installer`. Preserve the logging/status/dry-run wrapper:\n\n```bash\nrun_cmd_claude_update() {\n local desc=\"Claude Code (verified installer update)\"\n local cmd_display=\"update_run_verified_installer claude latest\"\n log_to_file \"Running: $cmd_display\"\n if [[ \"$DRY_RUN\" == \"true\" ]]; then\n log_item \"skip\" \"$desc\" \"dry-run: $cmd_display\"\n return 0\n fi\n log_item \"run\" \"$desc\"\n local exit_code=0\n local output=\"\"\n output=$(update_run_verified_installer claude latest 2>&1) || exit_code=$?\n [[ -n \"$output\" ]] && log_to_file \"Output: $output\"\n if [[ \"$VERBOSE\" == \"true\" ]] && [[ \"$QUIET\" \\!= \"true\" ]]; then\n [[ -n \"$output\" ]] && echo \"$output\"\n fi\n if [[ -n \"${UPDATE_LOG_FILE:-}\" ]] && [[ -n \"$output\" ]]; then\n echo \"$output\" >> \"$UPDATE_LOG_FILE\"\n fi\n if [[ $exit_code -eq 0 ]]; then\n # ... same success handling as before (lines 1278-1286) ...\n return 0\n else\n # ... same failure handling as before (lines 1287-1295) ...\n return 1\n fi\n}\n```\n\n## Caller Flow Impact\nThe caller at lines 1150-1161 currently does:\n```\nif \\! run_cmd_claude_update; then\n # fallback to update_run_verified_installer\n run_cmd \"...\" update_run_verified_installer claude latest\nfi\n```\nAfter this fix, both the primary and fallback paths use the same mechanism. The fallback is still valuable as defense-in-depth (different error handling path), but we should add a comment noting this.\n\n## Key Consideration: Security Dependency\n`update_run_verified_installer` requires `update_require_security` to succeed (needs security.sh loaded and checksums.yaml present). The current function has no such dependency. We must handle the case where security is unavailable — either:\n(a) Fall back to bare `claude update` (loses the fix but does not break)\n(b) Fail with a clear error (safer, forces user to fix security setup)\n(c) Use a non-verified installer call (less safe)\n\nRecommendation: Option (a) with a warning, so existing users without security.sh do not have their update break. Log a prominent warning that the update is using stable channel.\n\n## Files Changed\n- scripts/lib/update.sh (lines 1230-1296: function rewrite; lines 1150-1161: add comment)\n\n## Acceptance Criteria\n- [ ] `run_cmd_claude_update()` calls `update_run_verified_installer claude latest` as primary path\n- [ ] Dry-run mode still works\n- [ ] Verbose/quiet/log-file output modes still work\n- [ ] Security-unavailable fallback gracefully degrades with warning\n- [ ] No bare \"claude update\" calls remain in the function\n- [ ] Caller flow at lines 1150-1161 still works (fallback is redundant but harmless)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T21:28:01.744937393Z","created_by":"ubuntu","updated_at":"2026-02-11T16:26:15.750488395Z","closed_at":"2026-02-11T16:26:15.750460422Z","close_reason":"implemented: run_cmd_claude_update() now uses update_run_verified_installer claude latest","source_repo":".","compaction_level":0,"original_size":0,"labels":["claude-code","core-fix","update"],"dependencies":[{"issue_id":"bd-gsjqf.2","depends_on_id":"bd-gsjqf","type":"parent-child","created_at":"2026-02-08T21:28:01.744937393Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.2","depends_on_id":"bd-gsjqf.1","type":"blocks","created_at":"2026-02-08T21:29:11.260478147Z","created_by":"ubuntu"}],"comments":[{"id":63,"issue_id":"bd-gsjqf.2","author":"Dicklesworthstone","text":"REASONING: We chose to rewrite the function body rather than (a) adding --channel latest (does not exist), (b) replacing run_cmd_claude_update calls with direct update_run_verified_installer calls (would lose logging/dry-run/verbose infrastructure), or (c) patching each of the 5 bare \"claude update\" strings (fragile, error-prone, does not simplify the code). The rewrite approach preserves the outer shell (logging, dry-run, status reporting) while replacing the inner mechanism entirely. KEY INSIGHT: The function run_cmd_claude_update is called from ONE place (line 1154). The caller already has a fallback to update_run_verified_installer (line 1157). After our fix, both paths use the same mechanism, which is redundant but harmless (defense-in-depth). SECURITY NOTE: update_run_verified_installer requires security.sh and checksums.yaml. If these are missing, the function will fail. We need a graceful fallback — see the \"Key Consideration\" section in the description.","created_at":"2026-02-08T21:30:23Z"},{"id":67,"issue_id":"bd-gsjqf.2","author":"Dicklesworthstone","text":"REVIEW FIX 1 — Security fallback missing from proposed code: The description recommends Option (a) (fall back to bare \"claude update\" with warning) but the proposed code snippet does NOT implement this. The actual implementation MUST check security availability BEFORE calling update_run_verified_installer. Corrected code pattern:\n\n```bash\nrun_cmd_claude_update() {\n local desc=\"Claude Code\"\n log_to_file \"Running: Claude Code update\"\n if [[ \"$DRY_RUN\" == \"true\" ]]; then\n log_item \"skip\" \"$desc\" \"dry-run\"\n return 0\n fi\n log_item \"run\" \"$desc\"\n local exit_code=0\n local output=\"\"\n \n if update_require_security; then\n # PRIMARY PATH: verified installer with latest channel\n output=$(update_run_verified_installer claude latest 2>&1) || exit_code=$?\n else\n # FALLBACK: bare \"claude update\" (stable channel) with WARNING\n log_to_file \"WARNING: security.sh unavailable — falling back to bare claude update (STABLE channel, not latest)\"\n if [[ \"$QUIET\" \\!= \"true\" ]]; then\n echo -e \" ${YELLOW}[warn]${NC} Security unavailable — using stable channel update\"\n fi\n output=$(claude update 2>&1) || exit_code=$?\n fi\n \n [[ -n \"$output\" ]] && log_to_file \"Output: $output\"\n if [[ \"$VERBOSE\" == \"true\" ]] && [[ \"$QUIET\" \\!= \"true\" ]]; then\n [[ -n \"$output\" ]] && echo \"$output\"\n fi\n if [[ -n \"${UPDATE_LOG_FILE:-}\" ]] && [[ -n \"$output\" ]]; then\n echo \"$output\" >> \"$UPDATE_LOG_FILE\"\n fi\n # ... success/failure handling preserved from lines 1278-1295 ...\nfi\n```\n\nThis ensures: (1) latest channel when security is available, (2) graceful degradation with visible warning when not, (3) never breaks for users without security.sh.","created_at":"2026-02-08T21:37:39Z"},{"id":71,"issue_id":"bd-gsjqf.2","author":"Dicklesworthstone","text":"REVIEW FIX 2 — Include unit tests in implementation: When implementing the function rewrite, ALSO add the test functions for Tests 1-5 from bd-gsjqf.4 into the test script. The implementer of .2 should:\n1. Rewrite run_cmd_claude_update() \n2. Create scripts/tests/test_update_channel.sh with at minimum Tests 1-5 (static analysis, dry-run, mock instrumentation, security fallback)\n3. Run Tests 1-5 and verify they pass\n4. bd-gsjqf.4 will add Tests 6-8 (alias, completeness sweep, channel verification) after .3 is done\n\nThis ensures the core fix is tested BEFORE the alias fix, and the test script grows incrementally.","created_at":"2026-02-08T21:39:43Z"},{"id":74,"issue_id":"bd-gsjqf.2","author":"Dicklesworthstone","text":"UX IMPROVEMENT IDEA: After the function rewrite, the log output should clearly indicate WHICH channel was used. Currently the desc is just \"Claude Code (native update)\". Change it to include channel info:\n- When security available: desc=\"Claude Code (latest channel via verified installer)\"\n- When fallback used: desc=\"Claude Code (stable channel — security unavailable)\"\n\nThis makes it immediately visible in `acfs-update` output and logs which path was taken, helping users self-diagnose if they see a downgrade. The --verbose output should also show the version before/after comparison (this is already done by the caller at lines 1163-1166 with VERSION_BEFORE/VERSION_AFTER, so just ensure the desc change does not interfere).","created_at":"2026-02-08T21:40:18Z"},{"id":78,"issue_id":"bd-gsjqf.2","author":"Dicklesworthstone","text":"FRESH-EYES REVIEW (3 findings):\n\nFIX 1 (CRITICAL): Add INTENTIONAL marker to security fallback invocation.\nThe security fallback in comment #2's corrected code has:\n output=$(claude update 2>&1) || exit_code=$?\nThis bare invocation will be caught by Test 7 (completeness sweep in bd-gsjqf.4) as a false positive. Add a trailing comment:\n output=$(claude update 2>&1) || exit_code=$? # INTENTIONAL: security-unavailable fallback (bd-gsjqf)\nAnd add \"INTENTIONAL\" to Test 7's exclusion grep in bd-gsjqf.4.\n\nFIX 2: Add lines 2151 and 2187 to this bead's scope.\nThese help/troubleshooting text lines in update.sh reference bare \"claude update\" but are not assigned to any sub-task:\n- Line 2151: \"agents: Claude Code (claude update)\" -> \"agents: Claude Code (verified installer, latest channel)\"\n- Line 2187: \"claude update\" -> \"curl -fsSL https://claude.ai/install.sh | bash -s -- latest\"\nSame file as the main fix, two simple string replacements. Should be done alongside the function rewrite.\n\nNOTE: VERBOSE streaming trade-off. The proposed code (in comment #2) buffers all output via output=$(...) then displays after completion. The original code streams via tee in VERBOSE+LOG mode. This is a deliberate simplification — the verified installer runs ~5-10 seconds so buffering is acceptable. Implementer CAN restore streaming if desired but the simpler approach is recommended.\n\nNOTE: \"\\!=\" in proposed code is a br display artifact. In actual code use \"!=\" (no backslash).\n","created_at":"2026-02-09T00:35:26Z"}]} +{"id":"bd-gsjqf.3","title":"Fix uca alias and any other bare \"claude update\" occurrences found in audit","description":"## What\nFix the `uca` alias in `acfs/zsh/acfs.zshrc` line 158 and any other bare `claude update` occurrences discovered by the audit (bd-gsjqf.1). Each occurrence needs a fix appropriate to its context.\n\n## Why\nThe uca alias is a convenience shortcut that chains 3 update commands. It currently starts with bare `~/.local/bin/claude update` which uses the stable channel. This is a separate occurrence from the `run_cmd_claude_update()` function — fixing update.sh alone is insufficient because users who type `uca` directly still get downgraded.\n\n## Current uca Alias (acfs.zshrc:158)\n```bash\nalias uca='~/.local/bin/claude update && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n```\n\n## CRITICAL: Preserve the Full Chain\nThe alias updates THREE tools, not just Claude:\n1. `~/.local/bin/claude update` — Claude Code (BROKEN: stable channel)\n2. `bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex` — Codex CLI\n3. `bun install -g --trust @google/gemini-cli@latest` — Gemini CLI\n\nA naive fix that replaces only the Claude part and drops the rest would DESTROY codex and gemini updates. This was caught in review round 4 and must not regress.\n\n## Fix Options for uca Alias\n\n### Option A: Inline the verified installer (simplest)\n```bash\nalias uca='(curl -fsSL https://claude.ai/install.sh | bash -s -- latest) && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n```\n**Pro**: Self-contained, no function dependency. **Con**: No SHA-256 verification (security.sh checksum not available in alias context). Hardcodes URL.\n\n### Option B: Call acfs-update with --only flag (if available)\n```bash\nalias uca='acfs-update --only=claude,codex,gemini'\n```\n**Pro**: Single source of truth. **Con**: Need to verify `--only` flag exists. Changes alias semantics.\n\n### Option C: Source update functions and call directly\n```bash\nalias uca='source ~/.config/acfs/scripts/lib/update.sh && run_cmd_claude_update && (bun install -g --trust @openai/codex@latest || ...) && ...'\n```\n**Pro**: Uses the fixed function. **Con**: Sourcing entire update.sh is heavyweight for an alias.\n\n### Option D: Create a thin wrapper script\nCreate `~/.local/bin/update-claude-latest` that wraps `update_run_verified_installer claude latest`, then:\n```bash\nalias uca='update-claude-latest && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n```\n**Pro**: Clean separation, verified installer. **Con**: Another script to maintain.\n\n### Recommendation\nOption A is the pragmatic choice for now — it matches what the verified installer does (same URL) minus the SHA-256 check, which is acceptable for interactive user-initiated updates. The security concern is lower for aliases (user is actively running it) vs automated update.sh (runs unattended). Add a comment above the alias noting this.\n\n## Additional Occurrences (from audit)\nThe bd-gsjqf.1 audit may find additional occurrences in:\n- Documentation files (update instructions in README, etc.)\n- Other shell config files\n- CI workflows\n- Test files\nEach should be fixed or annotated as appropriate.\n\n## Files Changed\n- acfs/zsh/acfs.zshrc (line 158: uca alias rewrite)\n- Any additional files found by bd-gsjqf.1 audit\n\n## Acceptance Criteria\n- [ ] uca alias uses latest channel for Claude update\n- [ ] Codex and Gemini update portions PRESERVED EXACTLY\n- [ ] No bare \"claude update\" in acfs.zshrc\n- [ ] All other occurrences from audit addressed\n- [ ] Comment above alias explains the latest channel choice","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T21:28:24.290689513Z","created_by":"ubuntu","updated_at":"2026-02-11T16:26:16.822709686Z","closed_at":"2026-02-11T16:26:16.822686402Z","close_reason":"implemented: uca alias and other bare claude update occurrences fixed","source_repo":".","compaction_level":0,"original_size":0,"labels":["alias","claude-code","update"],"dependencies":[{"issue_id":"bd-gsjqf.3","depends_on_id":"bd-gsjqf","type":"parent-child","created_at":"2026-02-08T21:28:24.290689513Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.3","depends_on_id":"bd-gsjqf.1","type":"blocks","created_at":"2026-02-08T21:40:48.762004535Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.3","depends_on_id":"bd-gsjqf.2","type":"blocks","created_at":"2026-02-08T21:29:12.568336336Z","created_by":"ubuntu"}],"comments":[{"id":64,"issue_id":"bd-gsjqf.3","author":"Dicklesworthstone","text":"CRITICAL CONTEXT: The uca alias was the source of a cascading review bug. In round 4, the fix proposed truncating the alias to just \"claude update --channel latest\", which would have DESTROYED the codex and gemini update commands. In round 6, we discovered --channel does not exist at all. So this bead has two landmines to avoid: (1) do not truncate the alias chain, (2) do not use --channel. The recommended fix (Option A: inline curl to install.sh) trades SHA-256 verification for simplicity. This is acceptable because the uca alias is run interactively by the user (not automated), and the installer URL is the same official URL used by update_run_verified_installer. If the user wants verification, they should use `acfs-update` instead of `uca`.","created_at":"2026-02-08T21:30:29Z"},{"id":68,"issue_id":"bd-gsjqf.3","author":"Dicklesworthstone","text":"REVIEW FIX 2 — Option A needs pipefail and installer arg verification:\n\n(A) PIPEFAIL BUG: Without `set -o pipefail`, if curl fails with `-f`, the bash process gets empty stdin and may exit 0, making the alias silently skip the Claude update while proceeding with codex/gemini. Corrected Option A:\n```bash\nalias uca='(set -o pipefail; curl -fsSL https://claude.ai/install.sh | bash -s -- latest) && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n```\n\n(B) INSTALLER ARG VERIFICATION: We assume install.sh accepts `latest` as a bare positional arg (based on agents.sh:148 pattern `bash -s -- latest`). Before implementing, we MUST verify this by testing: `curl -fsSL https://claude.ai/install.sh | bash -s -- latest` and checking that it installs the latest-channel version (2.1.37), not stable (2.1.25). Compare with `npm view @anthropic-ai/claude-code dist-tags` to confirm.\n\n(C) ALTERNATIVE: If install.sh does NOT accept positional args for channel selection, a different approach is needed entirely — possibly `npm install -g @anthropic-ai/claude-code@latest` or `bun install -g @anthropic-ai/claude-code@latest`.","created_at":"2026-02-08T21:37:51Z"},{"id":72,"issue_id":"bd-gsjqf.3","author":"Dicklesworthstone","text":"REVIEW FIX 3 — Pre-implementation verification step: Before changing the alias, run this test to confirm install.sh accepts \"latest\" as a positional arg:\n```bash\n# DRY TEST: Download installer and check if it accepts channel args\ncurl -fsSL https://claude.ai/install.sh > /tmp/claude_install.sh\nhead -50 /tmp/claude_install.sh # Examine arg parsing\ngrep -i \"channel\\|latest\\|stable\\|\\$1\\|\\${1\" /tmp/claude_install.sh | head -20\n```\nIf the installer does NOT use $1 for channel selection, the entire verified installer approach may need revision. In that case, consider `bun install -g @anthropic-ai/claude-code@latest` as the update mechanism instead (this is what CLAUDE_PACKAGE in agents.sh line 21 references: \"@anthropic-ai/claude-code@latest\").","created_at":"2026-02-08T21:39:53Z"},{"id":79,"issue_id":"bd-gsjqf.3","author":"Dicklesworthstone","text":"FRESH-EYES REVIEW (2 findings):\n\nFIX 1: Add explicit documentation file list to this bead.\nPrior analysis found 7 documentation files that need updating (beyond the uca alias). These should be listed explicitly so the implementer doesn't miss them:\n\n1. README.md:620 — Update table: change \"claude update\" to \"acfs-update or uca\"\n2. README.md:1242 — Update instruction: change to \"Use acfs-update (installs latest channel via verified installer) or uca alias\"\n3. acfs/onboard/lessons/08_keeping_updated.md:131 — Change troubleshooting: \"curl -fsSL https://claude.ai/install.sh | bash -s -- latest\"\n4. apps/web/components/lessons/keeping-updated-lesson.tsx:218 — Update web lesson component to match\n5. acfs.manifest.yaml:652 — Change \"built-in claude update command\" to \"verified installer\"\n6. PLAN_TO_CREATE_ACFS.md:190 — Leave as historical, add annotation only\n\nIMPORTANT: After editing acfs.manifest.yaml, MUST run: cd packages/manifest && bun run generate\nThis regenerates manifest_index.sh with correct SHA256. Failure to do this BREAKS the curl|bash installer for all users. (See ACFS manifest SHA256 drift recurring issue.)\n\nFIX 2: The recommended Option A (inline curl) in the description is correct but needs the pipefail fix from comment #2. For clarity, the FINAL alias should be:\n```bash\n# Update all coding agent CLIs (Claude latest channel, Codex, Gemini)\nalias uca='(set -o pipefail; curl -fsSL https://claude.ai/install.sh | bash -s -- latest) && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest'\n```\nNote: install.sh has its OWN internal checksum verification (confirmed by inspection: downloads manifest.json from GCS, verifies binary SHA-256). So uca gets single-layer verification from install.sh, while acfs-update gets double-layer (ACFS SHA-256 on install.sh + install.sh's own binary SHA-256).\n","created_at":"2026-02-09T00:35:42Z"}]} +{"id":"bd-gsjqf.4","title":"Comprehensive testing for ACFS Claude Code update channel fix","description":"## What\nCreate a comprehensive, committed test script (`scripts/tests/test_update_channel.sh`) that validates the Claude Code update channel fix. This REPLACES the test plan in bd-53zvp (which was invalidated because it assumed `--channel latest` flag exists).\n\n## Why\nThe original test bead (bd-53zvp) had 4 tests based on the wrong fix approach (`--channel latest`). This bead provides corrected tests that verify the actual fix (verified installer). The user requires \"comprehensive unit tests and e2e test scripts with great, detailed logging.\"\n\n## Key Discovery: How to Verify Channel\n`claude --version` outputs `2.1.37 (Claude Code)` — it does NOT show the channel name. To verify the channel programmatically:\n```bash\nnpm view @anthropic-ai/claude-code dist-tags\n# => { stable: '2.1.25', latest: '2.1.37', next: '2.1.37' }\n```\nCompare the installed version against these dist-tags to determine which channel is active.\n\n## Key Discovery: update.sh is Source-Safe\nLine 2395: `if [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then main \"$@\"; fi` — so we CAN `source scripts/lib/update.sh` to get function definitions without triggering main execution. This enables proper unit testing.\n\n## Relationship to bd-53zvp\nbd-53zvp should be CLOSED as \"superseded by bd-gsjqf.4\" once testing passes.\n\n## Test Script: scripts/tests/test_update_channel.sh\n\nThe script must be executable, self-contained, and produce detailed logging. Structure:\n\n```bash\n#\\!/usr/bin/env bash\n# Test suite for Claude Code update channel fix (bd-gsjqf)\n# Validates that all update paths use the latest channel, not stable.\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\nPASS=0; FAIL=0; SKIP=0\nLOG_FILE=\"/tmp/test_update_channel_$(date +%Y%m%d_%H%M%S).log\"\n\nlog() { echo \"[$(date +%H:%M:%S)] $*\" | tee -a \"$LOG_FILE\"; }\npass() { ((PASS++)); log \" PASS: $1\"; }\nfail() { ((FAIL++)); log \" FAIL: $1\"; }\nskip() { ((SKIP++)); log \" SKIP: $1\"; }\nsection() { log \"\"; log \"=== $1 ===\"; }\n\n# ... test functions below ...\n```\n\n### Test 1: Static Analysis — No Bare \"claude update\" in Function Body\n```bash\ntest_no_bare_claude_update_in_function() {\n section \"Test 1: Static analysis — no bare claude update in run_cmd_claude_update()\"\n local func_body\n # Extract function body between \"run_cmd_claude_update()\" and next function\n func_body=$(sed -n '/^run_cmd_claude_update()/,/^[a-z_]*() {/p' \"$REPO_ROOT/scripts/lib/update.sh\")\n \n # Check for bare \"claude update\" that is NOT in a variable assignment, comment, or log string\n local bare_calls\n bare_calls=$(echo \"$func_body\" | grep -n \"claude update\" | grep -v \"^[[:space:]]*#\" | grep -v \"cmd_display\\|desc=\\|log_to_file\\|log_item\\|echo.*warn\" || true)\n \n if [[ -z \"$bare_calls\" ]]; then\n pass \"No bare 'claude update' invocations in run_cmd_claude_update()\"\n else\n fail \"Found bare 'claude update' invocations:\"\n log \" $bare_calls\"\n fi\n}\n```\n\n### Test 2: Static Analysis — update_run_verified_installer Is Called\n```bash\ntest_verified_installer_in_function() {\n section \"Test 2: Static analysis — verified installer is called\"\n local func_body\n func_body=$(sed -n '/^run_cmd_claude_update()/,/^[a-z_]*() {/p' \"$REPO_ROOT/scripts/lib/update.sh\")\n \n if echo \"$func_body\" | grep -q \"update_run_verified_installer\"; then\n pass \"run_cmd_claude_update() calls update_run_verified_installer\"\n local call_line\n call_line=$(echo \"$func_body\" | grep \"update_run_verified_installer\")\n log \" Found: $call_line\"\n else\n fail \"run_cmd_claude_update() does NOT call update_run_verified_installer\"\n fi\n}\n```\n\n### Test 3: Dry-Run Behavior\n```bash\ntest_dry_run() {\n section \"Test 3: Dry-run mode\"\n # Source update.sh to get function definitions (safe: has BASH_SOURCE guard)\n (\n export DRY_RUN=true\n export VERBOSE=false\n export QUIET=true\n source \"$REPO_ROOT/scripts/lib/update.sh\"\n \n local output\n output=$(run_cmd_claude_update 2>&1) || true\n \n if echo \"$output\" | grep -qi \"skip\\|dry.run\"; then\n echo \"PASS\"\n else\n echo \"FAIL: output=$output\"\n fi\n )\n local result=$?\n # ... evaluate pass/fail from subshell output\n}\n```\n\n### Test 4: Function Instrumentation (Mock Test)\n```bash\ntest_function_calls_verified_installer() {\n section \"Test 4: Mock — verify function calls update_run_verified_installer\"\n (\n export DRY_RUN=false\n export VERBOSE=false\n export QUIET=true\n source \"$REPO_ROOT/scripts/lib/update.sh\"\n \n # Override the real function with a mock\n update_run_verified_installer() {\n echo \"MOCK_VERIFIED_INSTALLER_CALLED args=$*\"\n return 0\n }\n update_require_security() { return 0; }\n \n local output\n output=$(run_cmd_claude_update 2>&1) || true\n \n if echo \"$output\" | grep -q \"MOCK_VERIFIED_INSTALLER_CALLED args=claude latest\"; then\n echo \"PASS\"\n else\n echo \"FAIL: output=$output\"\n fi\n )\n}\n```\n\n### Test 5: Security Fallback Behavior\n```bash\ntest_security_fallback() {\n section \"Test 5: Security unavailable — graceful fallback\"\n (\n export DRY_RUN=false\n export VERBOSE=false\n export QUIET=false\n source \"$REPO_ROOT/scripts/lib/update.sh\"\n \n # Mock security as unavailable\n update_require_security() { return 1; }\n # Mock claude update to succeed\n claude() { echo \"mock claude update stable\"; return 0; }\n \n local output\n output=$(run_cmd_claude_update 2>&1) || true\n \n if echo \"$output\" | grep -qi \"warn\\|security\\|stable\\|fallback\"; then\n echo \"PASS: warning present\"\n else\n echo \"FAIL: no warning about security fallback. output=$output\"\n fi\n )\n}\n```\n\n### Test 6: uca Alias Definition\n```bash\ntest_uca_alias() {\n section \"Test 6: uca alias definition\"\n local alias_file=\"$REPO_ROOT/acfs/zsh/acfs.zshrc\"\n local alias_line\n alias_line=$(grep \"^alias uca=\" \"$alias_file\" || true)\n \n if [[ -z \"$alias_line\" ]]; then\n fail \"uca alias not found in acfs.zshrc\"\n return\n fi\n log \" Alias: $alias_line\"\n \n # Check NO bare \"claude update\" (the string \"claude update\" without install.sh context)\n if echo \"$alias_line\" | grep -q \"claude update\" && \\! echo \"$alias_line\" | grep -q \"install.sh\"; then\n fail \"uca alias still contains bare 'claude update'\"\n else\n pass \"uca alias does not contain bare 'claude update'\"\n fi\n \n # Check codex preserved\n if echo \"$alias_line\" | grep -q \"codex\"; then\n pass \"uca alias preserves codex update\"\n else\n fail \"uca alias MISSING codex update\"\n fi\n \n # Check gemini preserved\n if echo \"$alias_line\" | grep -q \"gemini\"; then\n pass \"uca alias preserves gemini update\"\n else\n fail \"uca alias MISSING gemini update\"\n fi\n \n # Check latest channel mechanism present\n if echo \"$alias_line\" | grep -q \"install.sh.*latest\\|verified_installer.*latest\"; then\n pass \"uca alias uses latest channel mechanism\"\n else\n fail \"uca alias does not reference latest channel\"\n fi\n}\n```\n\n### Test 7: Completeness Sweep\n```bash\ntest_completeness_sweep() {\n section \"Test 7: Completeness sweep — no bare claude update anywhere\"\n local findings\n findings=$(grep -rn \"claude update\" \"$REPO_ROOT\" \\\n --include=\"*.sh\" --include=\"*.zsh\" --include=\"*.zshrc\" --include=\"*.bashrc\" \\\n --exclude-dir=.git --exclude-dir=.beads --exclude-dir=node_modules \\\n | grep -v \"^[[:space:]]*#\" \\\n | grep -v \"verified_installer\\|install.sh\\|KNOWN_INSTALLERS\\|cmd_display\\|desc=\\|log_to_file\\|log_item\\|echo.*warn\\|npm view\\|test_\\|TEST\" \\\n || true)\n \n if [[ -z \"$findings\" ]]; then\n pass \"No bare 'claude update' invocations found in repo\"\n else\n fail \"Found potential bare 'claude update' invocations:\"\n log \"$findings\"\n fi\n}\n```\n\n### Test 8: Channel Version Verification (live, optional)\n```bash\ntest_channel_version() {\n section \"Test 8: Channel version alignment (live check)\"\n if \\! command -v npm &>/dev/null; then\n skip \"npm not available — cannot check dist-tags\"\n return\n fi\n \n local dist_tags\n dist_tags=$(npm view @anthropic-ai/claude-code dist-tags 2>/dev/null || true)\n if [[ -z \"$dist_tags\" ]]; then\n skip \"Cannot reach npm registry\"\n return\n fi\n log \" dist-tags: $dist_tags\"\n \n local installed_version\n installed_version=$(claude --version 2>/dev/null | head -1 | awk '{print $1}' || true)\n log \" Installed: $installed_version\"\n \n local latest_version\n latest_version=$(echo \"$dist_tags\" | grep -oP \"latest: '\\K[^']+\")\n log \" Latest channel: $latest_version\"\n \n local stable_version\n stable_version=$(echo \"$dist_tags\" | grep -oP \"stable: '\\K[^']+\")\n log \" Stable channel: $stable_version\"\n \n if [[ \"$installed_version\" == \"$latest_version\" ]]; then\n pass \"Installed version ($installed_version) matches latest channel\"\n elif [[ \"$installed_version\" == \"$stable_version\" ]]; then\n fail \"Installed version ($installed_version) matches STABLE channel — downgrade detected\\!\"\n else\n log \" INFO: version $installed_version doesn't match either channel exactly\"\n skip \"Version mismatch — may be between releases\"\n fi\n}\n```\n\n### Main Runner\n```bash\nmain() {\n log \"Claude Code Update Channel Fix — Test Suite (bd-gsjqf.4)\"\n log \"Date: $(date)\"\n log \"Log file: $LOG_FILE\"\n log \"\"\n \n test_no_bare_claude_update_in_function\n test_verified_installer_in_function\n test_dry_run\n test_function_calls_verified_installer\n test_security_fallback\n test_uca_alias\n test_completeness_sweep\n test_channel_version\n \n section \"RESULTS\"\n log \"PASS: $PASS | FAIL: $FAIL | SKIP: $SKIP\"\n log \"Log: $LOG_FILE\"\n \n if [[ $FAIL -gt 0 ]]; then\n log \"STATUS: FAILED\"\n return 1\n else\n log \"STATUS: PASSED\"\n return 0\n fi\n}\n\nmain \"$@\"\n```\n\n## Files Created\n- scripts/tests/test_update_channel.sh (NEW — the test script)\n\n## Acceptance Criteria\n- [ ] Test script is executable and runs without errors\n- [ ] All 8 tests pass (or SKIP with reason for live checks)\n- [ ] Detailed log file produced with timestamps\n- [ ] bd-53zvp closed as superseded\n- [ ] Test script committed to repo","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T21:28:52.138955425Z","created_by":"ubuntu","updated_at":"2026-02-11T16:39:42.992137163Z","closed_at":"2026-02-11T16:39:42.992103209Z","close_reason":"Created comprehensive test script with 8 tests (9 assertions): static analysis, dry-run, mock instrumentation, security fallback, uca alias, completeness sweep, live channel check. All pass. Committed in f0c653af.","source_repo":".","compaction_level":0,"original_size":0,"labels":["claude-code","testing","update"],"dependencies":[{"issue_id":"bd-gsjqf.4","depends_on_id":"bd-gsjqf","type":"parent-child","created_at":"2026-02-08T21:28:52.138955425Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.4","depends_on_id":"bd-gsjqf.2","type":"blocks","created_at":"2026-02-08T21:29:13.427604501Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.4","depends_on_id":"bd-gsjqf.3","type":"blocks","created_at":"2026-02-08T21:29:14.969380580Z","created_by":"ubuntu"}],"comments":[{"id":65,"issue_id":"bd-gsjqf.4","author":"Dicklesworthstone","text":"WHY THIS REPLACES bd-53zvp: The original test bead (bd-53zvp) was created before we discovered that --channel latest does not exist. All 4 of its tests are based on the wrong fix approach. Rather than trying to patch each test, it is cleaner to create a fresh test plan that tests the actual fix (verified installer). bd-53zvp should be closed as superseded once this bead is complete. The 7 tests here cover: static analysis (no bare claude update), dry-run behavior, function instrumentation, alias definition, alias chain integrity, security fallback, and completeness sweep.","created_at":"2026-02-08T21:30:33Z"},{"id":70,"issue_id":"bd-gsjqf.4","author":"Dicklesworthstone","text":"REVIEW FIX — Test script improvements over original plan:\n1. FORMAL SCRIPT: Tests are now in scripts/tests/test_update_channel.sh (committed to repo, runnable in CI) instead of ad-hoc manual commands\n2. CHANNEL VERIFICATION: Test 8 uses `npm view dist-tags` to verify installed version matches latest channel — `claude --version` alone does NOT show channel\n3. SOURCEABILITY: update.sh has BASH_SOURCE guard at line 2395 — confirmed safe to source for function-level testing\n4. MOCK APPROACH: Tests 4-5 use subshell-local function overrides (not `export -f`) to mock update_run_verified_installer and update_require_security — more reliable\n5. DETAILED LOGGING: Every test logs timestamps, pass/fail, and actual output to a log file for debugging\n6. FUNCTION EXTRACTION: Test 1-2 use sed to extract just the function body, avoiding false positives from variable assignments like cmd_display=\"claude update\"\n7. 8 TESTS TOTAL: static analysis (x2), dry-run, mock instrumentation, security fallback, alias definition, completeness sweep, live channel verification","created_at":"2026-02-08T21:39:34Z"},{"id":73,"issue_id":"bd-gsjqf.4","author":"Dicklesworthstone","text":"IMPLEMENTATION NOTE — Incremental test script construction:\n- bd-gsjqf.2 creates scripts/tests/test_update_channel.sh with Tests 1-5 (function-level tests)\n- bd-gsjqf.3 may add alias-specific assertions to the existing script\n- bd-gsjqf.4 completes the script by adding Tests 6-8 (alias definition, completeness sweep, live channel check)\n- bd-gsjqf.4 then runs the FULL test suite (all 8 tests) and verifies everything passes together\n- The test script should use a simple pass/fail/skip counter with timestamps for debugging","created_at":"2026-02-08T21:40:02Z"},{"id":80,"issue_id":"bd-gsjqf.4","author":"Dicklesworthstone","text":"FRESH-EYES REVIEW (3 findings):\n\nFIX 1 (CRITICAL BUG): ((PASS++)) will crash under set -e when PASS=0.\nThe test script uses:\n pass() { ((PASS++)); log \" PASS: $1\"; }\n fail() { ((FAIL++)); log \" FAIL: $1\"; }\n skip() { ((SKIP++)); log \" SKIP: $1\"; }\n\nUnder set -e, ((PASS++)) returns exit code 1 when PASS=0 (because 0++ evaluates to 0, which is falsy in bash arithmetic). The FIRST call to pass() will kill the script!\n\nFix: Use safe arithmetic:\n pass() { PASS=$((PASS + 1)); log \" PASS: $1\"; }\n fail() { FAIL=$((FAIL + 1)); log \" FAIL: $1\"; }\n skip() { SKIP=$((SKIP + 1)); log \" SKIP: $1\"; }\n\nThis is a well-known bash gotcha documented in MEMORY.md.\n\nFIX 2: Test 7 (completeness sweep) needs INTENTIONAL exclusion.\nThe security fallback in bd-gsjqf.2 has ONE intentional bare \"claude update\" tagged with \"# INTENTIONAL: security-unavailable fallback\". Add \"INTENTIONAL\" to the exclusion grep:\n | grep -v \"verified_installer\\|install.sh\\|KNOWN_INSTALLERS\\|cmd_display\\|desc=\\|log_to_file\\|log_item\\|echo.*warn\\|npm view\\|test_\\|TEST\\|INTENTIONAL\"\n\nNOTE: Tests 3-5 work without extra stubbing.\nWhen sourcing update.sh: log_to_file (line 121), log_item (line 238), counters (line 45-47), color vars (lines 24-42), and UPDATE_LOG_FILE=\"\" (line 67) are all auto-initialized. BASH_SOURCE guard (line 2395) prevents main() from running. Tests only need to mock update_require_security + update_run_verified_installer + claude binary.\n","created_at":"2026-02-09T00:35:52Z"}]} +{"id":"bd-gsjqf.5","title":"End-to-end verification and close GH #125","description":"## What\nFinal end-to-end verification that the complete fix works on the live system, then close GitHub issue #125.\n\n## Why\nThis is the gate task that verifies everything works together before we close the issue. It runs AFTER all code changes (bd-gsjqf.2, bd-gsjqf.3) and AFTER testing (bd-gsjqf.4).\n\n## CRITICAL: How to Verify Channel\n`claude --version` outputs `2.1.37 (Claude Code)` — it does NOT show the channel name. To verify:\n```bash\n# Get dist-tags (shows version per channel)\nnpm view @anthropic-ai/claude-code dist-tags\n# => { stable: '2.1.25', latest: '2.1.37', next: '2.1.37' }\n\n# Get installed version\nclaude --version | awk '{print $1}'\n# => 2.1.37\n\n# Compare: if installed matches latest dist-tag, we are on latest channel\n```\n\n## E2E Verification Steps\n\n### Step 1: Record baseline\n```bash\necho \"=== Baseline ===\"\nclaude --version\nnpm view @anthropic-ai/claude-code dist-tags\necho \"Current version should match 'latest' dist-tag, NOT 'stable'\"\n```\n\n### Step 2: Run the test suite first\n```bash\nbash scripts/tests/test_update_channel.sh\n# Must exit 0 with all tests passing\n```\n\n### Step 3: Run full ACFS update (live)\n```bash\n# Run with verbose to see which update mechanism is used\nacfs-update --verbose 2>&1 | tee /tmp/acfs_update_e2e.log\n```\n\n### Step 4: Verify in update log\n```bash\n# Check logs for evidence of verified installer\ngrep -i \"verified\\|installer\\|install.sh\" /tmp/acfs_update_e2e.log\n# Should show verified installer was used\ngrep -i \"claude update\" /tmp/acfs_update_e2e.log\n# Should NOT show bare \"claude update\" as an executed command\n# (may appear in log messages/variable values, that is OK)\n```\n\n### Step 5: Verify version after update\n```bash\necho \"=== Post-update ===\"\nclaude --version\nnpm view @anthropic-ai/claude-code dist-tags\ninstalled=$(claude --version | awk '{print $1}')\nlatest=$(npm view @anthropic-ai/claude-code dist-tags 2>/dev/null | grep -oP \"latest: '\\K[^']+\")\nif [[ \"$installed\" == \"$latest\" ]]; then\n echo \"PASS: version $installed matches latest channel\"\nelse\n echo \"FAIL: version $installed does NOT match latest ($latest)\"\nfi\n```\n\n### Step 6: Test uca alias (live)\n```bash\n# Source the alias\nsource ~/.config/acfs/acfs/zsh/acfs.zshrc 2>/dev/null\n# Verify alias definition\ntype uca\n# Run it (this will actually update - be prepared)\nuca\n# Verify still on latest\nclaude --version\n```\n\n### Step 7: Idempotency check\n```bash\n# Run update again — should not downgrade\nacfs-update --verbose 2>&1 | tee /tmp/acfs_update_e2e_2.log\ninstalled_after=$(claude --version | awk '{print $1}')\necho \"Version after second update: $installed_after (should still be latest)\"\n```\n\n## Close GH #125\nOnce all verification passes:\n```bash\ngh issue close 125 --repo Dicklesworthstone/agentic_coding_flywheel_setup --comment \"Fixed in bd-gsjqf: replaced all bare 'claude update' calls with update_run_verified_installer using latest channel. The verified installer (https://claude.ai/install.sh with SHA-256 verification) is now the primary update mechanism. Fallback to bare 'claude update' (stable channel) only when security.sh is unavailable, with a visible warning. The uca alias was also updated to use the latest channel. See bd-gsjqf epic and sub-tasks (.1-.5) for full implementation details.\"\n```\n\n## Also Close/Update Related Beads\n- bd-53zvp: Close as \"superseded by bd-gsjqf.4\" (test plan was invalid due to non-existent --channel flag)\n- bd-gsjqf epic: Update status to reflect completion\n- All bd-gsjqf sub-tasks: Verify all closed\n\n## Files\nNone changed — this is verification only.\n\n## Acceptance Criteria\n- [ ] scripts/tests/test_update_channel.sh exits 0 (all tests pass)\n- [ ] acfs-update --verbose uses verified installer (visible in logs)\n- [ ] Installed version matches 'latest' dist-tag (NOT 'stable')\n- [ ] uca alias works and keeps latest channel\n- [ ] Second update run does not downgrade (idempotency)\n- [ ] GH #125 closed with explanatory comment\n- [ ] bd-53zvp closed as superseded\n- [ ] All bd-gsjqf sub-tasks completed","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-08T21:29:05.803925903Z","created_by":"ubuntu","updated_at":"2026-02-11T16:41:28.470820254Z","closed_at":"2026-02-11T16:41:28.470796539Z","close_reason":"E2E verification complete: 9/9 tests pass, Claude 2.1.39 matches latest channel (stable=2.1.29), GH#125 already closed on 2026-02-09.","source_repo":".","compaction_level":0,"original_size":0,"labels":["claude-code","update","verification"],"dependencies":[{"issue_id":"bd-gsjqf.5","depends_on_id":"bd-gsjqf","type":"parent-child","created_at":"2026-02-08T21:29:05.803925903Z","created_by":"ubuntu"},{"issue_id":"bd-gsjqf.5","depends_on_id":"bd-gsjqf.4","type":"blocks","created_at":"2026-02-08T21:29:15.829188476Z","created_by":"ubuntu"}],"comments":[{"id":66,"issue_id":"bd-gsjqf.5","author":"Dicklesworthstone","text":"GATE TASK: This is intentionally the last sub-task because it verifies the entire fix end-to-end on the live system. It should only run after all code changes are merged and all unit/integration tests pass. The GH #125 close comment should reference the epic (bd-gsjqf) and note that the fix was: (1) replaced bare \"claude update\" with verified installer in update.sh, (2) fixed uca alias to use latest channel, (3) audit confirmed no other occurrences. OVERARCHING GOAL: This fix ensures that users on the latest Claude Code channel are not silently downgraded to stable when running ACFS updates — a significant UX issue reported by joyshmitz.","created_at":"2026-02-08T21:30:38Z"},{"id":81,"issue_id":"bd-gsjqf.5","author":"Dicklesworthstone","text":"FRESH-EYES NOTE: Step 6 (uca alias test) assumes zsh shell.\nThe instruction \"source acfs.zshrc\" will fail in bash. This step should be run from a zsh shell or prefixed with:\n zsh -c 'source /path/to/acfs.zshrc && type uca && uca'\n\nAlso consider adding:\n- Step 8: Verify update log files in ~/.acfs/logs/updates/ show verified installer path\n- Step 9: Verify acfs.manifest.yaml SHA256 is still valid after bd-gsjqf.3's changes (run: cd packages/manifest && bun run generate && git diff)\n","created_at":"2026-02-09T00:36:02Z"}]} {"id":"bd-iucu","title":"SRPS integration: lesson, webapp, and tests","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-21T09:34:20.834248192Z","created_by":"ubuntu","updated_at":"2026-01-21T09:35:05.899207498Z","closed_at":"2026-01-21T09:35:05.899159768Z","close_reason":"Completed: SRPS integration committed","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-jt48","title":"Deep exploration: BR (beads_rust)","description":"## Goal\nPerform deep exploration of BR (beads_rust) and revise its description with comprehensive testing.\n\n## Phase 0: Pre-flight Verification\n\n```bash\n#!/bin/bash\nLOG=/tmp/br-preflight.log\necho \"=== BR Pre-flight ===\" | tee $LOG\n\n[[ -d /dp/beads_rust ]] && echo \"PASS: Directory exists\" || exit 1\ncommand -v br &>/dev/null && echo \"PASS: br installed: $(which br)\" || echo \"WARN: br not in PATH\"\nbr --version 2>&1 | tee -a $LOG\n\nSNAPSHOT_DIR=/tmp/br-exploration-snapshots\nmkdir -p $SNAPSHOT_DIR\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/flywheel.ts $SNAPSHOT_DIR/flywheel.ts.before\ncp /data/projects/agentic_coding_flywheel_setup/apps/web/lib/tldr-content.ts $SNAPSHOT_DIR/tldr-content.ts.before\n```\n\n## Phase 1: Research\n\n### 1.1 Documentation\n```bash\ncat /dp/beads_rust/README.md\ncat /dp/beads_rust/AGENTS.md 2>/dev/null\n```\n\n### 1.2 Code Investigation\n- JSONL schema: examine actual .beads/issues.jsonl format\n- Dependency system: blocks/blocked_by implementation\n- Auto-flush mechanism: when/how it triggers\n- bd alias: how it works\n- CLI commands: full inventory\n\n### 1.3 Verify CLI Commands\n```bash\n#!/bin/bash\necho \"=== BR CLI Verification ===\" | tee /tmp/br-cli.log\n\nbr --help 2>&1 | tee -a /tmp/br-cli.log\nbr create --help 2>&1 && echo \"PASS: br create\" || echo \"FAIL\"\nbr update --help 2>&1 && echo \"PASS: br update\" || echo \"FAIL\"\nbr show --help 2>&1 && echo \"PASS: br show\" || echo \"FAIL\"\nbr list --help 2>&1 && echo \"PASS: br list\" || echo \"FAIL\"\nbr sync --help 2>&1 && echo \"PASS: br sync\" || echo \"FAIL\"\nbr ready 2>&1 && echo \"PASS: br ready\" || echo \"FAIL\"\nbr blocked 2>&1 && echo \"PASS: br blocked\" || echo \"FAIL\"\n\n# Test bd alias\ncommand -v bd &>/dev/null && echo \"PASS: bd alias exists\" || echo \"WARN: bd alias missing\"\n```\n\n### 1.4 Verify JSONL Schema\n```bash\n# Examine actual schema from a real bead\nhead -1 /data/projects/agentic_coding_flywheel_setup/.beads/issues.jsonl | jq . 2>/dev/null | tee /tmp/br-schema.json\n```\n\n### 1.5 External Context\n```bash\n/xf search 'beads_rust OR br cli OR issue tracking' 2>&1 | head -30\ncass search 'br beads issue' --robot --limit 10 2>&1\n```\n\n## Phase 2: Analysis\n\nDocument with VERIFICATION:\n- [ ] JSONL schema fields: [list actual fields from schema]\n- [ ] CLI commands VERIFIED: create, update, show, list, sync, ready, blocked\n- [ ] bd alias: VERIFIED working ✓/✗\n- [ ] Auto-flush: trigger conditions verified\n- [ ] Tech stack: Rust VERIFIED\n- [ ] Synergies VERIFIED:\n - [ ] bv: bv reads br data ✓/✗\n - [ ] mail: any integration ✓/✗\n - [ ] ntm: any integration ✓/✗\n\n## Phase 3: Revision\n\nUpdate with VERIFIED content:\n- List only commands that actually work\n- Document actual JSONL schema fields\n- Only list verified synergies\n\n## Phase 4: Testing\n\n```bash\n#!/bin/bash\ncd /data/projects/agentic_coding_flywheel_setup/apps/web\n\nbun run type-check 2>&1 | tee /tmp/br-typecheck.log\nbun run lint 2>&1 | tee /tmp/br-lint.log\nbun run build 2>&1 | tee /tmp/br-build.log\n\nlsof -t -i :3000 | xargs kill 2>/dev/null; sleep 2\nbun run dev &\nsleep 10\ncurl -sL http://localhost:3000/flywheel | grep -qi 'br' && echo \"PASS\" || echo \"FAIL\"\ncurl -sL http://localhost:3000/tldr | grep -qi 'br' && echo \"PASS\" || echo \"FAIL\"\nkill %1\n\nRESULTS=/tmp/br-exploration-test-results.log\ncat /tmp/br-cli.log > $RESULTS\ngrep -E \"PASS|FAIL\" /tmp/br-*.log >> $RESULTS\n```\n\n## Phase 5: Commit\n\n```bash\n[[ $(grep -c \"FAIL\" /tmp/br-exploration-test-results.log) -gt 0 ]] && exit 1\n\ngit add apps/web/lib/flywheel.ts apps/web/lib/tldr-content.ts\ngit commit -m \"docs(flywheel): update BR with verified CLI commands and schema\n\n- Verified all CLI commands work\n- Documented actual JSONL schema\n- Verified bd alias\n- Tested auto-flush behavior\n\nCo-Authored-By: Claude Opus 4.5 \"\n\nbr update bd-jt48 --status closed\nbr sync --flush-only && git add .beads/ && git push\n```\n\n## Acceptance Criteria\n- [ ] CLI commands VERIFIED\n- [ ] JSONL schema documented\n- [ ] bd alias VERIFIED\n- [ ] All tests PASS\n- [ ] Pushed\n","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-26T20:00:46.192044397Z","created_by":"ubuntu","updated_at":"2026-01-27T05:34:18.534127728Z","closed_at":"2026-01-27T05:34:18.534103483Z","close_reason":"Deep exploration completed by EmeraldCrane. Verified SQLite+JSONL hybrid, 40 commands, non-invasive design.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-ltmu","title":"JFP: Add Learning Hub lesson metadata","description":"Expose the JFP lesson in the Learning Hub list.\\n\\nScope:\\n- Update apps/web/lib/lessons.ts to include a jfp lesson entry (slug jfp) pointing to the existing lesson component and content.\\n- Ensure TOTAL_LESSONS remains accurate and ordering remains sensible.\\n\\nValidation:\\n- bun run build (apps/web) if convenient; otherwise note not run.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-21T09:45:35.798383561Z","created_by":"ubuntu","updated_at":"2026-01-21T09:50:13.516596379Z","closed_at":"2026-01-21T09:50:13.516542527Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-mhmdd","title":"Deep code audit & fixes","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-04T05:11:08.707179681Z","created_by":"ubuntu","updated_at":"2026-02-04T05:19:45.700648014Z","closed_at":"2026-02-04T05:19:45.700625381Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-nvmp","title":"Add jfp installer checksum so manifest generator can run","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-21T20:48:57.335271911Z","created_by":"ubuntu","updated_at":"2026-01-21T21:47:44.408444370Z","closed_at":"2026-01-21T21:47:44.408383265Z","close_reason":"Already fixed (checksums.yaml includes jfp; generate:validate passes)","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-nvmp","depends_on_id":"bd-3hg8","type":"discovered-from","created_at":"2026-01-21T20:48:57.430830777Z","created_by":"ubuntu"}]} {"id":"bd-pkta","title":"Profile full installer runtime in Docker (baseline + hotspots)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-21T19:00:21.329191150Z","created_by":"ubuntu","updated_at":"2026-01-22T01:25:55.941958502Z","closed_at":"2026-01-22T01:25:55.940359722Z","close_reason":"Profiling completed via state.json analysis. Baseline: 1078s total. Hotspots identified: (1) cli_tools 455s/42.2% - apt packages, GitHub releases, lazygit/lazydocker builds; (2) languages 372s/34.5% - rust/cargo compilation (batched cargo install already implemented in bd-3vx8); (3) stack 96s/8.9% - Dicklesworthstone tool downloads. Optimization priority: cli_tools phase which has most room for parallelization of apt operations and binary downloads.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-pkta","depends_on_id":"bd-2z32","type":"discovered-from","created_at":"2026-01-21T19:00:21.367573510Z","created_by":"ubuntu"}]} {"id":"bd-q6eb","title":"Create GIIL lesson: Deep dive into Get Image from Internet Link tool","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-27T06:22:19.855457223Z","created_by":"ubuntu","updated_at":"2026-01-27T06:26:42.806142774Z","closed_at":"2026-01-27T06:26:42.806124219Z","close_reason":"GIIL lesson already exists and is fully integrated: TSX component at apps/web/components/lessons/giil-lesson.tsx, registered in lessons.ts, and exported from index.tsx.","source_repo":".","compaction_level":0,"original_size":0} diff --git a/.github/workflows/installer-canary-strict.yml b/.github/workflows/installer-canary-strict.yml index 22383c04..be9e2fbd 100644 --- a/.github/workflows/installer-canary-strict.yml +++ b/.github/workflows/installer-canary-strict.yml @@ -31,18 +31,21 @@ jobs: env: ACFS_CHECKSUMS_REF: main run: | - set -o pipefail chmod +x ./tests/vm/test_install_ubuntu.sh + UBUNTU="${{ inputs.ubuntu || 'all' }}" + MODE="${{ inputs.mode || 'vibe' }}" + + rc=0 if [[ "${{ github.event_name }}" == "schedule" ]]; then - ./tests/vm/test_install_ubuntu.sh --all --mode "vibe" --strict 2>&1 | tee canary.log - elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.ubuntu }}" == "all" ]]; then - ./tests/vm/test_install_ubuntu.sh --all --mode "${{ inputs.mode }}" --strict 2>&1 | tee canary.log + (set -o pipefail; ./tests/vm/test_install_ubuntu.sh --all --mode "vibe" --strict 2>&1 | tee canary.log) || rc=$? + elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "$UBUNTU" == "all" ]]; then + (set -o pipefail; ./tests/vm/test_install_ubuntu.sh --all --mode "$MODE" --strict 2>&1 | tee canary.log) || rc=$? else - ./tests/vm/test_install_ubuntu.sh --ubuntu "${{ inputs.ubuntu }}" --mode "${{ inputs.mode }}" --strict 2>&1 | tee canary.log + (set -o pipefail; ./tests/vm/test_install_ubuntu.sh --ubuntu "$UBUNTU" --mode "$MODE" --strict 2>&1 | tee canary.log) || rc=$? fi - echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT" + echo "exit_code=$rc" >> "$GITHUB_OUTPUT" - name: Detect checksum mismatch id: detect diff --git a/.github/workflows/installer-canary.yml b/.github/workflows/installer-canary.yml index 6791c2ee..553bef7e 100644 --- a/.github/workflows/installer-canary.yml +++ b/.github/workflows/installer-canary.yml @@ -28,8 +28,11 @@ jobs: run: | chmod +x ./tests/vm/test_install_ubuntu.sh - if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.ubuntu }}" == "all" ]]; then - ./tests/vm/test_install_ubuntu.sh --all --mode "${{ inputs.mode }}" + UBUNTU="${{ inputs.ubuntu || '24.04' }}" + MODE="${{ inputs.mode || 'vibe' }}" + + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "$UBUNTU" == "all" ]]; then + ./tests/vm/test_install_ubuntu.sh --all --mode "$MODE" else - ./tests/vm/test_install_ubuntu.sh --ubuntu "${{ inputs.ubuntu }}" --mode "${{ inputs.mode }}" + ./tests/vm/test_install_ubuntu.sh --ubuntu "$UBUNTU" --mode "$MODE" fi diff --git a/.github/workflows/installer-notification-receiver.yml b/.github/workflows/installer-notification-receiver.yml index 7fbf126b..cc4541fd 100644 --- a/.github/workflows/installer-notification-receiver.yml +++ b/.github/workflows/installer-notification-receiver.yml @@ -491,21 +491,29 @@ jobs: TOOL_NAME="${{ needs.validate-dispatch.outputs.tool_name }}" SOURCE_REPO="${{ needs.validate-dispatch.outputs.source_repo }}" - gh pr create \ - --base main \ - --head "auto/remove-${TOOL_NAME}" \ - --title "chore(checksums): Remove $TOOL_NAME" \ - --body "## Tool Removal: $TOOL_NAME + cat > /tmp/removal-pr-body.md << 'PREOF' + ## Tool Removal: $TOOL_NAME + + This PR removes **$TOOL_NAME** from checksums.yaml. + + ### Reason + The upstream repository ($SOURCE_REPO) has indicated the installer should be removed. -This PR removes **$TOOL_NAME** from checksums.yaml. + ### Checklist + - [ ] Confirmed tool is no longer needed + - [ ] Verified this is intentional, not an error -### Reason -The upstream repository ($SOURCE_REPO) has indicated the installer should be removed. + --- + *Generated by ACFS Installer Notification Receiver* + PREOF -### Checklist -- [ ] Confirmed tool is no longer needed -- [ ] Verified this is intentional, not an error + # Substitute variables in the template + sed -i "s/\$TOOL_NAME/$TOOL_NAME/g" /tmp/removal-pr-body.md + sed -i "s|\$SOURCE_REPO|$SOURCE_REPO|g" /tmp/removal-pr-body.md ---- -*Generated by ACFS Installer Notification Receiver*" \ + gh pr create \ + --base main \ + --head "auto/remove-${TOOL_NAME}" \ + --title "chore(checksums): Remove $TOOL_NAME" \ + --body-file /tmp/removal-pr-body.md \ --label "automated,checksum-removal,needs-review" diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 4149a553..a497c62c 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -309,6 +309,18 @@ jobs: su - ubuntu -c "zsh -ic 'dcg doctor'" su - ubuntu -c "zsh -ic 'ru --version'" su - ubuntu -c "zsh -ic 'onboard --help'" + # beads_rust (required) - issue tracking + su - ubuntu -c "zsh -ic 'br --version'" + # New stack tools (bd-1ega) + su - ubuntu -c "zsh -ic 'ms --version'" + su - ubuntu -c "zsh -ic 'apr --help || true'" + su - ubuntu -c "zsh -ic 'jfp --version || true'" + su - ubuntu -c "zsh -ic 'pt --help || true'" + su - ubuntu -c "zsh -ic 'brenner --version || brenner --help || true'" + su - ubuntu -c "zsh -ic 'rch --version || rch --help || true'" + su - ubuntu -c "zsh -ic 'wa --version || wa --help || true'" + su - ubuntu -c "zsh -ic 'sysmoni --version || sysmoni --help || true'" + # Agents su - ubuntu -c "zsh -ic 'claude --version'" su - ubuntu -c "zsh -ic 'codex --version'" su - ubuntu -c "zsh -ic 'gemini --version'" diff --git a/.gitignore b/.gitignore index 8b30576a..ad6b1415 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ a.out .vercel .env*.local tests/artifacts/ + +# Stray bv/CLI artifacts (malformed flag creates files like --graph-format.svg) +--graph-format* diff --git a/.shellcheckrc b/.shellcheckrc index 805fab39..4fb94f74 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -3,4 +3,39 @@ # SC2015: Note that A && B || C is not if-then-else. C may run when A is true. # We use this pattern intentionally for error handling (e.g., cmd || true) -disable=SC2015 +# +# SC2317: Command appears to be unreachable. Check usage (or ignore if invoked indirectly). +# We use dynamic function calls via function references (e.g., "$fix_function" "fix") +# +# SC2016: Expressions don't expand in single quotes, use double quotes for that. +# Intentional - we use single quotes to pass literal strings to subshells +# +# SC1091: Not following: file was not specified as input +# Dynamic sourcing of related scripts is intentional +# +# SC2059: Don't use variables in printf format string +# Intentional - we use ANSI color variables in format strings +# +# SC2034: Variable appears unused +# Many variables are used by sourcing scripts or for documentation +# +# SC2155: Declare and assign separately to avoid masking return values +# Acceptable risk in simple cases where the command always succeeds +# +# SC2030/SC2031: Variable modified in subshell +# Intentional pattern in pipeline processing +# SC2086: Double quote to prevent globbing and word splitting +# Intentional word splitting in some cases for argument expansion +# +# SC2002: Useless cat +# Sometimes cat is clearer for readability in pipelines +# +# SC2076: Remove quotes from right-hand side of =~ +# SC2128: Expanding an array without an index +# SC2178: Variable was used as an array but is now assigned a string +# SC2120/SC2119: Function references arguments, but none are ever passed +# These are pre-existing patterns in newproj TUI code +# +# SC2001/SC2028/SC2129/SC2153/SC2181/SC2295: Style and info-level suggestions +# Accepted patterns in this codebase +disable=SC2015,SC2317,SC2016,SC1091,SC2059,SC2034,SC2155,SC2030,SC2031,SC2086,SC2002,SC2076,SC2128,SC2178,SC2120,SC2119,SC2001,SC2028,SC2129,SC2153,SC2181,SC2295 diff --git a/AGENTS.md b/AGENTS.md index 3110151a..bf0c8abf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,97 +1,210 @@ # AGENTS.md — Agentic Coding Flywheel Setup (ACFS) +> Guidelines for AI coding agents working in this multi-component Bash/TypeScript codebase. + +--- + ## RULE 0 - THE FUNDAMENTAL OVERRIDE PREROGATIVE If I tell you to do something, even if it goes against what follows below, YOU MUST LISTEN TO ME. I AM IN CHARGE, NOT YOU. --- -## RULE 1 – ABSOLUTE (DO NOT EVER VIOLATE THIS) +## RULE NUMBER 1: NO FILE DELETION -You may NOT delete any file or directory unless I explicitly give the exact command **in this session**. +**YOU ARE NEVER ALLOWED TO DELETE A FILE WITHOUT EXPRESS PERMISSION.** Even a new file that you yourself created, such as a test code file. You have a horrible track record of deleting critically important files or otherwise throwing away tons of expensive work. As a result, you have permanently lost any and all rights to determine that a file or folder should be deleted. -- This includes files you just created (tests, tmp files, scripts, etc.). -- You do not get to decide that something is "safe" to remove. -- If you think something should be removed, stop and ask. You must receive clear written approval **before** any deletion command is even proposed. - -Treat "never delete files without permission" as a hard invariant. +**YOU MUST ALWAYS ASK AND RECEIVE CLEAR, WRITTEN PERMISSION BEFORE EVER DELETING A FILE OR FOLDER OF ANY KIND.** --- -## IRREVERSIBLE GIT & FILESYSTEM ACTIONS +## Irreversible Git & Filesystem Actions — DO NOT EVER BREAK GLASS -Absolutely forbidden unless I give the **exact command and explicit approval** in the same message: +1. **Absolutely forbidden commands:** `git reset --hard`, `git clean -fd`, `rm -rf`, or any command that can delete or overwrite code/data must never be run unless the user explicitly provides the exact command and states, in the same message, that they understand and want the irreversible consequences. +2. **No guessing:** If there is any uncertainty about what a command might delete or overwrite, stop immediately and ask the user for specific approval. "I think it's safe" is never acceptable. +3. **Safer alternatives first:** When cleanup or rollbacks are needed, request permission to use non-destructive options (`git status`, `git diff`, `git stash`, copying to backups) before ever considering a destructive command. +4. **Mandatory explicit plan:** Even after explicit user authorization, restate the command verbatim, list exactly what will be affected, and wait for a confirmation that your understanding is correct. Only then may you execute it—if anything remains ambiguous, refuse and escalate. +5. **Document the confirmation:** When running any approved destructive command, record (in the session notes / final response) the exact user text that authorized it, the command actually run, and the execution time. If that record is absent, the operation did not happen. -- `git reset --hard` -- `git clean -fd` -- `rm -rf` -- Any command that can delete or overwrite code/data +--- -Rules: +## Git Branch: ONLY Use `main`, NEVER `master` + +**The default branch is `main`. The `master` branch exists only for legacy URL compatibility.** -1. If you are not 100% sure what a command will delete, do not propose or run it. Ask first. -2. Prefer safe tools: `git status`, `git diff`, `git stash`, copying to backups, etc. -3. After approval, restate the command verbatim, list what it will affect, and wait for confirmation. -4. When a destructive command is run, record in your response: - - The exact user text authorizing it - - The command run - - When you ran it +- **All work happens on `main`** — commits, PRs, feature branches all merge to `main` +- **Never reference `master` in code or docs** — if you see `master` anywhere, it's a bug that needs fixing +- **The `master` branch must stay synchronized with `main`** — after pushing to `main`, also push to `master`: + ```bash + git push origin main:master + ``` -If that audit trail is missing, then you must act as if the operation never happened. +**If you see `master` referenced anywhere:** +1. Update it to `main` +2. Ensure `master` is synchronized: `git push origin main:master` --- -## Node / JS Toolchain +## Toolchain: Bash & Bun -- Use **bun** for everything JS/TS. -- ❌ Never use `npm`, `yarn`, or `pnpm`. -- Lockfiles: only `bun.lock`. Do not introduce any other lockfile. -- Target **latest Node.js**. No need to support old Node versions. -- **Note:** `bun install -g ` is valid syntax (alias for `bun add -g`). Do not "fix" it. +### Installer / Scripts: Bash ---- +The installer and scripting layer uses **Bash** (POSIX-compatible where possible). -## Project Architecture +- **Linting:** `shellcheck` for all `.sh` files +- **Target OS:** Ubuntu 25.10 (installer auto-upgrades from 22.04+) +- **Idempotent:** Installer is safe to re-run; phases resume on failure +- **One-liner:** `curl -fsSL ... | bash -s -- --yes --mode vibe` + +### Website: Bun & Next.js -ACFS is a **multi-component project** consisting of: +Use **bun** for everything JS/TS. Never use `npm`, `yarn`, or `pnpm`. -### A) Website Wizard (`apps/web/`) - **Framework:** Next.js 16 App Router - **Runtime:** Bun - **Hosting:** Vercel + Cloudflare for cost optimization -- **Purpose:** Step-by-step wizard guiding beginners from "I have a laptop" to "fully configured VPS" -- **No backend required:** All state via URL params + localStorage +- **Lockfiles:** Only `bun.lock`. Do not introduce any other lockfile. +- **Target:** Latest Node.js. No need to support old Node versions. +- **Note:** `bun install -g ` is valid syntax (alias for `bun add -g`). Do not "fix" it. -### B) Installer (`install.sh` + `scripts/`) -- **Language:** Bash (POSIX-compatible where possible) -- **Target:** Ubuntu 25.10 (auto-upgrades from 22.04+ via sequential do-release-upgrade) -- **Auto-Upgrade:** Older Ubuntu versions are automatically upgraded to 25.10 before ACFS install - - Upgrade path: 22.04 → 24.04 → 25.04 → 25.10 (EOL interim releases like 24.10 may be skipped) - - Takes 30-60 minutes per version hop; multiple reboots handled via systemd resume service - - Skip with `--skip-ubuntu-upgrade` flag -- **One-liner:** `curl -fsSL ... | bash -s -- --yes --mode vibe` -- **Idempotent:** Safe to re-run -- **Checkpointed:** Phases resume on failure +### Key Dependencies + +| Component | Purpose | +|-----------|---------| +| `next` (16.x) | App Router framework for wizard website | +| `react` / `react-dom` (19.x) | UI rendering | +| `tailwindcss` (4.x) | Utility-first CSS | +| `@tanstack/react-form` | Form state management | +| `@tanstack/react-query` | Async state management | +| `framer-motion` | Animations | +| `@playwright/test` | E2E testing | +| `eslint` / `eslint-config-next` | Linting | +| `typescript` (5.x) | Type checking | + +--- + +## Code Editing Discipline + +### No Script-Based Changes + +**NEVER** run a script that processes/changes code files in this repo. Brittle regex-based transformations create far more problems than they solve. + +- **Always make code changes manually**, even when there are many instances +- For many simple changes: use parallel subagents +- For subtle/complex changes: do them methodically yourself + +### No File Proliferation + +If you want to change something or add a feature, **revise existing code files in place**. + +**NEVER** create variations like: +- `install_v2.sh` +- `install_improved.sh` +- `install_enhanced.sh` + +New files are reserved for **genuinely new functionality** that makes zero sense to include in any existing file. The bar for creating new files is **incredibly high**. + +--- + +## Backwards Compatibility -### C) Onboarding TUI (`packages/onboard/`) -- **Command:** `onboard` -- **Purpose:** Interactive tutorial teaching Linux basics + agent workflow -- **Tech:** Shell script or simple Rust/Go binary (TBD) +We do not care about backwards compatibility—we're in early development with no users. We want to do things the **RIGHT** way with **NO TECH DEBT**. -### D) Module Manifest (`acfs.manifest.yaml`) -- **Purpose:** Single source of truth for all tools installed -- **Contains:** Tool definitions, install commands, verify commands -- **Generates:** Website content, installer modules, doctor checks +- Never create "compatibility shims" +- Never create wrapper functions for deprecated APIs +- Just fix the code directly -### E) ACFS Configs (`acfs/`) -- **Shell config:** `acfs/zsh/acfs.zshrc` -- **Tmux config:** `acfs/tmux/tmux.conf` -- **Onboard lessons:** `acfs/onboard/lessons/` -- **Installed to:** `~/.acfs/` on target VPS +--- + +## Compiler Checks (CRITICAL) + +**After any substantive code changes, you MUST verify no errors were introduced:** + +```bash +# Bash scripts: lint with shellcheck +shellcheck install.sh scripts/**/*.sh + +# Website: type-check and lint +cd apps/web && bun run type-check && bun run lint + +# Website: build verification +cd apps/web && bun run build +``` + +If you see errors, **carefully understand and resolve each issue**. Read sufficient context to fix them the RIGHT way. --- -## Repo Layout +## Testing + +### Testing Policy + +Scripts include integration tests. The website uses Playwright for E2E testing. Tests must cover: +- Happy path +- Edge cases (empty input, max values, boundary conditions) +- Error conditions + +### Installer Tests + +```bash +# Local lint +shellcheck install.sh scripts/lib/*.sh + +# Full installer integration test (Docker, same as CI) +./tests/vm/test_install_ubuntu.sh +``` + +### Website Tests + +```bash +cd apps/web +bun install # Install dependencies +bun run dev # Dev server +bun run build # Production build +bun run lint # ESLint check +bun run type-check # TypeScript check +bun run test # Playwright E2E tests +``` + +### Test Structure + +| Directory | Focus Areas | +|-----------|-------------| +| `tests/vm/` | Full installer integration tests (Docker-based, Ubuntu images) | +| `tests/e2e/` | End-to-end installer flow tests | +| `tests/unit/` | Unit tests for library functions | +| `tests/smoke/` | Quick smoke tests | +| `scripts/tests/` | Script-level tests (security, manifest drift, etc.) | +| `apps/web/e2e/` | Playwright production smoke tests | + +--- + +## Third-Party Library Usage + +If you aren't 100% sure how to use a third-party library, **SEARCH ONLINE** to find the latest documentation and current best practices. + +--- + +## ACFS — This Project + +**This is the project you're working on.** ACFS (Agentic Coding Flywheel Setup) is a multi-component project that takes a beginner from "I have a laptop" to a fully configured VPS with coding agents, dev tools, and coordination infrastructure. + +### What It Does + +Provides a step-by-step wizard website, a one-liner installer, and an onboarding TUI to configure Ubuntu VPS instances with a complete agentic coding environment: shell setup, languages, dev tools, coding agents, and the Dicklesworthstone coordination stack. + +### Components + +| Component | Location | Purpose | +|-----------|----------|---------| +| Website Wizard | `apps/web/` | Next.js 16 App Router wizard guiding beginners | +| Installer | `install.sh` + `scripts/` | Bash installer, idempotent, checkpointed | +| Onboarding TUI | `packages/onboard/` | Interactive tutorial for Linux basics + agent workflow | +| Module Manifest | `acfs.manifest.yaml` | Single source of truth for all tools installed | +| ACFS Configs | `acfs/` | Shell, tmux, onboard configs installed to `~/.acfs/` | +| Manifest Parser | `packages/manifest/` | YAML parser + code generators | + +### Repo Layout ``` agentic_coding_flywheel_setup/ @@ -122,21 +235,22 @@ agentic_coding_flywheel_setup/ │ ├── scripts/ │ ├── lib/ # Installer library functions -│ └── providers/ # VPS provider guides +│ ├── generated/ # Auto-generated from manifest (NEVER edit) +│ ├── providers/ # VPS provider guides +│ ├── tests/ # Script-level tests +│ └── e2e/ # E2E test scripts │ └── tests/ - └── vm/ - └── test_install_ubuntu.sh + ├── vm/ # Docker-based installer integration + ├── e2e/ # End-to-end flow tests + ├── unit/ # Unit tests + └── smoke/ # Quick smoke tests ``` ---- - -## Generated Files — NEVER Edit Manually +### Generated Files — NEVER Edit Manually The following files are **auto-generated** from the manifest. Edits to these files will be **overwritten** on the next regeneration. -### Generated Locations - ``` scripts/generated/ # ALL files in this directory ├── install_*.sh # Category installer scripts @@ -144,62 +258,33 @@ scripts/generated/ # ALL files in this directory └── manifest_index.sh # Bash arrays with module metadata ``` -### How to Modify Generated Code +**How to modify generated code:** 1. **Identify the generator source**: `packages/manifest/src/generate.ts` 2. **Modify the generator**, not the output files 3. **Regenerate**: `cd packages/manifest && bun run generate` 4. **Verify**: `shellcheck scripts/generated/*.sh` -### Key Generator Components - -| File | Purpose | -|------|---------| -| `packages/manifest/src/generate.ts` | Main generator logic | -| `packages/manifest/src/schema.ts` | Zod schema for manifest validation | -| `packages/manifest/src/types.ts` | TypeScript interfaces | -| `acfs.manifest.yaml` | Source manifest (this IS hand-edited) | - -### Why This Matters - -If you manually edit a generated file: -- Your changes **will be lost** on next `bun run generate` -- Other developers won't know about your fix -- CI/CD may regenerate and overwrite your work +### Installer Architecture -Always fix the generator, then regenerate. - ---- - -## Code Editing Discipline - -- Do **not** run scripts that bulk-modify code (codemods, invented one-off scripts, giant `sed`/regex refactors). -- Large mechanical changes: break into smaller, explicit edits and review diffs. -- Subtle/complex changes: edit by hand, file-by-file, with careful reasoning. - ---- - -## Backwards Compatibility & File Sprawl - -We optimize for a clean architecture now, not backwards compatibility. - -- No "compat shims" or "v2" file clones. -- When changing behavior, migrate callers and remove old code. -- New files are only for genuinely new domains that don't fit existing modules. -- The bar for adding files is very high. - ---- +- **Auto-Upgrade:** Older Ubuntu versions are automatically upgraded to 25.10 before ACFS install + - Upgrade path: 22.04 -> 24.04 -> 25.04 -> 25.10 (EOL interim releases like 24.10 may be skipped) + - Takes 30-60 minutes per version hop; multiple reboots handled via systemd resume service + - Skip with `--skip-ubuntu-upgrade` flag +- **One-liner:** `curl -fsSL ... | bash -s -- --yes --mode vibe` +- **Idempotent:** Safe to re-run +- **Checkpointed:** Phases resume on failure -## Console Output (for installer scripts) +### Console Output (for installer scripts) The installer uses colored output for progress visibility: ```bash echo -e "\033[34m[1/8] Step description\033[0m" # Blue progress steps echo -e "\033[90m Details...\033[0m" # Gray indented details -echo -e "\033[33m⚠️ Warning message\033[0m" # Yellow warnings -echo -e "\033[31m✖ Error message\033[0m" # Red errors -echo -e "\033[32m✔ Success message\033[0m" # Green success +echo -e "\033[33m Warning message\033[0m" # Yellow warnings +echo -e "\033[31m Error message\033[0m" # Red errors +echo -e "\033[32m Success message\033[0m" # Green success ``` Rules: @@ -207,15 +292,13 @@ Rules: - `--quiet` flag suppresses progress but not errors - All output functions should use the logging library (`scripts/lib/logging.sh`) ---- - -## Third-Party Tools Installed by ACFS +### Third-Party Tools Installed by ACFS These are installed on target VPS (not development machine). -> **OS Requirement:** Ubuntu 25.10 (installer auto-upgrades from 22.04+; see Installer section above) +> **OS Requirement:** Ubuntu 25.10 (installer auto-upgrades from 22.04+) -### Shell & Terminal UX +**Shell & Terminal UX:** - **zsh** + **oh-my-zsh** + **powerlevel10k** - **lsd** (or eza fallback) — Modern ls - **atuin** — Shell history with Ctrl-R @@ -223,32 +306,32 @@ These are installed on target VPS (not development machine). - **zoxide** — Better cd - **direnv** — Directory-specific env vars -### Languages & Package Managers +**Languages & Package Managers:** - **bun** — JS/TS runtime + package manager - **uv** — Fast Python tooling - **rust/cargo** — Rust toolchain - **go** — Go toolchain -### Dev Tools +**Dev Tools:** - **tmux** — Terminal multiplexer - **ripgrep** (`rg`) — Fast search - **ast-grep** (`sg`) — Structural search/replace - **lazygit** — Git TUI - **bat** — Better cat -### Coding Agents +**Coding Agents:** - **Claude Code** — Anthropic's coding agent - **Codex CLI** — OpenAI's coding agent - **Gemini CLI** — Google's coding agent -### Cloud & Database +**Cloud & Database:** - **PostgreSQL 18** — Database - **HashiCorp Vault** — Secrets management - **Wrangler** — Cloudflare CLI - **Supabase CLI** — Supabase management - **Vercel CLI** — Vercel deployment -### Dicklesworthstone Stack (10 tools + utilities) +**Dicklesworthstone Stack (10 tools + utilities):** 1. **ntm** — Named Tmux Manager (agent cockpit) 2. **mcp_agent_mail** — Agent coordination via mail-like messaging 3. **ultimate_bug_scanner** (`ubs`) — Bug scanning with guardrails @@ -264,43 +347,7 @@ These are installed on target VPS (not development machine). - **giil** — Download cloud images (iCloud, Dropbox, Google Photos) for visual debugging - **csctf** — Convert AI chat share links to Markdown/HTML archives ---- - -## MCP Agent Mail — Multi-Agent Coordination - -Agent Mail is available as an MCP server for coordinating work across agents. - -What Agent Mail gives: -- Identities, inbox/outbox, searchable threads. -- Advisory file reservations (leases) to avoid agents clobbering each other. -- Persistent artifacts in git (human-auditable). - -Core patterns: - -1. **Same repo** - - Register identity: - - `ensure_project` then `register_agent` with the repo's absolute path as `project_key`. - - Reserve files before editing: - - `file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true)`. - - Communicate: - - `send_message(..., thread_id="FEAT-123")`. - - `fetch_inbox`, then `acknowledge_message`. - - Fast reads: - - `resource://inbox/{Agent}?project=&limit=20`. - - `resource://thread/{id}?project=&include_bodies=true`. - -2. **Macros vs granular:** - - Prefer macros when speed is more important than fine-grained control: - - `macro_start_session`, `macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake`. - - Use granular tools when you need explicit behavior. - -Common pitfalls: -- "from_agent not registered" → call `register_agent` with correct `project_key`. -- `FILE_RESERVATION_CONFLICT` → adjust patterns, wait for expiry, or use non-exclusive reservation. - ---- - -## Website Development (apps/web) +### Website Development (apps/web) ```bash cd apps/web @@ -319,134 +366,118 @@ Key patterns: --- -## Installer Testing - -```bash -# Local lint -shellcheck install.sh scripts/lib/*.sh - -# Full installer integration test (Docker, same as CI) -./tests/vm/test_install_ubuntu.sh -``` +## MCP Agent Mail — Multi-Agent Coordination ---- +A mail-like layer that lets coding agents coordinate asynchronously via MCP tools and resources. Provides identities, inbox/outbox, searchable threads, and advisory file reservations with human-auditable artifacts in Git. -## Landing the Plane (Session Completion) +### Why It's Useful -**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. +- **Prevents conflicts:** Explicit file reservations (leases) for files/globs +- **Token-efficient:** Messages stored in per-project archive, not in context +- **Quick reads:** `resource://inbox/...`, `resource://thread/...` -**MANDATORY WORKFLOW:** +### Same Repository Workflow -1. **File issues for remaining work** - Create issues for anything that needs follow-up -2. **Run quality gates** (if code changed) - Tests, linters, builds -3. **Update issue status** - Close finished work, update in-progress items -4. **PUSH TO REMOTE** - This is MANDATORY: - ```bash - git pull --rebase - br sync --flush-only - git add .beads/ - git commit -m "Update beads" - git push - git status # MUST show "up to date with origin" +1. **Register identity:** + ``` + ensure_project(project_key=) + register_agent(project_key, program, model) ``` -5. **Clean up** - Clear stashes, prune remote branches -6. **Verify** - All changes committed AND pushed -7. **Hand off** - Provide context for next session - -**CRITICAL RULES:** -- Work is NOT complete until `git push` succeeds -- NEVER stop before pushing - that leaves work stranded locally -- NEVER say "ready to push when you are" - YOU must push -- If push fails, resolve and retry until it succeeds - - ---- -## Issue Tracking with br (Beads) +2. **Reserve files before editing:** + ``` + file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true) + ``` -All issue tracking goes through **Beads**. No other TODO systems. +3. **Communicate with threads:** + ``` + send_message(..., thread_id="FEAT-123") + fetch_inbox(project_key, agent_name) + acknowledge_message(project_key, agent_name, message_id) + ``` -**Note:** `bd` is a backward-compatibility alias (installed by `acfs/zsh/acfs.zshrc`) for the beads_rust CLI: `br`. -The primary command is `br`. The old `bd` (golang beads) is deprecated but aliased for compatibility. +4. **Quick reads:** + ``` + resource://inbox/{Agent}?project=&limit=20 + resource://thread/{id}?project=&include_bodies=true + ``` -Key invariants: +### Macros vs Granular Tools -- `.beads/` is authoritative state and **must always be committed** with code changes. -- Do not edit `.beads/*.jsonl` directly; only via `br` / `bd`. +- **Prefer macros for speed:** `macro_start_session`, `macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake` +- **Use granular tools for control:** `register_agent`, `file_reservation_paths`, `send_message`, `fetch_inbox`, `acknowledge_message` -### Basics +### Common Pitfalls -Check ready work: +- `"from_agent not registered"`: Always `register_agent` in the correct `project_key` first +- `"FILE_RESERVATION_CONFLICT"`: Adjust patterns, wait for expiry, or use non-exclusive reservation +- **Auth errors:** If JWT+JWKS enabled, include bearer token with matching `kid` -```bash -br ready --json -``` - -Create issues: +--- -```bash -br create "Issue title" -t bug|feature|task -p 0-4 --json -br create "Issue title" -p 1 --deps discovered-from:br-123 --json -``` +## Beads (br) — Dependency-Aware Issue Tracking -Update: +Beads provides a lightweight, dependency-aware issue database and CLI (`br` - beads_rust) for selecting "ready work," setting priorities, and tracking status. It complements MCP Agent Mail's messaging and file reservations. -```bash -br update br-42 --status in_progress --json -br update br-42 --priority 1 --json -``` +**Important:** `br` is non-invasive—it NEVER runs git commands automatically. You must manually commit changes after `br sync --flush-only`. -Complete: +### Conventions -```bash -br close br-42 --reason "Completed" --json -``` +- **Single source of truth:** Beads for task status/priority/dependencies; Agent Mail for conversation and audit +- **Shared identifiers:** Use Beads issue ID (e.g., `br-123`) as Mail `thread_id` and prefix subjects with `[br-123]` +- **Reservations:** When starting a task, call `file_reservation_paths()` with the issue ID in `reason` -Types: +### Typical Agent Flow -- `bug`, `feature`, `task`, `epic`, `chore` - -Priorities: - -- `0` critical (security, data loss, broken builds) -- `1` high -- `2` medium (default) -- `3` low -- `4` backlog +1. **Pick ready work (Beads):** + ```bash + br ready --json # Choose highest priority, no blockers + ``` -Agent workflow: +2. **Reserve edit surface (Mail):** + ``` + file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true, reason="br-123") + ``` -1. `br ready` to find unblocked work. -2. Claim: `br update --status in_progress`. -3. Implement + test. -4. If you discover new work, create a new bead with `discovered-from:`. -5. Close when done. -6. Commit `.beads/` in the same commit as code changes. +3. **Announce start (Mail):** + ``` + send_message(..., thread_id="br-123", subject="[br-123] Start: ", ack_required=true) + ``` -Sync: +4. **Work and update:** Reply in-thread with progress -- Run `br sync --flush-only` (or `bd sync --flush-only`) to export to `.beads/issues.jsonl` without git operations. -- Then run `git add .beads/ && git commit -m "Update beads"` to commit changes. +5. **Complete and release:** + ```bash + br close 123 --reason "Completed" + br sync --flush-only # Export to JSONL (no git operations) + ``` + ``` + release_file_reservations(project_key, agent_name, paths=["src/**"]) + ``` + Final Mail reply: `[br-123] Completed` with summary -Never: +### Mapping Cheat Sheet -- Use markdown TODO lists. -- Use other trackers. -- Duplicate tracking. +| Concept | Value | +|---------|-------| +| Mail `thread_id` | `br-###` | +| Mail subject | `[br-###] ...` | +| File reservation `reason` | `br-###` | +| Commit messages | Include `br-###` for traceability | --- -### Using bv as an AI sidecar +## bv — Graph-Aware Triage Engine -bv is a graph-aware triage engine for Beads projects (.beads/beads.jsonl). Instead of parsing JSONL or hallucinating graph traversal, use robot flags for deterministic, dependency-aware outputs with precomputed metrics (PageRank, betweenness, critical path, cycles, HITS, eigenvector, k-core). +bv is a graph-aware triage engine for Beads projects (`.beads/beads.jsonl`). It computes PageRank, betweenness, critical path, cycles, HITS, eigenvector, and k-core metrics deterministically. -**Scope boundary:** bv handles *what to work on* (triage, priority, planning). For agent-to-agent coordination (messaging, work claiming, file reservations), use MCP Agent Mail, which should be available to you as an an MCP server (if it's not, then flag to the user; they might need to start Agent Mail using the `am` alias or by running `cd "<directory_where_they_installed_agent_mail>/mcp_agent_mail" && bash scripts/run_server_with_token.sh)' if the alias isn't available or isn't working. +**Scope boundary:** bv handles *what to work on* (triage, priority, planning). For agent-to-agent coordination (messaging, work claiming, file reservations), use MCP Agent Mail. -**⚠️ CRITICAL: Use ONLY `--robot-*` flags. Bare `bv` launches an interactive TUI that blocks your session.** +**CRITICAL: Use ONLY `--robot-*` flags. Bare `bv` launches an interactive TUI that blocks your session.** -#### The Workflow: Start With Triage +### The Workflow: Start With Triage -**`bv --robot-triage` is your single entry point.** It returns everything you need in one call: +**`bv --robot-triage` is your single entry point.** It returns: - `quick_ref`: at-a-glance counts + top 3 picks - `recommendations`: ranked actionable items with scores, reasons, unblock info - `quick_wins`: low-effort high-impact items @@ -454,10 +485,12 @@ bv is a graph-aware triage engine for Beads projects (.beads/beads.jsonl). Inste - `project_health`: status/type/priority distributions, graph metrics - `commands`: copy-paste shell commands for next steps +```bash bv --robot-triage # THE MEGA-COMMAND: start here bv --robot-next # Minimal: just the single top pick + claim command +``` -#### Other bv Commands +### Command Reference **Planning:** | Command | Returns | @@ -468,418 +501,353 @@ bv --robot-next # Minimal: just the single top pick + claim command **Graph Analysis:** | Command | Returns | |---------|---------| -| `--robot-insights` | Full metrics: PageRank, betweenness, HITS (hubs/authorities), eigenvector, critical path, cycles, k-core, articulation points, slack | -| `--robot-label-health` | Per-label health: `health_level` (healthy\|warning\|critical), `velocity_score`, `staleness`, `blocked_count` | +| `--robot-insights` | Full metrics: PageRank, betweenness, HITS, eigenvector, critical path, cycles, k-core, articulation points, slack | +| `--robot-label-health` | Per-label health: `health_level`, `velocity_score`, `staleness`, `blocked_count` | | `--robot-label-flow` | Cross-label dependency: `flow_matrix`, `dependencies`, `bottleneck_labels` | -| `--robot-label-attention [--attention-limit=N]` | Attention-ranked labels by: (pagerank × staleness × block_impact) / velocity | +| `--robot-label-attention [--attention-limit=N]` | Attention-ranked labels | **History & Change Tracking:** | Command | Returns | |---------|---------| -| `--robot-history` | Bead-to-commit correlations: `stats`, `histories` (per-bead events/commits/milestones), `commit_index` | -| `--robot-diff --diff-since <ref>` | Changes since ref: new/closed/modified issues, cycles introduced/resolved | +| `--robot-history` | Bead-to-commit correlations | +| `--robot-diff --diff-since <ref>` | Changes since ref: new/closed/modified issues, cycles | -**Other Commands:** +**Other:** | Command | Returns | |---------|---------| | `--robot-burndown <sprint>` | Sprint burndown, scope changes, at-risk items | | `--robot-forecast <id\|all>` | ETA predictions with dependency-aware scheduling | | `--robot-alerts` | Stale issues, blocking cascades, priority mismatches | -| `--robot-suggest` | Hygiene: duplicates, missing deps, label suggestions, cycle breaks | +| `--robot-suggest` | Hygiene: duplicates, missing deps, label suggestions | | `--robot-graph [--graph-format=json\|dot\|mermaid]` | Dependency graph export | -| `--export-graph <file.html>` | Self-contained interactive HTML visualization | +| `--export-graph <file.html>` | Interactive HTML visualization | -#### Scoping & Filtering +### Scoping & Filtering +```bash bv --robot-plan --label backend # Scope to label's subgraph bv --robot-insights --as-of HEAD~30 # Historical point-in-time -bv --recipe actionable --robot-plan # Pre-filter: ready to work (no blockers) -bv --recipe high-impact --robot-triage # Pre-filter: top PageRank scores +bv --recipe actionable --robot-plan # Pre-filter: ready to work +bv --recipe high-impact --robot-triage # Pre-filter: top PageRank bv --robot-triage --robot-triage-by-track # Group by parallel work streams bv --robot-triage --robot-triage-by-label # Group by domain +``` -#### Understanding Robot Output +### Understanding Robot Output **All robot JSON includes:** -- `data_hash` — Fingerprint of source beads.jsonl (verify consistency across calls) +- `data_hash` — Fingerprint of source beads.jsonl - `status` — Per-metric state: `computed|approx|timeout|skipped` + elapsed ms -- `as_of` / `as_of_commit` — Present when using `--as-of`; contains ref and resolved SHA +- `as_of` / `as_of_commit` — Present when using `--as-of` **Two-phase analysis:** -- **Phase 1 (instant):** degree, topo sort, density — always available immediately -- **Phase 2 (async, 500ms timeout):** PageRank, betweenness, HITS, eigenvector, cycles — check `status` flags +- **Phase 1 (instant):** degree, topo sort, density +- **Phase 2 (async, 500ms timeout):** PageRank, betweenness, HITS, eigenvector, cycles -**For large graphs (>500 nodes):** Some metrics may be approximated or skipped. Always check `status`. - -#### jq Quick Reference +### jq Quick Reference +```bash bv --robot-triage | jq '.quick_ref' # At-a-glance summary bv --robot-triage | jq '.recommendations[0]' # Top recommendation bv --robot-plan | jq '.plan.summary.highest_impact' # Best unblock target bv --robot-insights | jq '.status' # Check metric readiness bv --robot-insights | jq '.Cycles' # Circular deps (must fix!) -bv --robot-label-health | jq '.results.labels[] | select(.health_level == "critical")' - -**Performance:** Phase 1 instant, Phase 2 async (500ms timeout). Prefer `--robot-plan` over `--robot-insights` when speed matters. Results cached by data hash. - -Use bv instead of parsing beads.jsonl—it computes PageRank, critical paths, cycles, and parallel tracks deterministically. +``` --- -### Morph Warp Grep — AI-Powered Code Search +## UBS — Ultimate Bug Scanner -Use `mcp__morph-mcp__warp_grep` for “how does X work?” discovery across the codebase. +**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run. -When to use: +### Commands -- You don’t know where something lives. -- You want data flow across multiple files (API → service → schema → types). -- You want all touchpoints of a cross-cutting concern (e.g., moderation, billing). +```bash +ubs file.sh file2.ts # Specific files (< 1s) — USE THIS +ubs $(git diff --name-only --cached) # Staged files — before commit +ubs --only=bash,js src/ # Language filter (3-5x faster) +ubs --ci --fail-on-warning . # CI mode — before PR +ubs . # Whole project (ignores node_modules, .venv) +``` -Example: +### Output Format ``` -mcp__morph-mcp__warp_grep( - repoPath: "/data/projects/communitai", - query: "How is the L3 Guardian appeals system implemented?" -) + Category (N errors) + file.sh:42:5 - Issue description + Suggested fix +Exit code: 1 ``` -Warp Grep: +Parse: `file:line:col` -> location | fix suggestion -> how to fix | Exit 0/1 -> pass/fail -- Expands a natural-language query to multiple search patterns. -- Runs targeted greps, reads code, follows imports, then returns concise snippets with line numbers. -- Reduces token usage by returning only relevant slices, not entire files. +### Fix Workflow -When **not** to use Warp Grep: - -- You already know the function/identifier name; use `rg`. -- You know the exact file; just open it. -- You only need a yes/no existence check. +1. Read finding -> category + fix suggestion +2. Navigate `file:line:col` -> view context +3. Verify real issue (not false positive) +4. Fix root cause (not symptom) +5. Re-run `ubs <file>` -> exit 0 +6. Commit -Comparison: +### Bug Severity -| Scenario | Tool | -| ---------------------------------- | ---------- | -| “How is auth session validated?” | warp_grep | -| “Where is `handleSubmit` defined?” | `rg` | -| “Replace `var` with `let`” | `ast-grep` | +- **Critical (always fix):** Injection, unquoted variables, unsafe eval, command injection +- **Important (production):** Unhandled errors, resource leaks, missing error checks +- **Contextual (judgment):** TODO/FIXME, console logs, debugging output --- -### cass — Cross-Agent Search - -`cass` indexes prior agent conversations (Claude Code, Codex, Cursor, Gemini, ChatGPT, etc.) so we can reuse solved problems. +## RCH — Remote Compilation Helper -Rules: +RCH offloads `cargo build`, `cargo test`, `cargo clippy`, and other compilation commands to a fleet of 8 remote Contabo VPS workers instead of building locally. This prevents compilation storms from overwhelming csd when many agents run simultaneously. -- Never run bare `cass` (TUI). Always use `--robot` or `--json`. +**RCH is installed at `~/.local/bin/rch` and is hooked into Claude Code's PreToolUse automatically.** Most of the time you don't need to do anything if you are Claude Code — builds are intercepted and offloaded transparently. -Examples: +To manually offload a build: +```bash +rch exec -- cargo build --release +rch exec -- cargo test +rch exec -- cargo clippy +``` +Quick commands: ```bash -cass health -cass search "authentication error" --robot --limit 5 -cass view /path/to/session.jsonl -n 42 --json -cass expand /path/to/session.jsonl -n 42 -C 3 --json -cass capabilities --json -cass robot-docs guide +rch doctor # Health check +rch workers probe --all # Test connectivity to all 8 workers +rch status # Overview of current state +rch queue # See active/waiting builds ``` -Tips: +If rch or its workers are unavailable, it fails open — builds run locally as normal. -- Use `--fields minimal` for lean output. -- Filter by agent with `--agent`. -- Use `--days N` to limit to recent history. +**Note for Codex/GPT-5.2:** Codex does not have the automatic PreToolUse hook, but you can (and should) still manually offload compute-intensive compilation commands using `rch exec -- <command>`. This avoids local resource contention when multiple agents are building simultaneously. -stdout is data-only, stderr is diagnostics; exit code 0 means success. +--- -Treat cass as a way to avoid re-solving problems other agents already handled. +## ast-grep vs ripgrep ---- +**Use `ast-grep` when structure matters.** It parses code and matches AST nodes, ignoring comments/strings, and can **safely rewrite** code. -## Memory System: cass-memory +- Refactors/codemods: rename APIs, change import forms +- Policy checks: enforce patterns across a repo +- Editor/automation: LSP mode, `--json` output -The Cass Memory System (cm) is a tool for giving agents an effective memory based on the ability to quickly search across previous coding agent sessions across an array of different coding agent tools (e.g., Claude Code, Codex, Gemini-CLI, Cursor, etc) and projects (and even across multiple machines, optionally) and then reflect on what they find and learn in new sessions to draw out useful lessons and takeaways; these lessons are then stored and can be queried and retrieved later, much like how human memory works. +**Use `ripgrep` when text is enough.** Fastest way to grep literals/regex. -The `cm onboard` command guides you through analyzing historical sessions and extracting valuable rules. +- Recon: find strings, TODOs, log lines, config values +- Pre-filter: narrow candidate files before ast-grep -### Quick Start +### Rule of Thumb -```bash -# 1. Check status and see recommendations -cm onboard status +- Need correctness or **applying changes** -> `ast-grep` +- Need raw speed or **hunting text** -> `rg` +- Often combine: `rg` to shortlist files, then `ast-grep` to match/modify -# 2. Get sessions to analyze (filtered by gaps in your playbook) -cm onboard sample --fill-gaps +### Examples -# 3. Read a session with rich context -cm onboard read /path/to/session.jsonl --template +```bash +# Find structured code (ignores comments) +ast-grep run -l TypeScript -p 'function $NAME($$$ARGS) { $$$BODY }' -# 4. Add extracted rules (one at a time or batch) -cm playbook add "Your rule content" --category "debugging" -# Or batch add: -cm playbook add --file rules.json +# Quick textual hunt +rg -n 'console.log' -t ts -# 5. Mark session as processed -cm onboard mark-done /path/to/session.jsonl +# Combine speed + precision +rg -l -t ts 'useState' | xargs ast-grep run -l TypeScript -p 'useState($INIT)' --json ``` -Before starting complex tasks, retrieve relevant context: +--- -```bash -cm context "<task description>" --json -``` +## Morph Warp Grep — AI-Powered Code Search -This returns: -- **relevantBullets**: Rules that may help with your task -- **antiPatterns**: Pitfalls to avoid -- **historySnippets**: Past sessions that solved similar problems -- **suggestedCassQueries**: Searches for deeper investigation +**Use `mcp__morph-mcp__warp_grep` for exploratory "how does X work?" questions.** An AI agent expands your query, greps the codebase, reads relevant files, and returns precise line ranges with full context. -### Protocol +**Use `ripgrep` for targeted searches.** When you know exactly what you're looking for. -1. **START**: Run `cm context "<task>" --json` before non-trivial work -2. **WORK**: Reference rule IDs when following them (e.g., "Following b-8f3a2c...") -3. **FEEDBACK**: Leave inline comments when rules help/hurt: - - `// [cass: helpful b-xyz] - reason` - - `// [cass: harmful b-xyz] - reason` -4. **END**: Just finish your work. Learning happens automatically. +**Use `ast-grep` for structural patterns.** When you need AST precision for matching/rewriting. -### Key Flags +### When to Use What -| Flag | Purpose | -|------|---------| -| `--json` | Machine-readable JSON output (required!) | -| `--limit N` | Cap number of rules returned | -| `--no-history` | Skip historical snippets for faster response | +| Scenario | Tool | Why | +|----------|------|-----| +| "How does the installer handle Ubuntu upgrades?" | `warp_grep` | Exploratory; don't know where to start | +| "Where is the checksum verification implemented?" | `warp_grep` | Need to understand architecture | +| "Find all uses of `logging.sh`" | `ripgrep` | Targeted literal search | +| "Find files with `echo -e`" | `ripgrep` | Simple pattern | +| "Replace `var` with `let` in TypeScript" | `ast-grep` | Structural refactor | -stdout = data only, stderr = diagnostics. Exit 0 = success. +### warp_grep Usage + +``` +mcp__morph-mcp__warp_grep( + repoPath: "/dp/agentic_coding_flywheel_setup", + query: "How does the installer handle Ubuntu version upgrades?" +) +``` + +Returns structured results with file paths, line ranges, and extracted code snippets. + +### Anti-Patterns + +- **Don't** use `warp_grep` to find a specific function name -> use `ripgrep` +- **Don't** use `ripgrep` to understand "how does X work" -> wastes time with manual reads +- **Don't** use `ripgrep` for codemods -> risks collateral edits + +<!-- bv-agent-instructions-v1 --> --- -## UBS Quick Reference for AI Agents +## Beads Workflow Integration -UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On** +This project uses [beads_rust](https://github.com/Dicklesworthstone/beads_rust) (`br`) for issue tracking. Issues are stored in `.beads/` and tracked in git. -**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run. +**Important:** `br` is non-invasive—it NEVER executes git commands. After `br sync --flush-only`, you must manually run `git add .beads/ && git commit`. + +### Essential Commands -**Commands:** ```bash -ubs file.ts file2.py # Specific files (< 1s) — USE THIS -ubs $(git diff --name-only --cached) # Staged files — before commit -ubs --only=js,python src/ # Language filter (3-5x faster) -ubs --ci --fail-on-warning . # CI mode — before PR -ubs --help # Full command reference -ubs sessions --entries 1 # Tail the latest install session log -ubs . # Whole project (ignores things like .venv and node_modules automatically) +# View issues (launches TUI - avoid in automated sessions) +bv + +# CLI commands for agents (use these instead) +br ready # Show issues ready to work (no blockers) +br list --status=open # All open issues +br show <id> # Full issue details with dependencies +br create --title="..." --type=task --priority=2 +br update <id> --status=in_progress +br close <id> --reason "Completed" +br close <id1> <id2> # Close multiple issues at once +br sync --flush-only # Export to JSONL (NO git operations) ``` -**Output Format:** -``` -⚠️ Category (N errors) - file.ts:42:5 – Issue description - 💡 Suggested fix -Exit code: 1 +### Workflow Pattern + +1. **Start**: Run `br ready` to find actionable work +2. **Claim**: Use `br update <id> --status=in_progress` +3. **Work**: Implement the task +4. **Complete**: Use `br close <id>` +5. **Sync**: Run `br sync --flush-only` then manually commit + +### Key Concepts + +- **Dependencies**: Issues can block other issues. `br ready` shows only unblocked work. +- **Priority**: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers, not words) +- **Types**: task, bug, feature, epic, chore +- **Blocking**: `br dep add <issue> <depends-on>` to add dependencies + +### Session Protocol + +**Before ending any session, run this checklist:** + +```bash +git status # Check what changed +git add <files> # Stage code changes +br sync --flush-only # Export beads to JSONL +git add .beads/ # Stage beads changes +git commit -m "..." # Commit everything together +git push # Push to remote ``` -Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail -**Fix Workflow:** -1. Read finding → category + fix suggestion -2. Navigate `file:line:col` → view context -3. Verify real issue (not false positive) -4. Fix root cause (not symptom) -5. Re-run `ubs <file>` → exit 0 -6. Commit +### Best Practices + +- Check `br ready` at session start to find available work +- Update status as you work (in_progress -> closed) +- Create new issues with `br create` when you discover tasks +- Use descriptive titles and set appropriate priority/type +- Always `br sync --flush-only && git add .beads/` before ending session -**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits. +<!-- end-bv-agent-instructions --> -**Bug Severity:** -- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks -- **Important** (production): Type narrowing, division-by-zero, resource leaks -- **Contextual** (judgment): TODO/FIXME, console logs +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. + +**MANDATORY WORKFLOW:** -**Anti-Patterns:** -- ❌ Ignore findings → ✅ Investigate each -- ❌ Full scan per edit → ✅ Scope to file -- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`) +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **Sync beads** - `br sync --flush-only` to export to JSONL +5. **Hand off** - Provide context for next session --- -## DCG Quick Reference for AI Agents +## Auxiliary Tools + +### DCG — Destructive Command Guard -DCG (Destructive Command Guard) is a Claude Code hook that **blocks dangerous git and filesystem commands** before execution. Sub-millisecond latency, mechanical enforcement. +DCG is a Claude Code hook that **blocks dangerous git and filesystem commands** before execution. Sub-millisecond latency, mechanical enforcement. -**Golden Rule:** DCG works automatically—you don't need to call it. When a dangerous command is blocked, use safer alternatives or ask the user to run it manually. +**Golden Rule:** DCG works automatically. When a dangerous command is blocked, use safer alternatives or ask the user to run it manually. -**Commands:** ```bash dcg test "<cmd>" [--explain] # Test if a command would be blocked dcg packs [--enabled] [--verbose] # List packs -dcg pack <pack-id> [--patterns] # Pack details + match patterns dcg allow-once <code> # One-time bypass code -dcg allow <rule-id> --reason "..." # Add allowlist entry (project/user) dcg doctor [--fix] [--format json] # Health check + auto-fix dcg install [--force] # Register Claude Code hook -dcg uninstall [--purge] # Remove hook (and optionally binary/config) ``` -**Auto-Blocked Commands:** -```bash -git reset --hard # Destroys uncommitted changes -git checkout -- <files> # Discards file changes permanently -git restore <files> # Same as checkout -- (not --staged) -git push --force / -f # Overwrites remote history -git clean -f # Deletes untracked files -git branch -D # Force-deletes without merge check -git stash drop / clear # Permanently deletes stashes -rm -rf <non-temp> # Recursive deletion -``` - -**Always Allowed:** -```bash -git checkout -b <branch> # Creates branch, doesn't touch files -git restore --staged # Only unstages, safe -git clean -n # Dry-run, preview only -rm -rf /tmp/... # Temp directories are ephemeral -git push --force-with-lease # Safe force push variant -``` - -**When Blocked:** -- You'll see a clear reason explaining why -- Ask the user to run the command manually if truly needed -- Consider safer alternatives (git stash, --force-with-lease) - -**Configuration:** -- Config file: `~/.config/dcg/config.toml` -- View available packs: `dcg packs` (shows all), `dcg packs --enabled` (shows active) -- Pack examples: `git`, `filesystem`, `database.postgresql`, `containers.docker`, `kubernetes` -```toml -# ~/.config/dcg/config.toml -[packs] -enabled = ["git", "filesystem", "database.postgresql", "containers.docker"] -``` -- Allowlist management: `dcg allow <rule-id> --reason "..." --project <path>` (or `--user`) - -**Common Scenarios:** -- **Blocked command** → Read the reason, prefer the safer alternative, or use `dcg allow-once <code>`. -- **Hook missing after updates** → `dcg install --force`. -- **Need to disable** → `dcg uninstall` (or `dcg uninstall --purge` for full removal). - -**Troubleshooting:** - -| Issue | Solution | -|-------|----------| -| DCG blocks legitimate command | Ask user to run manually, or use allow-once code if provided | -| Hook not registered | Run `dcg install` | -| DCG not blocking anything | Run `dcg doctor` to verify hook is active | -| False positive | Check if command matches safe patterns; report to GitHub if bug | -| Config not being read | Verify `~/.config/dcg/config.toml` format is valid TOML | - -**Agent Integration Tips:** -- DCG is automatic—no need to call `dcg test` before commands -- When blocked, explain to user why the command is dangerous -- Suggest safer alternatives (e.g., `--force-with-lease` instead of `--force`) -- Never try to bypass DCG—ask user to run dangerous commands manually -- DCG has sub-millisecond latency, designed to not slow down your workflow - ---- - -## RU Quick Reference for AI Agents +### RU — Repo Updater -RU (Repo Updater) is a multi-repo sync tool with **AI-driven commit automation**. +Multi-repo sync tool with AI-driven commit automation. -**Common Commands:** ```bash ru sync # Clone missing + pull updates for all repos ru sync --parallel 4 # Parallel sync (4 workers) ru status # Check repo status without changes -ru status --fetch # Fetch + show ahead/behind -ru list --paths # List all repo paths -``` - -**Agent Sweep (commit automation):** -```bash ru agent-sweep --dry-run # Preview dirty repos to process ru agent-sweep --parallel 4 # AI-driven commits in parallel -ru agent-sweep --with-release # Include version tag + release ``` -**Exit Codes:** -- `0` = Success -- `1` = Partial failure (some repos failed) -- `2` = Conflicts exist (manual resolution needed) -- `5` = Interrupted (use `--resume`) - -**Best Practices:** -- Use `ru status` before `ru sync` to preview changes -- Use `ru agent-sweep --dry-run` before full automation -- Scope with `--repos=pattern` for targeted operations - ---- - -## giil Quick Reference for AI Agents +### giil — Cloud Image Downloader -giil (Get Image from Internet Link) downloads **cloud-hosted images** to the terminal for visual debugging. +Downloads cloud-hosted images to the terminal for visual debugging. -**Usage:** ```bash giil "https://share.icloud.com/..." # Download iCloud photo giil "https://www.dropbox.com/s/..." # Download Dropbox image giil "https://photos.google.com/..." # Download Google Photos image -giil "..." --output ~/screenshots # Custom output directory -giil "..." --json # JSON metadata output -giil "..." --all # Download all photos from album ``` -**Supported Platforms:** -- iCloud (share.icloud.com) -- Dropbox (dropbox.com/s/, dl.dropbox.com) -- Google Photos (photos.google.com) -- Google Drive (drive.google.com) - -**Exit Codes:** -- `0` = Success -- `10` = Network error -- `11` = Auth required (not publicly shared) -- `12` = Not found (expired link) -- `13` = Unsupported type (video, doc) - -**Visual Debugging Workflow:** -1. User screenshots bug on phone -2. Shares iCloud/Dropbox link with agent -3. `giil "<url>"` downloads to working directory -4. Agent analyzes the image - ---- +Supports: iCloud, Dropbox, Google Photos, Google Drive. -## csctf Quick Reference for AI Agents +### csctf — Chat Share to File -csctf (Chat Shared Conversation to File) converts **AI chat share links** to Markdown/HTML. +Converts AI chat share links to Markdown/HTML archives. -**Usage:** ```bash csctf "https://chatgpt.com/share/..." # ChatGPT conversation -csctf "https://gemini.google.com/share/..." # Gemini conversation -csctf "https://claude.ai/share/..." # Claude conversation +csctf "https://claude.ai/share/..." # Claude conversation csctf "..." --md-only # Markdown only (no HTML) -csctf "..." --json # JSON metadata output -csctf "..." --publish-to-gh-pages --yes # Publish to GitHub Pages ``` -**Output:** -- `<slug>.md` — Clean Markdown with code blocks -- `<slug>.html` — Static HTML with syntax highlighting +### cass — Cross-Agent Search + +Indexes prior agent conversations so solved problems can be reused. + +```bash +cass search "authentication error" --robot --limit 5 +cass view /path/to/session.jsonl -n 42 --json +cass capabilities --json +``` + +**Never run bare `cass` (TUI). Always use `--robot` or `--json`.** -**Use Cases:** -- Archive important AI conversations for reference -- Build searchable knowledge base -- Share solutions with team members -- Document debugging sessions for future learning +### cm — Cass Memory System + +Procedural memory for agents based on cross-session analysis. + +```bash +cm onboard status # Check status +cm onboard sample --fill-gaps # Get sessions to analyze +cm context "<task description>" --json # Retrieve relevant context before work +``` --- -## Note for Codex/GPT-5.2 +Note for Codex/GPT-5.2: You constantly bother me and stop working with concerned questions that look similar to this: @@ -893,7 +861,7 @@ Next steps (pick one) 1. Decide how to handle the unrelated modified files above so we can resume cleanly. ``` -NEVER EVER DO THAT AGAIN. The answer is literally ALWAYS the same: those are changes created by the potentially dozen of other agents working on the project at the same time. This is not only a common occurence, it happens multiple times PER MINUTE. The way to deal with it is simple: you NEVER, under ANY CIRCUMSTANCE, stash, revert, overwrite, or otherwise disturb in ANY way the work of other agents. Just treat those changes identically to changes that you yourself made. Just fool yourself into thinking YOU made the changes and simply don't recall it for some reason. +NEVER EVER DO THAT AGAIN. The answer is literally ALWAYS the same: those are changes created by the potentially dozen of other agents working on the project at the same time. This is not only a common occurrence, it happens multiple times PER MINUTE. The way to deal with it is simple: you NEVER, under ANY CIRCUMSTANCE, stash, revert, overwrite, or otherwise disturb in ANY way the work of other agents. Just treat those changes identically to changes that you yourself made. Just fool yourself into thinking YOU made the changes and simply don't recall it for some reason. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc49751..7a2104f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING**: `git_safety_guard` replaced by **DCG (Destructive Command Guard)** + - The legacy `git_safety_guard.py` hook has been fully removed + - DCG is now the only supported command safety mechanism + - Automatic cleanup of legacy files during `acfs update` + +### Migration: git_safety_guard → DCG + +If you installed ACFS before January 11, 2026, your installation may have legacy `git_safety_guard` files. + +**Symptoms of old installation:** +- `acfs doctor` shows "Git safety guard" warning +- Files exist at `~/.acfs/claude/hooks/git_safety_guard.py` or `~/.claude/hooks/` + +**Migration steps:** +1. Run `acfs update` - this automatically removes legacy files +2. Install DCG: `dcg install` +3. Verify: `acfs doctor` should show only DCG check (no git_safety_guard warnings) + +**Why DCG?** +- Built in Rust for sub-millisecond latency (vs Python startup overhead) +- Modular pack system for extensibility +- Dedicated repository and maintenance at [dcg](https://github.com/Dicklesworthstone/destructive_command_guard) + ### Added +- **New Flywheel Tools**: + - **beads_rust (br)** - Rust port of issue tracker, replaces golang beads + - `bd` alias maintained for backward compatibility + - Companion **bv** (beads_viewer) for graph-aware task triage + - **meta_skill (ms)** - Knowledge management and skill distribution + - **remote_compilation_helper (rch)** - Build acceleration for agent swarms + - **wezterm_automata (wa)** - Multi-agent orchestration via WezTerm + - **brenner_bot** - Research session CLI and orchestration + +- **Utility Tools** (9 new): + - **toon_rust (tru)** - Token-optimized notation format + - **rust_proxy** - Transparent proxy routing + - **rano** - Network observer for AI CLIs + - **xf** - X (Twitter) archive search + - **markdown_web_browser (mdwb)** - Website to Markdown converter + - **process_triage (pt)** - Zombie process detector + - **aadc** - ASCII diagram corrector + - **source_to_prompt_tui (s2p)** - Code to LLM prompt generator + - **coding_agent_usage_tracker (caut)** - LLM provider usage tracker + +- **E2E Testing**: New comprehensive test suite at `tests/e2e/test_new_tools_e2e.sh` + - Verifies all 16 new tools install correctly + - Integration tests for acfs doctor, bd alias, flywheel.ts + - JSON output for CI integration + - **Automatic Ubuntu Upgrade**: The installer now automatically upgrades Ubuntu to 25.10 before running the main ACFS installation - Detects current Ubuntu version and calculates sequential upgrade path - Handles reboots automatically via systemd resume service diff --git a/README.md b/README.md index b280bd1f..02a64dff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Agentic Coding Flywheel Setup (ACFS) -![Version](https://img.shields.io/badge/Version-0.5.0-bd93f9?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-0.6.0-bd93f9?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Ubuntu%2025.10-6272a4?style=for-the-badge) ![License](https://img.shields.io/badge/License-MIT-50fa7b?style=for-the-badge) ![Shell](https://img.shields.io/badge/Shell-Bash-ff79c6?style=for-the-badge) @@ -508,11 +508,29 @@ curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_f This checks: - OS compatibility (Ubuntu 22.04+; installer upgrades to 25.10) - Architecture (x86_64 or ARM64) -- Memory and disk space +- Memory and disk space (minimum 4GB RAM, 10GB free disk) - Network connectivity to required URLs - APT lock status - Potential conflicts (nvm, pyenv, existing ACFS) +**Network checks performed:** +| Check | What it verifies | Fix if failing | +|-------|------------------|----------------| +| DNS resolution | Can resolve github.com, raw.githubusercontent.com | Check `/etc/resolv.conf` or add `8.8.8.8` | +| GitHub HTTPS | Can reach github.com:443 | Check firewall, proxy, or VPN settings | +| Installer URLs | Raw GitHub, Homebrew, Oh-My-Zsh, Rust, etc. | May need to retry; transient failures OK | +| APT mirrors | Default Ubuntu mirror reachable | Check `/etc/apt/sources.list` or try different mirror | + +**Common preflight failures:** + +| Error | Cause | Solution | +|-------|-------|----------| +| "Cannot resolve github.com" | DNS misconfigured | Add `nameserver 8.8.8.8` to `/etc/resolv.conf` | +| "Cannot reach github.com" | Firewall blocking HTTPS | Allow outbound port 443 | +| "APT mirror slow or unreachable" | Regional mirror down | Edit `/etc/apt/sources.list` to use `archive.ubuntu.com` | +| "APT lock held" | Another apt process running | Wait for it to finish or `sudo kill <pid>` | +| "Insufficient disk space" | Less than 10GB free | Clean up with `sudo apt autoremove` or expand disk | + ### Console Output The installer uses semantic colors for progress visibility: @@ -599,7 +617,7 @@ acfs-update --yes --quiet # Automated/CI mode with minimal output | **Runtime** | Rust | `rustup update stable` | | **Runtime** | uv (Python) | `uv self update` | | **Runtime** | Go | `apt upgrade` (if apt-managed) | -| **Agents** | Claude Code | `claude update` | +| **Agents** | Claude Code | `claude update --channel latest` | | **Agents** | Codex, Gemini | `bun install -g @latest` | | **Cloud** | Wrangler, Vercel | `bun install -g @latest` | | **Cloud** | Supabase | GitHub release tarball (sha256 checksums) | @@ -677,7 +695,7 @@ acfs continue # View upgrade progress after reboot ### `acfs newproj` — New Project Wizard -Create a new project directory with ACFS defaults (git init, optional bd, Claude settings, AGENTS.md). +Create a new project directory with ACFS defaults (git init, optional br/beads, Claude settings, AGENTS.md). The interactive wizard is recommended for beginners. Interactive wizard (recommended): @@ -690,7 +708,7 @@ acfs newproj -i myapp # Prefill project name The wizard guides you through: - Project naming and location - Tech stack detection/selection -- Feature selection (bd, Claude settings, AGENTS.md, UBS ignore) +- Feature selection (br/beads, Claude settings, AGENTS.md, UBS ignore) - AGENTS.md customization preview <details> @@ -765,7 +783,7 @@ CLI mode (automation): ```bash acfs newproj myapp acfs newproj myapp /custom/path -acfs newproj myapp --no-bd +acfs newproj myapp --no-br ``` Notes: @@ -976,6 +994,17 @@ Plan (Beads) ──> Coordinate (Agent Mail) ──> Execute (NTM + Agents) | Memory-Augmented Debugging | Past solutions for current bugs | 15 min | | Coordinated Feature Dev | Multiple agents, one feature | 2+ hours | +### Tool Status Page + +The [Tool Status page](https://agent-flywheel.com/tools) provides a searchable catalog of all installed tools: + +- **Search & Filter**: Find tools by name, CLI command, features, or tech stack +- **Category Browsing**: Filter by "Flywheel Stack" (core agentic tools) or "Utilities" +- **Tool Details**: Each card shows the tool name, CLI command, GitHub stars, features, and tech stack +- **Live Data**: Content is auto-generated from `acfs.manifest.yaml` — never manually edited + +This page helps users discover tools they may not know about and understand how each fits into the agentic coding workflow. + ### Interactive Website Components The wizard website includes specialized components for guiding beginners: @@ -1210,7 +1239,7 @@ alias gmi='gemini --yolo' **Installation & Updates:** Claude Code should be installed and updated using its native mechanisms: - **Install:** ACFS uses the official native installer (`claude.ai/install.sh`), checksum-verified via `checksums.yaml` (installs to `~/.local/bin/claude`) -- **Update:** Use `claude update` (built-in) or run `acfs update --agents-only` +- **Update:** Use `claude update --channel latest` (built-in) or run `acfs update --agents-only` This ensures proper authentication handling and avoids issues with alternative package manager builds. For Codex and Gemini, ACFS uses standard bun global package updates. @@ -1320,7 +1349,20 @@ $ acfs doctor ### Generated Doctor Checks -Doctor checks can be generated from the manifest (`scripts/generated/doctor_checks.sh`) to keep verification logic close to `acfs.manifest.yaml`. Today, the user-facing `acfs doctor` command is implemented in `scripts/lib/doctor.sh` and does not yet consume the generated `doctor_checks.sh` output. +Doctor checks are generated from the manifest (`scripts/generated/doctor_checks.sh`) to keep verification logic close to `acfs.manifest.yaml`. The `acfs doctor` command automatically sources these generated checks to verify all manifest-defined tools. + +**How it works:** +1. The manifest generator creates `doctor_checks.sh` with verify commands for each module +2. `acfs doctor` sources this file and runs each verification check +3. Failed checks display a **fix suggestion** with the exact command to reinstall + +**Example output with fix suggestion:** +``` + ✗ tools.lazygit - Lazygit terminal UI not found + Fix: acfs install --only tools.lazygit +``` + +This architecture ensures doctor checks stay in sync with the installer—if a tool is in the manifest, it will be verified. ### Options @@ -3558,6 +3600,84 @@ This section covers common issues and their solutions. For quick debugging, star | No internet | "curl: (6) Could not resolve host" | Check DNS, try `ping google.com` | | Old bash | Syntax errors | Upgrade to bash 4+ | +### Installation Failure Recovery + +When the installer fails mid-way through, it provides an **auto-resume hint** with a precise command to continue from where it left off. + +**What you'll see on failure:** + +``` +[ERROR] ACFS installation failed! + +To debug: + 1. Check the log: cat /var/log/acfs/install.log + 2. If installed, run: acfs doctor (try as ubuntu) + +╔══════════════════════════════════════════════════════════════╗ +║ To resume installation from this point: ║ +╚══════════════════════════════════════════════════════════════╝ + + curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/.../install.sh | bash -s -- --resume --yes + + Failed phase: phase_9 + Failed step: install_stack +``` + +**Key features of the resume hint:** + +| Feature | Description | +|---------|-------------| +| **Pinned commit** | Uses exact SHA from original run for reproducibility | +| **Preserved flags** | Includes all original flags (--skip-*, --mode, --strict) | +| **Automatic detection** | Reads failed phase/step from `~/.acfs/state.json` | +| **Copyable command** | Ready to paste and run immediately | + +**Manual recovery steps:** + +1. **Review the error**: + ```bash + # Check the full log + cat /var/log/acfs/install.log | tail -50 + + # Or search for ERROR + grep -i error /var/log/acfs/install.log + ``` + +2. **Run diagnostics**: + ```bash + # As the target user (ubuntu) + acfs doctor + + # If running as root + sudo -u ubuntu -i bash -lc 'acfs doctor' + ``` + +3. **Resume installation**: + ```bash + # Use the exact command from the failure output + # Or use the generic resume command: + curl -sSL https://acfs.sh | bash -s -- --resume --yes --mode vibe + ``` + +4. **Check state file** (advanced): + ```bash + # View current installation state + cat ~/.acfs/state.json | jq . + + # See the stored resume hint + jq '.resume_hint' ~/.acfs/state.json + ``` + +**Common failure scenarios:** + +| Scenario | Typical Cause | Recovery | +|----------|---------------|----------| +| Network timeout | Transient connectivity | Wait, then resume | +| APT lock held | Unattended-upgrades | Wait 2-3 min, resume | +| Disk full | Insufficient space | Free space, resume | +| SSH disconnect | Session timeout | Reconnect, resume | +| Tool install failed | Upstream unavailable | Check status, resume | + ### APT Lock Errors **Symptom**: `E: Could not get lock /var/lib/dpkg/lock-frontend` @@ -3580,6 +3700,165 @@ This section covers common issues and their solutions. For quick debugging, star sudo apt-get update ``` +### Install Logs & Summary JSON + +Every ACFS install run produces two artifacts for debugging and tooling: + +**Log File Location:** +``` +~/.acfs/logs/install-YYYYMMDD_HHMMSS.log +``` + +The log file captures all stderr output from the installer, with: +- Header containing version, date, and mode +- All progress messages and errors +- ANSI colors stripped after completion +- Footer with completion timestamp + +**Summary JSON Location:** +``` +~/.acfs/logs/install_summary_YYYYMMDD_HHMMSS.json +``` + +**Summary JSON Schema (v1):** +```json +{ + "schema_version": 1, + "status": "success", // "success" or "failure" + "timestamp": "2026-01-27T...", // ISO 8601 + "total_seconds": 1200, // Wall clock time + "environment": { + "acfs_version": "0.9.0", + "mode": "vibe", + "ubuntu_version": "25.04", + "target_user": "ubuntu", + "target_home": "/home/ubuntu" + }, + "phases": [ + {"id": "phase_0", "duration_seconds": 5}, + {"id": "phase_1", "duration_seconds": 45}, + // ... completed phases in order + ], + "failure": null, // null on success, or: + // "failure": { + // "phase": "phase_9", + // "step": "install_stack", + // "error": "curl failed with exit code 7", + // "resume_hint": "curl -sSL ... | bash -s -- --resume --yes" + // } + "log_file": "/home/ubuntu/.acfs/logs/install-20260127_120000.log" +} +``` + +**Accessing logs:** +```bash +# Find the latest log +ls -lt ~/.acfs/logs/install-*.log | head -1 + +# Find the latest summary +ls -lt ~/.acfs/logs/install_summary_*.json | head -1 + +# Parse summary JSON +jq . ~/.acfs/logs/install_summary_*.json | head -1 + +# Get failed phase (if any) +jq '.failure // "No failure"' ~/.acfs/logs/install_summary_*.json | tail -1 + +# Get phase timings +jq '.phases[] | "\(.id): \(.duration_seconds)s"' ~/.acfs/logs/install_summary_*.json | tail -1 +``` + +**Sharing logs for support:** + +```bash +# Create a support bundle (strips sensitive data) +acfs support-bundle > support-bundle.txt + +# Or manually share (review for secrets first): +cat ~/.acfs/logs/install-*.log | tail -200 # Last 200 lines +cat ~/.acfs/logs/install_summary_*.json | tail -1 # Latest summary +``` + +### Support Bundle Command + +The `acfs support-bundle` command collects all diagnostic data into a single archive for troubleshooting. + +**Usage:** +```bash +acfs support-bundle [options] +``` + +**Options:** + +| Option | Description | +|--------|-------------| +| `--verbose, -v` | Show detailed output during collection | +| `--output, -o DIR` | Output directory (default: `~/.acfs/support`) | +| `--no-redact` | Disable secret redaction (WARNING: bundle may contain secrets) | +| `--help, -h` | Show help | + +**Output files:** +``` +~/.acfs/support/<timestamp>/ # Unpacked bundle directory +~/.acfs/support/<timestamp>.tar.gz # Compressed archive (shareable) +~/.acfs/support/<timestamp>/manifest.json # Bundle manifest +``` + +**What's collected:** + +| File | Description | +|------|-------------| +| `state.json` | Installation state and checkpoints | +| `VERSION` | ACFS version | +| `checksums.yaml` | Upstream verification checksums | +| `logs/install-*.log` | Recent install logs (up to 10) | +| `logs/install_summary_*.json` | Recent install summaries | +| `doctor.json` | Health check results | +| `versions.json` | Installed tool versions | +| `environment.json` | OS, memory, disk, user info | +| `os-release` | Linux distribution info | +| `journal-acfs.log` | Systemd journal for ACFS services | +| `config/.zshrc` | Shell configuration | + +**Security & Redaction:** + +By default, sensitive data is automatically redacted: + +| Pattern | Example | Redacted To | +|---------|---------|-------------| +| OpenAI API keys | `sk-abc123...` | `<REDACTED:api_key>` | +| AWS keys | `AKIAIOSFODNN...` | `<REDACTED:aws_key>` | +| GitHub tokens | `ghp_xxxx...` | `<REDACTED:github_token>` | +| Vault tokens | `hvs.xxxx...` | `<REDACTED:vault_token>` | +| Slack tokens | `xoxb-xxxx...` | `<REDACTED:slack_token>` | +| Bearer tokens | `Bearer xxx...` | `Bearer <REDACTED:bearer>` | +| JWTs | `eyJhbGc...` | `<REDACTED:jwt>` | +| Passwords | `"password": "..."` | `"password": "<REDACTED:password>"` | + +**Example workflow:** + +```bash +# Create support bundle +acfs support-bundle + +# Output: ~/.acfs/support/20260127_120000.tar.gz + +# Share the archive when filing an issue +# The archive is safe to share (secrets redacted) +``` + +**Disable redaction (use with caution):** +```bash +# WARNING: Bundle may contain API keys, tokens, and passwords +acfs support-bundle --no-redact +``` + +**When to use:** +- Installation failed and you need to share logs +- Filing a GitHub issue about ACFS +- Diagnosing tool installation problems +- Sharing system state with support + ### Shell Not Changing to zsh **Symptom**: Still seeing bash prompt after install. @@ -3654,6 +3933,42 @@ gemini # Follow Google login flow export PATH="$HOME/.bun/bin:$HOME/.local/bin:$HOME/.cargo/bin:$PATH" ``` +### Doctor Shows Missing Tools + +**Symptom**: `acfs doctor` shows failed checks for tools you expected to be installed. + +**Understanding doctor output:** + +Doctor checks are generated directly from the manifest, so they verify the exact same tools the installer provides. When a check fails, doctor shows a copy-pasteable fix command: + +``` + ✗ tools.lazygit - Lazygit terminal UI not found + Fix: acfs install --only tools.lazygit +``` + +**Solutions**: + +1. **Re-run the specific module** (use the fix suggestion): + ```bash + acfs install --only tools.lazygit # Install just that tool + acfs install --only lang.go # Install a language runtime + acfs install --only stack.dcg # Install a stack tool + ``` + +2. **Re-run an entire phase** (for multiple failures in one category): + ```bash + acfs install --only-phase 4 # Re-run Phase 4: Tools + acfs install --only-phase 8 # Re-run Phase 8: Stack + ``` + +3. **Run auto-fix mode** (applies safe, deterministic fixes): + ```bash + acfs doctor --fix + acfs doctor --fix --dry-run # Preview fixes first + ``` + +**Note**: Doctor checks match the manifest verify commands exactly. If a tool was skipped during installation (e.g., using `--mode safe`), the check will fail. This is expected—run `acfs doctor` to see which tools are missing and decide which to install. + ### Tmux Configuration Errors **Symptom**: Tmux won't start or shows config errors. diff --git a/VERSION b/VERSION index 8f0916f7..a918a2aa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/acfs.manifest.yaml b/acfs.manifest.yaml index 4a7c3e4b..21c83fd5 100644 --- a/acfs.manifest.yaml +++ b/acfs.manifest.yaml @@ -179,6 +179,17 @@ modules: CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) fi curl "${CURL_ARGS[@]}" -o ~/.acfs/zsh/acfs.zshrc "${ACFS_RAW}/acfs/zsh/acfs.zshrc" + - | + # Install ACFS shell completions (zsh) + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + mkdir -p ~/.acfs/completions + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" -o ~/.acfs/completions/_acfs "${ACFS_RAW}/scripts/completions/_acfs" + # Also install bash completions for users who switch shells + curl "${CURL_ARGS[@]}" -o ~/.acfs/completions/acfs.bash "${ACFS_RAW}/scripts/completions/acfs.bash" - | # Install pre-configured Powerlevel10k settings (prevents config wizard on first login) ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" @@ -638,7 +649,7 @@ modules: verify: - ~/.local/bin/claude --version || ~/.local/bin/claude --help notes: - - "Uses native installer with built-in 'claude update' command" + - "Updated via verified installer (curl claude.ai/install.sh | bash -- latest); 'claude update --channel' flag does not exist" - "Binary installed to ~/.local/bin/claude" - id: agents.codex @@ -1262,7 +1273,7 @@ modules: language: "Rust" stars: 128 cli_name: "br" - cli_aliases: ["bd"] + cli_aliases: [] command_example: "br ready --json" dependencies: - lang.rust @@ -2208,6 +2219,77 @@ modules: verify: - command -v acfs-update + - id: acfs.nightly + description: Nightly auto-update timer (systemd) + category: acfs + phase: 10 + run_as: target_user + optional: true + enabled_by_default: true + generated: false + tags: [orchestration, maintenance] + dependencies: + - acfs.update + installed_check: + run_as: target_user + command: "systemctl --user is-enabled acfs-nightly-update.timer 2>/dev/null" + install: + - mkdir -p ~/.acfs/scripts ~/.config/systemd/user + - | + # Install nightly update wrapper script + if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/lib/nightly_update.sh" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/lib/nightly_update.sh" ~/.acfs/scripts/nightly-update.sh + elif [[ -f "scripts/lib/nightly_update.sh" ]]; then + cp "scripts/lib/nightly_update.sh" ~/.acfs/scripts/nightly-update.sh + else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/lib/nightly_update.sh" -o ~/.acfs/scripts/nightly-update.sh + fi + chmod +x ~/.acfs/scripts/nightly-update.sh + - | + # Install systemd timer unit + if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.timer" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.timer" ~/.config/systemd/user/acfs-nightly-update.timer + elif [[ -f "scripts/templates/acfs-nightly-update.timer" ]]; then + cp "scripts/templates/acfs-nightly-update.timer" ~/.config/systemd/user/acfs-nightly-update.timer + else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/templates/acfs-nightly-update.timer" -o ~/.config/systemd/user/acfs-nightly-update.timer + fi + - | + # Install systemd service unit + if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.service" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.service" ~/.config/systemd/user/acfs-nightly-update.service + elif [[ -f "scripts/templates/acfs-nightly-update.service" ]]; then + cp "scripts/templates/acfs-nightly-update.service" ~/.config/systemd/user/acfs-nightly-update.service + else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/templates/acfs-nightly-update.service" -o ~/.config/systemd/user/acfs-nightly-update.service + fi + - | + # Reload systemd and enable the timer + systemctl --user daemon-reload + systemctl --user enable --now acfs-nightly-update.timer + verify: + - systemctl --user is-enabled acfs-nightly-update.timer + notes: + - "Runs acfs-update --yes --quiet daily at 4am with pre-flight health checks" + - "Skips if load average > nproc or disk < 2GB free" + - "Safe cleanup of build artifacts when disk < 5GB" + - "Logs to ~/.acfs/logs/updates/nightly-*.log" + - id: acfs.doctor description: ACFS doctor command for health checks category: acfs diff --git a/acfs/onboard/docs/ntm/command_palette.md b/acfs/onboard/docs/ntm/command_palette.md index 36018066..09d8ae96 100644 --- a/acfs/onboard/docs/ntm/command_palette.md +++ b/acfs/onboard/docs/ntm/command_palette.md @@ -94,7 +94,7 @@ Pick the next bead you can actually do usefully now and start coding on it immed OK, so start systematically and methodically and meticulously and diligently executing those remaining beads tasks that you created in the optimal logical order! Don't forget to mark beads as you work on them. ### do_all_of_it | Do All Of It -OK, please do ALL of that now. Track work via bd beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work. +OK, please do ALL of that now. Track work via br beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work. ## Git & Operations diff --git a/acfs/onboard/lessons/07_flywheel_loop.md b/acfs/onboard/lessons/07_flywheel_loop.md index 9d2c7332..42555082 100644 --- a/acfs/onboard/lessons/07_flywheel_loop.md +++ b/acfs/onboard/lessons/07_flywheel_loop.md @@ -218,6 +218,8 @@ One final lesson: keeping everything updated. onboard 8 ``` +Also recommended: learn how git works with multiple agents in `onboard 21`. + --- *The Agentic Coding Flywheel Setup - https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup* diff --git a/acfs/onboard/lessons/08_keeping_updated.md b/acfs/onboard/lessons/08_keeping_updated.md index b6696c25..1497dd28 100644 --- a/acfs/onboard/lessons/08_keeping_updated.md +++ b/acfs/onboard/lessons/08_keeping_updated.md @@ -128,7 +128,7 @@ Try updating directly: ```bash # Claude -claude update +claude update --channel latest # Codex bun install -g --trust @openai/codex@latest diff --git a/acfs/onboard/lessons/16_beads_rust.md b/acfs/onboard/lessons/16_beads_rust.md index 07463cc2..2747c5fe 100644 --- a/acfs/onboard/lessons/16_beads_rust.md +++ b/acfs/onboard/lessons/16_beads_rust.md @@ -15,7 +15,7 @@ beads_rust (`br`) is a local-first issue tracker designed for AI agents. Issues - JSON output for agent consumption - Works offline, syncs on commit -> **Note:** The `bd` alias is available for backward compatibility with the original golang beads. +> **Note:** `br` is the primary command for beads_rust issue tracking. --- @@ -116,16 +116,6 @@ bv --robot-insights --- -## The bd Alias - -For backward compatibility, `bd` is aliased to `br`: - -```bash -# These are equivalent: -bd list --status open -br list --status open -``` - --- ## Quick Reference diff --git a/acfs/onboard/lessons/20_newproj.md b/acfs/onboard/lessons/20_newproj.md index 088e582a..47baf401 100644 --- a/acfs/onboard/lessons/20_newproj.md +++ b/acfs/onboard/lessons/20_newproj.md @@ -76,7 +76,7 @@ Creates `~/code/myproject`. | Flag | Effect | |------|--------| | `--interactive` | TUI wizard (recommended for first use) | -| `--no-bd` | Skip beads initialization | +| `--no-br` | Skip beads initialization | | `--no-claude` | Skip Claude settings | | `--no-agents` | Skip AGENTS.md creation | diff --git a/acfs/onboard/lessons/21_git_strategy.md b/acfs/onboard/lessons/21_git_strategy.md new file mode 100644 index 00000000..473320c3 --- /dev/null +++ b/acfs/onboard/lessons/21_git_strategy.md @@ -0,0 +1,159 @@ +# Git Strategy for Multi-Agent Work + +**Goal:** Understand how git works when multiple agents edit the same repo simultaneously. + +--- + +## The Single-Branch Model + +ACFS uses **one branch (`main`) with one worktree**. All agents commit directly to `main`. + +This may surprise you if you're used to feature branches, but it's the right call when +dozens of agents work concurrently on the same repo. + +--- + +## Why Not Branches or Worktrees? + +Traditional git workflows assume humans working sequentially on isolated features. +Agent swarms break those assumptions: + +**Branch-per-agent creates merge hell.** With 10+ agents making frequent commits, +merging N branches back to main produces cascading conflicts that waste more time +than they save. + +**Worktrees add filesystem complexity.** Each worktree is a full checkout. With many +agents, disk usage multiplies and path confusion leads to cross-worktree edits that +corrupt state. + +**Agents lose context across branches.** When an agent switches branches, its +in-context understanding of the codebase becomes stale. Single-branch means every +agent always sees the latest state. + +**Logical conflicts survive textual merges.** Even when two changes don't conflict +at the text level, they can break semantics. A function signature change on one +branch and a new callsite on another will merge cleanly but fail to compile. On a +single branch, the second agent sees the signature change immediately and adapts. + +--- + +## How Conflicts Are Prevented + +Instead of branch isolation, ACFS uses three complementary mechanisms: + +### 1. File Reservations (Agent Mail) + +Before editing files, agents reserve them: + +```bash +# Agent reserves files it plans to edit +file_reservation_paths( + project_key="/data/projects/my-repo", + agent_name="BlueLake", + paths=["src/auth/*.rs"], + ttl_seconds=3600, + exclusive=true, + reason="bd-42: refactor auth" +) +``` + +Other agents see the reservation and work on different files. Conflicts are +caught **before** edits happen, not after. + +### 2. Pre-Commit Guard + +The Agent Mail pre-commit hook checks reservations at commit time: + +```bash +# Install the guard +mcp-agent-mail install-precommit-guard +``` + +If you try to commit a file reserved by another agent, the commit is blocked +with an explanation of who holds the reservation. + +### 3. DCG (Destructive Command Guard) + +DCG blocks dangerous git commands that could destroy other agents' work: + +- `git reset --hard` -- would wipe uncommitted changes from all agents +- `git checkout -- .` -- same problem +- `git clean -fd` -- deletes untracked files other agents may need + +See `onboard 10` for DCG details. + +--- + +## The Recommended Workflow + +``` +1. Pull latest git pull --rebase +2. Reserve files file_reservation_paths(...) +3. Edit and test cargo test / bun test / go test +4. Commit immediately git add <files> && git commit +5. Push git push +6. Release reservation release_file_reservations(...) +``` + +**Key principles:** + +- **Commit early, commit often.** Small commits reduce the window for conflicts. +- **Push after every commit.** Unpushed commits are invisible to other agents. +- **Reserve before editing.** Don't touch files without a reservation. +- **Release when done.** Don't hold reservations longer than needed. + +--- + +## What About Logical Conflicts? + +The issue reporter correctly notes that avoiding textual merge conflicts doesn't +guarantee semantic correctness. ACFS addresses this with: + +- **Frequent small commits** keep the delta small, reducing logical conflict surface +- **UBS scanning** (`ubs <changed-files>`) catches many semantic issues before commit +- **Compiler checks** (`cargo check`, `go vet`, `tsc`) run before every commit +- **Test suites** catch regressions immediately +- **Agent Mail threads** let agents coordinate on shared interfaces + +For projects where this isn't sufficient, consider: +- Splitting the repo into smaller, focused crates/packages +- Using workspace-level dependency management (Cargo workspaces, npm workspaces) +- Defining clear module boundaries with stable interfaces + +--- + +## Quick Reference + +| Mechanism | What It Does | +|-----------|-------------| +| File reservations | Prevents two agents editing same files | +| Pre-commit guard | Blocks commits to reserved files | +| DCG | Blocks destructive git commands | +| `git pull --rebase` | Stays current with other agents' work | +| `main:master` push | Keeps legacy URLs working | + +--- + +## AGENTS.md Sets the Rules + +Each project's `AGENTS.md` file configures agent behavior, including: +- Branch policy (always `main`) +- Commit conventions +- File editing discipline +- How to handle unexpected changes from other agents + +When you create a project with `acfs newproj`, this is set up automatically. + +--- + +## Next + +Learn about SRPS (Structured Repository Problem Solving): + +```bash +onboard 23 +``` + +--- + +*The Agentic Coding Flywheel Setup - https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup* diff --git a/acfs/tmux/tmux.conf b/acfs/tmux/tmux.conf index 8ee8432d..48b989bf 100644 --- a/acfs/tmux/tmux.conf +++ b/acfs/tmux/tmux.conf @@ -9,8 +9,13 @@ set -g mouse on # Set default terminal for proper color support -set -g default-terminal "screen-256color" +# tmux-256color enables bracketed paste, styled underlines, and other modern features +# that screen-256color lacks (see tmux FAQ and terminfo docs) +set -g default-terminal "tmux-256color" set -ga terminal-overrides ",*256col*:Tc" +# Allow passthrough sequences (kitty graphics, sixel, etc.) and cursor shape overrides +set -g allow-passthrough on +set -ga terminal-overrides ",*:Ss=\\E[%p1%d q:Se=\\E[2 q" # Start windows and panes at 1, not 0 set -g base-index 1 diff --git a/acfs/zsh/acfs.zshrc b/acfs/zsh/acfs.zshrc index 707542be..e649391b 100644 --- a/acfs/zsh/acfs.zshrc +++ b/acfs/zsh/acfs.zshrc @@ -23,6 +23,8 @@ if [[ -n "$TERM" ]] && ! infocmp "$TERM" &>/dev/null; then fi # --- Paths (early) --- +# User ~/bin takes highest precedence (for custom shims) +[[ -d "$HOME/bin" ]] && export PATH="$HOME/bin:$PATH" export PATH="$HOME/.cargo/bin:$PATH" # Go (support both apt-style and /usr/local/go) @@ -153,7 +155,7 @@ alias install='sudo apt install' alias search='apt search' # Update agent CLIs -alias uca='~/.local/bin/claude update && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest' +alias uca='(curl -fsSL https://claude.ai/install.sh | bash -s -- latest) && (bun install -g --trust @openai/codex@latest || bun install -g --trust @openai/codex) && bun install -g --trust @google/gemini-cli@latest' # --- Custom functions --- mkcd() { mkdir -p "$1" && cd "$1" || return; } @@ -372,6 +374,39 @@ acfs() { return 1 fi ;; + changelog|changes|log) + if [[ -f "$acfs_home/scripts/lib/changelog.sh" ]]; then + bash "$acfs_home/scripts/lib/changelog.sh" "$@" + elif [[ -x "$acfs_bin" ]]; then + "$acfs_bin" changelog "$@" + else + echo "Error: changelog.sh not found" + echo "Re-run the ACFS installer to get the latest scripts" + return 1 + fi + ;; + notifications|notify) + if [[ -f "$acfs_home/scripts/lib/notifications.sh" ]]; then + bash "$acfs_home/scripts/lib/notifications.sh" "$@" + elif [[ -x "$acfs_bin" ]]; then + "$acfs_bin" notifications "$@" + else + echo "Error: notifications.sh not found" + echo "Re-run the ACFS installer to get the latest scripts" + return 1 + fi + ;; + export-config|export) + if [[ -f "$acfs_home/scripts/lib/export-config.sh" ]]; then + bash "$acfs_home/scripts/lib/export-config.sh" "$@" + elif [[ -x "$acfs_bin" ]]; then + "$acfs_bin" export-config "$@" + else + echo "Error: export-config.sh not found" + echo "Re-run the ACFS installer to get the latest scripts" + return 1 + fi + ;; version|-v|--version) if [[ -f "$acfs_home/VERSION" ]]; then cat "$acfs_home/VERSION" @@ -385,7 +420,7 @@ acfs() { echo "Usage: acfs <command>" echo "" echo "Commands:" - echo " newproj Create new project (git, bd, AGENTS.md, Claude settings)" + echo " newproj Create new project (git, br, AGENTS.md, Claude settings)" echo " Use 'acfs newproj -i' for interactive TUI wizard" echo " info Quick system overview (hostname, IP, uptime, progress)" echo " cheatsheet Command reference (aliases, shortcuts)" @@ -396,6 +431,9 @@ acfs() { echo " status Quick one-line health summary (fast, no network)" echo " session List/export/import agent sessions (cass)" echo " support-bundle Collect diagnostic data for troubleshooting" + echo " changelog Show recent changes (--all, --since 7d, --json)" + echo " export-config Export config for backup/migration (--json, --minimal)" + echo " notifications Manage push notifications via ntfy.sh" echo " update Update ACFS tools to latest versions" echo " version Show ACFS version" echo " help Show this help message" @@ -418,6 +456,14 @@ acfs() { esac } +# --- ACFS Tab Completion (zsh) --- +# Load acfs completions if the function is available +if [[ -f "$HOME/.acfs/completions/_acfs" ]]; then + # Add to fpath before compinit, or load directly if compinit already ran + fpath=("$HOME/.acfs/completions" $fpath) + autoload -Uz _acfs 2>/dev/null +fi + # --- Agent aliases (dangerously enabled by design) --- alias cc='NODE_OPTIONS="--max-old-space-size=32768" ~/.local/bin/claude --dangerously-skip-permissions' alias cod='codex --dangerously-bypass-approvals-and-sandbox' @@ -428,8 +474,16 @@ alias bdev='bun run dev' alias bl='bun run lint' alias bt='bun run type-check' -# Beads shortcuts: alias old bd command to new br (beads_rust) -alias bd='br' +# --- br (beads_rust) alias guard --- +# Older ACFS versions incorrectly aliased br='bun run dev'. Remove stale alias if br binary exists. +# whence -p finds the binary path, ignoring aliases/functions (zsh-specific) +if whence -p br &>/dev/null && alias br &>/dev/null; then + unalias br 2>/dev/null +fi +# bd is the legacy Go beads binary name; alias it to br (beads_rust) +if whence -p br &>/dev/null; then + alias bd='br' +fi # MCP Agent Mail helper (installer usually adds `am`, but keep a fallback) alias am='cd ~/mcp_agent_mail 2>/dev/null && scripts/run_server_with_token.sh || echo "mcp_agent_mail not found in ~/mcp_agent_mail"' @@ -454,3 +508,34 @@ bindkey "^[[H" beginning-of-line bindkey "^[[F" end-of-line bindkey "^[[1~" beginning-of-line bindkey "^[[4~" end-of-line + +# --- Beads Viewer (bv) protection --- +# Prevent gcloud's 'bv' (BigQuery Visualizer) from shadowing beads_viewer. +# This function ensures the correct bv is always invoked, regardless of PATH order. +# Must be defined AFTER .zshrc.local is sourced (where gcloud SDK may modify PATH). +bv() { + local bv_bin="" + # Check known locations in order of preference + for candidate in "$HOME/.local/bin/bv" "$HOME/.bun/bin/bv" "$HOME/go/bin/bv" "$HOME/.cargo/bin/bv"; do + if [[ -x "$candidate" ]]; then + bv_bin="$candidate" + break + fi + done + # Fallback: search PATH but skip gcloud's bv + if [[ -z "$bv_bin" ]]; then + while IFS= read -r p; do + if [[ "$p" != *"google-cloud-sdk"* ]]; then + bv_bin="$p" + break + fi + done < <(whence -ap bv 2>/dev/null) + fi + if [[ -n "$bv_bin" ]]; then + "$bv_bin" "$@" + else + echo "Error: beads_viewer (bv) not found. Install with:" >&2 + echo " curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh | bash" >&2 + return 1 + fi +} diff --git a/apps/web/app/flywheel/page.tsx b/apps/web/app/flywheel/page.tsx index ba0d1cbe..6b22244b 100644 --- a/apps/web/app/flywheel/page.tsx +++ b/apps/web/app/flywheel/page.tsx @@ -585,7 +585,7 @@ function ToolCard({ tool, index }: { tool: FlywheelTool; index: number }) { {/* Install command */} {tool.installCommand && ( <div className="mb-3 flex items-center gap-2 rounded-lg bg-muted/50 p-2"> - <code className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-muted-foreground"> + <code className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap font-mono text-xs text-muted-foreground"> {tool.installCommand.length > 60 ? tool.installCommand.slice(0, 60) + "..." : tool.installCommand} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 56b38d65..c3b80e42 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -74,7 +74,8 @@ --secondary-foreground: oklch(0.85 0.01 260); --muted: oklch(0.16 0.015 260); - --muted-foreground: oklch(0.6 0.02 260); + /* WCAG AA compliant: 0.7 on 0.16 bg = ~5.5:1 contrast ratio */ + --muted-foreground: oklch(0.7 0.02 260); /* Warm amber accent */ --accent: oklch(0.78 0.16 75); @@ -115,7 +116,8 @@ /* ============================================ Typography Scale - Fluid (1.25 Major Third) ============================================ */ - --text-xs: clamp(0.6875rem, 0.65rem + 0.15vw, 0.75rem); + /* Minimum 12px for accessibility (0.75rem) */ + --text-xs: clamp(0.75rem, 0.7rem + 0.15vw, 0.8125rem); --text-sm: clamp(0.8125rem, 0.775rem + 0.2vw, 0.875rem); --text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); --text-lg: clamp(1.125rem, 1.05rem + 0.4vw, 1.375rem); @@ -124,6 +126,7 @@ --text-3xl: clamp(2rem, 1.75rem + 1.25vw, 3rem); --text-4xl: clamp(2.5rem, 2.1rem + 2vw, 4rem); --text-5xl: clamp(3rem, 2.5rem + 2.5vw, 5rem); + --text-6xl: clamp(3.5rem, 3rem + 3vw, 6rem); /* Line heights per tier (tighter for headlines) */ --leading-xs: 1.6; @@ -135,6 +138,7 @@ --leading-3xl: 1.2; --leading-4xl: 1.1; --leading-5xl: 1.05; + --leading-6xl: 1; /* Letter spacing (looser for small, tighter for headlines) */ --tracking-xs: 0.025em; @@ -146,6 +150,7 @@ --tracking-3xl: -0.025em; --tracking-4xl: -0.03em; --tracking-5xl: -0.035em; + --tracking-6xl: -0.04em; /* ============================================ Spacing System - 8px Base Unit @@ -218,7 +223,8 @@ --secondary: oklch(0.94 0.01 260); --secondary-foreground: oklch(0.2 0.02 260); --muted: oklch(0.94 0.01 260); - --muted-foreground: oklch(0.45 0.02 260); + /* WCAG AA compliant: 0.4 on 0.94 bg = ~5:1 contrast ratio */ + --muted-foreground: oklch(0.4 0.02 260); --accent: oklch(0.65 0.18 75); --accent-foreground: oklch(0.15 0.02 260); --destructive: oklch(0.55 0.25 25); @@ -513,18 +519,32 @@ Component Utilities ============================================ */ -/* Glass morphism effect */ +/* Glass morphism effect - Stripe-level refinement */ .glass { - background: oklch(0.14 0.02 260 / 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid oklch(0.3 0.02 260 / 0.3); + background: oklch(0.14 0.02 260 / 0.85); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid oklch(0.35 0.02 260 / 0.4); + transition: backdrop-filter 200ms ease, border-color 200ms ease, background 200ms ease; +} + +.glass:hover { + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-color: oklch(0.4 0.02 260 / 0.5); + background: oklch(0.15 0.02 260 / 0.88); } .glass-subtle { background: oklch(0.14 0.02 260 / 0.6); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + transition: backdrop-filter 200ms ease; +} + +.glass-subtle:hover { + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); } /* Glow effects */ @@ -562,6 +582,19 @@ background-clip: text; } +/* Display typography - for hero sections and large displays */ +.text-display-5xl { + font-size: var(--text-5xl); + letter-spacing: var(--tracking-5xl); + line-height: var(--leading-5xl); +} + +.text-display-6xl { + font-size: var(--text-6xl); + letter-spacing: var(--tracking-6xl); + line-height: var(--leading-6xl); +} + /* Terminal styling */ .terminal-window { background: oklch(0.08 0.015 260); diff --git a/apps/web/app/glossary/page.tsx b/apps/web/app/glossary/page.tsx index 60179f65..628b4c5d 100644 --- a/apps/web/app/glossary/page.tsx +++ b/apps/web/app/glossary/page.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { BookOpen, ChevronDown, Home, Search, Terminal, X } from "lucide-react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { EmptyState } from "@/components/ui/empty-state"; import { jargonDictionary } from "@/lib/jargon"; import { cn } from "@/lib/utils"; @@ -139,6 +140,10 @@ export default function GlossaryPage() { }, [entries, query, category]); const clearQuery = useCallback(() => setQuery(""), []); + const resetFilters = useCallback(() => { + setQuery(""); + setCategory("all"); + }, []); // If the user lands on /glossary#some-key, scroll to it and open the entry. useEffect(() => { @@ -177,7 +182,7 @@ export default function GlossaryPage() { <div className="pointer-events-none fixed inset-0 bg-gradient-cosmic opacity-50" /> <div className="pointer-events-none fixed inset-0 bg-grid-pattern opacity-20" /> - <div className="relative mx-auto max-w-5xl px-6 py-8 md:px-12 md:py-12"> + <div className="relative mx-auto max-w-5xl px-6 pt-8 pb-24 md:px-12 md:py-12"> {/* Header */} <div className="mb-8 flex items-center justify-between"> <Link @@ -228,6 +233,7 @@ export default function GlossaryPage() { value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search terms (e.g., SSH, tmux, API key)…" + aria-label="Search glossary terms" className="w-full rounded-xl border border-border/50 bg-background px-9 py-2 text-sm outline-none transition-colors focus:border-primary/50 focus:ring-2 focus:ring-primary/20" /> {query.length > 0 && ( @@ -268,11 +274,23 @@ export default function GlossaryPage() { {/* Results */} <div className="space-y-3"> {filtered.length === 0 ? ( - <Card className="border-border/50 bg-card/60 p-6 text-center"> - <p className="text-sm text-muted-foreground"> - No matches. Try a different search or switch back to{" "} - <span className="font-medium text-foreground">All</span>. - </p> + <Card className="border-border/50 bg-card/60 p-6"> + <EmptyState + icon={Search} + title="No matches found" + description="Try a different search or switch back to All." + action={ + <Button + size="sm" + variant="outline" + onClick={resetFilters} + className="border-primary/30 hover:bg-primary/10" + > + Reset filters + </Button> + } + variant="compact" + /> </Card> ) : ( filtered.map((entry) => ( @@ -281,16 +299,16 @@ export default function GlossaryPage() { id={entry.key} className="group overflow-hidden rounded-2xl border border-border/50 bg-card/60" > - <summary className="flex cursor-pointer list-none items-start justify-between gap-4 p-5 transition-colors hover:bg-muted/20"> + <summary className="flex cursor-pointer list-none items-start justify-between gap-4 p-5 rounded-2xl outline-none transition-colors hover:bg-muted/20 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"> <div className="min-w-0"> <div className="flex flex-wrap items-center gap-2"> <h2 className="text-lg font-semibold text-foreground"> {entry.term} </h2> - <span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[11px] text-muted-foreground"> + <span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-xs text-muted-foreground"> #{entry.key} </span> - <span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[11px] text-muted-foreground"> + <span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-xs text-muted-foreground"> {CATEGORY_LABELS[entry.category]} </span> </div> @@ -371,6 +389,24 @@ export default function GlossaryPage() { )} </div> </div> + + {/* Mobile thumb-zone nav */} + <div className="fixed inset-x-0 bottom-0 z-30 border-t border-border/50 bg-background/95 px-4 py-3 backdrop-blur-md bottom-nav-safe md:hidden"> + <div className="flex items-center gap-3"> + <Button asChild size="lg" variant="outline" className="flex-1"> + <Link href="/"> + <Home className="mr-2 h-4 w-4" /> + Home + </Link> + </Button> + <Button asChild size="lg" className="flex-1"> + <Link href="/wizard/os-selection"> + <Terminal className="mr-2 h-4 w-4" /> + Wizard + </Link> + </Button> + </div> + </div> </div> ); } diff --git a/apps/web/app/learn/[slug]/lesson-content.tsx b/apps/web/app/learn/[slug]/lesson-content.tsx index eeca0cae..3ab5f5bc 100644 --- a/apps/web/app/learn/[slug]/lesson-content.tsx +++ b/apps/web/app/learn/[slug]/lesson-content.tsx @@ -133,7 +133,7 @@ function LessonSidebar({ <span className="block text-lg font-bold tracking-tight bg-gradient-to-r from-white via-white to-white/60 bg-clip-text text-transparent"> Learning Hub </span> - <span className="text-[11px] text-white/40 uppercase tracking-[0.2em] font-medium"> + <span className="text-xs text-white/60 uppercase tracking-[0.2em] font-medium"> ACFS Academy </span> </div> @@ -183,7 +183,7 @@ function LessonSidebar({ </div> <div className="flex items-center justify-between mt-4 text-xs"> - <span className="text-white/40">{completedLessons.length} of {LESSONS.length}</span> + <span className="text-white/60">{completedLessons.length} of {LESSONS.length}</span> <span className="text-emerald-400/80">{LESSONS.length - completedLessons.length} remaining</span> </div> </div> @@ -227,7 +227,7 @@ function LessonSidebar({ ? "bg-gradient-to-br from-emerald-400 to-emerald-600 border-emerald-400/50 text-white shadow-[0_0_20px_rgba(16,185,129,0.5)]" : isCurrent ? "bg-gradient-to-br from-primary to-violet-500 border-primary/50 text-white shadow-[0_0_20px_rgba(var(--primary-rgb),0.5)]" - : "bg-white/[0.05] border-white/10 text-white/40 group-hover:border-white/30 group-hover:bg-white/[0.08] group-hover:text-white/70" + : "bg-white/[0.05] border-white/10 text-white/60 group-hover:border-white/30 group-hover:bg-white/[0.08] group-hover:text-white/80" }`} > {isCompleted ? ( @@ -249,7 +249,7 @@ function LessonSidebar({ }`}> {lesson.title} </span> - <span className="flex items-center gap-1.5 text-[11px] text-white/30 mt-1"> + <span className="flex items-center gap-1.5 text-xs text-white/60 mt-1"> <Clock className="h-3 w-3" /> {lesson.duration} </span> @@ -257,7 +257,7 @@ function LessonSidebar({ {/* Active indicator */} {isCurrent && ( - <div className="flex items-center gap-1 text-[10px] font-medium text-primary"> + <div className="flex items-center gap-1 text-xs font-medium text-primary"> <span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" /> NOW </div> @@ -273,7 +273,7 @@ function LessonSidebar({ <div className="p-6 border-t border-white/[0.05]"> <Link href="/" - className="group flex items-center gap-4 text-sm text-white/40 transition-all duration-500 hover:text-white/80" + className="group flex items-center gap-4 text-sm text-white/60 transition-all duration-500 hover:text-white/90" > <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/[0.03] border border-white/[0.08] transition-all duration-500 group-hover:scale-110 group-hover:bg-white/[0.08] group-hover:border-white/20"> <Home className="h-5 w-5 transition-transform duration-500 group-hover:-translate-y-0.5" /> @@ -453,8 +453,8 @@ export function LessonContent({ lesson }: Props) { <div className="flex items-center gap-3"> <div className="flex items-center gap-2 px-3 py-2 rounded-full bg-white/[0.05] border border-white/[0.08]"> <span className="text-sm font-bold text-primary tabular-nums">{lesson.id + 1}</span> - <span className="text-white/30">/</span> - <span className="text-sm text-white/40 tabular-nums">{LESSONS.length}</span> + <span className="text-white/50">/</span> + <span className="text-sm text-white/60 tabular-nums">{LESSONS.length}</span> </div> </div> </div> @@ -512,7 +512,7 @@ export function LessonContent({ lesson }: Props) { <div className="relative overflow-hidden rounded-2xl border border-amber-500/30 bg-gradient-to-br from-amber-500/10 via-black/40 to-orange-500/5 backdrop-blur-2xl p-8"> {/* Animated accent */} <div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-amber-400/70 to-transparent" /> - <div className="absolute inset-0 bg-gradient-to-r from-amber-500/5 via-transparent to-orange-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> + <div className="absolute inset-0 bg-gradient-to-r from-amber-500/5 via-transparent to-orange-500/5 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-500" /> <div className="relative flex gap-6"> <div className="relative shrink-0"> @@ -564,7 +564,7 @@ export function LessonContent({ lesson }: Props) { <div className={`absolute -inset-2 rounded-[28px] transition-all duration-700 ${ isCompleted ? "bg-gradient-to-r from-emerald-500/40 via-emerald-400/30 to-emerald-500/40 blur-2xl opacity-100" - : "bg-gradient-to-r from-primary/40 via-violet-500/30 to-primary/40 blur-2xl opacity-0 group-hover:opacity-100" + : "bg-gradient-to-r from-primary/40 via-violet-500/30 to-primary/40 blur-2xl opacity-0 group-hover:opacity-100 group-focus-within:opacity-100" }`} /> <div className="relative overflow-hidden rounded-3xl border border-white/10 bg-gradient-to-br from-white/[0.08] via-white/[0.03] to-white/[0.05] backdrop-blur-2xl p-10"> @@ -650,15 +650,15 @@ export function LessonContent({ lesson }: Props) { {prevLesson ? ( <Link href={`/learn/${prevLesson.slug}`} - className="group relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.02] p-6 transition-all duration-500 hover:bg-white/[0.05] hover:border-white/20 hover:shadow-[0_0_40px_rgba(255,255,255,0.05)] hover:-translate-y-1" + className="group relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.02] p-6 transition-all duration-500 hover:bg-white/[0.05] hover:border-white/20 hover:shadow-[0_0_40px_rgba(255,255,255,0.05)] hover:-translate-y-1 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background outline-none" > - <div className="absolute inset-0 bg-gradient-to-r from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> + <div className="absolute inset-0 bg-gradient-to-r from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity duration-500" /> <div className="relative flex items-center gap-5"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-white/[0.05] border border-white/[0.08] transition-all duration-500 group-hover:scale-110 group-hover:bg-white/[0.1] group-hover:border-white/20"> <ChevronLeft className="h-6 w-6 text-white/60 transition-all duration-500 group-hover:text-white group-hover:-translate-x-1" /> </div> <div> - <div className="text-xs text-white/30 mb-1 uppercase tracking-wider font-medium">Previous</div> + <div className="text-xs text-white/50 mb-1 uppercase tracking-wider font-medium">Previous</div> <div className="text-lg font-semibold text-white/80 transition-colors group-hover:text-white">{prevLesson.title}</div> </div> </div> @@ -669,12 +669,12 @@ export function LessonContent({ lesson }: Props) { {nextLesson ? ( <Link href={`/learn/${nextLesson.slug}`} - className="group relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.02] p-6 transition-all duration-500 hover:bg-white/[0.05] hover:border-white/20 hover:shadow-[0_0_40px_rgba(255,255,255,0.05)] hover:-translate-y-1 text-right" + className="group relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.02] p-6 transition-all duration-500 hover:bg-white/[0.05] hover:border-white/20 hover:shadow-[0_0_40px_rgba(255,255,255,0.05)] hover:-translate-y-1 text-right focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background outline-none" > - <div className="absolute inset-0 bg-gradient-to-l from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> + <div className="absolute inset-0 bg-gradient-to-l from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity duration-500" /> <div className="relative flex items-center justify-end gap-5"> <div> - <div className="text-xs text-white/30 mb-1 uppercase tracking-wider font-medium">Next</div> + <div className="text-xs text-white/50 mb-1 uppercase tracking-wider font-medium">Next</div> <div className="text-lg font-semibold text-white/80 transition-colors group-hover:text-white">{nextLesson.title}</div> </div> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-white/[0.05] border border-white/[0.08] transition-all duration-500 group-hover:scale-110 group-hover:bg-white/[0.1] group-hover:border-white/20"> diff --git a/apps/web/app/learn/commands/page.tsx b/apps/web/app/learn/commands/page.tsx index 2328978c..ae36b974 100644 --- a/apps/web/app/learn/commands/page.tsx +++ b/apps/web/app/learn/commands/page.tsx @@ -159,10 +159,10 @@ const COMMANDS: CommandEntry[] = [ learnMoreHref: "/learn/ntm-palette", }, { - name: "bd", + name: "br", fullName: "Beads CLI", description: "Create/update issues and dependencies", - example: "bd ready", + example: "br ready", category: "stack", learnMoreHref: "/learn/tools/beads", }, @@ -506,7 +506,7 @@ function CategoryCard({ <code className="font-mono text-lg font-bold text-white"> {cmd.name} </code> - <span className="text-sm font-medium text-white/40"> + <span className="text-sm font-medium text-white/60"> {cmd.fullName} </span> </div> @@ -517,7 +517,7 @@ function CategoryCard({ <div className="flex items-center gap-4 shrink-0"> <Link href={`#${anchorId}`} - className="text-xs text-white/30 hover:text-white/60 transition-colors font-mono" + className="text-xs text-white/50 hover:text-white/70 transition-colors font-mono" > #{anchorId} </Link> @@ -668,14 +668,14 @@ export default function CommandReferencePage() { <div className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-primary/30 via-violet-500/20 to-primary/30 blur-lg opacity-0 group-focus-within:opacity-100 transition-opacity duration-500" /> <div className="relative"> - <Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-white/30 transition-colors group-focus-within:text-primary" /> + <Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-white/50 transition-colors group-focus-within:text-primary" /> <input type="text" placeholder="Search commands..." aria-label="Search commands" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.03] py-4 pl-14 pr-5 text-white placeholder:text-white/30 backdrop-blur-xl transition-all duration-300 focus:border-primary/50 focus:bg-white/[0.05] focus:outline-none focus:shadow-[0_0_30px_rgba(var(--primary-rgb),0.15)]" + className="w-full rounded-xl border border-white/[0.08] bg-white/[0.03] py-4 pl-14 pr-5 text-white placeholder:text-white/50 backdrop-blur-xl transition-all duration-300 focus:border-primary/50 focus:bg-white/[0.05] focus:outline-none focus:shadow-[0_0_30px_rgba(var(--primary-rgb),0.15)]" /> </div> </div> @@ -731,10 +731,10 @@ export default function CommandReferencePage() { <div className="relative inline-flex mb-6"> <div className="absolute inset-0 bg-white/10 rounded-2xl blur-xl" /> <div className="relative flex h-16 w-16 items-center justify-center rounded-2xl bg-white/[0.05] border border-white/[0.08]"> - <Search className="h-8 w-8 text-white/30" /> + <Search className="h-8 w-8 text-white/50" /> </div> </div> - <p className="text-lg text-white/40"> + <p className="text-lg text-white/60"> No commands match your search. </p> <p className="text-sm text-white/25 mt-2"> diff --git a/apps/web/app/learn/glossary/page.tsx b/apps/web/app/learn/glossary/page.tsx index 2cd5ec94..5d75ac6f 100644 --- a/apps/web/app/learn/glossary/page.tsx +++ b/apps/web/app/learn/glossary/page.tsx @@ -5,6 +5,7 @@ import { useMemo, useState, type ReactNode } from "react"; import { ArrowLeft, BookOpen, Home, Search, Wrench, ShieldCheck, Type, FileQuestion, Sparkles, ChevronDown, ChevronRight } from "lucide-react"; import { getAllTerms, type JargonTerm } from "@/lib/jargon"; import { motion, springs, staggerContainer, fadeUp } from "@/components/motion"; +import { EmptyState } from "@/components/ui/empty-state"; type GlossaryCategory = "concepts" | "tools" | "protocols" | "acronyms"; type CategoryFilter = "all" | GlossaryCategory; @@ -20,7 +21,7 @@ function toAnchorId(value: string): string { const TOOL_TERMS = new Set([ "tmux", "zsh", "bash", "bun", "uv", "cargo", "rust", "go", "git", "gh", "lazygit", "rg", "ripgrep", "fzf", "direnv", "zoxide", "atuin", "ntm", - "bv", "bd", "ubs", "cass", "cm", "caam", "slb", "dcg", "vault", "wrangler", + "bv", "br", "ubs", "cass", "cm", "caam", "slb", "dcg", "vault", "wrangler", "supabase", "vercel", "postgres", ]); @@ -153,7 +154,7 @@ function TermCard({ term }: { term: JargonTerm }) { </div> <Link href={`#${anchorId}`} - className="text-xs text-white/30 hover:text-white/60 transition-colors font-mono shrink-0" + className="text-xs text-white/50 hover:text-white/70 transition-colors font-mono shrink-0" > #{anchorId} </Link> @@ -356,13 +357,13 @@ export default function GlossaryPage() { <div className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-primary/30 via-emerald-500/20 to-primary/30 blur-lg opacity-0 group-focus-within:opacity-100 transition-opacity duration-500" /> <div className="relative"> - <Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-white/30 transition-colors group-focus-within:text-primary" /> + <Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-white/50 transition-colors group-focus-within:text-primary" /> <input type="text" placeholder="Search terms..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.03] py-4 pl-14 pr-5 text-white placeholder:text-white/30 backdrop-blur-xl transition-all duration-300 focus:border-primary/50 focus:bg-white/[0.05] focus:outline-none focus:shadow-[0_0_30px_rgba(var(--primary-rgb),0.15)]" + className="w-full rounded-xl border border-white/[0.08] bg-white/[0.03] py-4 pl-14 pr-5 text-white placeholder:text-white/50 backdrop-blur-xl transition-all duration-300 focus:border-primary/50 focus:bg-white/[0.05] focus:outline-none focus:shadow-[0_0_30px_rgba(var(--primary-rgb),0.15)]" /> </div> </div> @@ -392,7 +393,7 @@ export default function GlossaryPage() { {/* Count display */} <motion.p - className="mb-10 text-center text-sm text-white/40" + className="mb-10 text-center text-sm text-white/60" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ ...springs.smooth, delay: 0.4 }} @@ -435,35 +436,35 @@ export default function GlossaryPage() { )) ) : ( <motion.div - className="py-20 text-center" - initial={{ opacity: 0, scale: 0.95 }} + className="rounded-2xl border border-white/[0.08] bg-white/[0.02] p-8 text-center backdrop-blur-xl" + initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={springs.smooth} > - <div className="relative inline-flex mb-6"> - <div className="absolute inset-0 bg-white/10 rounded-2xl blur-xl" /> - <div className="relative flex h-20 w-20 items-center justify-center rounded-2xl bg-white/[0.05] border border-white/[0.08]"> - <FileQuestion className="h-10 w-10 text-white/30" /> - </div> - </div> - <h3 className="mb-3 text-xl font-bold text-white"> - No terms found - </h3> - <p className="mx-auto max-w-sm text-white/50 mb-6"> - Try adjusting your search or category filter to find what you're looking for. - </p> - <motion.button - onClick={() => { - setSearchQuery(""); - setCategory("all"); - }} - className="rounded-full bg-gradient-to-r from-primary/20 to-violet-500/20 border border-primary/30 px-6 py-3 text-sm font-medium text-white transition-all duration-300 hover:from-primary/30 hover:to-violet-500/30 hover:shadow-[0_0_30px_rgba(var(--primary-rgb),0.3)]" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - transition={springs.snappy} - > - Clear filters - </motion.button> + <EmptyState + icon={FileQuestion} + title="No terms found" + description="Try adjusting your search or category filter to find what you're looking for." + action={ + <motion.button + onClick={() => { + setSearchQuery(""); + setCategory("all"); + }} + className="rounded-full bg-gradient-to-r from-primary/20 to-violet-500/20 border border-primary/30 px-6 py-3 text-sm font-medium text-white transition-all duration-300 hover:from-primary/30 hover:to-violet-500/30 hover:shadow-[0_0_30px_rgba(var(--primary-rgb),0.3)]" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + transition={springs.snappy} + > + Clear filters + </motion.button> + } + iconContainerClassName="bg-white/[0.05] border border-white/[0.08] shadow-[0_0_30px_rgba(255,255,255,0.08)]" + iconClassName="text-white/60" + titleClassName="text-white" + descriptionClassName="text-white/50" + variant="default" + /> </motion.div> )} </motion.div> diff --git a/apps/web/app/learn/page.tsx b/apps/web/app/learn/page.tsx index c6384f3a..6fc107de 100644 --- a/apps/web/app/learn/page.tsx +++ b/apps/web/app/learn/page.tsx @@ -86,7 +86,7 @@ function LessonCard({ > {/* Ambient glow on hover */} {isAccessible && ( - <div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-emerald-500/5 opacity-0 transition-opacity duration-500 group-hover:opacity-100" /> + <div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-emerald-500/5 opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-within:opacity-100" /> )} {/* Top gradient line */} @@ -149,7 +149,7 @@ function LessonCard({ {/* Hover arrow */} {isAccessible && ( - <ChevronRight className="absolute bottom-4 right-4 h-5 w-5 text-primary/40 opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:text-primary group-hover:opacity-100" /> + <ChevronRight className="absolute bottom-4 right-4 h-5 w-5 text-primary/40 opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:text-primary group-hover:opacity-100 group-focus-within:translate-x-1 group-focus-within:text-primary group-focus-within:opacity-100" /> )} </div> </motion.div> @@ -267,9 +267,9 @@ export default function LearnDashboard() { <div className="flex items-center gap-3 sm:gap-4"> <span className="hidden text-xs text-muted-foreground/60 lg:block"> - <kbd className="rounded border border-white/10 bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px]">j</kbd> + <kbd className="rounded border border-white/10 bg-white/[0.04] px-1.5 py-0.5 font-mono text-xs">j</kbd> / - <kbd className="rounded border border-white/10 bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px]">k</kbd> + <kbd className="rounded border border-white/10 bg-white/[0.04] px-1.5 py-0.5 font-mono text-xs">k</kbd> {" "}to navigate </span> <Link @@ -440,7 +440,7 @@ export default function LearnDashboard() { transition={prefersReducedMotion ? { duration: 0 } : { ...springs.smooth, delay: 0.3 }} > <h2 className="mb-5 text-xl font-semibold lg:mb-6 lg:text-2xl">All Lessons</h2> - <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-5"> + <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 lg:gap-5"> {LESSONS.map((lesson, index) => { const status = getLessonStatus(lesson.id, completedLessons); const accessibleIndex = accessibleLessons.findIndex( @@ -522,7 +522,7 @@ export default function LearnDashboard() { {item.desc} </div> </div> - <ChevronRight className="ml-auto h-4 w-4 shrink-0 text-muted-foreground/40 opacity-0 transition-all group-hover:translate-x-0.5 group-hover:opacity-100" /> + <ChevronRight className="ml-auto h-4 w-4 shrink-0 text-muted-foreground/40 opacity-0 transition-all group-hover:translate-x-0.5 group-hover:opacity-100 group-focus-within:translate-x-0.5 group-focus-within:opacity-100" /> </Link> </motion.div> ))} diff --git a/apps/web/app/learn/tools/[tool]/tool-data.tsx b/apps/web/app/learn/tools/[tool]/tool-data.tsx index 6d98ff79..d1da4915 100644 --- a/apps/web/app/learn/tools/[tool]/tool-data.tsx +++ b/apps/web/app/learn/tools/[tool]/tool-data.tsx @@ -105,7 +105,7 @@ export const TOOLS: Record<ToolId, ToolCard> = { glowColor: "rgba(52,211,153,0.4)", docsUrl: "https://github.com/Dicklesworthstone/beads_viewer", docsLabel: "GitHub", - quickCommand: "bd ready", + quickCommand: "br ready", relatedTools: ["agent-mail", "ubs"], }, "agent-mail": { diff --git a/apps/web/app/learn/tools/[tool]/tool-page-content.tsx b/apps/web/app/learn/tools/[tool]/tool-page-content.tsx index 57597b21..a1a68678 100644 --- a/apps/web/app/learn/tools/[tool]/tool-page-content.tsx +++ b/apps/web/app/learn/tools/[tool]/tool-page-content.tsx @@ -70,7 +70,7 @@ function RelatedToolCard({ toolId }: { toolId: ToolId }) { </div> </div> <ChevronRight - className="relative h-4 w-4 text-white/40 group-hover:text-white/70 transition-colors" + className="relative h-4 w-4 text-white/60 group-hover:text-white/80 transition-colors" aria-hidden="true" /> </motion.div> @@ -102,7 +102,7 @@ function CopyButton({ text }: { text: string }) { return ( <button onClick={handleCopy} - className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 rounded-md px-2 py-1 text-xs text-white/40 transition-colors hover:bg-white/10 hover:text-white/70" + className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 rounded-md px-2 py-1 text-xs text-white/60 transition-colors hover:bg-white/10 hover:text-white/80" aria-label={copied ? "Copied!" : "Copy command"} > {copied ? ( @@ -281,7 +281,7 @@ export function ToolPageContent({ tool: doc }: ToolPageContentProps) { <div className="w-3 h-3 rounded-full bg-red-500/70" aria-hidden="true" /> <div className="w-3 h-3 rounded-full bg-yellow-500/70" aria-hidden="true" /> <div className="w-3 h-3 rounded-full bg-green-500/70" aria-hidden="true" /> - <span className="ml-2 text-xs text-white/30"> + <span className="ml-2 text-xs text-white/50"> terminal </span> </div> diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 8e3400f8..b2db1d1a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import Image from "next/image"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ArrowRight, Terminal, @@ -26,6 +26,7 @@ import { BookOpen, } from "lucide-react"; import { motion, AnimatePresence } from "@/components/motion"; +import { useDrag } from "@use-gesture/react"; import { Button } from "@/components/ui/button"; import { Jargon } from "@/components/jargon"; import { springs, fadeUp, staggerContainer, fadeScale } from "@/components/motion"; @@ -91,9 +92,10 @@ function AnimatedTerminal() { transition={springs.smooth} > <div className="terminal-header"> - <div className="terminal-dot terminal-dot-red" /> - <div className="terminal-dot terminal-dot-yellow" /> - <div className="terminal-dot terminal-dot-green" /> + {/* Decorative window controls - hidden from screen readers since they're non-functional */} + <div className="terminal-dot terminal-dot-red" aria-hidden="true" /> + <div className="terminal-dot terminal-dot-yellow" aria-hidden="true" /> + <div className="terminal-dot terminal-dot-green" aria-hidden="true" /> <span className="ml-3 font-mono text-xs text-muted-foreground"> ubuntu@vps ~ </span> @@ -338,7 +340,7 @@ function FlywheelSection() { > <div className="mb-4 flex items-center justify-center gap-3"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary">Ecosystem</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">Ecosystem</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> <h2 className="mb-4 font-mono text-3xl font-bold tracking-tight"> @@ -372,7 +374,7 @@ function FlywheelSection() { > <span className="text-xs font-bold text-white">{tool.name}</span> </motion.div> - <span className="text-[10px] text-muted-foreground text-center">{tool.desc}</span> + <span className="text-xs text-muted-foreground text-center">{tool.desc}</span> </motion.div> ))} </motion.div> @@ -413,6 +415,24 @@ const WORKFLOW_STEPS = [ function WorkflowStepsSection() { const { ref, isInView } = useScrollReveal({ threshold: 0.1 }); + const scrollRef = useRef<HTMLDivElement>(null); + + const bind = useDrag( + ({ active, movement: [mx], memo }) => { + const scroller = scrollRef.current; + if (!scroller) return memo; + const start = memo ?? scroller.scrollLeft; + if (active) { + scroller.scrollLeft = start - mx; + } + return start; + }, + { + axis: "x", + filterTaps: true, + threshold: 8, + } + ); return ( <section ref={ref as React.RefObject<HTMLElement>} className="border-t border-border/30 bg-card/30 py-24"> @@ -434,7 +454,11 @@ function WorkflowStepsSection() { {/* Horizontal scroll on mobile, wrap on desktop */} <div className="relative -mx-6 px-6 sm:mx-0 sm:px-0"> <motion.div - className="flex gap-3 overflow-x-auto pb-4 sm:flex-wrap sm:justify-center sm:overflow-visible sm:pb-0 scrollbar-hide" + ref={scrollRef} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {...(bind() as any)} + style={{ touchAction: "pan-y" }} + className="flex gap-3 overflow-x-auto pb-4 sm:flex-wrap sm:justify-center sm:overflow-visible sm:pb-0 scrollbar-hide cursor-grab active:cursor-grabbing select-none" variants={staggerContainer} initial="hidden" animate={isInView ? "visible" : "hidden"} @@ -488,7 +512,7 @@ function AboutSection() { > <div className="mb-6 flex items-center justify-center gap-3"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary">About</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">About</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> @@ -638,7 +662,7 @@ function WhyVPSSection() { > <div className="mb-4 flex items-center justify-center gap-3"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary">The Foundation</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">The Foundation</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> <h2 className="mb-4 font-mono text-3xl font-bold tracking-tight sm:text-4xl">Why a VPS?</h2> @@ -719,7 +743,7 @@ function IsThisForYouSection() { <motion.div className="mb-12 text-center" initial={{ opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} transition={springs.smooth}> <div className="mb-4 flex items-center justify-center gap-3"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary">Honest Assessment</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">Honest Assessment</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> <h2 className="mb-4 font-mono text-3xl font-bold tracking-tight sm:text-4xl">Is This For You?</h2> @@ -809,7 +833,7 @@ function WhatDoesThisCostSection() { <motion.div className="mb-12 text-center" initial={{ opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} transition={springs.smooth}> <div className="mb-4 flex items-center justify-center gap-3"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary">Investment</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">Investment</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> <h2 className="mb-4 font-mono text-3xl font-bold tracking-tight sm:text-4xl">What Does This Cost?</h2> @@ -914,31 +938,31 @@ export default function HomePage() { <span className="font-mono text-lg font-bold tracking-tight">Agent Flywheel</span> </div> <div className="flex items-center gap-2 sm:gap-4"> - {/* Mobile: icon-only buttons with proper touch targets */} + {/* Mobile: icon-only buttons with 44px touch targets (Apple HIG) */} <a href="https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup" target="_blank" rel="noopener noreferrer" - className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" + className="flex h-11 w-11 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" aria-label="GitHub" > - <GitBranch className="h-4 w-4 sm:hidden" /> + <GitBranch className="h-5 w-5 sm:hidden" /> <span className="hidden text-sm sm:inline">GitHub</span> </a> <Link href="/learn" - className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:gap-1 sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" + className="flex h-11 w-11 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:gap-1 sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" aria-label="Learn" > - <BookOpen className="h-4 w-4" /> + <BookOpen className="h-5 w-5 sm:h-4 sm:w-4" /> <span className="hidden text-sm sm:inline">Learn</span> </Link> <Link href="/tldr" - className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:gap-1 sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" + className="flex h-11 w-11 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:h-auto sm:w-auto sm:gap-1 sm:rounded-none sm:bg-transparent sm:hover:bg-transparent" aria-label="TL;DR" > - <Zap className="h-4 w-4" /> + <Zap className="h-5 w-5 sm:h-4 sm:w-4" /> <span className="hidden text-sm sm:inline">TL;DR</span> </Link> <Button asChild size="sm" variant="outline" className="border-primary/30 hover:bg-primary/10"> @@ -1104,19 +1128,19 @@ export default function HomePage() { href="https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup" target="_blank" rel="noopener noreferrer" - className="transition-colors hover:text-foreground" + className="rounded-sm underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" > GitHub </a> <Link href="/learn" - className="transition-colors hover:text-foreground" + className="rounded-sm underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" > Learning Hub </Link> <Link href="/tldr" - className="transition-colors hover:text-foreground" + className="rounded-sm underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" > TL;DR </Link> @@ -1124,7 +1148,7 @@ export default function HomePage() { href="https://github.com/Dicklesworthstone/ntm" target="_blank" rel="noopener noreferrer" - className="transition-colors hover:text-foreground" + className="rounded-sm underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" > NTM </a> @@ -1132,7 +1156,7 @@ export default function HomePage() { href="https://github.com/Dicklesworthstone/mcp_agent_mail" target="_blank" rel="noopener noreferrer" - className="transition-colors hover:text-foreground" + className="rounded-sm underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" > Agent Mail </a> diff --git a/apps/web/app/tools/page.tsx b/apps/web/app/tools/page.tsx index 6461abd6..d52b0e73 100644 --- a/apps/web/app/tools/page.tsx +++ b/apps/web/app/tools/page.tsx @@ -35,6 +35,8 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import { ErrorBoundary } from "@/components/ui/error-boundary"; +import { EmptyState } from "@/components/ui/empty-state"; +import { Button } from "@/components/ui/button"; import { manifestTools, type ManifestWebTool } from "@/lib/generated/manifest-web-index"; // ============================================================================= @@ -180,7 +182,7 @@ function ToolCard({ tool, index }: ToolCardProps) { href={tool.href} target="_blank" rel="noopener noreferrer" - className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/5 text-muted-foreground ring-1 ring-white/10 transition-all hover:bg-white/10 hover:text-white" + className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/5 text-muted-foreground ring-1 ring-white/10 transition-all hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-ring outline-none" aria-label={`View ${tool.displayName} on GitHub`} > <ExternalLink className="h-4 w-4" /> @@ -302,6 +304,7 @@ function SearchInput({ value, onChange }: SearchInputProps) { value={value} onChange={(e) => onChange(e.target.value)} placeholder="Search tools..." + aria-label="Search tools" className={cn( "w-full rounded-xl bg-card/50 py-3 pl-10 pr-10 text-sm", "border border-border/50 backdrop-blur-sm", @@ -314,6 +317,7 @@ function SearchInput({ value, onChange }: SearchInputProps) { <button onClick={() => onChange("")} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-white" + aria-label="Clear search" > <X className="h-4 w-4" /> </button> @@ -476,30 +480,27 @@ export default function ToolsPage() { {/* Tools grid */} {filteredTools.length > 0 ? ( - <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> + <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {filteredTools.map((tool, index) => ( <ToolCard key={tool.id} tool={tool} index={index} /> ))} </div> ) : ( - <div className="py-16 text-center"> - <Search className="mx-auto h-12 w-12 text-muted-foreground/50" /> - <h3 className="mt-4 text-lg font-medium text-white"> - No tools found - </h3> - <p className="mt-2 text-sm text-muted-foreground"> - Try adjusting your search or filter criteria. - </p> - <button - onClick={() => { - setSearchQuery(""); - setSelectedCategory(null); - }} - className="mt-4 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition-colors" - > - Clear filters - </button> - </div> + <EmptyState + icon={Search} + title="No tools found" + description="Try adjusting your search or filter criteria." + action={ + <Button + onClick={() => { + setSearchQuery(""); + setSelectedCategory(null); + }} + > + Clear filters + </Button> + } + /> )} </div> </section> diff --git a/apps/web/app/troubleshooting/page.tsx b/apps/web/app/troubleshooting/page.tsx index c9fed64a..f235457f 100644 --- a/apps/web/app/troubleshooting/page.tsx +++ b/apps/web/app/troubleshooting/page.tsx @@ -736,6 +736,7 @@ export default function TroubleshootingPage() { placeholder="Search issues (e.g., 'connection refused', 'permission denied')..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + aria-label="Search troubleshooting issues" className="w-full rounded-xl border border-border/50 bg-card/50 py-3 pl-12 pr-4 text-foreground placeholder:text-muted-foreground focus:border-primary/40 focus:outline-none focus:ring-2 focus:ring-primary/20" /> </div> diff --git a/apps/web/app/wizard/accounts/page.tsx b/apps/web/app/wizard/accounts/page.tsx index 4a379bf0..56b5c010 100644 --- a/apps/web/app/wizard/accounts/page.tsx +++ b/apps/web/app/wizard/accounts/page.tsx @@ -102,7 +102,7 @@ function ServiceCard({ service, isChecked, onToggle }: ServiceCardProps) { /> <label htmlFor={checkboxId} - className="text-[10px] text-muted-foreground" + className="text-xs text-muted-foreground" > Authenticated </label> @@ -171,7 +171,7 @@ function ServiceCard({ service, isChecked, onToggle }: ServiceCardProps) { <Terminal className="h-3 w-3 shrink-0" /> <span> After install:{" "} - <code className="rounded bg-muted/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"> + <code className="rounded bg-muted/50 px-1.5 py-0.5 font-mono text-xs text-muted-foreground"> {service.postInstallCommand} </code> </span> diff --git a/apps/web/app/wizard/generate-ssh-key/page.tsx b/apps/web/app/wizard/generate-ssh-key/page.tsx index b9f95eb3..9b96da83 100644 --- a/apps/web/app/wizard/generate-ssh-key/page.tsx +++ b/apps/web/app/wizard/generate-ssh-key/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { Key, ShieldCheck, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { CommandCard } from "@/components/command-card"; +import { CodeBlock } from "@/components/ui/code-block"; import { AlertCard, DetailsSection, OutputPreview } from "@/components/alert-card"; import { markStepComplete } from "@/lib/wizardSteps"; import { useWizardAnalytics } from "@/lib/hooks/useWizardAnalytics"; @@ -416,9 +417,9 @@ export default function GenerateSSHKeyPage() { <GuideTip> Your public key looks something like this: - <code className="mt-2 block overflow-x-auto rounded bg-muted p-2 font-mono text-xs"> - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGx... acfs - </code> + <div className="mt-2"> + <CodeBlock code="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGx... acfs" variant="compact" /> + </div> Make sure you copy the WHOLE thing, from "ssh-ed25519" to "acfs"! </GuideTip> @@ -436,9 +437,9 @@ export default function GenerateSSHKeyPage() { <br /><br /> <strong>"Permission denied":</strong> Try this command first, then run the ssh-keygen command again: - <code className="my-2 block overflow-x-auto rounded bg-muted px-3 py-2 font-mono text-xs"> - mkdir -p ~/.ssh && chmod 700 ~/.ssh - </code> + <div className="my-2"> + <CodeBlock code="mkdir -p ~/.ssh && chmod 700 ~/.ssh" variant="compact" /> + </div> <strong>"File already exists":</strong> You already have a key! You can use your existing key, or type "y" and press Enter to overwrite it. </p> diff --git a/apps/web/app/wizard/install-terminal/page.tsx b/apps/web/app/wizard/install-terminal/page.tsx index 71484a36..596aeaa3 100644 --- a/apps/web/app/wizard/install-terminal/page.tsx +++ b/apps/web/app/wizard/install-terminal/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { ExternalLink, Terminal, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { CommandCard } from "@/components/command-card"; +import { CodeBlock } from "@/components/ui/code-block"; import { AlertCard } from "@/components/alert-card"; import { OutputPreview } from "@/components/alert-card"; import { TrackedLink } from "@/components/tracked-link"; @@ -369,9 +370,9 @@ function WindowsContent() { <div className="space-y-4"> <GuideStep number={1} title="Type the command"> In the Windows Terminal window, type exactly: - <code className="mt-2 block overflow-x-auto rounded bg-muted px-3 py-2 font-mono text-sm"> - ssh -V - </code> + <div className="mt-2"> + <CodeBlock code="ssh -V" variant="compact" /> + </div> <em className="mt-1 block text-xs"> That's "ssh" (lowercase), a space, a dash, and a capital "V" </em> diff --git a/apps/web/app/wizard/launch-onboarding/page.tsx b/apps/web/app/wizard/launch-onboarding/page.tsx index 8905cefb..2400f42f 100644 --- a/apps/web/app/wizard/launch-onboarding/page.tsx +++ b/apps/web/app/wizard/launch-onboarding/page.tsx @@ -572,7 +572,7 @@ export default function LaunchOnboardingPage() { {/* SSH Config tip */} <details className="mt-6 group"> - <summary className="cursor-pointer font-medium text-[oklch(0.75_0.18_195)] hover:text-[oklch(0.65_0.18_195)] transition-colors"> + <summary className="cursor-pointer font-medium text-[oklch(0.75_0.18_195)] hover:text-[oklch(0.65_0.18_195)] transition-colors rounded outline-none focus-visible:ring-2 focus-visible:ring-ring"> 💡 Pro tip: Set up SSH config for easier access </summary> <div className="mt-4 space-y-4 pl-6 border-l-2 border-[oklch(0.75_0.18_195/0.3)]"> @@ -643,7 +643,7 @@ export default function LaunchOnboardingPage() { {/* Manual editing escape hatch */} <Card className="border-border/50 bg-card/50 p-4 backdrop-blur-sm"> <details className="group"> - <summary className="cursor-pointer font-semibold text-foreground hover:text-primary transition-colors"> + <summary className="cursor-pointer font-semibold text-foreground hover:text-primary transition-colors rounded outline-none focus-visible:ring-2 focus-visible:ring-ring"> How to edit files manually (when AI gets something wrong) </summary> <div className="mt-4 space-y-6 pl-4 border-l-2 border-primary/20"> diff --git a/apps/web/app/wizard/layout.tsx b/apps/web/app/wizard/layout.tsx index 89875417..2c84300b 100644 --- a/apps/web/app/wizard/layout.tsx +++ b/apps/web/app/wizard/layout.tsx @@ -127,7 +127,7 @@ export default function WizardLayout({ <ThemeToggle /> <Link href="/" - className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" aria-label="Home" > <Home className="h-4 w-4" /> diff --git a/apps/web/app/wizard/os-selection/page.tsx b/apps/web/app/wizard/os-selection/page.tsx index c1290fd5..0683c37e 100644 --- a/apps/web/app/wizard/os-selection/page.tsx +++ b/apps/web/app/wizard/os-selection/page.tsx @@ -39,6 +39,7 @@ function OSCard({ icon, title, description, selected, detected, onClick }: OSCar ? "border-primary bg-primary/10 shadow-lg shadow-primary/10" : "border-border/50 bg-card/50 hover:border-primary/30 hover:bg-card/80 hover:shadow-md" )} + style={{ touchAction: "manipulation" }} onClick={onClick} role="radio" tabIndex={0} @@ -175,7 +176,7 @@ export default function OSSelectionPage() { </div> {/* OS Options */} - <div data-os-selection className="grid gap-6 sm:grid-cols-3" role="radiogroup" aria-label="Select your operating system"> + <div data-os-selection className="grid gap-6 sm:grid-cols-3" role="radiogroup" aria-label="Select your operating system" style={{ touchAction: "pan-y" }}> <OSCard icon={<Apple className="h-10 w-10" />} title="Mac" diff --git a/apps/web/app/wizard/run-installer/page.tsx b/apps/web/app/wizard/run-installer/page.tsx index e75d6e9f..ee57f0ec 100644 --- a/apps/web/app/wizard/run-installer/page.tsx +++ b/apps/web/app/wizard/run-installer/page.tsx @@ -253,7 +253,7 @@ export default function RunInstallerPage() { <TrackedLink href="https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup/blob/main/install.sh" trackingId="install-sh-source" - className="inline-flex items-center gap-1.5 rounded-lg border border-[oklch(0.75_0.18_195/0.3)] bg-[oklch(0.75_0.18_195/0.1)] px-2.5 py-1.5 text-[11px] font-medium text-[oklch(0.75_0.18_195)] transition-colors hover:bg-[oklch(0.75_0.18_195/0.2)] sm:text-xs" + className="inline-flex items-center gap-1.5 rounded-lg border border-[oklch(0.75_0.18_195/0.3)] bg-[oklch(0.75_0.18_195/0.1)] px-2.5 py-1.5 text-xs font-medium text-[oklch(0.75_0.18_195)] transition-colors hover:bg-[oklch(0.75_0.18_195/0.2)]" > <Code className="h-3 w-3" /> View install.sh source @@ -262,7 +262,7 @@ export default function RunInstallerPage() { <TrackedLink href="https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup" trackingId="github-repo" - className="inline-flex items-center gap-1.5 rounded-lg border border-border/50 bg-card/50 px-2.5 py-1.5 text-[11px] font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:text-foreground sm:text-xs" + className="inline-flex items-center gap-1.5 rounded-lg border border-border/50 bg-card/50 px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:text-foreground" > Full repository <ExternalLink className="h-2.5 w-2.5" /> diff --git a/apps/web/app/wizard/windows-terminal-setup/page.tsx b/apps/web/app/wizard/windows-terminal-setup/page.tsx index 66a05c6a..d40266b5 100644 --- a/apps/web/app/wizard/windows-terminal-setup/page.tsx +++ b/apps/web/app/wizard/windows-terminal-setup/page.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; import { AlertCard, OutputPreview } from "@/components/alert-card"; +import { CodeBlock } from "@/components/ui/code-block"; import { useVPSIP } from "@/lib/userPreferences"; import { withCurrentSearch } from "@/lib/utils"; import { @@ -174,9 +175,7 @@ export default function WindowsTerminalSetupPage() { <div className="pl-11 space-y-4"> <div> <p className="text-sm font-medium mb-2">Name:</p> - <code className="block rounded bg-muted px-3 py-2 font-mono text-sm"> - My VPS - </code> + <CodeBlock code="My VPS" variant="compact" /> <p className="text-xs text-muted-foreground mt-1"> (or whatever name you prefer, like "ACFS Server" or "Ubuntu VPS") </p> @@ -206,9 +205,7 @@ export default function WindowsTerminalSetupPage() { </div> <div> <p className="text-sm font-medium mb-2">Starting directory (optional):</p> - <code className="block rounded bg-muted px-3 py-2 font-mono text-sm"> - %USERPROFILE% - </code> + <CodeBlock code="%USERPROFILE%" variant="compact" /> </div> <div> <p className="text-sm font-medium mb-2">Icon (optional):</p> diff --git a/apps/web/app/workflow/page.tsx b/apps/web/app/workflow/page.tsx index c2cb62e1..12b060a2 100644 --- a/apps/web/app/workflow/page.tsx +++ b/apps/web/app/workflow/page.tsx @@ -106,7 +106,7 @@ function CollapsibleSection({ <div className="flex items-center gap-2"> <span className="text-lg font-semibold tracking-tight">{title}</span> {badge && ( - <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-primary/20 text-primary"> + <span className="text-xs font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-primary/20 text-primary"> {badge} </span> )} @@ -408,7 +408,7 @@ const PROMPT_BEST_OF_ALL_WORLDS = `I asked 3 competing LLMs to do the exact same const PROMPT_100_IDEAS = `OK so now I want you to come up with your top 10 most brilliant ideas for adding extremely powerful and cool functionality that will make this system far more compelling, useful, intuitive, versatile, powerful, robust, reliable, etc for the users. Use ultrathink. But be pragmatic and don't think of features that will be extremely hard to implement or which aren't necessarily worth the additional complexity burden they would introduce. But I don't want you to just think of 10 ideas: I want you to seriously think hard and come up with one HUNDRED ideas and then only tell me your 10 VERY BEST and most brilliant, clever, and radically innovative and powerful ideas.`; -const PROMPT_CREATE_BEADS = `OK so please take ALL of that and elaborate on it more and then create a comprehensive and granular set of beads for all this with tasks, subtasks, and dependency structure overlaid, with detailed comments so that the whole thing is totally self-contained and self-documenting (including relevant background, reasoning/justification, considerations, etc.-- anything we'd want our "future self" to know about the goals and intentions and thought process and how it serves the over-arching goals of the project.) Use the \`bd\` tool repeatedly to create the actual beads. Use ultrathink.`; +const PROMPT_CREATE_BEADS = `OK so please take ALL of that and elaborate on it more and then create a comprehensive and granular set of beads for all this with tasks, subtasks, and dependency structure overlaid, with detailed comments so that the whole thing is totally self-contained and self-documenting (including relevant background, reasoning/justification, considerations, etc.-- anything we'd want our "future self" to know about the goals and intentions and thought process and how it serves the over-arching goals of the project.) Use the \`br\` tool repeatedly to create the actual beads. Use ultrathink.`; const PROMPT_REVIEW_BEADS = `Check over each bead super carefully-- are you sure it makes sense? Is it optimal? Could we change anything to make the system work better for users? If so, revise the beads. It's a lot easier and faster to operate in "plan space" before we start implementing these things! Use ultrathink.`; @@ -450,7 +450,7 @@ const PROMPT_IMPROVE_README = `What else can we put in there to make the README const PROMPT_DO_GH_FLOW = `Do all the GitHub stuff: commit, deploy, create tag, bump version, release, monitor gh actions, compute checksums, etc.`; -const PROMPT_DO_ALL_OF_IT = `OK, please do ALL of that now. Track work via bd beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work.`; +const PROMPT_DO_ALL_OF_IT = `OK, please do ALL of that now. Track work via br beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work.`; const PROMPT_CHECK_MAIL = `Be sure to check your agent mail and to promptly respond if needed to any messages, and also acknowledge any contact requests; make sure you know the names of all active agents using the MCP Agent Mail system.`; @@ -638,7 +638,7 @@ export default function WorkflowPage() { <div className="text-center mb-6"> <div className="mb-3 flex items-center justify-center gap-2"> <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[10px] font-bold uppercase tracking-[0.25em] text-primary">Ecosystem</span> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary">Ecosystem</span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> </div> <h3 className="text-xl font-bold tracking-tight mb-2">The Self-Reinforcing Flywheel</h3> @@ -666,7 +666,7 @@ export default function WorkflowPage() { </div> <div className="text-center"> <span className="text-xs font-bold block">{tool.name}</span> - <span className="text-[10px] text-muted-foreground">{tool.desc}</span> + <span className="text-xs text-muted-foreground">{tool.desc}</span> </div> </motion.div> ))} @@ -789,15 +789,15 @@ export default function WorkflowPage() { <p className="text-xs text-muted-foreground mt-1">Deterministic triage output (recommended)</p> </div> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> - <code className="text-sm font-mono text-primary">bd ready</code> + <code className="text-sm font-mono text-primary">br ready</code> <p className="text-xs text-muted-foreground mt-1">Show beads ready to work on</p> </div> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> - <code className="text-sm font-mono text-primary">bd stats</code> + <code className="text-sm font-mono text-primary">br stats</code> <p className="text-xs text-muted-foreground mt-1">Project statistics overview</p> </div> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> - <code className="text-sm font-mono text-primary">bd blocked</code> + <code className="text-sm font-mono text-primary">br blocked</code> <p className="text-xs text-muted-foreground mt-1">Show blocked issues</p> </div> </div> @@ -862,11 +862,11 @@ export default function WorkflowPage() { <p className="mb-3">Key coordination tools agents use:</p> <div className="grid gap-3 sm:grid-cols-2"> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> - <code className="text-sm font-mono text-primary">bd update ID --status=in_progress</code> + <code className="text-sm font-mono text-primary">br update ID --status=in_progress</code> <p className="text-xs text-muted-foreground mt-1">Claim a bead before working</p> </div> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> - <code className="text-sm font-mono text-primary">bd close ID</code> + <code className="text-sm font-mono text-primary">br close ID</code> <p className="text-xs text-muted-foreground mt-1">Mark a bead complete</p> </div> <div className="rounded-lg border border-border/50 p-3 bg-card/50"> diff --git a/apps/web/components/agent-commands/AgentCardContent.tsx b/apps/web/components/agent-commands/AgentCardContent.tsx index 09c3c282..bc6ccef3 100644 --- a/apps/web/components/agent-commands/AgentCardContent.tsx +++ b/apps/web/components/agent-commands/AgentCardContent.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Copy, Check, Terminal, Sparkles, Code2 } from "lucide-react"; -import { motion, AnimatePresence, springs } from "@/components/motion"; +import { motion, AnimatePresence, springs, useReducedMotion } from "@/components/motion"; import { CommandCard } from "@/components/command-card"; import { cn } from "@/lib/utils"; import type { AgentInfo } from "./AgentHeroCard"; @@ -31,6 +31,8 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { const [activeTab, setActiveTab] = useState<TabId>("examples"); const [copiedAlias, setCopiedAlias] = useState<string | null>(null); const personality = agentPersonalities[agent.id]; + const prefersReducedMotion = useReducedMotion(); + const reducedMotion = prefersReducedMotion ?? false; const handleCopy = async (text: string) => { try { @@ -55,10 +57,10 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { <AnimatePresence> {isExpanded && ( <motion.div - initial={{ opacity: 0, height: 0 }} - animate={{ opacity: 1, height: "auto" }} - exit={{ opacity: 0, height: 0 }} - transition={springs.smooth} + initial={reducedMotion ? { opacity: 0 } : { opacity: 0, height: 0 }} + animate={reducedMotion ? { opacity: 1 } : { opacity: 1, height: "auto" }} + exit={reducedMotion ? { opacity: 0 } : { opacity: 0, height: 0 }} + transition={reducedMotion ? { duration: 0 } : springs.smooth} className="overflow-hidden" > <div className="border-t border-white/[0.06] bg-black/20"> @@ -93,18 +95,18 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { {activeTab === "examples" && ( <motion.div key="examples" - initial={{ opacity: 0, x: -10 }} + initial={reducedMotion ? {} : { opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} - exit={{ opacity: 0, x: 10 }} - transition={springs.snappy} + exit={reducedMotion ? {} : { opacity: 0, x: 10 }} + transition={reducedMotion ? { duration: 0 } : springs.snappy} className="space-y-3" > {agent.examples.map((example, i) => ( <motion.div key={i} - initial={{ opacity: 0, y: 10 }} + initial={reducedMotion ? {} : { opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - transition={{ ...springs.smooth, delay: i * 0.05 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: i * 0.05 }} > <CommandCard command={example.command} @@ -118,20 +120,20 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { {activeTab === "tips" && ( <motion.div key="tips" - initial={{ opacity: 0, x: -10 }} + initial={reducedMotion ? {} : { opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} - exit={{ opacity: 0, x: 10 }} - transition={springs.snappy} + exit={reducedMotion ? {} : { opacity: 0, x: 10 }} + transition={reducedMotion ? { duration: 0 } : springs.snappy} > <ul className="space-y-3"> {agent.tips.map((tip, i) => ( <motion.li key={i} className="group/tip flex items-start gap-3 rounded-xl border border-white/[0.06] bg-white/[0.02] p-4 transition-all duration-300 hover:border-primary/30 hover:bg-white/[0.04]" - initial={{ opacity: 0, y: 10 }} + initial={reducedMotion ? {} : { opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - whileHover={{ x: 4, scale: 1.01 }} - transition={{ ...springs.smooth, delay: i * 0.05 }} + whileHover={reducedMotion ? {} : { x: 4, scale: 1.01 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: i * 0.05 }} > <div className={cn( @@ -153,10 +155,10 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { {activeTab === "aliases" && ( <motion.div key="aliases" - initial={{ opacity: 0, x: -10 }} + initial={reducedMotion ? {} : { opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} - exit={{ opacity: 0, x: 10 }} - transition={springs.snappy} + exit={reducedMotion ? {} : { opacity: 0, x: 10 }} + transition={reducedMotion ? { duration: 0 } : springs.snappy} > <p className="mb-4 text-sm text-white/50"> All these commands launch {agent.name}. Copy and paste into @@ -175,14 +177,14 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { ? "border-emerald-500/50 bg-emerald-500/10" : "border-white/[0.06] bg-white/[0.02] hover:border-primary/40 hover:bg-white/[0.04]" )} - initial={{ opacity: 0, y: 10 }} + initial={reducedMotion ? {} : { opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - whileHover={{ x: 4, scale: 1.02 }} - transition={{ ...springs.smooth, delay: i * 0.05 }} - whileTap={{ scale: 0.98 }} + whileHover={reducedMotion ? {} : { x: 4, scale: 1.02 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: i * 0.05 }} + whileTap={reducedMotion ? {} : { scale: 0.98 }} > <div className="flex items-center gap-3"> - <Terminal className="h-4 w-4 text-white/40 group-hover/alias:text-primary transition-colors" /> + <Terminal className="h-4 w-4 text-white/60 group-hover/alias:text-primary transition-colors" /> <code className="font-mono text-base text-white/80">{alias}</code> </div> <AnimatePresence mode="wait"> @@ -205,7 +207,7 @@ export function AgentCardContent({ agent, isExpanded }: AgentCardContentProps) { initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} - className="flex items-center gap-1 text-white/40 opacity-0 transition-opacity group-hover/alias:opacity-100" + className="flex items-center gap-1 text-white/60 opacity-0 transition-opacity group-hover/alias:opacity-100" > <Copy className="h-4 w-4" /> <span className="hidden text-xs sm:inline"> diff --git a/apps/web/components/agent-commands/AgentHeroCard.tsx b/apps/web/components/agent-commands/AgentHeroCard.tsx index 8c1ca778..ee734e07 100644 --- a/apps/web/components/agent-commands/AgentHeroCard.tsx +++ b/apps/web/components/agent-commands/AgentHeroCard.tsx @@ -282,7 +282,7 @@ export function AgentHeroCard({ <span className="text-sm text-white/60"> {agent.model} </span> - <div className="flex items-center gap-2 text-sm text-white/40"> + <div className="flex items-center gap-2 text-sm text-white/60"> <span>{agent.examples.length} commands</span> <span className="text-white/20">·</span> <span>{agent.tips.length} tips</span> diff --git a/apps/web/components/agent-commands/QuickAccessBar.tsx b/apps/web/components/agent-commands/QuickAccessBar.tsx index 7f24658f..85c53b86 100644 --- a/apps/web/components/agent-commands/QuickAccessBar.tsx +++ b/apps/web/components/agent-commands/QuickAccessBar.tsx @@ -133,7 +133,7 @@ export function QuickAccessBar() { <AnimatePresence> {isCopied && ( <motion.span - className="absolute -top-1 right-0 rounded-full bg-emerald-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-lg shadow-emerald-500/30" + className="absolute -top-1 right-0 rounded-full bg-emerald-500 px-1.5 py-0.5 text-xs font-bold text-white shadow-lg shadow-emerald-500/30" initial={{ scale: 0, y: 10 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0, y: -10 }} @@ -149,7 +149,7 @@ export function QuickAccessBar() { </div> {/* Hint text */} - <p className="mt-2 text-center text-[11px] text-white/40"> + <p className="mt-2 text-center text-xs text-white/60"> Tap to copy command </p> </motion.div> diff --git a/apps/web/components/alert-card.tsx b/apps/web/components/alert-card.tsx index 0cfebaf4..c3a8a54a 100644 --- a/apps/web/components/alert-card.tsx +++ b/apps/web/components/alert-card.tsx @@ -1,12 +1,16 @@ "use client"; +import * as React from "react"; +import { AnimatePresence, m } from "framer-motion"; import { cn } from "@/lib/utils"; +import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; import { AlertCircle, AlertTriangle, CheckCircle2, Info, Sparkles, + X, type LucideIcon, } from "lucide-react"; @@ -18,6 +22,14 @@ interface AlertCardProps { title?: string; children: React.ReactNode; className?: string; + /** Whether the alert can be dismissed */ + dismissible?: boolean; + /** Callback when dismissed */ + onDismiss?: () => void; + /** Auto-dismiss after this many milliseconds (0 = no auto-dismiss) */ + autoDismissMs?: number; + /** Whether to show countdown progress bar when auto-dismissing */ + showProgress?: boolean; } const variantStyles: Record< @@ -78,30 +90,96 @@ export function AlertCard({ title, children, className, + dismissible = false, + onDismiss, + autoDismissMs = 0, + showProgress = false, }: AlertCardProps) { const styles = variantStyles[variant]; const IconComponent = icon || styles.defaultIcon; + const prefersReducedMotion = useReducedMotion(); + const [dismissed, setDismissed] = React.useState(false); + + const handleDismiss = React.useCallback(() => { + if (dismissed) return; + setDismissed(true); + onDismiss?.(); + }, [dismissed, onDismiss]); + + React.useEffect(() => { + if (!autoDismissMs || autoDismissMs <= 0 || dismissed) return; + const timeout = window.setTimeout(() => { + handleDismiss(); + }, autoDismissMs); + return () => window.clearTimeout(timeout); + }, [autoDismissMs, dismissed, handleDismiss]); + + const showProgressBar = showProgress && autoDismissMs > 0; return ( - <div - className={cn( - "rounded-xl border p-4 backdrop-blur-sm transition-all", - styles.container, - className - )} - > - <div className="flex gap-3"> - <IconComponent - className={cn("mt-0.5 h-5 w-5 shrink-0", styles.icon)} - /> - <div className="min-w-0 flex-1 space-y-1"> - {title && ( - <p className={cn("font-medium", styles.title)}>{title}</p> + <AnimatePresence> + {!dismissed && ( + <m.div + className={cn( + "relative rounded-xl border p-4 backdrop-blur-sm transition-all", + styles.container, + className )} - <div className="text-sm text-muted-foreground">{children}</div> - </div> - </div> - </div> + initial={prefersReducedMotion ? {} : { opacity: 0, y: -8, scale: 0.98 }} + animate={{ opacity: 1, y: 0, scale: 1 }} + exit={prefersReducedMotion ? {} : { opacity: 0, y: -8, scale: 0.98 }} + transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2 }} + > + {dismissible && ( + <button + onClick={handleDismiss} + className={cn( + "absolute right-3 top-3 flex h-11 w-11 items-center justify-center", + "rounded-lg text-current/60 transition-colors", + "hover:bg-current/10 hover:text-current", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + )} + aria-label="Dismiss alert" + > + <X className="h-4 w-4" /> + </button> + )} + + {showProgressBar && ( + <div + className={cn( + "pointer-events-none absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-b-xl", + styles.icon + )} + > + <div className="absolute inset-0 bg-current/15" /> + <m.div + className="h-full bg-current/45" + initial={{ width: "100%" }} + animate={{ width: "0%" }} + transition={ + prefersReducedMotion + ? { duration: 0 } + : { duration: autoDismissMs / 1000, ease: "linear" } + } + /> + </div> + )} + + <div className="flex gap-3"> + <IconComponent + className={cn("mt-0.5 h-5 w-5 shrink-0", styles.icon)} + /> + <div className="min-w-0 flex-1 space-y-1"> + {title && ( + <p className={cn("font-medium", styles.title)}>{title}</p> + )} + <div className="text-sm text-muted-foreground">{children}</div> + </div> + </div> + </m.div> + )} + </AnimatePresence> ); } diff --git a/apps/web/components/analytics-provider.tsx b/apps/web/components/analytics-provider.tsx index dd537c71..ac5a3e8c 100644 --- a/apps/web/components/analytics-provider.tsx +++ b/apps/web/components/analytics-provider.tsx @@ -109,8 +109,8 @@ function AnalyticsTracker() { // Track enhanced session start trackSessionStart(); - // Track returning vs new user - const visitCount = parseInt(safeGetItem('acfs_visit_count') || '0', 10) + 1; + // Track returning vs new user (use || 0 to handle NaN from corrupted storage) + const visitCount = (parseInt(safeGetItem('acfs_visit_count') || '0', 10) || 0) + 1; safeSetItem('acfs_visit_count', visitCount.toString()); setUserProperties({ diff --git a/apps/web/components/command-builder-panel.tsx b/apps/web/components/command-builder-panel.tsx index ac9881c7..b4d85d00 100644 --- a/apps/web/components/command-builder-panel.tsx +++ b/apps/web/components/command-builder-panel.tsx @@ -28,7 +28,7 @@ function LocationBadge({ location }: { location: "local" | "vps" }) { return ( <span className={cn( - "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider", + "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium uppercase tracking-wider", location === "vps" ? "bg-[oklch(0.72_0.19_145/0.15)] text-[oklch(0.72_0.19_145)]" : "bg-primary/15 text-primary", diff --git a/apps/web/components/command-card.tsx b/apps/web/components/command-card.tsx index 258c69c4..c587f4c6 100644 --- a/apps/web/components/command-card.tsx +++ b/apps/web/components/command-card.tsx @@ -10,6 +10,7 @@ import { cn, safeGetItem, safeSetItem } from "@/lib/utils"; import { useDetectedOS, useUserOS } from "@/lib/userPreferences"; import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; import { springs } from "@/components/motion"; +import { trackInteraction } from "@/lib/analytics"; export interface CommandCardProps { /** The default command to display */ @@ -136,6 +137,13 @@ export function CommandCard({ try { await navigator.clipboard.writeText(displayCommand); setCopied(true); + // Track copy event for analytics + trackInteraction("copy", persistKey || "command-card", "command", { + command_length: displayCommand.length, + command_preview: displayCommand.slice(0, 50), + run_location: runLocation, + os, + }); setTimeout(() => { setCopied(false); setCopyAnimation(false); @@ -151,12 +159,19 @@ export function CommandCard({ document.execCommand("copy"); document.body.removeChild(textarea); setCopied(true); + // Track copy event for analytics (fallback path) + trackInteraction("copy", persistKey || "command-card", "command", { + command_length: displayCommand.length, + command_preview: displayCommand.slice(0, 50), + run_location: runLocation, + os, + }); setTimeout(() => { setCopied(false); setCopyAnimation(false); }, 2000); } - }, [displayCommand]); + }, [displayCommand, persistKey, runLocation, os]); const handleCheckboxChange = useCallback( (checked: CheckedState) => { @@ -321,6 +336,11 @@ export function CodeBlock({ try { await navigator.clipboard.writeText(code); setCopied(true); + // Track copy event for analytics + trackInteraction("copy", `code-block-${language}`, "code-block", { + code_length: code.length, + language, + }); setTimeout(() => setCopied(false), 2000); } catch { // Fallback @@ -333,9 +353,14 @@ export function CodeBlock({ document.execCommand("copy"); document.body.removeChild(textarea); setCopied(true); + // Track copy event for analytics (fallback path) + trackInteraction("copy", `code-block-${language}`, "code-block", { + code_length: code.length, + language, + }); setTimeout(() => setCopied(false), 2000); } - }, [code]); + }, [code, language]); return ( <div diff --git a/apps/web/components/connection-check.tsx b/apps/web/components/connection-check.tsx index 0a780005..71701603 100644 --- a/apps/web/components/connection-check.tsx +++ b/apps/web/components/connection-check.tsx @@ -164,7 +164,7 @@ export function TwoComputersExplainer({ className }: { className?: string }) { export function WhereAmICheck({ className }: { className?: string }) { return ( <details className={cn("group rounded-xl border border-border/50 bg-card/30", className)}> - <summary className="flex cursor-pointer items-center gap-2 p-4 font-medium hover:bg-muted/50"> + <summary className="flex cursor-pointer items-center gap-2 p-4 font-medium hover:bg-muted/50 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"> <Terminal className="h-4 w-4 text-primary" /> <span>How do I know if I'm connected to my VPS?</span> </summary> diff --git a/apps/web/components/flywheel-visualization.tsx b/apps/web/components/flywheel-visualization.tsx index 0be3d271..82473687 100644 --- a/apps/web/components/flywheel-visualization.tsx +++ b/apps/web/components/flywheel-visualization.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useCallback, useEffect } from "react"; +import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { LayoutGrid, ShieldCheck, @@ -17,10 +17,27 @@ import { Copy, Check, ChevronRight, + ChevronLeft, Sparkles, + Shield, + GitMerge, + Cloud, + Terminal, + Bot, + BookOpen, + Activity, + Archive, + FileText, + ListTodo, + ShieldAlert, } from "lucide-react"; import { flywheelTools, flywheelDescription, getAllConnections, type FlywheelTool } from "@/lib/flywheel"; import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +// ============================================================================= +// ICON MAP - Extended for all tools +// ============================================================================= const iconMap: Record<string, React.ComponentType<{ className?: string }>> = { LayoutGrid, @@ -31,34 +48,159 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = { Brain, Search, KeyRound, + Shield, + GitMerge, + Cloud, + Terminal, + Bot, + BookOpen, + Activity, + Archive, + FileText, + ListTodo, + ShieldAlert, + Sparkles, + Zap, +}; + +// ============================================================================= +// DEDUPLICATED TOOLS - Remove duplicates and get unique tools +// ============================================================================= + +function getUniqueTools(): FlywheelTool[] { + const seen = new Set<string>(); + const unique: FlywheelTool[] = []; + for (const tool of flywheelTools) { + if (!seen.has(tool.id)) { + seen.add(tool.id); + unique.push(tool); + } + } + return unique; +} + +// ============================================================================= +// TIER CLASSIFICATION - Separate primary (hub) tools from secondary +// ============================================================================= + +const PRIMARY_TOOL_IDS = new Set([ + "ntm", // Named Tmux Manager - orchestration hub + "mail", // Agent Mail - coordination hub + "bv", // Beads Viewer - task management hub + "cass", // Session Search - memory hub + "cm", // Memory System + "ubs", // Bug Scanner + "slb", // Safety Layer + "dcg", // Destructive Guard +]); + +function classifyTools(tools: FlywheelTool[]): { primary: FlywheelTool[]; secondary: FlywheelTool[] } { + const primary: FlywheelTool[] = []; + const secondary: FlywheelTool[] = []; + + for (const tool of tools) { + if (PRIMARY_TOOL_IDS.has(tool.id)) { + primary.push(tool); + } else { + secondary.push(tool); + } + } + + return { primary, secondary }; +} + +// ============================================================================= +// LAYOUT CONSTANTS - Refined for visual harmony +// ============================================================================= + +const DESKTOP_CONFIG = { + containerSize: 640, + innerRadius: 150, + outerRadius: 260, + innerNodeSize: 76, + outerNodeSize: 58, + centerSize: 88, }; -// Layout constants -const CONTAINER_SIZE = 480; -const RADIUS = 170; -const CENTER = CONTAINER_SIZE / 2; -const NODE_SIZE = 80; +// ============================================================================= +// POSITION CALCULATIONS +// ============================================================================= -// Calculate node positions in a circle -function getNodePosition(index: number, total: number) { - const angle = (index / total) * 2 * Math.PI - Math.PI / 2; +function getCirclePosition( + index: number, + total: number, + radius: number, + center: number, + startAngle: number = -Math.PI / 2 +) { + const angle = startAngle + (index / total) * 2 * Math.PI; return { - x: CENTER + Math.cos(angle) * RADIUS, - y: CENTER + Math.sin(angle) * RADIUS, + x: center + Math.cos(angle) * radius, + y: center + Math.sin(angle) * radius, }; } -// Generate a curved path between two points -function getCurvedPath(from: { x: number; y: number }, to: { x: number; y: number }) { +function getCurvedPath(from: { x: number; y: number }, to: { x: number; y: number }, center: number) { const midX = (from.x + to.x) / 2; const midY = (from.y + to.y) / 2; const pullFactor = 0.35; - const controlX = midX + (CENTER - midX) * pullFactor; - const controlY = midY + (CENTER - midY) * pullFactor; + const controlX = midX + (center - midX) * pullFactor; + const controlY = midY + (center - midY) * pullFactor; return `M ${from.x} ${from.y} Q ${controlX} ${controlY} ${to.x} ${to.y}`; } -// Connection line component +// ============================================================================= +// COLOR UTILITIES +// ============================================================================= + +const colorMap: Record<string, string> = { + "from-sky-400": "#38bdf8", + "from-sky-500": "#0ea5e9", + "from-violet-400": "#a78bfa", + "from-violet-500": "#8b5cf6", + "from-rose-400": "#fb7185", + "from-rose-500": "#f43f5e", + "from-emerald-400": "#34d399", + "from-emerald-500": "#10b981", + "from-cyan-400": "#22d3ee", + "from-cyan-500": "#06b6d4", + "from-pink-400": "#f472b6", + "from-pink-500": "#ec4899", + "from-amber-400": "#fbbf24", + "from-amber-500": "#f59e0b", + "from-yellow-400": "#facc15", + "from-yellow-500": "#eab308", + "from-teal-500": "#14b8a6", + "from-indigo-400": "#818cf8", + "from-indigo-500": "#6366f1", + "from-blue-500": "#3b82f6", + "from-red-400": "#f87171", + "from-red-500": "#ef4444", + "from-purple-500": "#a855f7", + "from-orange-500": "#f97316", +}; + +function getColorFromGradient(colorClass: string): string { + for (const [key, value] of Object.entries(colorMap)) { + if (colorClass.includes(key)) return value; + } + return "#a78bfa"; +} + +// ============================================================================= +// CONNECTION LINE COMPONENT - Desktop only (Enhanced with glow trails) +// ============================================================================= + +interface ConnectionLineProps { + fromPos: { x: number; y: number }; + toPos: { x: number; y: number }; + isHighlighted: boolean; + fromColor: string; + toColor: string; + connectionId: string; + center: number; +} + function ConnectionLine({ fromPos, toPos, @@ -66,40 +208,19 @@ function ConnectionLine({ fromColor, toColor, connectionId, -}: { - fromPos: { x: number; y: number }; - toPos: { x: number; y: number }; - isHighlighted: boolean; - fromColor: string; - toColor: string; - connectionId: string; -}) { - const path = getCurvedPath(fromPos, toPos); + center, +}: ConnectionLineProps) { + const path = getCurvedPath(fromPos, toPos, center); const gradientId = `gradient-${connectionId}`; - - // Extract color for gradient - const getColor = (colorClass: string) => { - const colorMap: Record<string, string> = { - "from-sky-400": "#38bdf8", - "from-violet-400": "#a78bfa", - "from-rose-400": "#fb7185", - "from-emerald-400": "#34d399", - "from-cyan-400": "#22d3ee", - "from-pink-400": "#f472b6", - "from-amber-400": "#fbbf24", - "from-yellow-400": "#facc15", - }; - for (const [key, value] of Object.entries(colorMap)) { - if (colorClass.includes(key)) return value; - } - return "#a78bfa"; - }; - - const color1 = getColor(fromColor); - const color2 = getColor(toColor); + const glowId = `glow-${connectionId}`; + const color1 = getColorFromGradient(fromColor); + const color2 = getColorFromGradient(toColor); return ( - <g> + <g + className="transition-all duration-500 ease-out" + style={{ opacity: isHighlighted ? 1 : 0.25 }} + > <defs> <linearGradient id={gradientId} @@ -109,84 +230,115 @@ function ConnectionLine({ x2={toPos.x} y2={toPos.y} > - <stop offset="0%" stopColor={color1} stopOpacity={isHighlighted ? 0.9 : 0.25} /> - <stop offset="100%" stopColor={color2} stopOpacity={isHighlighted ? 0.9 : 0.25} /> + <stop offset="0%" stopColor={color1} stopOpacity={isHighlighted ? 1 : 0.4} /> + <stop offset="50%" stopColor={isHighlighted ? "#fff" : color1} stopOpacity={isHighlighted ? 0.6 : 0.2} /> + <stop offset="100%" stopColor={color2} stopOpacity={isHighlighted ? 1 : 0.4} /> </linearGradient> + <filter id={glowId} x="-50%" y="-50%" width="200%" height="200%"> + <feGaussianBlur stdDeviation={isHighlighted ? "4" : "2"} result="blur" /> + <feMerge> + <feMergeNode in="blur" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> </defs> - {/* Glow effect when highlighted */} + {/* Outer glow layer */} {isHighlighted && ( <path d={path} fill="none" stroke={`url(#${gradientId})`} - strokeWidth={8} + strokeWidth={10} strokeLinecap="round" - style={{ filter: "blur(6px)", opacity: 0.5 }} + style={{ filter: "blur(8px)", opacity: 0.4 }} /> )} - {/* Main connection line */} + {/* Mid glow layer */} + <path + d={path} + fill="none" + stroke={`url(#${gradientId})`} + strokeWidth={isHighlighted ? 6 : 3} + strokeLinecap="round" + style={{ filter: `blur(${isHighlighted ? 4 : 2}px)`, opacity: isHighlighted ? 0.6 : 0.3 }} + /> + + {/* Main line */} <path d={path} fill="none" stroke={`url(#${gradientId})`} strokeWidth={isHighlighted ? 2.5 : 1.5} strokeLinecap="round" - className="transition-all duration-300" /> - {/* Animated flowing dash */} + {/* Animated energy flow */} <path d={path} fill="none" stroke={`url(#${gradientId})`} strokeWidth={isHighlighted ? 2 : 1} strokeLinecap="round" - strokeDasharray="12 38" - className="animate-flow" + strokeDasharray={isHighlighted ? "6 18" : "4 20"} style={{ - opacity: isHighlighted ? 0.8 : 0.4, - animation: `flow ${isHighlighted ? 2 : 4}s linear infinite`, + opacity: isHighlighted ? 0.9 : 0.5, + animation: `flow ${isHighlighted ? 1.5 : 3}s linear infinite`, }} /> </g> ); } -// Tool node component -function ToolNode({ - tool, - position, - index, - isSelected, - isConnected, - isDimmed, - onSelect, - onHover, -}: { +// ============================================================================= +// DESKTOP TOOL NODE - Circular layout nodes (Enhanced with depth & glow) +// ============================================================================= + +interface DesktopToolNodeProps { tool: FlywheelTool; position: { x: number; y: number }; - index: number; + size: number; isSelected: boolean; isConnected: boolean; isDimmed: boolean; onSelect: () => void; onHover: (hovering: boolean) => void; -}) { + isPrimary: boolean; + index: number; +} + +function DesktopToolNode({ + tool, + position, + size, + isSelected, + isConnected, + isDimmed, + onSelect, + onHover, + isPrimary, + index, +}: DesktopToolNodeProps) { const Icon = iconMap[tool.icon] || Zap; + const iconSize = isPrimary ? "h-6 w-6" : "h-5 w-5"; + const fontSize = isPrimary ? "text-[11px]" : "text-[9px]"; + const color = getColorFromGradient(tool.color); return ( <div - className="absolute transition-all duration-300" + className="absolute transition-all duration-500 ease-out" style={{ - left: position.x - NODE_SIZE / 2, - top: position.y - NODE_SIZE / 2, - width: NODE_SIZE, - height: NODE_SIZE, - opacity: isDimmed ? 0.35 : 1, - transform: `scale(${isSelected ? 1.1 : 1})`, - animationDelay: `${index * 0.1}s`, + left: position.x - size / 2, + top: position.y - size / 2, + width: size, + height: size, + opacity: isDimmed ? 0.3 : 1, + transform: `scale(${isSelected ? 1.15 : isConnected ? 1.05 : 1})`, + zIndex: isSelected ? 30 : isConnected ? 20 : 10, + filter: isDimmed ? "grayscale(0.5)" : "none", + // Subtle floating animation for primary tools + animation: isPrimary && !isDimmed ? `float${index % 3} 4s ease-in-out infinite` : "none", }} > <button @@ -195,41 +347,87 @@ function ToolNode({ onMouseLeave={() => onHover(false)} aria-label={`${tool.name}: ${tool.tagline}`} aria-pressed={isSelected} - className={` - relative flex h-full w-full flex-col items-center justify-center gap-1.5 rounded-2xl border p-2 - transition-all duration-200 outline-none - focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background - ${ - isSelected - ? "border-white/50 bg-white/20 shadow-xl" - : isConnected - ? "border-white/30 bg-white/10" - : "border-white/10 bg-card/80 hover:border-white/25 hover:bg-white/10" - } - `} + className={cn( + "group relative flex h-full w-full flex-col items-center justify-center gap-1.5 rounded-2xl border p-2", + "transition-all duration-300 ease-out outline-none", + "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background", + isSelected + ? "border-white/60 bg-white/20 shadow-2xl" + : isConnected + ? "border-white/40 bg-white/15 shadow-xl" + : "border-white/15 bg-card/90 hover:border-white/30 hover:bg-white/10 shadow-lg" + )} + style={{ + // Color-coordinated shadow + boxShadow: isSelected + ? `0 0 40px ${color}50, 0 20px 40px rgba(0,0,0,0.3)` + : isConnected + ? `0 0 25px ${color}30, 0 10px 30px rgba(0,0,0,0.2)` + : `0 8px 24px rgba(0,0,0,0.2)`, + }} > - {/* Glow background */} + {/* Animated gradient ring for selected state */} + {isSelected && ( + <div + className="absolute -inset-[2px] rounded-2xl opacity-80" + style={{ + background: `conic-gradient(from 0deg, ${color}, transparent, ${color})`, + animation: "spin 3s linear infinite", + }} + /> + )} + + {/* Inner container */} + <div className="absolute inset-[1px] rounded-[14px] bg-card/95" /> + + {/* Gradient glow background */} <div - className={`absolute inset-0 rounded-2xl blur-xl bg-gradient-to-br ${tool.color} transition-opacity duration-300`} - style={{ opacity: isSelected ? 0.6 : isConnected ? 0.35 : 0.15 }} + className={cn( + "absolute inset-0 rounded-2xl blur-xl transition-opacity duration-500 bg-gradient-to-br", + tool.color + )} + style={{ opacity: isSelected ? 0.7 : isConnected ? 0.4 : 0.15 }} /> - {/* Icon */} + {/* Shine effect on hover */} <div - className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg ${tool.color}`} - > - <Icon className="h-5 w-5 text-white" /> + className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" + style={{ + background: "linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(255,255,255,0.05) 100%)", + }} + /> + + {/* Icon container with glow */} + <div className="relative z-10"> + <div + className={cn( + "relative flex items-center justify-center rounded-xl bg-gradient-to-br shadow-lg", + tool.color, + isPrimary ? "h-11 w-11" : "h-9 w-9" + )} + style={{ + boxShadow: `0 4px 16px ${color}40`, + }} + > + <Icon className={cn("text-white drop-shadow-sm", iconSize)} /> + </div> </div> {/* Label */} - <span className="relative z-10 text-[10px] font-bold uppercase tracking-wider text-white"> + <span className={cn( + "relative z-10 font-bold uppercase tracking-wider text-white drop-shadow-sm", + fontSize + )}> {tool.shortName} </span> - {/* Star count badge */} - {tool.stars && tool.stars >= 100 && ( - <div className="absolute -right-1 -top-1 flex items-center gap-0.5 rounded-full bg-amber-500/20 px-1.5 py-0.5 text-[9px] font-bold text-amber-400"> - <Star className="h-2.5 w-2.5 fill-current" /> + {/* Star badge with glow */} + {tool.stars && tool.stars >= 50 && ( + <div + className="absolute -right-1 -top-1 flex items-center gap-0.5 rounded-full bg-amber-500/30 px-1.5 py-0.5 text-[8px] font-bold text-amber-300 backdrop-blur-sm border border-amber-400/30" + style={{ boxShadow: "0 2px 8px rgba(251,191,36,0.3)" }} + > + <Star className="h-2 w-2 fill-current" /> {tool.stars >= 1000 ? `${(tool.stars / 1000).toFixed(0)}K` : tool.stars} </div> )} @@ -238,49 +436,86 @@ function ToolNode({ ); } -// Center hub component -function CenterHub() { +// ============================================================================= +// CENTER HUB - Desktop only (Enhanced with animated rings) +// ============================================================================= + +function CenterHub({ size }: { size: number }) { return ( <div - className="absolute" + className="absolute pointer-events-none" style={{ - left: CENTER - 36, - top: CENTER - 36, - width: 72, - height: 72, + left: DESKTOP_CONFIG.containerSize / 2 - size / 2, + top: DESKTOP_CONFIG.containerSize / 2 - size / 2, + width: size, + height: size, }} > - <div className="flex h-full w-full items-center justify-center rounded-full border border-primary/40 bg-primary/20 animate-glow-pulse"> - <Sparkles className="h-8 w-8 text-primary" /> + {/* Outer pulsing ring */} + <div + className="absolute inset-0 rounded-full border-2 border-primary/30" + style={{ animation: "pulse-ring 2s ease-in-out infinite" }} + /> + + {/* Middle animated gradient ring */} + <div + className="absolute inset-2 rounded-full" + style={{ + background: "conic-gradient(from 0deg, transparent, hsl(var(--primary) / 0.3), transparent, hsl(var(--primary) / 0.2), transparent)", + animation: "spin 8s linear infinite", + }} + /> + + {/* Inner glow */} + <div + className="absolute inset-3 rounded-full bg-primary/20 blur-md" + style={{ animation: "glow-pulse 3s ease-in-out infinite" }} + /> + + {/* Core content */} + <div className="absolute inset-3 flex flex-col items-center justify-center rounded-full border border-primary/40 bg-card/90 backdrop-blur-md shadow-xl"> + <div + className="relative" + style={{ animation: "float0 3s ease-in-out infinite" }} + > + <Sparkles className="h-8 w-8 text-primary drop-shadow-lg" /> + <div className="absolute inset-0 blur-sm"> + <Sparkles className="h-8 w-8 text-primary opacity-50" /> + </div> + </div> + <span className="mt-1.5 text-[10px] font-bold uppercase tracking-[0.15em] text-primary/90"> + Flywheel + </span> </div> </div> ); } -// Tool detail panel -function ToolDetailPanel({ - tool, - onClose, -}: { +// ============================================================================= +// DESKTOP DETAIL PANEL (Enhanced with better visual hierarchy) +// ============================================================================= + +interface ToolDetailPanelProps { tool: FlywheelTool; onClose: () => void; -}) { +} + +function ToolDetailPanel({ tool, onClose }: ToolDetailPanelProps) { const Icon = iconMap[tool.icon] || Zap; const [copied, setCopied] = useState(false); + const uniqueTools = useMemo(() => getUniqueTools(), []); + const color = getColorFromGradient(tool.color); const copyInstallCommand = async () => { if (!tool.installCommand) return; - try { await navigator.clipboard.writeText(tool.installCommand); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { - // Fallback for older browsers or when clipboard permission is denied const textArea = document.createElement("textarea"); textArea.value = tool.installCommand; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; + textArea.style.cssText = "position:fixed;opacity:0"; document.body.appendChild(textArea); textArea.select(); try { @@ -288,25 +523,54 @@ function ToolDetailPanel({ setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { - // Silent fail - user can manually copy + // execCommand is deprecated; failure is acceptable as this is a fallback } document.body.removeChild(textArea); } }; return ( - <div className="relative overflow-hidden rounded-2xl border border-border/50 bg-card/90 backdrop-blur-xl animate-scale-in"> - {/* Background gradient */} - <div className={`absolute inset-0 opacity-10 bg-gradient-to-br ${tool.color}`} /> + <div + className="relative overflow-hidden rounded-2xl border border-border/50 bg-card/95 backdrop-blur-xl shadow-2xl" + style={{ + animation: "panel-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1)", + boxShadow: `0 0 60px ${color}15, 0 25px 50px rgba(0,0,0,0.25)`, + }} + > + {/* Animated gradient background */} + <div + className={cn("absolute inset-0 opacity-[0.08] bg-gradient-to-br", tool.color)} + style={{ animation: "gradient-shift 8s ease-in-out infinite" }} + /> + + {/* Subtle pattern overlay */} + <div + className="absolute inset-0 opacity-[0.03]" + style={{ + backgroundImage: "radial-gradient(circle at 2px 2px, white 1px, transparent 0)", + backgroundSize: "24px 24px", + }} + /> <div className="relative p-6"> {/* Header */} - <div className="flex items-start justify-between gap-3"> + <div className="flex items-start justify-between gap-4"> <div className="flex items-center gap-4"> <div - className={`flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg ${tool.color}`} + className={cn( + "relative flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br shadow-xl", + tool.color + )} + style={{ boxShadow: `0 8px 32px ${color}40` }} > - <Icon className="h-7 w-7 text-white" /> + <Icon className="h-8 w-8 text-white drop-shadow-md" /> + {/* Shine effect */} + <div + className="absolute inset-0 rounded-2xl" + style={{ + background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, transparent 50%)", + }} + /> </div> <div> <h3 className="text-xl font-bold text-foreground">{tool.name}</h3> @@ -315,7 +579,7 @@ function ToolDetailPanel({ </div> <button onClick={onClose} - className="hidden lg:flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + className="flex h-9 w-9 items-center justify-center rounded-xl text-muted-foreground transition-all hover:bg-muted hover:text-foreground hover:scale-105 active:scale-95" aria-label="Close" > <X className="h-4 w-4" /> @@ -323,23 +587,35 @@ function ToolDetailPanel({ </div> {/* Description */} - <p className="mt-4 text-sm leading-relaxed text-muted-foreground">{tool.description}</p> + <p className="mt-5 text-sm leading-relaxed text-muted-foreground">{tool.description}</p> {/* Stars badge */} {tool.stars && ( - <div className="mt-4 inline-flex items-center gap-1.5 rounded-full bg-amber-500/10 px-3 py-1 text-sm font-semibold text-amber-400"> + <div + className="mt-4 inline-flex items-center gap-2 rounded-full bg-amber-500/15 px-4 py-1.5 text-sm font-semibold text-amber-400 border border-amber-400/20" + style={{ boxShadow: "0 2px 12px rgba(251,191,36,0.15)" }} + > <Star className="h-4 w-4 fill-current" /> <span>{tool.stars.toLocaleString()} GitHub stars</span> </div> )} {/* Features */} - <div className="mt-5"> - <h4 className="mb-2 text-xs font-bold uppercase tracking-wider text-muted-foreground">Key Features</h4> - <ul className="space-y-1.5"> + <div className="mt-6"> + <h4 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground">Key Features</h4> + <ul className="space-y-2"> {tool.features.slice(0, 4).map((feature, i) => ( - <li key={i} className="flex items-start gap-2 text-sm text-foreground"> - <Check className="mt-0.5 h-4 w-4 shrink-0 text-primary" /> + <li + key={i} + className="flex items-start gap-2.5 text-sm text-foreground" + style={{ animation: `fade-in-up 0.3s ease-out ${i * 0.05}s both` }} + > + <div + className={cn("mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-gradient-to-br", tool.color)} + style={{ boxShadow: `0 2px 8px ${color}30` }} + > + <Check className="h-3 w-3 text-white" /> + </div> <span>{feature}</span> </li> ))} @@ -348,33 +624,46 @@ function ToolDetailPanel({ {/* Install command */} {tool.installCommand && ( - <div className="mt-5"> + <div className="mt-6"> <h4 className="mb-2 text-xs font-bold uppercase tracking-wider text-muted-foreground">Quick Install</h4> - <div className="flex items-center gap-2 rounded-lg bg-muted/50 p-3 font-mono text-xs"> + <div className="flex items-center gap-2 rounded-xl bg-black/30 p-3.5 font-mono text-xs border border-border/30 backdrop-blur-sm"> <code className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-foreground"> - {tool.installCommand.length > 50 ? tool.installCommand.slice(0, 50) + "..." : tool.installCommand} + {tool.installCommand.length > 45 ? tool.installCommand.slice(0, 45) + "..." : tool.installCommand} </code> <button onClick={copyInstallCommand} - className="shrink-0 rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + className={cn( + "shrink-0 rounded-lg p-2 transition-all", + copied + ? "bg-primary/20 text-primary" + : "text-muted-foreground hover:bg-white/10 hover:text-foreground" + )} aria-label="Copy install command" > - {copied ? <Check className="h-4 w-4 text-primary" /> : <Copy className="h-4 w-4" />} + {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} </button> </div> </div> )} {/* Action buttons */} - <div className="mt-5 flex flex-wrap gap-2"> - <Button asChild size="sm" className={`bg-gradient-to-r ${tool.color} text-white hover:opacity-90`}> + <div className="mt-6 flex flex-wrap gap-3"> + <Button + asChild + size="sm" + className={cn( + "h-11 bg-gradient-to-r text-white shadow-lg hover:opacity-90 hover:shadow-xl transition-all hover:-translate-y-0.5", + tool.color + )} + style={{ boxShadow: `0 4px 20px ${color}40` }} + > <a href={tool.href} target="_blank" rel="noopener noreferrer"> - <ExternalLink className="mr-1.5 h-4 w-4" /> + <ExternalLink className="mr-2 h-4 w-4" /> View on GitHub </a> </Button> {tool.demoUrl && ( - <Button asChild size="sm" variant="outline"> + <Button asChild size="sm" variant="outline" className="h-11 hover:-translate-y-0.5 transition-all"> <a href={tool.demoUrl} target="_blank" rel="noopener noreferrer"> Try Demo <ChevronRight className="ml-1 h-4 w-4" /> @@ -384,163 +673,468 @@ function ToolDetailPanel({ </div> {/* Connections */} - <div className="mt-6 border-t border-border/50 pt-5"> - <h4 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground">Integrates With</h4> - <div className="space-y-2"> - {tool.connectsTo.map((targetId) => { - const targetTool = flywheelTools.find((t) => t.id === targetId); - if (!targetTool) return null; - const TargetIcon = iconMap[targetTool.icon] || Zap; - - return ( - <div - key={targetId} - className="flex items-center gap-3 rounded-xl bg-muted/30 p-3 border border-border/30" - > + {tool.connectsTo.length > 0 && ( + <div className="mt-7 border-t border-border/50 pt-6"> + <h4 className="mb-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Integrates With</h4> + <div className="space-y-2.5"> + {tool.connectsTo.slice(0, 4).map((targetId, i) => { + const targetTool = uniqueTools.find((t) => t.id === targetId); + if (!targetTool) return null; + const TargetIcon = iconMap[targetTool.icon] || Zap; + const targetColor = getColorFromGradient(targetTool.color); + + return ( <div - className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br ${targetTool.color}`} + key={targetId} + className="flex items-center gap-3 rounded-xl bg-white/5 p-3 border border-border/30 hover:bg-white/10 transition-colors cursor-default" + style={{ animation: `fade-in-up 0.3s ease-out ${i * 0.05}s both` }} > - <TargetIcon className="h-4 w-4 text-white" /> - </div> - <div className="min-w-0 flex-1"> - <p className="text-sm font-semibold text-foreground">{targetTool.shortName}</p> - <p className="text-xs text-muted-foreground line-clamp-1"> - {tool.connectionDescriptions[targetId] || "Integration"} - </p> + <div + className={cn( + "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-md", + targetTool.color + )} + style={{ boxShadow: `0 4px 12px ${targetColor}30` }} + > + <TargetIcon className="h-5 w-5 text-white" /> + </div> + <div className="min-w-0 flex-1"> + <p className="text-sm font-semibold text-foreground">{targetTool.shortName}</p> + <p className="text-xs text-muted-foreground line-clamp-1"> + {tool.connectionDescriptions[targetId] || "Integration"} + </p> + </div> </div> - </div> - ); - })} + ); + })} + </div> </div> - </div> + )} </div> </div> ); } -// Placeholder panel +// ============================================================================= +// PLACEHOLDER PANEL - Desktop only (Enhanced) +// ============================================================================= + function PlaceholderPanel() { return ( - <div className="rounded-2xl border border-border/50 bg-card/60 p-6 backdrop-blur-sm animate-scale-in"> - <div className="flex flex-col items-center justify-center py-8 text-center"> - <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 ring-1 ring-primary/30"> - <Sparkles className="h-7 w-7 text-primary" /> + <div + className="rounded-2xl border border-border/50 bg-card/80 p-6 backdrop-blur-sm shadow-xl" + style={{ + boxShadow: "0 0 40px rgba(var(--primary-rgb), 0.05), 0 20px 40px rgba(0,0,0,0.15)", + }} + > + <div className="flex flex-col items-center justify-center py-10 text-center"> + <div + className="relative mb-5" + style={{ animation: "float0 3s ease-in-out infinite" }} + > + <div className="flex h-20 w-20 items-center justify-center rounded-full bg-primary/15 ring-2 ring-primary/30 shadow-lg"> + <Sparkles className="h-9 w-9 text-primary" /> + </div> + <div className="absolute inset-0 rounded-full bg-primary/10 blur-xl" /> </div> <h3 className="mb-2 text-lg font-semibold text-foreground">Explore the Flywheel</h3> - <p className="text-sm text-muted-foreground">Click a tool to see its connections and features</p> + <p className="text-sm text-muted-foreground">Click any tool to see details and integrations</p> </div> - <div className="rounded-xl bg-muted/30 p-4 border border-border/30"> + <div className="rounded-xl bg-white/5 p-5 border border-border/30"> <p className="text-sm leading-relaxed text-muted-foreground">{flywheelDescription.description}</p> </div> </div> ); } -// Mobile bottom sheet -function MobileBottomSheet({ - tool, - onClose, -}: { +// ============================================================================= +// MOBILE TOOL CARD - Horizontal carousel cards (Enhanced with depth) +// ============================================================================= + +interface MobileToolCardProps { + tool: FlywheelTool; + isActive: boolean; + onSelect: () => void; +} + +function MobileToolCard({ tool, isActive, onSelect }: MobileToolCardProps) { + const Icon = iconMap[tool.icon] || Zap; + const color = getColorFromGradient(tool.color); + + return ( + <button + onClick={onSelect} + className={cn( + "relative flex h-full min-w-[280px] max-w-[280px] flex-col rounded-2xl border p-5 text-left transition-all duration-300 snap-center", + "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2", + "active:scale-[0.98]", // Touch feedback + isActive + ? "border-white/50 bg-white/15 shadow-2xl scale-[1.02]" + : "border-white/15 bg-card/90 hover:border-white/25 shadow-lg" + )} + style={{ + boxShadow: isActive + ? `0 0 40px ${color}25, 0 20px 40px rgba(0,0,0,0.3)` + : "0 8px 24px rgba(0,0,0,0.2)", + }} + > + {/* Active state gradient ring */} + {isActive && ( + <div + className="absolute -inset-[1px] rounded-2xl opacity-60" + style={{ + background: `linear-gradient(135deg, ${color}60, transparent 50%, ${color}40)`, + }} + /> + )} + + {/* Inner background */} + <div className="absolute inset-[1px] rounded-[15px] bg-card/95" style={{ display: isActive ? "block" : "none" }} /> + + {/* Gradient glow */} + <div + className={cn("absolute inset-0 rounded-2xl blur-xl transition-opacity duration-300 bg-gradient-to-br", tool.color)} + style={{ opacity: isActive ? 0.35 : 0.1 }} + /> + + {/* Header */} + <div className="relative z-10 flex items-start gap-3.5"> + <div + className={cn( + "flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg", + tool.color + )} + style={{ boxShadow: `0 4px 16px ${color}40` }} + > + <Icon className="h-7 w-7 text-white drop-shadow-sm" /> + {/* Shine */} + <div + className="absolute inset-0 rounded-xl" + style={{ + background: "linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 50%)", + }} + /> + </div> + <div className="min-w-0 flex-1 pt-0.5"> + <h3 className="font-bold text-foreground text-base">{tool.shortName}</h3> + <p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">{tool.tagline}</p> + </div> + {tool.stars && tool.stars >= 50 && ( + <div + className="flex items-center gap-0.5 rounded-full bg-amber-500/25 px-2 py-1 text-[10px] font-bold text-amber-300 border border-amber-400/30" + style={{ boxShadow: "0 2px 8px rgba(251,191,36,0.2)" }} + > + <Star className="h-2.5 w-2.5 fill-current" /> + {tool.stars >= 1000 ? `${(tool.stars / 1000).toFixed(0)}K` : tool.stars} + </div> + )} + </div> + + {/* Description */} + <p className="relative z-10 mt-4 text-sm leading-relaxed text-muted-foreground line-clamp-3"> + {tool.description} + </p> + + {/* Footer */} + <div className="relative z-10 mt-auto pt-4 flex items-center justify-between border-t border-border/30"> + <span className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{tool.language}</span> + <div className={cn( + "flex items-center gap-1 text-xs font-medium transition-all", + isActive ? "text-primary translate-x-1" : "text-muted-foreground" + )}> + <span>Details</span> + <ChevronRight className="h-4 w-4" /> + </div> + </div> + </button> + ); +} + +// ============================================================================= +// MOBILE BOTTOM SHEET - Full detail view (Enhanced with gestures) +// ============================================================================= + +interface MobileBottomSheetProps { tool: FlywheelTool | null; onClose: () => void; -}) { +} + +function MobileBottomSheet({ tool, onClose }: MobileBottomSheetProps) { + const [copied, setCopied] = useState(false); + const uniqueTools = useMemo(() => getUniqueTools(), []); + const sheetRef = useRef<HTMLDivElement>(null); + const [isDragging, setIsDragging] = useState(false); + const [dragY, setDragY] = useState(0); + const startY = useRef(0); + + // Reset drag state when tool changes + const toolId = tool?.id; + const prevToolId = useRef(toolId); + if (toolId !== prevToolId.current) { + prevToolId.current = toolId; + // dragY will be reset naturally since we only set it during touch interactions + } + useEffect(() => { if (tool) { document.body.style.overflow = "hidden"; + + // Handle escape key + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleEscape); + return () => { document.body.style.overflow = ""; + document.removeEventListener("keydown", handleEscape); }; } return; - }, [tool]); + }, [tool, onClose]); + + // Touch gesture handling for swipe-to-dismiss + const handleTouchStart = (e: React.TouchEvent) => { + startY.current = e.touches[0].clientY; + setIsDragging(true); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (!isDragging) return; + const deltaY = e.touches[0].clientY - startY.current; + if (deltaY > 0) { + setDragY(deltaY); + } + }; + + const handleTouchEnd = () => { + setIsDragging(false); + if (dragY > 100) { + onClose(); + } + setDragY(0); + }; + + const copyInstallCommand = async () => { + if (!tool?.installCommand) return; + try { + await navigator.clipboard.writeText(tool.installCommand); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + const textArea = document.createElement("textarea"); + textArea.value = tool.installCommand; + textArea.style.cssText = "position:fixed;opacity:0"; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand("copy"); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // execCommand is deprecated; failure is acceptable as this is a fallback + } + document.body.removeChild(textArea); + } + }; if (!tool) return null; const Icon = iconMap[tool.icon] || Zap; + const color = getColorFromGradient(tool.color); return ( <> {/* Backdrop */} <div - className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm lg:hidden animate-fade-in" + className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md lg:hidden" onClick={onClose} aria-hidden="true" + style={{ + animation: "fadeIn 200ms ease-out", + opacity: isDragging ? 1 - dragY / 300 : 1, + }} /> {/* Sheet */} - <div className="fixed inset-x-0 bottom-0 z-50 lg:hidden animate-slide-up"> - <div className="flex max-h-[70vh] flex-col rounded-t-3xl border-t border-border/50 bg-card/95 backdrop-blur-xl"> - {/* Handle */} - <div className="flex shrink-0 justify-center pt-3 pb-2"> - <div className="h-1 w-10 rounded-full bg-muted-foreground/30" /> + <div + role="dialog" + aria-modal="true" + aria-label={`${tool.name} details`} + ref={sheetRef} + className="fixed inset-x-0 bottom-0 z-50 lg:hidden touch-none" + style={{ + animation: "slideUp 350ms cubic-bezier(0.16, 1, 0.3, 1)", + transform: `translateY(${dragY}px)`, + transition: isDragging ? "none" : "transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)", + }} + onTouchStart={handleTouchStart} + onTouchMove={handleTouchMove} + onTouchEnd={handleTouchEnd} + > + <div + className="flex max-h-[90vh] flex-col rounded-t-3xl border-t border-border/50 bg-card/98 backdrop-blur-xl shadow-2xl" + style={{ boxShadow: `0 -10px 60px ${color}20, 0 -20px 40px rgba(0,0,0,0.4)` }} + > + {/* Handle - larger touch target */} + <div className="flex shrink-0 justify-center pt-4 pb-3"> + <div + className={cn( + "h-1.5 w-14 rounded-full transition-colors", + isDragging ? "bg-muted-foreground/60" : "bg-muted-foreground/30" + )} + /> </div> - {/* Scrollable content - iOS needs overscroll-contain for proper scrolling */} + {/* Content */} <div - className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 pb-8" - style={{ WebkitOverflowScrolling: 'touch' }} + className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 pb-12" + style={{ WebkitOverflowScrolling: "touch" }} > {/* Header */} <div className="flex items-center gap-4 py-4"> <div - className={`flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg ${tool.color}`} + className={cn( + "relative flex items-center justify-center rounded-2xl bg-gradient-to-br shadow-xl", + tool.color + )} + style={{ + width: 72, + height: 72, + boxShadow: `0 8px 32px ${color}40`, + }} > - <Icon className="h-7 w-7 text-white" /> + <Icon className="h-9 w-9 text-white drop-shadow-md" /> + <div + className="absolute inset-0 rounded-2xl" + style={{ + background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, transparent 50%)", + }} + /> </div> <div className="flex-1"> <h3 className="text-xl font-bold text-foreground">{tool.name}</h3> - <p className="text-sm text-muted-foreground">{tool.tagline}</p> + <p className="text-sm text-muted-foreground mt-0.5">{tool.tagline}</p> </div> <button onClick={onClose} - className="flex h-11 w-11 items-center justify-center rounded-full bg-muted text-foreground" + className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/80 text-foreground active:scale-95 transition-transform" aria-label="Close" > <X className="h-5 w-5" /> </button> </div> + {/* Stars */} + {tool.stars && ( + <div + className="inline-flex items-center gap-2 rounded-full bg-amber-500/15 px-4 py-2 text-sm font-semibold text-amber-400 border border-amber-400/20" + style={{ boxShadow: "0 2px 12px rgba(251,191,36,0.15)" }} + > + <Star className="h-4 w-4 fill-current" /> + <span>{tool.stars.toLocaleString()} stars</span> + </div> + )} + {/* Description */} - <p className="text-sm leading-relaxed text-muted-foreground">{tool.description}</p> + <p className="mt-5 text-sm leading-relaxed text-muted-foreground">{tool.description}</p> + + {/* Install command */} + {tool.installCommand && ( + <div className="mt-6"> + <h4 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground">Install</h4> + <button + onClick={copyInstallCommand} + className={cn( + "flex w-full items-center gap-3 rounded-xl p-4 font-mono text-xs text-left transition-all active:scale-[0.98]", + copied ? "bg-primary/20 border-primary/30" : "bg-black/30 border-border/30", + "border" + )} + > + <code className="flex-1 text-foreground break-all"> + {tool.installCommand.length > 55 ? tool.installCommand.slice(0, 55) + "..." : tool.installCommand} + </code> + {copied ? ( + <Check className="h-5 w-5 text-primary shrink-0" /> + ) : ( + <Copy className="h-5 w-5 text-muted-foreground shrink-0" /> + )} + </button> + </div> + )} - {/* Action button */} - <Button asChild className={`mt-5 w-full bg-gradient-to-r ${tool.color} text-white`}> + {/* Primary action */} + <Button + asChild + className={cn( + "mt-6 w-full h-14 bg-gradient-to-r text-white shadow-lg text-base font-semibold", + tool.color + )} + style={{ boxShadow: `0 8px 32px ${color}40` }} + > <a href={tool.href} target="_blank" rel="noopener noreferrer"> View on GitHub - <ExternalLink className="ml-2 h-4 w-4" /> + <ExternalLink className="ml-2 h-5 w-5" /> </a> </Button> - {/* Connections */} - <div className="mt-6"> - <h4 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground">Integrates With</h4> - <div className="space-y-2"> - {tool.connectsTo.map((targetId) => { - const targetTool = flywheelTools.find((t) => t.id === targetId); - if (!targetTool) return null; - const TargetIcon = iconMap[targetTool.icon] || Zap; - - return ( + {/* Features */} + <div className="mt-8"> + <h4 className="mb-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Key Features</h4> + <ul className="space-y-3"> + {tool.features.slice(0, 5).map((feature, i) => ( + <li key={i} className="flex items-start gap-3 text-sm text-foreground"> <div - key={targetId} - className="flex items-center gap-3 rounded-xl bg-muted/30 p-3 border border-border/30" + className={cn("mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br", tool.color)} + style={{ boxShadow: `0 2px 8px ${color}30` }} > + <Check className="h-3.5 w-3.5 text-white" /> + </div> + <span>{feature}</span> + </li> + ))} + </ul> + </div> + + {/* Connections */} + {tool.connectsTo.length > 0 && ( + <div className="mt-8 border-t border-border/50 pt-6"> + <h4 className="mb-4 text-xs font-bold uppercase tracking-wider text-muted-foreground"> + Integrates With + </h4> + <div className="space-y-3"> + {tool.connectsTo.slice(0, 5).map((targetId) => { + const targetTool = uniqueTools.find((t) => t.id === targetId); + if (!targetTool) return null; + const TargetIcon = iconMap[targetTool.icon] || Zap; + const targetColor = getColorFromGradient(targetTool.color); + + return ( <div - className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br ${targetTool.color}`} + key={targetId} + className="flex items-center gap-3 rounded-xl bg-white/5 p-3.5 border border-border/30" > - <TargetIcon className="h-5 w-5 text-white" /> - </div> - <div className="min-w-0 flex-1"> - <p className="text-sm font-semibold text-foreground">{targetTool.shortName}</p> - <p className="text-xs text-muted-foreground"> - {tool.connectionDescriptions[targetId] || "Integration"} - </p> + <div + className={cn( + "flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-md", + targetTool.color + )} + style={{ boxShadow: `0 4px 12px ${targetColor}30` }} + > + <TargetIcon className="h-6 w-6 text-white" /> + </div> + <div className="min-w-0 flex-1"> + <p className="text-sm font-semibold text-foreground">{targetTool.shortName}</p> + <p className="text-xs text-muted-foreground mt-0.5"> + {tool.connectionDescriptions[targetId] || "Integration"} + </p> + </div> </div> - </div> - ); - })} + ); + })} + </div> </div> - </div> + )} </div> </div> </div> @@ -548,78 +1142,424 @@ function MobileBottomSheet({ ); } -// Stats badge -function StatsBadge() { +// ============================================================================= +// STATS BADGE (Enhanced with glassmorphism) +// ============================================================================= + +function StatsBadge({ toolCount }: { toolCount: number }) { return ( - <div className="mt-6 flex justify-center"> - <div className="inline-flex items-center gap-3 rounded-full border border-primary/20 bg-primary/5 px-4 py-2 backdrop-blur-sm"> - <div className="flex items-center gap-1.5"> - <div className="flex h-5 w-5 items-center justify-center rounded-full bg-primary/20"> - <Zap className="h-3 w-3 text-primary" /> + <div className="flex justify-center"> + <div + className="inline-flex items-center gap-4 rounded-full border border-primary/25 bg-primary/10 px-5 py-2.5 backdrop-blur-md shadow-lg" + style={{ boxShadow: "0 4px 24px rgba(var(--primary-rgb), 0.15)" }} + > + <div className="flex items-center gap-2"> + <div + className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/25" + style={{ boxShadow: "0 2px 8px rgba(var(--primary-rgb), 0.2)" }} + > + <Zap className="h-3.5 w-3.5 text-primary" /> </div> - <span className="text-sm font-semibold text-foreground">{flywheelDescription.metrics.toolCount}</span> + <span className="text-sm font-bold text-foreground">{toolCount}</span> <span className="text-xs text-muted-foreground">tools</span> </div> - <div className="h-4 w-px bg-primary/30" /> - <div className="flex items-center gap-1.5"> - <Star className="h-4 w-4 text-amber-400 fill-current" /> - <span className="text-sm font-semibold text-foreground">{flywheelDescription.metrics.totalStars}</span> + <div className="h-5 w-px bg-primary/30" /> + <div className="flex items-center gap-2"> + <Star className="h-4 w-4 text-amber-400 fill-current drop-shadow-sm" /> + <span className="text-sm font-bold text-foreground">{flywheelDescription.metrics.totalStars}</span> <span className="text-xs text-muted-foreground">stars</span> </div> - <div className="h-4 w-px bg-primary/30" /> - <div className="flex items-center gap-1"> - <span className="relative flex h-2 w-2"> - <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span> - <span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span> + <div className="h-5 w-px bg-primary/30" /> + <div className="flex items-center gap-1.5"> + <span className="relative flex h-2.5 w-2.5"> + <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" /> + <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-500 shadow-sm" style={{ boxShadow: "0 0 8px rgba(34,197,94,0.5)" }} /> </span> - <span className="text-[12px] text-green-400">Active</span> + <span className="text-xs font-medium text-green-400">Active</span> </div> </div> </div> ); } -// Main component -export default function FlywheelVisualization() { - const [selectedToolId, setSelectedToolId] = useState<string | null>(null); - const [hoveredToolId, setHoveredToolId] = useState<string | null>(null); +// ============================================================================= +// DESKTOP VISUALIZATION - Two-tier concentric rings (Enhanced) +// ============================================================================= + +interface DesktopVisualizationProps { + tools: FlywheelTool[]; + selectedToolId: string | null; + hoveredToolId: string | null; + onSelectTool: (id: string) => void; + onHoverTool: (id: string | null) => void; +} +function DesktopVisualization({ + tools, + selectedToolId, + hoveredToolId, + onSelectTool, + onHoverTool, +}: DesktopVisualizationProps) { + const { primary, secondary } = useMemo(() => classifyTools(tools), [tools]); const activeToolId = selectedToolId || hoveredToolId; - const displayedTool = flywheelTools.find((t) => t.id === activeToolId) ?? null; - const selectedTool = flywheelTools.find((t) => t.id === selectedToolId) ?? null; + const center = DESKTOP_CONFIG.containerSize / 2; - // Calculate positions const positions = useMemo(() => { - return flywheelTools.reduce((acc, tool, index) => { - acc[tool.id] = getNodePosition(index, flywheelTools.length); - return acc; - }, {} as Record<string, { x: number; y: number }>); - }, []); + const pos: Record<string, { x: number; y: number }> = {}; + + primary.forEach((tool, index) => { + pos[tool.id] = getCirclePosition(index, primary.length, DESKTOP_CONFIG.innerRadius, center); + }); + + secondary.forEach((tool, index) => { + pos[tool.id] = getCirclePosition(index, secondary.length, DESKTOP_CONFIG.outerRadius, center); + }); + + return pos; + }, [primary, secondary, center]); - // Get connections const connections = useMemo(() => getAllConnections(), []); const isConnectionHighlighted = useCallback( (from: string, to: string) => { if (!activeToolId) return false; - const activeTool = flywheelTools.find((t) => t.id === activeToolId); + const activeTool = tools.find((t) => t.id === activeToolId); if (!activeTool) return false; return ( (from === activeToolId && activeTool.connectsTo.includes(to)) || (to === activeToolId && activeTool.connectsTo.includes(from)) ); }, - [activeToolId] + [activeToolId, tools] ); const isToolConnected = useCallback( (toolId: string) => { if (!activeToolId || toolId === activeToolId) return false; - const activeTool = flywheelTools.find((t) => t.id === activeToolId); + const activeTool = tools.find((t) => t.id === activeToolId); return activeTool?.connectsTo.includes(toolId) ?? false; }, - [activeToolId] + [activeToolId, tools] + ); + + return ( + <div + className="relative mx-auto" + style={{ width: DESKTOP_CONFIG.containerSize, height: DESKTOP_CONFIG.containerSize }} + > + {/* Ambient background glow */} + <div + className="absolute inset-0 rounded-full bg-primary/5 blur-3xl" + style={{ transform: "scale(1.2)" }} + /> + + {/* SVG connections */} + <svg + className="absolute inset-0" + width={DESKTOP_CONFIG.containerSize} + height={DESKTOP_CONFIG.containerSize} + aria-hidden="true" + > + <defs> + {/* Ambient glow for center */} + <radialGradient id="center-glow" cx="50%" cy="50%" r="50%"> + <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.15" /> + <stop offset="70%" stopColor="hsl(var(--primary))" stopOpacity="0.05" /> + <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0" /> + </radialGradient> + </defs> + + <style> + {` + @keyframes flow { + from { stroke-dashoffset: 0; } + to { stroke-dashoffset: -48; } + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + @keyframes pulse-ring { + 0%, 100% { transform: scale(1); opacity: 0.3; } + 50% { transform: scale(1.05); opacity: 0.6; } + } + @keyframes glow-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.7; } + } + @keyframes float0 { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-4px); } + } + @keyframes float1 { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-3px); } + } + @keyframes float2 { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-5px); } + } + @keyframes panel-enter { + from { opacity: 0; transform: translateX(10px); } + to { opacity: 1; transform: translateX(0); } + } + @keyframes gradient-shift { + 0%, 100% { opacity: 0.08; } + 50% { opacity: 0.12; } + } + @keyframes fade-in-up { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } + } + `} + </style> + + {/* Center ambient glow */} + <circle + cx={center} + cy={center} + r={DESKTOP_CONFIG.innerRadius * 0.9} + fill="url(#center-glow)" + /> + + {/* Decorative orbital rings */} + <circle + cx={center} + cy={center} + r={DESKTOP_CONFIG.innerRadius} + fill="none" + stroke="currentColor" + strokeWidth="1.5" + strokeDasharray="6 8" + className="text-primary/15" + /> + <circle + cx={center} + cy={center} + r={DESKTOP_CONFIG.outerRadius} + fill="none" + stroke="currentColor" + strokeWidth="1" + strokeDasharray="4 10" + className="text-primary/10" + /> + + {/* Connection lines */} + {connections.map(({ from, to }) => { + const fromTool = tools.find((t) => t.id === from); + const toTool = tools.find((t) => t.id === to); + const fromPos = positions[from]; + const toPos = positions[to]; + if (!fromPos || !toPos || !fromTool || !toTool) return null; + + return ( + <ConnectionLine + key={`${from}-${to}`} + fromPos={fromPos} + toPos={toPos} + isHighlighted={isConnectionHighlighted(from, to)} + fromColor={fromTool.color} + toColor={toTool.color} + connectionId={`${from}-${to}`} + center={center} + /> + ); + })} + </svg> + + {/* Center hub */} + <CenterHub size={DESKTOP_CONFIG.centerSize} /> + + {/* Primary tools (inner ring) */} + {primary.map((tool, index) => ( + <DesktopToolNode + key={tool.id} + tool={tool} + position={positions[tool.id]} + size={DESKTOP_CONFIG.innerNodeSize} + isSelected={tool.id === selectedToolId} + isConnected={isToolConnected(tool.id)} + isDimmed={!!activeToolId && tool.id !== activeToolId && !isToolConnected(tool.id)} + onSelect={() => onSelectTool(tool.id)} + onHover={(hovering) => onHoverTool(hovering ? tool.id : null)} + isPrimary={true} + index={index} + /> + ))} + + {/* Secondary tools (outer ring) */} + {secondary.map((tool, index) => ( + <DesktopToolNode + key={tool.id} + tool={tool} + position={positions[tool.id]} + size={DESKTOP_CONFIG.outerNodeSize} + isSelected={tool.id === selectedToolId} + isConnected={isToolConnected(tool.id)} + isDimmed={!!activeToolId && tool.id !== activeToolId && !isToolConnected(tool.id)} + onSelect={() => onSelectTool(tool.id)} + onHover={(hovering) => onHoverTool(hovering ? tool.id : null)} + isPrimary={false} + index={index} + /> + ))} + </div> ); +} + +// ============================================================================= +// MOBILE VISUALIZATION - Horizontal carousel (Enhanced) +// ============================================================================= + +interface MobileVisualizationProps { + tools: FlywheelTool[]; + selectedToolId: string | null; + onSelectTool: (id: string) => void; +} + +function MobileVisualization({ tools, selectedToolId, onSelectTool }: MobileVisualizationProps) { + const coreScrollRef = useRef<HTMLDivElement>(null); + const supportingScrollRef = useRef<HTMLDivElement>(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + const [activeSection, setActiveSection] = useState<"core" | "supporting">("core"); + const { primary, secondary } = useMemo(() => classifyTools(tools), [tools]); + + const checkScroll = useCallback(() => { + const el = activeSection === "core" ? coreScrollRef.current : supportingScrollRef.current; + if (!el) return; + setCanScrollLeft(el.scrollLeft > 10); + setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 10); + }, [activeSection]); + + useEffect(() => { + const coreEl = coreScrollRef.current; + const supportingEl = supportingScrollRef.current; + + const handleCoreScroll = () => { + setActiveSection("core"); + checkScroll(); + }; + const handleSupportingScroll = () => { + setActiveSection("supporting"); + checkScroll(); + }; + + checkScroll(); + coreEl?.addEventListener("scroll", handleCoreScroll, { passive: true }); + supportingEl?.addEventListener("scroll", handleSupportingScroll, { passive: true }); + + return () => { + coreEl?.removeEventListener("scroll", handleCoreScroll); + supportingEl?.removeEventListener("scroll", handleSupportingScroll); + }; + }, [checkScroll]); + + const scroll = (direction: "left" | "right") => { + const el = activeSection === "core" ? coreScrollRef.current : supportingScrollRef.current; + if (!el) return; + const scrollAmount = 300; + el.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth" }); + }; + + return ( + <div className="relative -mx-4 sm:-mx-6"> + {/* Navigation arrows - appear on interaction */} + <button + onClick={() => scroll("left")} + disabled={!canScrollLeft} + className={cn( + "absolute left-2 top-1/2 z-20 -translate-y-1/2 flex h-11 w-11 items-center justify-center rounded-full bg-card/95 border border-border/50 backdrop-blur-md shadow-xl transition-all active:scale-95", + canScrollLeft ? "opacity-100" : "opacity-0 pointer-events-none" + )} + style={{ boxShadow: canScrollLeft ? "0 4px 20px rgba(0,0,0,0.3)" : "none" }} + aria-label="Scroll left" + > + <ChevronLeft className="h-5 w-5" /> + </button> + <button + onClick={() => scroll("right")} + disabled={!canScrollRight} + className={cn( + "absolute right-2 top-1/2 z-20 -translate-y-1/2 flex h-11 w-11 items-center justify-center rounded-full bg-card/95 border border-border/50 backdrop-blur-md shadow-xl transition-all active:scale-95", + canScrollRight ? "opacity-100" : "opacity-0 pointer-events-none" + )} + style={{ boxShadow: canScrollRight ? "0 4px 20px rgba(0,0,0,0.3)" : "none" }} + aria-label="Scroll right" + > + <ChevronRight className="h-5 w-5" /> + </button> + + {/* Primary tools section */} + <div className="mb-8"> + <div className="mb-4 px-4 sm:px-6 flex items-center gap-2"> + <div className="flex h-6 w-6 items-center justify-center rounded-lg bg-primary/20"> + <Sparkles className="h-3.5 w-3.5 text-primary" /> + </div> + <h3 className="text-xs font-bold uppercase tracking-wider text-primary">Core Tools</h3> + <span className="text-xs text-muted-foreground">({primary.length})</span> + </div> + <div + ref={coreScrollRef} + className="flex gap-4 overflow-x-auto px-4 sm:px-6 pb-3 snap-x snap-mandatory scrollbar-hide" + style={{ WebkitOverflowScrolling: "touch" }} + onFocus={() => setActiveSection("core")} + > + {primary.map((tool) => ( + <MobileToolCard + key={tool.id} + tool={tool} + isActive={tool.id === selectedToolId} + onSelect={() => onSelectTool(tool.id)} + /> + ))} + </div> + </div> + + {/* Secondary tools section */} + {secondary.length > 0 && ( + <div> + <div className="mb-4 px-4 sm:px-6 flex items-center gap-2"> + <div className="flex h-6 w-6 items-center justify-center rounded-lg bg-muted"> + <Zap className="h-3.5 w-3.5 text-muted-foreground" /> + </div> + <h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Supporting Tools</h3> + <span className="text-xs text-muted-foreground">({secondary.length})</span> + </div> + <div + ref={supportingScrollRef} + className="flex gap-4 overflow-x-auto px-4 sm:px-6 pb-3 snap-x snap-mandatory scrollbar-hide" + style={{ WebkitOverflowScrolling: "touch" }} + onFocus={() => setActiveSection("supporting")} + > + {secondary.map((tool) => ( + <MobileToolCard + key={tool.id} + tool={tool} + isActive={tool.id === selectedToolId} + onSelect={() => onSelectTool(tool.id)} + /> + ))} + </div> + </div> + )} + </div> + ); +} + +// ============================================================================= +// MAIN COMPONENT +// ============================================================================= + +export default function FlywheelVisualization() { + const [selectedToolId, setSelectedToolId] = useState<string | null>(null); + const [hoveredToolId, setHoveredToolId] = useState<string | null>(null); + + const uniqueTools = useMemo(() => getUniqueTools(), []); + const activeToolId = selectedToolId || hoveredToolId; + const displayedTool = uniqueTools.find((t) => t.id === activeToolId) ?? null; + const selectedTool = uniqueTools.find((t) => t.id === selectedToolId) ?? null; const handleSelectTool = useCallback((toolId: string) => { setSelectedToolId((prev) => (prev === toolId ? null : toolId)); @@ -631,110 +1571,37 @@ export default function FlywheelVisualization() { return ( <div className="relative"> - {/* Header */} - <div className="mb-8 md:mb-12 text-center"> - <div className="mb-4 flex items-center justify-center gap-3"> - <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[12px] font-bold uppercase tracking-[0.25em] text-primary">Ecosystem</span> - <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> + {/* Header with refined typography */} + <div className="mb-10 md:mb-14 text-center"> + <div className="mb-5 flex items-center justify-center gap-3"> + <div className="h-px w-10 bg-gradient-to-r from-transparent via-primary/60 to-transparent" /> + <span className="text-[11px] font-bold uppercase tracking-[0.3em] text-primary">Ecosystem</span> + <div className="h-px w-10 bg-gradient-to-l from-transparent via-primary/60 to-transparent" /> </div> - <h2 className="mb-4 font-mono text-2xl md:text-3xl lg:text-4xl font-bold tracking-tight text-foreground"> + <h2 className="mb-5 font-mono text-2xl md:text-3xl lg:text-4xl font-bold tracking-tight text-foreground"> {flywheelDescription.title} </h2> - <p className="mx-auto max-w-2xl text-sm md:text-base text-muted-foreground"> + <p className="mx-auto max-w-2xl text-sm md:text-base text-muted-foreground leading-relaxed"> {flywheelDescription.subtitle} </p> </div> - <div className="grid gap-8 lg:grid-cols-[1fr,380px] xl:grid-cols-[1fr,420px]"> - {/* Flywheel visualization */} - <div className="relative flex flex-col items-center justify-center"> - {/* Responsive wrapper - clip overflow on mobile */} - <div className="w-full max-w-[312px] sm:max-w-[384px] md:max-w-[480px] aspect-square overflow-hidden"> - <div - className="relative origin-top-left scale-[0.65] sm:scale-[0.8] md:scale-100" - style={{ width: CONTAINER_SIZE, height: CONTAINER_SIZE }} - > - {/* SVG connections */} - <svg className="absolute inset-0" width={CONTAINER_SIZE} height={CONTAINER_SIZE} aria-hidden="true"> - <style> - {` - @keyframes flow { - from { stroke-dashoffset: 0; } - to { stroke-dashoffset: -100; } - } - `} - </style> - - {/* Decorative rings */} - <circle - cx={CENTER} - cy={CENTER} - r={RADIUS + 25} - fill="none" - stroke="currentColor" - strokeWidth="1" - strokeDasharray="8 6" - className="text-primary/10" - /> - <circle - cx={CENTER} - cy={CENTER} - r={RADIUS * 0.45} - fill="none" - stroke="currentColor" - strokeWidth="1" - className="text-primary/5" - /> - - {/* Connection lines */} - {connections.map(({ from, to }) => { - const fromTool = flywheelTools.find((t) => t.id === from); - const toTool = flywheelTools.find((t) => t.id === to); - const fromPos = positions[from]; - const toPos = positions[to]; - if (!fromPos || !toPos || !fromTool || !toTool) return null; - - return ( - <ConnectionLine - key={`${from}-${to}`} - fromPos={fromPos} - toPos={toPos} - isHighlighted={isConnectionHighlighted(from, to)} - fromColor={fromTool.color} - toColor={toTool.color} - connectionId={`${from}-${to}`} - /> - ); - })} - </svg> - - {/* Center hub */} - <CenterHub /> - - {/* Tool nodes */} - {flywheelTools.map((tool, index) => ( - <ToolNode - key={tool.id} - tool={tool} - position={positions[tool.id]} - index={index} - isSelected={tool.id === selectedToolId} - isConnected={isToolConnected(tool.id)} - isDimmed={!!activeToolId && tool.id !== activeToolId && !isToolConnected(tool.id)} - onSelect={() => handleSelectTool(tool.id)} - onHover={(hovering) => setHoveredToolId(hovering ? tool.id : null)} - /> - ))} - </div> + {/* Desktop layout */} + <div className="hidden lg:grid lg:grid-cols-[1fr,400px] xl:grid-cols-[1fr,440px] gap-10"> + <div className="flex flex-col items-center justify-center"> + <DesktopVisualization + tools={uniqueTools} + selectedToolId={selectedToolId} + hoveredToolId={hoveredToolId} + onSelectTool={handleSelectTool} + onHoverTool={setHoveredToolId} + /> + <div className="mt-10"> + <StatsBadge toolCount={uniqueTools.length} /> </div> - - {/* Stats badge */} - <StatsBadge /> </div> - {/* Detail panel (desktop) */} - <div className="hidden lg:flex lg:flex-col"> + <div className="flex flex-col"> {displayedTool ? ( <ToolDetailPanel key={displayedTool.id} tool={displayedTool} onClose={handleCloseDetail} /> ) : ( @@ -743,8 +1610,39 @@ export default function FlywheelVisualization() { </div> </div> + {/* Mobile/Tablet layout */} + <div className="lg:hidden"> + <MobileVisualization + tools={uniqueTools} + selectedToolId={selectedToolId} + onSelectTool={handleSelectTool} + /> + <div className="mt-10"> + <StatsBadge toolCount={uniqueTools.length} /> + </div> + </div> + {/* Mobile bottom sheet */} <MobileBottomSheet tool={selectedTool} onClose={handleCloseDetail} /> + + {/* CSS animations */} + <style jsx global>{` + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } + } + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + .scrollbar-hide::-webkit-scrollbar { + display: none; + } + `}</style> </div> ); } diff --git a/apps/web/components/jargon.tsx b/apps/web/components/jargon.tsx index 8fee0358..c649e252 100644 --- a/apps/web/components/jargon.tsx +++ b/apps/web/components/jargon.tsx @@ -12,10 +12,11 @@ import { import Link from "next/link"; import { createPortal } from "react-dom"; import { motion, AnimatePresence, springs } from "@/components/motion"; -import { X, Lightbulb } from "lucide-react"; +import { Lightbulb } from "lucide-react"; import { cn } from "@/lib/utils"; import { getJargon, type JargonTerm } from "@/lib/jargon"; import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; +import { BottomSheet } from "@/components/ui/bottom-sheet"; interface JargonProps { /** The term key to look up in the dictionary */ @@ -99,21 +100,6 @@ export function Jargon({ term, children, className, gradientHeading }: JargonPro setTooltipLayout({ position, style: { left, ...verticalStyle } }); }, [isOpen, isMobile]); - // Lock body scroll when mobile sheet is open - useEffect(() => { - if (isOpen && isMobile) { - const scrollY = window.scrollY; - document.body.style.position = "fixed"; - document.body.style.top = `-${scrollY}px`; - document.body.style.width = "100%"; - return () => { - document.body.style.position = ""; - document.body.style.top = ""; - document.body.style.width = ""; - window.scrollTo(0, scrollY); - }; - } - }, [isOpen, isMobile]); const handleMouseEnter = useCallback(() => { if (isMobile) return; @@ -164,25 +150,6 @@ export function Jargon({ term, children, className, gradientHeading }: JargonPro setIsOpen(false); }, []); - // Handle click outside for mobile - useEffect(() => { - if (!isOpen || !isMobile) return; - - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as Node; - if ( - triggerRef.current && - !triggerRef.current.contains(target) && - tooltipRef.current && - !tooltipRef.current.contains(target) - ) { - setIsOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [isOpen, isMobile]); if (!jargonData) { // If term not found, just render children without styling @@ -241,7 +208,7 @@ export function Jargon({ term, children, className, gradientHeading }: JargonPro } transition={prefersReducedMotion ? { duration: 0.12 } : springs.snappy} className={cn( - "fixed z-[9999] w-80 max-w-[calc(100vw-2rem)]", + "fixed z-50 w-80 max-w-[calc(100vw-2rem)]", "rounded-xl border border-border/50 bg-card/95 p-4 shadow-2xl backdrop-blur-xl", // Gradient accent line at top "before:absolute before:inset-x-0 before:h-1 before:rounded-t-xl before:bg-gradient-to-r before:from-primary/50 before:via-[oklch(0.7_0.2_330/0.5)] before:to-primary/50", @@ -271,60 +238,18 @@ export function Jargon({ term, children, className, gradientHeading }: JargonPro document.body )} - {/* Mobile Bottom Sheet - rendered via portal to escape stacking contexts */} - {canUsePortal && createPortal( - <AnimatePresence> - {isOpen && isMobile && ( - <> - {/* Backdrop */} - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0 }} - transition={prefersReducedMotion ? { duration: 0.12 } : { duration: 0.2 }} - className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm" - onClick={handleClose} - aria-hidden="true" - /> - - {/* Sheet */} - <motion.div - ref={tooltipRef} - role="dialog" - aria-modal="true" - aria-labelledby={`jargon-sheet-title-${termKey}`} - initial={prefersReducedMotion ? { opacity: 0 } : { y: "100%" }} - animate={prefersReducedMotion ? { opacity: 1 } : { y: 0 }} - exit={prefersReducedMotion ? { opacity: 0 } : { y: "100%" }} - transition={prefersReducedMotion ? { duration: 0.12 } : springs.smooth} - className="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[80vh] flex-col rounded-t-3xl border-t border-border/50 bg-card/98 shadow-2xl backdrop-blur-xl" - > - {/* Handle */} - <div className="flex shrink-0 justify-center pt-3 pb-1"> - <div className="h-1 w-10 rounded-full bg-muted-foreground/30" /> - </div> - - {/* Close button */} - <button - onClick={handleClose} - className="absolute right-4 top-4 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground" - aria-label="Close" - > - <X className="h-5 w-5" /> - </button> - - {/* Content - iOS needs overscroll-contain and touch-action for proper scrolling */} - <div - className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pt-2 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" - style={{ WebkitOverflowScrolling: 'touch' }} - > - <SheetContent term={jargonData} termKey={termKey} /> - </div> - </motion.div> - </> - )} - </AnimatePresence>, - document.body + {/* Mobile Bottom Sheet */} + {canUsePortal && ( + <BottomSheet + open={isOpen && isMobile} + onClose={handleClose} + title={jargonData.term} + showHandle + closeOnBackdrop + swipeable={!prefersReducedMotion} + > + <SheetContent term={jargonData} termKey={termKey} /> + </BottomSheet> )} </> ); @@ -360,14 +285,14 @@ function TooltipContent({ term, termKey }: { term: JargonTerm; termKey: string } )} {/* Tap for more hint */} - <p className="text-[11px] text-muted-foreground/60"> + <p className="text-xs text-muted-foreground/60"> Hover or focus to learn more </p> <Link href={glossaryHref} className={cn( - "inline-block text-[11px] font-medium text-primary underline-offset-4 hover:underline", + "inline-block text-xs font-medium text-primary underline-offset-4 hover:underline", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded-sm" )} > diff --git a/apps/web/components/learn/confetti-celebration.tsx b/apps/web/components/learn/confetti-celebration.tsx index ff2cb258..5d87a4ea 100644 --- a/apps/web/components/learn/confetti-celebration.tsx +++ b/apps/web/components/learn/confetti-celebration.tsx @@ -167,7 +167,7 @@ export function FinalCelebrationModal({ return ( <div - className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm" + className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm" onClick={onClose} role="dialog" aria-modal="true" @@ -233,7 +233,7 @@ export function CompletionToast({ message, isVisible }: CompletionToastProps) { return ( <div - className={`fixed left-1/2 top-20 z-[90] -translate-x-1/2 rounded-full border border-primary/30 bg-card/95 px-6 py-3 shadow-lg backdrop-blur-sm ${ + className={`fixed left-1/2 top-20 z-50 -translate-x-1/2 rounded-full border border-primary/30 bg-card/95 px-6 py-3 shadow-lg backdrop-blur-sm ${ prefersReducedMotion ? "" : "animate-in fade-in slide-in-from-top-4 duration-300" diff --git a/apps/web/components/learn/gradient-card.tsx b/apps/web/components/learn/gradient-card.tsx index fb0c0b32..9ba15e24 100644 --- a/apps/web/components/learn/gradient-card.tsx +++ b/apps/web/components/learn/gradient-card.tsx @@ -83,31 +83,41 @@ export function GradientCard({ return ( <motion.div className={cn( - "group relative overflow-hidden rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm transition-all duration-300", - hoverable && "hover:border-primary/30 active:scale-[0.98] active:bg-card/70", + "group relative overflow-hidden rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm", + // Layered shadows for depth - Stripe-style elevation + "shadow-[0_2px_4px_rgba(0,0,0,0.08),0_4px_12px_rgba(0,0,0,0.08)]", + // Smooth transition for all hover states + "transition-[border-color,background,transform] duration-300 ease-out", + hoverable && "hover:border-primary/30 hover:bg-card/70 active:scale-[0.98]", onClick && "cursor-pointer", className )} variants={fadeUp} whileHover={ hoverable - ? { y: -4, boxShadow: "0 20px 40px -12px oklch(0.75 0.18 195 / 0.15)" } + ? { + y: -6, + boxShadow: "0 12px 24px -8px oklch(0.75 0.18 195 / 0.2), 0 4px 12px -4px rgba(0,0,0,0.15)" + } : undefined } transition={{ ...springs.snappy, delay: staggerDelay(index, 0.08) }} onClick={onClick} > - {/* Gradient glow on hover */} + {/* Gradient glow on hover - enhanced with larger spread */} <motion.div className={cn( - "absolute -right-20 -top-20 h-40 w-40 rounded-full blur-3xl", + "pointer-events-none absolute -right-24 -top-24 h-48 w-48 rounded-full blur-3xl", gradientGlowColors[variant] )} - initial={{ opacity: 0 }} - whileHover={{ opacity: 0.3 }} + initial={{ opacity: 0, scale: 0.8 }} + whileHover={{ opacity: 0.35, scale: 1 }} transition={springs.smooth} /> + {/* Subtle inner glow for premium feel */} + <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 transition-opacity duration-300 group-hover:opacity-100 bg-gradient-to-br from-white/[0.02] via-transparent to-transparent" /> + <div className="relative z-10">{children}</div> </motion.div> ); diff --git a/apps/web/components/learn/section-header.tsx b/apps/web/components/learn/section-header.tsx index df720828..b96cbb22 100644 --- a/apps/web/components/learn/section-header.tsx +++ b/apps/web/components/learn/section-header.tsx @@ -59,7 +59,7 @@ export function SectionHeader({ )} > <div className="h-px w-8 bg-gradient-to-r from-transparent via-primary/50 to-transparent" /> - <span className="text-[11px] font-bold uppercase tracking-[0.25em] text-primary"> + <span className="text-xs font-bold uppercase tracking-[0.25em] text-primary"> {label} </span> <div className="h-px w-8 bg-gradient-to-l from-transparent via-primary/50 to-transparent" /> diff --git a/apps/web/components/lessons/agent-mail-lesson.tsx b/apps/web/components/lessons/agent-mail-lesson.tsx index 1f6a23e7..f90e09de 100644 --- a/apps/web/components/lessons/agent-mail-lesson.tsx +++ b/apps/web/components/lessons/agent-mail-lesson.tsx @@ -364,7 +364,7 @@ function ProblemCard({ <div className="flex-1"> <span className="text-white/50 line-through">{problem}</span> </div> - <div className="text-white/30 group-hover:text-white/50 transition-colors">→</div> + <div className="text-white/50 group-hover:text-white/70 transition-colors">→</div> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-400 group-hover:bg-emerald-500/30 transition-colors"> ✓ </div> @@ -516,7 +516,7 @@ function CoordinationFlow() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: i * 0.1 + 0.2 }} - className="hidden sm:block text-white/30 text-xl font-light" + className="hidden sm:block text-white/50 text-xl font-light" > → </motion.span> diff --git a/apps/web/components/lessons/agents-login-lesson.tsx b/apps/web/components/lessons/agents-login-lesson.tsx index 478adca0..d6b165ab 100644 --- a/apps/web/components/lessons/agents-login-lesson.tsx +++ b/apps/web/components/lessons/agents-login-lesson.tsx @@ -308,7 +308,7 @@ function AgentInfoCard({ <Bot className="h-7 w-7 text-white" /> </div> <span className="font-bold text-white">{name}</span> - <span className="text-xs text-white/40 mt-1">{company}</span> + <span className="text-xs text-white/60 mt-1">{company}</span> <div className="mt-4 flex flex-col gap-2 w-full"> <code className="px-3 py-1.5 rounded-lg bg-black/40 border border-white/[0.08] text-xs font-mono text-white/70"> @@ -395,7 +395,7 @@ function LoginStep({ <h4 className="font-bold text-white mb-3 group-hover:text-primary transition-colors">{agent}</h4> <div className="mb-3 rounded-xl bg-black/30 border border-white/[0.06] overflow-hidden group-hover:bg-black/40 transition-colors"> <pre className="p-3 text-sm font-mono text-emerald-400"> - <span className="text-white/30">$ </span> + <span className="text-white/50">$ </span> {command} </pre> </div> @@ -445,7 +445,7 @@ function CodexLoginSection() { </ol> <div className="rounded-xl bg-black/30 border border-white/[0.06] overflow-hidden"> <pre className="p-3 text-sm font-mono text-emerald-400"> - <span className="text-white/30">$ </span>codex login --device-auth + <span className="text-white/50">$ </span>codex login --device-auth </pre> </div> </div> @@ -461,22 +461,22 @@ function CodexLoginSection() { </ol> <div className="rounded-xl bg-black/30 border border-white/[0.06] overflow-hidden"> <pre className="p-3 text-xs font-mono text-emerald-400 overflow-x-auto"> - <span className="text-white/30"># On laptop:</span>{"\n"} - <span className="text-white/30">$ </span>ssh -L 1455:localhost:1455 ubuntu@YOUR_VPS_IP{"\n"} - <span className="text-white/30"># Then on VPS:</span>{"\n"} - <span className="text-white/30">$ </span>codex login + <span className="text-white/50"># On laptop:</span>{"\n"} + <span className="text-white/50">$ </span>ssh -L 1455:localhost:1455 ubuntu@YOUR_VPS_IP{"\n"} + <span className="text-white/50"># Then on VPS:</span>{"\n"} + <span className="text-white/50">$ </span>codex login </pre> </div> </div> {/* Option 3: Standard */} <div> - <p className="text-xs font-semibold text-white/40 mb-2"> + <p className="text-xs font-semibold text-white/60 mb-2"> Option 3: Standard (if you have a browser) </p> <div className="rounded-xl bg-black/30 border border-white/[0.06] overflow-hidden"> <pre className="p-3 text-sm font-mono text-emerald-400"> - <span className="text-white/30">$ </span>codex login + <span className="text-white/50">$ </span>codex login </pre> </div> </div> diff --git a/apps/web/components/lessons/beads-lesson.tsx b/apps/web/components/lessons/beads-lesson.tsx index 81884f8c..8c109549 100644 --- a/apps/web/components/lessons/beads-lesson.tsx +++ b/apps/web/components/lessons/beads-lesson.tsx @@ -50,8 +50,8 @@ export function BeadsLesson() { machine-readable outputs for agents. </Paragraph> <TipBox variant="info"> - The <code>bd</code> command is an alias for <code>br</code> for backward compatibility. - Both commands work identically. + <code>br</code> is the CLI for the beads_rust issue tracker. + Use <code>br --help</code> for all available commands. </TipBox> <div className="mt-8"> @@ -213,7 +213,7 @@ export function BeadsLesson() { <PriorityRow priority="1" label="High" description="Important work" color="text-amber-400" /> <PriorityRow priority="2" label="Medium" description="Default priority" color="text-primary" /> <PriorityRow priority="3" label="Low" description="Nice to have" color="text-white/60" /> - <PriorityRow priority="4" label="Backlog" description="Future consideration" color="text-white/40" /> + <PriorityRow priority="4" label="Backlog" description="Future consideration" color="text-white/60" /> </div> </div> </div> @@ -383,8 +383,8 @@ function PriorityRow({ > <span className={`text-sm font-mono font-bold w-6 text-center ${color}`}>{priority}</span> <span className={`text-sm font-medium ${color}`}>{label}</span> - <span className="text-xs text-white/40">—</span> - <span className="text-xs text-white/40 group-hover:text-white/60 transition-colors">{description}</span> + <span className="text-xs text-white/50">—</span> + <span className="text-xs text-white/60 group-hover:text-white/80 transition-colors">{description}</span> </motion.div> ); } diff --git a/apps/web/components/lessons/cm-lesson.tsx b/apps/web/components/lessons/cm-lesson.tsx index 7ac286a2..2167221a 100644 --- a/apps/web/components/lessons/cm-lesson.tsx +++ b/apps/web/components/lessons/cm-lesson.tsx @@ -376,7 +376,7 @@ function MemoryDiagram() { <Database className="h-10 w-10 text-blue-400" /> </div> <span className="text-sm font-semibold text-white group-hover:text-blue-400 transition-colors">Past Sessions</span> - <span className="text-xs text-white/40">Raw conversations</span> + <span className="text-xs text-white/60">Raw conversations</span> </motion.div> {/* Arrow */} @@ -384,7 +384,7 @@ function MemoryDiagram() { initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.2 }} - className="text-white/30 text-2xl hidden md:block" + className="text-white/50 text-2xl hidden md:block" > → </motion.div> @@ -392,7 +392,7 @@ function MemoryDiagram() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.2 }} - className="text-white/30 text-2xl md:hidden rotate-90" + className="text-white/50 text-2xl md:hidden rotate-90" > → </motion.div> @@ -409,7 +409,7 @@ function MemoryDiagram() { <Brain className="h-10 w-10 text-primary" /> </div> <span className="text-sm font-semibold text-white group-hover:text-primary transition-colors">CM Analysis</span> - <span className="text-xs text-white/40">Extract lessons</span> + <span className="text-xs text-white/60">Extract lessons</span> </motion.div> {/* Arrow */} @@ -417,7 +417,7 @@ function MemoryDiagram() { initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.3 }} - className="text-white/30 text-2xl hidden md:block" + className="text-white/50 text-2xl hidden md:block" > → </motion.div> @@ -425,7 +425,7 @@ function MemoryDiagram() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }} - className="text-white/30 text-2xl md:hidden rotate-90" + className="text-white/50 text-2xl md:hidden rotate-90" > → </motion.div> @@ -442,7 +442,7 @@ function MemoryDiagram() { <BookOpen className="h-10 w-10 text-emerald-400" /> </div> <span className="text-sm font-semibold text-white group-hover:text-emerald-400 transition-colors">Playbook</span> - <span className="text-xs text-white/40">Actionable rules</span> + <span className="text-xs text-white/60">Actionable rules</span> </motion.div> </div> </div> diff --git a/apps/web/components/lessons/flywheel-loop-lesson.tsx b/apps/web/components/lessons/flywheel-loop-lesson.tsx index fd3cf78c..a638b274 100644 --- a/apps/web/components/lessons/flywheel-loop-lesson.tsx +++ b/apps/web/components/lessons/flywheel-loop-lesson.tsx @@ -257,7 +257,7 @@ dcg doctor # Check status`, {...{ code: `# 1. Plan your work bv --robot-triage # Check tasks -bd ready # See what's ready to work on +br ready # See what's ready to work on # 2. Start your agents ntm spawn myproject --cc=2 --cod=1 @@ -279,7 +279,7 @@ ubs . # Check for bugs cm reflect # Distill learnings # 8. Close the task -bd close <task-id>`, +br close <task-id>`, showLineNumbers: true, }} /> @@ -334,14 +334,14 @@ mkcd /data/projects/my-first-project git init # 3. Initialize beads for task tracking -bd init +br init # (Recommended) Create a dedicated Beads sync branch # Beads uses git worktrees for syncing; syncing to your current branch (often \`main\`) # can cause worktree conflicts. Once you have a \`main\` branch and a remote, run: git branch beads-sync main git push -u origin beads-sync -bd config set sync.branch=beads-sync +br config set sync.branch=beads-sync # 4. Spawn your agents ntm spawn my-first-project --cc=2 --cod=1 --gmi=1 @@ -415,7 +415,7 @@ function FlywheelDiagram() { delay: 0.1, }} /> - <ArrowRight className="h-5 w-5 text-white/30 hidden md:block" /> + <ArrowRight className="h-5 w-5 text-white/50 hidden md:block" /> <FlywheelNode {...{ label: "Coordinate", @@ -425,7 +425,7 @@ function FlywheelDiagram() { delay: 0.2, }} /> - <ArrowRight className="h-5 w-5 text-white/30 hidden md:block" /> + <ArrowRight className="h-5 w-5 text-white/50 hidden md:block" /> <FlywheelNode {...{ label: "Execute", @@ -448,7 +448,7 @@ function FlywheelDiagram() { delay: 0.4, }} /> - <ArrowRight className="h-5 w-5 text-white/30 rotate-180 hidden md:block" /> + <ArrowRight className="h-5 w-5 text-white/50 rotate-180 hidden md:block" /> <FlywheelNode {...{ label: "Scan", @@ -550,7 +550,7 @@ function ToolCard({ <h4 className="font-bold text-white"> {number}. {name} </h4> - <span className="text-xs text-white/40">- {subtitle}</span> + <span className="text-xs text-white/60">- {subtitle}</span> </div> <code className="inline-block px-2 py-1 rounded bg-black/30 border border-white/[0.08] text-xs font-mono text-primary mb-3"> {command} diff --git a/apps/web/components/lessons/keeping-updated-lesson.tsx b/apps/web/components/lessons/keeping-updated-lesson.tsx index 06cceedb..17f4e739 100644 --- a/apps/web/components/lessons/keeping-updated-lesson.tsx +++ b/apps/web/components/lessons/keeping-updated-lesson.tsx @@ -215,7 +215,7 @@ sudo dpkg --configure -a`} title="Agent update failed" description="Try updating directly:" solution={`# Claude -claude update +claude update --channel latest # Codex bun install -g --trust @openai/codex@latest diff --git a/apps/web/components/lessons/lesson-components.tsx b/apps/web/components/lessons/lesson-components.tsx index 718f9cfb..b568add8 100644 --- a/apps/web/components/lessons/lesson-components.tsx +++ b/apps/web/components/lessons/lesson-components.tsx @@ -5,13 +5,16 @@ import { motion } from "@/components/motion"; import { Check, Copy, - Terminal, Lightbulb, AlertTriangle, ChevronRight, Sparkles, Zap, } from "lucide-react"; +import { + CodeBlock as SharedCodeBlock, + type CodeBlockProps as SharedCodeBlockProps, +} from "@/components/ui/code-block"; // ============================================================================= // SECTION COMPONENT - Beautiful section dividers with gradient headers @@ -69,105 +72,12 @@ export function Paragraph({ children, highlight }: ParagraphProps) { } // ============================================================================= -// CODE BLOCK - Interactive terminal-style code display +// CODE BLOCK - Re-exported from shared ui/code-block // ============================================================================= -interface CodeBlockProps { - code: string; - language?: string; - filename?: string; - showLineNumbers?: boolean; -} - -export function CodeBlock({ - code, - language = "bash", - filename, - showLineNumbers = false, -}: CodeBlockProps) { - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - // Clipboard access denied - silently fail - } - }; - - const lines = code.trim().split("\n"); - - return ( - <div className="group relative rounded-2xl overflow-hidden border border-white/[0.08] bg-black/60 backdrop-blur-xl"> - {/* Terminal header */} - <div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] bg-white/[0.02]"> - <div className="flex items-center gap-3"> - <div className="flex items-center gap-1.5"> - <div className="w-3 h-3 rounded-full bg-red-500/80" /> - <div className="w-3 h-3 rounded-full bg-yellow-500/80" /> - <div className="w-3 h-3 rounded-full bg-green-500/80" /> - </div> - {filename && ( - <span className="text-xs text-white/40 font-mono">{filename}</span> - )} - {!filename && ( - <div className="flex items-center gap-1.5 text-white/40"> - <Terminal className="h-3.5 w-3.5" /> - <span className="text-xs font-mono">{language}</span> - </div> - )} - </div> - <button - type="button" - onClick={handleCopy} - className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-300 text-white/40 hover:text-white hover:bg-white/10" - > - {copied ? ( - <> - <Check className="h-3.5 w-3.5 text-emerald-400" /> - <span className="text-emerald-400">Copied!</span> - </> - ) : ( - <> - <Copy className="h-3.5 w-3.5" /> - <span>Copy</span> - </> - )} - </button> - </div> - - {/* Code content */} - <div className="relative p-5 overflow-x-auto"> - <pre className="font-mono text-sm"> - {lines.map((line, i) => ( - <div key={i} className="flex"> - {showLineNumbers && ( - <span className="select-none w-8 text-white/20 text-right pr-4"> - {i + 1} - </span> - )} - <code className="text-white/90"> - {line.startsWith("$") ? ( - <> - <span className="text-emerald-400">$</span> - <span className="text-white/90">{line.slice(1)}</span> - </> - ) : line.startsWith("#") ? ( - <span className="text-white/40">{line}</span> - ) : ( - line - )} - </code> - </div> - ))} - </pre> - - {/* Subtle glow effect */} - <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-violet-500/5 pointer-events-none" /> - </div> - </div> - ); +export function CodeBlock( + props: Omit<SharedCodeBlockProps, "variant" | "copyable">, +) { + return <SharedCodeBlock {...props} variant="terminal" copyable />; } // ============================================================================= @@ -204,7 +114,7 @@ export function FeatureCard({ > {/* Gradient overlay on hover */} <div - className={`absolute inset-0 rounded-2xl bg-gradient-to-br ${gradient} opacity-0 group-hover:opacity-100 transition-opacity duration-500`} + className={`absolute inset-0 rounded-2xl bg-gradient-to-br ${gradient} opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-500`} /> <div className="relative flex items-start gap-4"> @@ -323,12 +233,12 @@ export function CommandList({ commands }: CommandListProps) { onClick={() => handleCopy(cmd.command, i)} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-black/40 border border-white/[0.08] font-mono text-sm text-emerald-400 transition-all duration-300 hover:bg-black/60 hover:border-emerald-500/30" > - <span className="text-white/30">$</span> + <span className="text-white/50">$</span> <span>{cmd.command}</span> {copiedIndex === i ? ( <Check className="h-4 w-4 text-emerald-400" /> ) : ( - <Copy className="h-4 w-4 text-white/30 opacity-0 group-hover:opacity-100 transition-opacity" /> + <Copy className="h-4 w-4 text-white/50 opacity-0 group-hover:opacity-100 transition-opacity" /> )} </button> <span className="text-white/50 text-sm">{cmd.description}</span> @@ -412,7 +322,7 @@ export function DiagramArrow({ direction = "right" }: { direction?: "right" | "d return ( <div className={`flex items-center justify-center ${direction === "down" ? "py-2" : "px-2"}`}> <ChevronRight - className={`h-6 w-6 text-white/30 ${direction === "down" ? "rotate-90" : ""}`} + className={`h-6 w-6 text-white/50 ${direction === "down" ? "rotate-90" : ""}`} /> </div> ); diff --git a/apps/web/components/lessons/ntm-core-lesson.tsx b/apps/web/components/lessons/ntm-core-lesson.tsx index 33a5d27d..bac649f1 100644 --- a/apps/web/components/lessons/ntm-core-lesson.tsx +++ b/apps/web/components/lessons/ntm-core-lesson.tsx @@ -499,7 +499,7 @@ function KeyboardShortcutTable({ {key} </kbd> {j < shortcut.keys.length - 1 && ( - <span className="text-white/30 text-xs">then</span> + <span className="text-white/50 text-xs">then</span> )} </span> ))} diff --git a/apps/web/components/lessons/prompt-engineering-lesson.tsx b/apps/web/components/lessons/prompt-engineering-lesson.tsx index a28c00cf..795a58de 100644 --- a/apps/web/components/lessons/prompt-engineering-lesson.tsx +++ b/apps/web/components/lessons/prompt-engineering-lesson.tsx @@ -666,7 +666,7 @@ function TemporalConcept({ > <Clock className="h-4 w-4 text-primary shrink-0" /> <span className="font-medium text-white">{concept}</span> - <span className="text-white/40">—</span> + <span className="text-white/50">—</span> <span className="text-sm text-white/50">{description}</span> </motion.div> ); @@ -691,7 +691,7 @@ function PrincipleCard({ > <Layers className="h-4 w-4 text-blue-400 shrink-0" /> <span className="font-medium text-blue-300">{principle}</span> - <span className="text-white/40">—</span> + <span className="text-white/50">—</span> <span className="text-sm text-white/50">{description}</span> </motion.div> ); @@ -722,7 +722,7 @@ function PatternBreakdown() { {patterns.map((p, i) => ( <div key={i} className="flex items-center gap-3 text-sm"> <span className="w-32 shrink-0 text-primary font-medium">{p.name}</span> - <span className="text-white/40">←</span> + <span className="text-white/50">←</span> <code className="text-white/60 font-mono text-xs">"{p.line}"</code> </div> ))} @@ -751,15 +751,15 @@ function QuickRefItem({ className="group grid grid-cols-3 gap-4 p-4 rounded-xl border border-white/[0.08] bg-white/[0.02] transition-all duration-300 hover:border-primary/30" > <div> - <span className="text-xs text-white/40 uppercase">Pattern</span> + <span className="text-xs text-white/60 uppercase">Pattern</span> <p className="font-bold text-primary">{pattern}</p> </div> <div> - <span className="text-xs text-white/40 uppercase">When</span> + <span className="text-xs text-white/60 uppercase">When</span> <p className="text-sm text-white/70">{when}</p> </div> <div> - <span className="text-xs text-white/40 uppercase">Key Phrases</span> + <span className="text-xs text-white/60 uppercase">Key Phrases</span> <p className="text-xs text-white/50 font-mono">{key_phrases}</p> </div> </motion.div> diff --git a/apps/web/components/lessons/real-world-case-study-lesson.tsx b/apps/web/components/lessons/real-world-case-study-lesson.tsx index 6eef4e26..75693d72 100644 --- a/apps/web/components/lessons/real-world-case-study-lesson.tsx +++ b/apps/web/components/lessons/real-world-case-study-lesson.tsx @@ -379,13 +379,13 @@ Write it to PLAN_FOR_CASS_MEMORY_SYSTEM.md"`} <div className="mt-6"> <CodeBlock code={`# Initialize beads in the project -bd init +br init # Have an agent transform the plan into beads cc "Read PLAN_FOR_CASS_MEMORY_SYSTEM.md carefully. Transform each section, feature, and implementation detail -into individual beads using the bd CLI. +into individual beads using the br CLI. Create epics for major phases, then break them into tasks. Set up dependencies so blockers are clear. @@ -437,13 +437,13 @@ ntm spawn cass-memory --cc=6 --cod=3 --gmi=2 bv --robot-triage # 2. Claim a task -bd update <id> --status in_progress +br update <id> --status in_progress # 3. Implement # (agent does the work) # 4. Close when done -bd close <id> +br close <id> # 5. Repeat`} showLineNumbers @@ -675,9 +675,9 @@ Create a hybrid plan taking the best of each. Write to PLAN.md" # 3. Transform plan into beads -bd init +br init cc "Read PLAN.md. Transform into 100+ beads with -dependencies and priorities. Use bd CLI." +dependencies and priorities. Use br CLI." # 4. Launch the swarm ntm spawn myproject --cc=3 --cod=2 --gmi=1 diff --git a/apps/web/components/lessons/safety-tools-lesson.tsx b/apps/web/components/lessons/safety-tools-lesson.tsx index eca7b34f..30a9b459 100644 --- a/apps/web/components/lessons/safety-tools-lesson.tsx +++ b/apps/web/components/lessons/safety-tools-lesson.tsx @@ -479,7 +479,7 @@ function SlbDiagram() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.1 }} - className="text-white/30 text-xl" + className="text-white/50 text-xl" > ↓ </motion.div> @@ -499,7 +499,7 @@ function SlbDiagram() { <span className="text-xs text-white/50 font-medium group-hover:text-emerald-400 transition-colors">Agent 1</span> </motion.div> - <span className="text-white/30 text-xl">+</span> + <span className="text-white/50 text-xl">+</span> <motion.div initial={{ opacity: 0, x: 20 }} @@ -520,7 +520,7 @@ function SlbDiagram() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.35 }} - className="text-white/30 text-xl" + className="text-white/50 text-xl" > ↓ </motion.div> @@ -632,7 +632,7 @@ function UseCase({ </div> <div> <span className="font-medium text-white group-hover:text-primary transition-colors">{scenario}</span> - <span className="text-white/40 mx-2">—</span> + <span className="text-white/50 mx-2">—</span> <span className="text-sm text-white/50">{description}</span> </div> </motion.div> diff --git a/apps/web/components/lessons/slb-case-study-lesson.tsx b/apps/web/components/lessons/slb-case-study-lesson.tsx index decc0a84..b9c4a7f4 100644 --- a/apps/web/components/lessons/slb-case-study-lesson.tsx +++ b/apps/web/components/lessons/slb-case-study-lesson.tsx @@ -188,7 +188,7 @@ a comprehensive and granular set of beads with: - Background, reasoning, justification - Anything our 'future self' would need to know -Use the bd tool repeatedly to create the actual beads."`} +Use the br tool repeatedly to create the actual beads."`} showLineNumbers /> </div> @@ -284,9 +284,9 @@ ntm spawn slb --cc=3 --cod=2 # Each agent runs: bv --robot-triage # What's ready? -bd update <id> --status in_progress +br update <id> --status in_progress # ... implement ... -bd close <id> +br close <id> # Commit agent runs every 15-20 min cc "Commit all changes in logical groupings with @@ -421,7 +421,7 @@ cc "Read the plan and all feedback. Create a revised plan incorporating the best suggestions." cc "Convert the plan into 50-100 beads with -dependencies. Use bd CLI." +dependencies. Use br CLI." # Hour 4+: Implementation ntm spawn myproject --cc=2 --cod=1 @@ -519,7 +519,7 @@ function TimelineCard() { <span className="text-xs font-mono text-white/50 w-20"> {step.time} </span> - <ArrowRight className="h-3 w-3 text-white/30 group-hover:text-primary/50 transition-colors" /> + <ArrowRight className="h-3 w-3 text-white/50 group-hover:text-primary/60 transition-colors" /> <span className="text-sm text-white/70 group-hover:text-white/90 transition-colors">{step.event}</span> </div> </motion.div> diff --git a/apps/web/components/lessons/tmux-basics-lesson.tsx b/apps/web/components/lessons/tmux-basics-lesson.tsx index 1daecd26..b5a55843 100644 --- a/apps/web/components/lessons/tmux-basics-lesson.tsx +++ b/apps/web/components/lessons/tmux-basics-lesson.tsx @@ -279,7 +279,7 @@ function CommandSection({ {key} </kbd> {i < keyCombo.length - 1 && ( - <span className="text-white/30">then</span> + <span className="text-white/50">then</span> )} </span> ))} @@ -320,7 +320,7 @@ function KeyboardShortcutGrid({ shortcuts }: { shortcuts: ShortcutItem[] }) { {key} </kbd> {j < shortcut.keys.length - 1 && ( - <span className="text-white/30 text-xs">+</span> + <span className="text-white/50 text-xs">+</span> )} </span> ))} diff --git a/apps/web/components/lessons/ubs-lesson.tsx b/apps/web/components/lessons/ubs-lesson.tsx index ee18061c..1a74adf2 100644 --- a/apps/web/components/lessons/ubs-lesson.tsx +++ b/apps/web/components/lessons/ubs-lesson.tsx @@ -339,7 +339,7 @@ function OutputExplainer({ className="group flex items-center gap-4 p-4 rounded-xl bg-white/[0.02] border border-white/[0.06] backdrop-blur-xl transition-all duration-300 hover:border-white/[0.12] hover:bg-white/[0.04]" > <code className={`font-mono text-sm font-medium ${color}`}>{pattern}</code> - <span className="text-white/40 group-hover:text-white/60 transition-colors">→</span> + <span className="text-white/50 group-hover:text-white/70 transition-colors">→</span> <span className="text-white/60 group-hover:text-white/80 transition-colors">{meaning}</span> </motion.div> ); diff --git a/apps/web/components/lessons/welcome-lesson.tsx b/apps/web/components/lessons/welcome-lesson.tsx index 9de97339..418a6809 100644 --- a/apps/web/components/lessons/welcome-lesson.tsx +++ b/apps/web/components/lessons/welcome-lesson.tsx @@ -320,7 +320,7 @@ function VPSComponent({ > <div className="text-white/60 mb-1 group-hover:text-primary group-hover:scale-110 transition-all duration-300">{icon}</div> <span className="text-xs font-medium text-white group-hover:text-primary transition-colors">{label}</span> - <span className="text-[10px] text-white/40 group-hover:text-white/60 transition-colors">{sublabel}</span> + <span className="text-xs text-white/60 group-hover:text-white/80 transition-colors">{sublabel}</span> </motion.div> ); } diff --git a/apps/web/components/motion/index.tsx b/apps/web/components/motion/index.tsx index d9b5c705..738d4341 100644 --- a/apps/web/components/motion/index.tsx +++ b/apps/web/components/motion/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { m, AnimatePresence, type Variants } from "framer-motion"; +import { m, AnimatePresence, useReducedMotion, type Variants } from "framer-motion"; /** * Spring configurations optimized for Stripe/Linear-style subtle, professional feel. @@ -138,6 +138,172 @@ export const staggerSlow: Variants = { }, }; +// ============================================================================= +// MODAL & SHEET ENTRANCE VARIANTS +// ============================================================================= + +/** Modal entrance - scale and fade from center (dialogs, popups) */ +export const modalEntrance: Variants = { + hidden: { + opacity: 0, + scale: 0.95, + y: 10, + }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: springs.smooth, + }, + exit: { + opacity: 0, + scale: 0.98, + y: 5, + transition: { duration: 0.15 }, + }, +}; + +/** Bottom sheet entrance - slide from bottom with spring physics */ +export const sheetEntrance: Variants = { + hidden: { + y: "100%", + opacity: 0.8, + }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 300, + damping: 30, + }, + }, + exit: { + y: "100%", + opacity: 0.8, + transition: { duration: 0.2 }, + }, +}; + +// ============================================================================= +// PREMIUM SCROLL REVEAL VARIANTS +// ============================================================================= + +/** Fade up with blur effect - premium reveal for hero sections */ +export const fadeUpBlur: Variants = { + hidden: { + opacity: 0, + y: 30, + filter: "blur(10px)", + }, + visible: { + opacity: 1, + y: 0, + filter: "blur(0px)", + transition: springs.smooth, + }, + exit: { + opacity: 0, + y: -15, + filter: "blur(5px)", + transition: { duration: 0.2 }, + }, +}; + +/** Scale up entrance - great for badges, pills, and small UI elements */ +export const scaleUp: Variants = { + hidden: { + opacity: 0, + scale: 0.8, + }, + visible: { + opacity: 1, + scale: 1, + transition: springs.snappy, + }, + exit: { + opacity: 0, + scale: 0.9, + transition: { duration: 0.1 }, + }, +}; + +// ============================================================================= +// ADDITIONAL STAGGER VARIANTS +// ============================================================================= + +/** Micro stagger for pill/tag lists - very quick succession */ +export const staggerMicro: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.02, + delayChildren: 0, + }, + }, +}; + +/** Cascade stagger for dashboard cards - elegant delayed reveal */ +export const staggerCascade: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.08, + delayChildren: 0.15, + staggerDirection: 1, + }, + }, +}; + +// ============================================================================= +// PRESENCE ANIMATION HELPERS +// ============================================================================= + +/** Motion props type for presence animations */ +export interface PresenceMotionProps { + initial: "hidden" | false; + animate: "visible"; + exit?: "exit"; + variants: Variants; +} + +/** + * Get animation props that respect reduced motion preference. + * Use this to conditionally apply animations based on user preferences. + * + * @param variants - The animation variants to use + * @param prefersReducedMotion - Whether user prefers reduced motion + * @returns Motion props object ready to spread onto a motion component + * + * @example + * ```tsx + * const prefersReducedMotion = useReducedMotion(); + * return ( + * <motion.div {...getPresenceProps(modalEntrance, prefersReducedMotion ?? false)}> + * {children} + * </motion.div> + * ); + * ``` + */ +export function getPresenceProps( + variants: Variants, + prefersReducedMotion: boolean +): PresenceMotionProps { + if (prefersReducedMotion) { + return { + initial: false, + animate: "visible", + variants, + }; + } + return { + initial: "hidden", + animate: "visible", + exit: "exit", + variants, + }; +} + /** * Button/interactive element hover and tap props * Use with spread: {...buttonMotion} @@ -199,5 +365,5 @@ export const MotionH4 = m.h4; // Re-export `m` as `motion` for backwards compatibility with LazyMotion strict mode // This allows existing code using `motion.div` etc. to work without changes -export { m as motion, AnimatePresence }; +export { m as motion, AnimatePresence, useReducedMotion }; export type { Variants }; diff --git a/apps/web/components/stepper.tsx b/apps/web/components/stepper.tsx index 78445fe1..3457b393 100644 --- a/apps/web/components/stepper.tsx +++ b/apps/web/components/stepper.tsx @@ -189,11 +189,15 @@ export function StepperMobile({ axis: "x", filterTaps: true, threshold: 10, + // Prevent the gesture library from calling preventDefault() on touch + // events, which breaks native scrolling and tap handling on Mobile Safari + preventScrollAxis: "y", + pointer: { touch: true }, } ); return ( - <div {...bind()} className={cn("touch-pan-x select-none", className)}> + <div {...bind()} className={cn("select-none", className)} style={{ touchAction: "pan-x pan-y" }}> {/* Progress bar with animated gradient */} <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"> <motion.div diff --git a/apps/web/components/tldr/tldr-hero.tsx b/apps/web/components/tldr/tldr-hero.tsx index 5f2027fe..6607e398 100644 --- a/apps/web/components/tldr/tldr-hero.tsx +++ b/apps/web/components/tldr/tldr-hero.tsx @@ -2,7 +2,7 @@ import { useRef, useState, useEffect } from "react"; import { motion, useReducedMotion, useInView, AnimatePresence } from "framer-motion"; -import { Cog, Zap, GitBranch, ArrowDown } from "lucide-react"; +import { Cog, Zap, GitBranch, ArrowDown, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { tldrPageData } from "@/lib/tldr-content"; @@ -16,7 +16,7 @@ interface TldrHeroProps { } // ============================================================================= -// ANIMATED STAT COMPONENT +// ANIMATED STAT COMPONENT - Enhanced with glassmorphism // ============================================================================= function AnimatedStat({ @@ -34,24 +34,39 @@ function AnimatedStat({ }) { return ( <motion.div - initial={reducedMotion ? {} : { opacity: 0, y: 20 }} - animate={isInView ? { opacity: 1, y: 0 } : {}} + initial={reducedMotion ? {} : { opacity: 0, y: 20, scale: 0.9 }} + animate={isInView ? { opacity: 1, y: 0, scale: 1 } : {}} transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.3 + index * 0.1, + type: "spring", + stiffness: 100, }} - className="text-center" + className="group relative text-center" > - <div className="text-2xl font-bold text-white sm:text-3xl md:text-4xl">{value}</div> - <div className="mt-1 text-xs font-medium uppercase tracking-wider text-muted-foreground"> - {label} + {/* Glassmorphism card */} + <div className="relative rounded-2xl border border-white/10 bg-white/5 px-4 py-3 backdrop-blur-sm transition-all duration-300 hover:border-white/20 hover:bg-white/10 sm:px-6 sm:py-4"> + {/* Subtle glow on hover */} + <div className="pointer-events-none absolute -inset-px rounded-2xl bg-gradient-to-b from-primary/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> + + {/* Value with gradient */} + <div className="relative text-2xl font-bold sm:text-3xl md:text-4xl"> + <span className="bg-gradient-to-r from-white via-white to-white/80 bg-clip-text text-transparent"> + {value} + </span> + </div> + + {/* Label */} + <div className="relative mt-1 text-xs font-medium uppercase tracking-wider text-muted-foreground"> + {label} + </div> </div> </motion.div> ); } // ============================================================================= -// FLOATING ICON COMPONENT +// FLOATING ICON COMPONENT - Enhanced with depth and glow // ============================================================================= function FloatingIcon({ @@ -59,26 +74,118 @@ function FloatingIcon({ className, delay = 0, reducedMotion, + floatDirection = "up", }: { icon: React.ComponentType<{ className?: string }>; className?: string; delay?: number; reducedMotion: boolean; + floatDirection?: "up" | "down" | "left" | "right"; }) { + const floatAnimations = { + up: { y: [0, -10, 0] }, + down: { y: [0, 10, 0] }, + left: { x: [0, -10, 0] }, + right: { x: [0, 10, 0] }, + }; + return ( <motion.div initial={reducedMotion ? {} : { opacity: 0, scale: 0.5 }} - animate={{ opacity: 1, scale: 1 }} + animate={{ + opacity: 1, + scale: 1, + ...(reducedMotion ? {} : floatAnimations[floatDirection]), + }} transition={{ - duration: reducedMotion ? 0 : 0.6, - delay: reducedMotion ? 0 : delay, + opacity: { duration: reducedMotion ? 0 : 0.6, delay: reducedMotion ? 0 : delay }, + scale: { duration: reducedMotion ? 0 : 0.6, delay: reducedMotion ? 0 : delay }, + y: { duration: 4, repeat: Infinity, ease: "easeInOut" }, + x: { duration: 4, repeat: Infinity, ease: "easeInOut" }, }} className={cn( - "absolute flex items-center justify-center rounded-2xl bg-gradient-to-br p-3 shadow-lg", + "absolute flex items-center justify-center rounded-2xl bg-gradient-to-br p-3 shadow-2xl", + "ring-1 ring-white/10", className )} + style={{ + boxShadow: "0 20px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.1)", + }} + > + <Icon className="h-6 w-6 text-white drop-shadow-lg" /> + {/* Inner glow */} + <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-t from-transparent to-white/20" /> + </motion.div> + ); +} + +// ============================================================================= +// GRADIENT TEXT SHIMMER EFFECT +// ============================================================================= + +function ShimmerText({ children, className }: { children: React.ReactNode; className?: string }) { + return ( + <span className={cn("relative inline-block", className)}> + <span className="bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent"> + {children} + </span> + {/* Shimmer overlay */} + <span + className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent bg-clip-text text-transparent animate-shimmer" + style={{ + backgroundSize: "200% 100%", + animation: "shimmer 3s linear infinite", + }} + aria-hidden="true" + > + {children} + </span> + </span> + ); +} + +// ============================================================================= +// ANIMATED SCROLL INDICATOR +// ============================================================================= + +function ScrollIndicator({ reducedMotion }: { reducedMotion: boolean }) { + return ( + <motion.div + initial={reducedMotion ? {} : { opacity: 0 }} + animate={{ opacity: 1 }} + exit={reducedMotion ? {} : { opacity: 0, y: 20 }} + transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.6 }} + className="mt-16 flex justify-center" > - <Icon className="h-6 w-6 text-white" /> + <motion.div + animate={reducedMotion ? {} : { y: [0, 8, 0] }} + transition={{ + duration: 2, + repeat: Infinity, + ease: "easeInOut", + }} + className="group flex cursor-pointer flex-col items-center gap-2 text-muted-foreground transition-colors hover:text-white" + onClick={() => { + window.scrollTo({ top: window.innerHeight * 0.8, behavior: "smooth" }); + }} + > + <span className="text-xs font-medium uppercase tracking-wider"> + Scroll to explore + </span> + {/* Animated arrow container */} + <div className="relative flex h-10 w-6 items-center justify-center rounded-full border border-white/20 bg-white/5 backdrop-blur-sm transition-colors group-hover:border-white/40 group-hover:bg-white/10"> + <motion.div + animate={reducedMotion ? {} : { y: [0, 4, 0] }} + transition={{ + duration: 1.5, + repeat: Infinity, + ease: "easeInOut", + }} + > + <ArrowDown className="h-4 w-4" /> + </motion.div> + </div> + </motion.div> </motion.div> ); } @@ -114,48 +221,87 @@ export function TldrHero({ className, id }: TldrHeroProps) { ref={containerRef} className={cn("relative overflow-hidden py-16 md:py-32", className)} > - {/* Background gradient */} - <div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-transparent" /> + {/* CSS for shimmer animation */} + <style jsx global>{` + @keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + .animate-shimmer { + animation: shimmer 3s linear infinite; + } + `}</style> + + {/* Background layers */} + <div className="pointer-events-none absolute inset-0"> + {/* Primary gradient */} + <div className="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-transparent" /> + + {/* Radial glow from center */} + <div className="absolute left-1/2 top-1/4 h-[500px] w-[800px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/10 blur-[120px]" /> + + {/* Secondary accent glow */} + <div className="absolute right-1/4 top-1/3 h-[300px] w-[400px] rounded-full bg-accent/5 blur-[100px]" /> + </div> - {/* Decorative grid - hidden on mobile for cleaner look */} - <div className="pointer-events-none absolute inset-0 hidden bg-[linear-gradient(to_right,rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:4rem_4rem] sm:block" /> + {/* Decorative grid - subtle dot pattern */} + <div + className="pointer-events-none absolute inset-0 hidden sm:block" + style={{ + backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.03) 1px, transparent 1px)`, + backgroundSize: "32px 32px", + }} + /> - {/* Floating icons - hidden on mobile to prevent overflow */} + {/* Floating icons - enhanced positioning and effects */} <div className="pointer-events-none absolute inset-0 hidden md:block" aria-hidden="true"> <FloatingIcon icon={Cog} className="left-[10%] top-[20%] from-primary to-primary/60" delay={0.8} reducedMotion={reducedMotion} + floatDirection="up" /> <FloatingIcon icon={Zap} - className="right-[15%] top-[30%] from-accent to-accent/60" + className="right-[15%] top-[25%] from-accent to-accent/60" delay={1.0} reducedMotion={reducedMotion} + floatDirection="down" /> <FloatingIcon icon={GitBranch} - className="bottom-[25%] left-[15%] from-success to-success/60" + className="bottom-[30%] left-[12%] from-success to-success/60" delay={1.2} reducedMotion={reducedMotion} + floatDirection="right" + /> + <FloatingIcon + icon={Sparkles} + className="bottom-[35%] right-[10%] from-violet-500 to-purple-600" + delay={1.4} + reducedMotion={reducedMotion} + floatDirection="left" /> </div> <div className="container relative mx-auto px-4"> <div className="mx-auto max-w-3xl text-center"> - {/* Badge */} + {/* Badge - enhanced with glow */} <motion.div - initial={reducedMotion ? {} : { opacity: 0, y: -10 }} - animate={isInView ? { opacity: 1, y: 0 } : {}} - transition={{ duration: reducedMotion ? 0 : 0.4 }} - className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary ring-1 ring-inset ring-primary/20" + initial={reducedMotion ? {} : { opacity: 0, y: -10, scale: 0.9 }} + animate={isInView ? { opacity: 1, y: 0, scale: 1 } : {}} + transition={{ duration: reducedMotion ? 0 : 0.4, type: "spring" }} + className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary ring-1 ring-inset ring-primary/20 backdrop-blur-sm" + style={{ + boxShadow: "0 0 30px -5px rgba(var(--primary-rgb, 139, 92, 246), 0.3)", + }} > <Cog className="h-4 w-4" /> <span>Open Source Ecosystem</span> </motion.div> - {/* Title */} + {/* Title - enhanced typography */} <motion.h1 initial={reducedMotion ? {} : { opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : {}} @@ -163,27 +309,27 @@ export function TldrHero({ className, id }: TldrHeroProps) { className="mt-6 text-3xl font-bold tracking-tight text-white sm:text-4xl md:text-6xl" > {hero.title} - <span className="block bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent"> + <ShimmerText className="block"> {hero.subtitle} - </span> + </ShimmerText> </motion.h1> - {/* Description */} + {/* Description - enhanced readability */} <motion.p initial={reducedMotion ? {} : { opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.2 }} - className="mt-4 text-base leading-relaxed text-muted-foreground sm:mt-6 sm:text-lg md:text-xl" + className="mt-4 text-base leading-relaxed text-muted-foreground/90 sm:mt-6 sm:text-lg md:text-xl" > {hero.description} </motion.p> - {/* Stats */} + {/* Stats - enhanced with glassmorphism cards */} <motion.div initial={reducedMotion ? {} : { opacity: 0 }} animate={isInView ? { opacity: 1 } : {}} transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.3 }} - className="mt-8 flex items-center justify-center gap-6 sm:mt-12 sm:gap-8 md:gap-16" + className="mt-8 flex flex-wrap items-center justify-center gap-3 sm:mt-12 sm:gap-4 md:gap-6" > {hero.stats.map((stat, i) => ( <AnimatedStat @@ -197,31 +343,10 @@ export function TldrHero({ className, id }: TldrHeroProps) { ))} </motion.div> - {/* Scroll indicator - hides after user scrolls */} + {/* Scroll indicator - enhanced design */} <AnimatePresence> {!hasScrolled && ( - <motion.div - initial={reducedMotion ? {} : { opacity: 0 }} - animate={isInView ? { opacity: 1 } : {}} - exit={reducedMotion ? {} : { opacity: 0, y: 20 }} - transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.6 }} - className="mt-16 flex justify-center" - > - <motion.div - animate={reducedMotion ? {} : { y: [0, 8, 0] }} - transition={{ - duration: 2, - repeat: Infinity, - ease: "easeInOut", - }} - className="flex flex-col items-center gap-2 text-muted-foreground" - > - <span className="text-xs font-medium uppercase tracking-wider"> - Scroll to explore - </span> - <ArrowDown className="h-5 w-5" /> - </motion.div> - </motion.div> + <ScrollIndicator reducedMotion={reducedMotion} /> )} </AnimatePresence> </div> diff --git a/apps/web/components/tldr/tldr-synergy-diagram.tsx b/apps/web/components/tldr/tldr-synergy-diagram.tsx index e1a7d7ea..dceaa5e8 100644 --- a/apps/web/components/tldr/tldr-synergy-diagram.tsx +++ b/apps/web/components/tldr/tldr-synergy-diagram.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useMemo } from "react"; +import { useRef, useMemo, useState, useCallback } from "react"; import { motion, useReducedMotion, useInView } from "framer-motion"; import { cn } from "@/lib/utils"; import { getColorDefinition } from "@/lib/colors"; @@ -20,6 +20,67 @@ interface NodePosition { y: number; } +// ============================================================================= +// CONSTANTS - Two-tier concentric layout with refined proportions +// ============================================================================= + +const DIAGRAM_CONFIG = { + viewBoxSize: 520, + center: 260, + innerRadius: 105, + innerNodeRadius: 28, + outerRadius: 195, + outerNodeRadius: 24, + centerRadius: 44, +}; + +// Primary tools shown in the inner ring (most connected/important) +const PRIMARY_TOOL_IDS = new Set([ + "mail", // Agent Mail - coordination hub + "bv", // Beads Viewer - task management hub + "cass", // Session Search - memory hub + "cm", // Memory System + "ubs", // Bug Scanner + "ntm", // Named Tmux Manager +]); + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function getCirclePosition( + index: number, + total: number, + radius: number, + center: number, + startAngle: number = -Math.PI / 2 +): NodePosition { + const angle = startAngle + (index / total) * 2 * Math.PI; + return { + x: center + Math.cos(angle) * radius, + y: center + Math.sin(angle) * radius, + }; +} + +function classifyTools(tools: TldrFlywheelTool[]): { + primary: TldrFlywheelTool[]; + secondary: TldrFlywheelTool[]; +} { + const coreTools = tools.filter((t) => t.category === "core"); + const primary: TldrFlywheelTool[] = []; + const secondary: TldrFlywheelTool[] = []; + + for (const tool of coreTools) { + if (PRIMARY_TOOL_IDS.has(tool.id)) { + primary.push(tool); + } else { + secondary.push(tool); + } + } + + return { primary, secondary }; +} + // ============================================================================= // MAIN COMPONENT // ============================================================================= @@ -32,53 +93,47 @@ export function TldrSynergyDiagram({ const isInView = useInView(containerRef, { once: true, margin: "-50px" }); const prefersReducedMotion = useReducedMotion(); const reducedMotion = prefersReducedMotion ?? false; + const [hoveredNode, setHoveredNode] = useState<string | null>(null); - // Filter to core tools only for the diagram - const coreTools = useMemo( - () => tools.filter((t) => t.category === "core"), - [tools] - ); + // Classify tools into two tiers + const { primary, secondary } = useMemo(() => classifyTools(tools), [tools]); - // Calculate node positions in a circle - adjust radius based on tool count + // Calculate node positions for both rings const nodePositions = useMemo(() => { const positions: Record<string, NodePosition> = {}; - const centerX = 200; - const centerY = 200; - // Scale radius based on number of tools to prevent overlap - const radius = coreTools.length > 8 ? 155 : 140; - - coreTools.forEach((tool, index) => { - // Start from top and go clockwise - const angle = (index / coreTools.length) * 2 * Math.PI - Math.PI / 2; - positions[tool.id] = { - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle), - }; + const { center, innerRadius, outerRadius } = DIAGRAM_CONFIG; + + // Inner ring (primary tools) + primary.forEach((tool, index) => { + positions[tool.id] = getCirclePosition(index, primary.length, innerRadius, center); + }); + + // Outer ring (secondary tools) + secondary.forEach((tool, index) => { + positions[tool.id] = getCirclePosition(index, secondary.length, outerRadius, center); }); return positions; - }, [coreTools]); + }, [primary, secondary]); - // Generate connection lines + // Generate connection lines between synergistic tools const connections = useMemo(() => { + const allCoreTools = [...primary, ...secondary]; const lines: Array<{ from: string; to: string; fromPos: NodePosition; toPos: NodePosition; }> = []; + const seen = new Set<string>(); - coreTools.forEach((tool) => { + allCoreTools.forEach((tool) => { tool.synergies.forEach((synergy) => { - const targetTool = coreTools.find((t) => t.id === synergy.toolId); + const targetTool = allCoreTools.find((t) => t.id === synergy.toolId); if (targetTool && nodePositions[tool.id] && nodePositions[synergy.toolId]) { - // Avoid duplicate lines (A->B and B->A) - const existingLine = lines.find( - (l) => - (l.from === synergy.toolId && l.to === tool.id) || - (l.from === tool.id && l.to === synergy.toolId) - ); - if (!existingLine) { + const key = [tool.id, synergy.toolId].sort().join("-"); + if (!seen.has(key)) { + seen.add(key); lines.push({ from: tool.id, to: synergy.toolId, @@ -91,7 +146,15 @@ export function TldrSynergyDiagram({ }); return lines; - }, [coreTools, nodePositions]); + }, [primary, secondary, nodePositions]); + + // Check if a connection should be highlighted + const isConnectionHighlighted = useCallback((from: string, to: string) => { + if (!hoveredNode) return false; + return from === hoveredNode || to === hoveredNode; + }, [hoveredNode]); + + const totalCoreTools = primary.length + secondary.length; return ( <div ref={containerRef} className={cn("relative", className)}> @@ -102,108 +165,226 @@ export function TldrSynergyDiagram({ className="mx-auto max-w-md" > <svg - viewBox="0 0 400 400" + viewBox={`0 0 ${DIAGRAM_CONFIG.viewBoxSize} ${DIAGRAM_CONFIG.viewBoxSize}`} className="h-auto w-full" aria-label="Flywheel tool synergy diagram showing connections between core tools" > - {/* All gradient definitions */} + {/* Definitions */} <defs> - <radialGradient id="centerGlow" cx="50%" cy="50%" r="50%"> - <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.2" /> + {/* Center glow gradient */} + <radialGradient id="tldr-centerGlow" cx="50%" cy="50%" r="50%"> + <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.3" /> + <stop offset="60%" stopColor="hsl(var(--primary))" stopOpacity="0.1" /> + <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0" /> + </radialGradient> + + {/* Ambient outer glow */} + <radialGradient id="tldr-ambientGlow" cx="50%" cy="50%" r="50%"> + <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.08" /> <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0" /> </radialGradient> - <linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%"> + + {/* Line gradient */} + <linearGradient id="tldr-lineGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.5" /> <stop offset="50%" stopColor="hsl(var(--primary))" stopOpacity="0.8" /> <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0.5" /> </linearGradient> + + {/* Highlighted line gradient */} + <linearGradient id="tldr-lineGradientHighlight" x1="0%" y1="0%" x2="100%" y2="0%"> + <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.8" /> + <stop offset="50%" stopColor="#fff" stopOpacity="0.9" /> + <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0.8" /> + </linearGradient> + {/* Tool-specific gradients */} - {coreTools.map((tool) => ( + {[...primary, ...secondary].map((tool) => ( <linearGradient - key={`gradient-${tool.id}`} - id={`gradient-${tool.id}`} + key={`tldr-gradient-${tool.id}`} + id={`tldr-gradient-${tool.id}`} x1="0%" y1="0%" x2="100%" y2="100%" > - <stop - offset="0%" - stopColor={getColorDefinition(tool.color).from} - /> - <stop - offset="100%" - stopColor={getColorDefinition(tool.color).to} - /> + <stop offset="0%" stopColor={getColorDefinition(tool.color).from} /> + <stop offset="100%" stopColor={getColorDefinition(tool.color).to} /> </linearGradient> ))} + + {/* Glow filter for highlighted state */} + <filter id="tldr-glow" x="-50%" y="-50%" width="200%" height="200%"> + <feGaussianBlur stdDeviation="3" result="blur" /> + <feMerge> + <feMergeNode in="blur" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> </defs> - {/* Center glow */} - <circle cx="200" cy="200" r="180" fill="url(#centerGlow)" /> + {/* Background ambient glow */} + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.outerRadius + 50} + fill="url(#tldr-ambientGlow)" + /> + + {/* Center glow effect */} + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.innerRadius * 0.85} + fill="url(#tldr-centerGlow)" + /> + + {/* Decorative orbital rings with animation */} + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.innerRadius} + fill="none" + stroke="hsl(var(--primary) / 0.15)" + strokeWidth="1.5" + strokeDasharray="6 8" + /> + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.outerRadius} + fill="none" + stroke="hsl(var(--primary) / 0.08)" + strokeWidth="1" + strokeDasharray="4 10" + /> {/* Connection lines */} <g className="connections"> - {connections.map((conn, index) => ( - <motion.line - key={`${conn.from}-${conn.to}`} - x1={conn.fromPos.x} - y1={conn.fromPos.y} - x2={conn.toPos.x} - y2={conn.toPos.y} - stroke="url(#lineGradient)" - strokeWidth="1.5" - strokeLinecap="round" - initial={reducedMotion ? {} : { opacity: 0 }} - animate={isInView ? { opacity: 1 } : {}} - transition={{ - duration: reducedMotion ? 0 : 0.8, - delay: reducedMotion ? 0 : 0.3 + index * 0.05, - }} - /> - ))} + {connections.map((conn, index) => { + const isHighlighted = isConnectionHighlighted(conn.from, conn.to); + return ( + <g key={`${conn.from}-${conn.to}`}> + {/* Glow layer when highlighted */} + {isHighlighted && ( + <motion.line + x1={conn.fromPos.x} + y1={conn.fromPos.y} + x2={conn.toPos.x} + y2={conn.toPos.y} + stroke="url(#tldr-lineGradientHighlight)" + strokeWidth="6" + strokeLinecap="round" + style={{ filter: "blur(4px)", opacity: 0.5 }} + initial={{ opacity: 0 }} + animate={{ opacity: 0.5 }} + /> + )} + {/* Main line */} + <motion.line + x1={conn.fromPos.x} + y1={conn.fromPos.y} + x2={conn.toPos.x} + y2={conn.toPos.y} + stroke={isHighlighted ? "url(#tldr-lineGradientHighlight)" : "url(#tldr-lineGradient)"} + strokeWidth={isHighlighted ? 2 : 1} + strokeLinecap="round" + initial={reducedMotion ? {} : { opacity: 0 }} + animate={isInView ? { opacity: isHighlighted ? 1 : 0.5 } : {}} + transition={{ + duration: reducedMotion ? 0 : 0.6, + delay: reducedMotion ? 0 : 0.3 + index * 0.02, + }} + /> + {/* Animated flow particles */} + <motion.line + x1={conn.fromPos.x} + y1={conn.fromPos.y} + x2={conn.toPos.x} + y2={conn.toPos.y} + stroke={isHighlighted ? "url(#tldr-lineGradientHighlight)" : "url(#tldr-lineGradient)"} + strokeWidth={isHighlighted ? 1.5 : 0.75} + strokeLinecap="round" + strokeDasharray={isHighlighted ? "4 14" : "3 18"} + initial={reducedMotion ? {} : { opacity: 0 }} + animate={isInView ? { opacity: isHighlighted ? 0.9 : 0.4 } : {}} + transition={{ + duration: reducedMotion ? 0 : 0.6, + delay: reducedMotion ? 0 : 0.3 + index * 0.02, + }} + style={{ + animation: reducedMotion ? "none" : `tldr-flow ${isHighlighted ? 1.5 : 2.5}s linear infinite`, + }} + /> + </g> + ); + })} </g> {/* Center "Flywheel" label */} <motion.g initial={reducedMotion ? {} : { opacity: 0, scale: 0.8 }} animate={isInView ? { opacity: 1, scale: 1 } : {}} - transition={{ duration: reducedMotion ? 0 : 0.5, delay: reducedMotion ? 0 : 0.2 }} + transition={{ + duration: reducedMotion ? 0 : 0.5, + delay: reducedMotion ? 0 : 0.2, + }} > + {/* Outer ring */} + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.centerRadius + 4} + fill="none" + stroke="hsl(var(--primary) / 0.2)" + strokeWidth="1" + style={{ + animation: reducedMotion ? "none" : "tldr-pulse-ring 3s ease-in-out infinite", + }} + /> + {/* Main circle */} <circle - cx="200" - cy="200" - r="35" + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.centerRadius} fill="hsl(var(--card))" - stroke="hsl(var(--primary) / 0.3)" + stroke="hsl(var(--primary) / 0.4)" strokeWidth="2" + filter="url(#tldr-glow)" + /> + {/* Inner glow */} + <circle + cx={DIAGRAM_CONFIG.center} + cy={DIAGRAM_CONFIG.center} + r={DIAGRAM_CONFIG.centerRadius - 6} + fill="hsl(var(--primary) / 0.1)" /> <text - x="200" - y="196" + x={DIAGRAM_CONFIG.center} + y={DIAGRAM_CONFIG.center - 4} textAnchor="middle" className="fill-primary text-xs font-bold uppercase tracking-wider" > Flywheel </text> <text - x="200" - y="210" + x={DIAGRAM_CONFIG.center} + y={DIAGRAM_CONFIG.center + 12} textAnchor="middle" - className="fill-muted-foreground text-[9px]" + className="fill-muted-foreground text-xs" > - {coreTools.length} Core Tools + {totalCoreTools} Core Tools </text> </motion.g> - {/* Tool nodes - smaller when more tools */} - {coreTools.map((tool, index) => { + {/* Primary tools (inner ring) - larger nodes */} + {primary.map((tool, index) => { const pos = nodePositions[tool.id]; if (!pos) return null; - // Smaller nodes when we have more tools - const nodeRadius = coreTools.length > 8 ? 22 : 28; - const ringRadius = coreTools.length > 8 ? 20 : 26; - const fontSize = coreTools.length > 8 ? "9px" : "11px"; + const nodeRadius = DIAGRAM_CONFIG.innerNodeRadius; + const ringRadius = nodeRadius - 3; + const isHovered = hoveredNode === tool.id; + const color = getColorDefinition(tool.color); return ( <motion.g @@ -216,16 +397,119 @@ export function TldrSynergyDiagram({ type: "spring", stiffness: 200, }} + onMouseEnter={() => setHoveredNode(tool.id)} + onMouseLeave={() => setHoveredNode(null)} + style={{ cursor: "pointer" }} > + {/* Glow effect on hover */} + {isHovered && ( + <circle + cx={pos.x} + cy={pos.y} + r={nodeRadius + 8} + fill={color.from} + opacity="0.25" + style={{ filter: "blur(8px)" }} + /> + )} + + {/* Node background with shadow */} + <circle + cx={pos.x} + cy={pos.y} + r={nodeRadius} + fill="hsl(var(--card))" + stroke={isHovered ? color.from : "hsl(var(--border) / 0.5)"} + strokeWidth={isHovered ? 2 : 1} + style={{ + filter: isHovered ? `drop-shadow(0 4px 12px ${color.from}40)` : "drop-shadow(0 2px 4px rgba(0,0,0,0.2))", + transition: "all 0.3s ease-out", + }} + /> + + {/* Gradient ring */} + <circle + cx={pos.x} + cy={pos.y} + r={ringRadius} + fill="none" + stroke={`url(#tldr-gradient-${tool.id})`} + strokeWidth={isHovered ? 3.5 : 2.5} + opacity={isHovered ? 1 : 0.75} + style={{ transition: "all 0.3s ease-out" }} + /> + + {/* Inner glow */} + <circle + cx={pos.x} + cy={pos.y} + r={ringRadius - 6} + fill={color.from} + opacity={isHovered ? 0.2 : 0.1} + /> + + {/* Tool label */} + <text + x={pos.x} + y={pos.y + 4} + textAnchor="middle" + className="fill-white font-bold text-[10px] drop-shadow-sm" + style={{ textShadow: "0 1px 2px rgba(0,0,0,0.5)" }} + > + {tool.shortName} + </text> + </motion.g> + ); + })} + + {/* Secondary tools (outer ring) - smaller nodes */} + {secondary.map((tool, index) => { + const pos = nodePositions[tool.id]; + if (!pos) return null; + const nodeRadius = DIAGRAM_CONFIG.outerNodeRadius; + const ringRadius = nodeRadius - 3; + const isHovered = hoveredNode === tool.id; + const color = getColorDefinition(tool.color); + + return ( + <motion.g + key={tool.id} + initial={reducedMotion ? {} : { opacity: 0, scale: 0 }} + animate={isInView ? { opacity: 1, scale: 1 } : {}} + transition={{ + duration: reducedMotion ? 0 : 0.4, + delay: reducedMotion ? 0 : 0.5 + index * 0.04, + type: "spring", + stiffness: 200, + }} + onMouseEnter={() => setHoveredNode(tool.id)} + onMouseLeave={() => setHoveredNode(null)} + style={{ cursor: "pointer" }} + > + {/* Glow effect on hover */} + {isHovered && ( + <circle + cx={pos.x} + cy={pos.y} + r={nodeRadius + 6} + fill={color.from} + opacity="0.2" + style={{ filter: "blur(6px)" }} + /> + )} + {/* Node background */} <circle cx={pos.x} cy={pos.y} r={nodeRadius} fill="hsl(var(--card))" - stroke="hsl(var(--border) / 0.5)" - strokeWidth="1" - className="transition-all duration-300 hover:stroke-border" + stroke={isHovered ? color.from : "hsl(var(--border) / 0.4)"} + strokeWidth={isHovered ? 1.5 : 1} + style={{ + filter: isHovered ? `drop-shadow(0 3px 10px ${color.from}30)` : "drop-shadow(0 1px 3px rgba(0,0,0,0.15))", + transition: "all 0.3s ease-out", + }} /> {/* Gradient ring */} @@ -234,9 +518,10 @@ export function TldrSynergyDiagram({ cy={pos.y} r={ringRadius} fill="none" - stroke={`url(#gradient-${tool.id})`} - strokeWidth="2" - opacity="0.6" + stroke={`url(#tldr-gradient-${tool.id})`} + strokeWidth={isHovered ? 2.5 : 2} + opacity={isHovered ? 0.9 : 0.6} + style={{ transition: "all 0.3s ease-out" }} /> {/* Tool label */} @@ -244,14 +529,28 @@ export function TldrSynergyDiagram({ x={pos.x} y={pos.y + 3} textAnchor="middle" - className="fill-white font-bold" - style={{ fontSize }} + className="fill-white font-bold text-[8px] drop-shadow-sm" + style={{ textShadow: "0 1px 2px rgba(0,0,0,0.5)" }} > {tool.shortName} </text> </motion.g> ); })} + + {/* CSS animations embedded in SVG */} + <style> + {` + @keyframes tldr-flow { + from { stroke-dashoffset: 0; } + to { stroke-dashoffset: -36; } + } + @keyframes tldr-pulse-ring { + 0%, 100% { opacity: 0.2; transform-origin: center; transform: scale(1); } + 50% { opacity: 0.5; transform-origin: center; transform: scale(1.05); } + } + `} + </style> </svg> {/* Legend */} @@ -262,7 +561,13 @@ export function TldrSynergyDiagram({ className="mt-6 text-center" > <p className="text-xs text-muted-foreground"> - Lines represent data flow and integration between tools + {hoveredNode ? ( + <span className="text-primary font-medium"> + Showing connections for {[...primary, ...secondary].find(t => t.id === hoveredNode)?.shortName ?? hoveredNode} + </span> + ) : ( + "Hover over tools to see connections" + )} </p> </motion.div> </motion.div> diff --git a/apps/web/components/tldr/tldr-tool-card.tsx b/apps/web/components/tldr/tldr-tool-card.tsx index ecf8502f..78c02a8f 100644 --- a/apps/web/components/tldr/tldr-tool-card.tsx +++ b/apps/web/components/tldr/tldr-tool-card.tsx @@ -23,6 +23,7 @@ import { RefreshCw, Cog, Image, + ChevronRight, } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatStarCount, formatStarCountFull } from "@/lib/format-stars"; @@ -86,28 +87,71 @@ function SynergyPill({ const linkedTool = allTools.find((t) => t.id === synergy.toolId); if (!linkedTool) return null; + const colorDef = getColorDefinition(linkedTool.color); + return ( - <div className="group/synergy relative flex items-center gap-2 rounded-lg bg-white/5 px-2 py-1.5 transition-colors hover:bg-white/10 sm:px-3 sm:py-2"> + <div + className="group/synergy relative flex items-center gap-2 rounded-xl bg-white/5 px-2 py-1.5 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20 sm:px-3 sm:py-2" + style={{ + boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", + }} + > + {/* Subtle color glow on hover */} + <div + className="pointer-events-none absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300 group-hover/synergy:opacity-100" + style={{ + background: `radial-gradient(circle at center, rgba(${colorDef.rgb}, 0.1), transparent 70%)`, + }} + /> + <div className={cn( - "flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-gradient-to-br sm:h-6 sm:w-6", + "relative flex h-5 w-5 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br shadow-md sm:h-6 sm:w-6", linkedTool.color )} > <DynamicIcon name={linkedTool.icon} className="h-2.5 w-2.5 text-white sm:h-3 sm:w-3" /> </div> - <div className="min-w-0 flex-1"> + <div className="relative min-w-0 flex-1"> <span className="block text-xs font-semibold text-white sm:text-xs"> {linkedTool.shortName} </span> - <span className="block truncate text-xs text-muted-foreground group-hover/synergy:text-foreground/70 sm:text-xs"> + <span className="block truncate text-xs text-muted-foreground transition-colors duration-200 group-hover/synergy:text-foreground/70 sm:text-xs"> {synergy.description} </span> </div> + <ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground opacity-0 transition-all duration-200 group-hover/synergy:translate-x-0.5 group-hover/synergy:opacity-100" /> </div> ); } +// ============================================================================= +// FEATURE LIST ITEM +// ============================================================================= + +function FeatureItem({ feature }: { feature: string }) { + return ( + <li className="group/feature flex items-start gap-2 text-xs text-foreground/80 transition-colors duration-200 hover:text-foreground sm:text-sm"> + <div className="mt-1 flex h-4 w-4 shrink-0 items-center justify-center rounded bg-primary/10 text-primary transition-colors duration-200 group-hover/feature:bg-primary/20"> + <ArrowUpRight className="h-2.5 w-2.5" aria-hidden="true" /> + </div> + {feature} + </li> + ); +} + +// ============================================================================= +// TECH BADGE +// ============================================================================= + +function TechBadge({ tech }: { tech: string }) { + return ( + <span className="rounded-lg bg-white/5 px-2 py-1 text-xs font-medium text-muted-foreground ring-1 ring-white/10 transition-all duration-200 hover:bg-white/10 hover:text-foreground/80"> + {tech} + </span> + ); +} + // ============================================================================= // MAIN COMPONENT // ============================================================================= @@ -118,6 +162,7 @@ export function TldrToolCard({ }: TldrToolCardProps) { const cardRef = useRef<HTMLDivElement>(null); const [spotlightOpacity, setSpotlightOpacity] = useState(0); + const [isHovered, setIsHovered] = useState(false); const isTouchDevice = useMemo( () => typeof window !== "undefined" && window.matchMedia("(hover: none)").matches, [] @@ -140,7 +185,7 @@ export function TldrToolCard({ [reducedMotion, isTouchDevice] ); - const spotlightRgb = getColorDefinition(tool.color).rgb; + const colorDef = getColorDefinition(tool.color); return ( <motion.div @@ -154,15 +199,36 @@ export function TldrToolCard({ <div ref={cardRef} onMouseMove={handleMouseMove} - onMouseEnter={() => setSpotlightOpacity(1)} - onMouseLeave={() => setSpotlightOpacity(0)} + onMouseEnter={() => { + setSpotlightOpacity(1); + setIsHovered(true); + }} + onMouseLeave={() => { + setSpotlightOpacity(0); + setIsHovered(false); + }} className={cn( - "relative h-full flex flex-col overflow-hidden rounded-xl sm:rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm", + "relative h-full flex flex-col overflow-hidden rounded-xl sm:rounded-2xl", + "border border-border/50 bg-card/50 backdrop-blur-sm", "transition-all duration-300", "hover:border-border hover:bg-card/70", - "active:scale-[0.98] active:border-border" + "active:scale-[0.98]" )} + style={{ + boxShadow: isHovered + ? `0 0 40px -10px rgba(${colorDef.rgb}, 0.3), 0 20px 40px -15px rgba(0, 0, 0, 0.4)` + : "0 4px 20px -5px rgba(0, 0, 0, 0.2)", + transition: "box-shadow 0.3s ease, border-color 0.3s ease, background-color 0.3s ease", + }} > + {/* Top border glow on hover */} + <div + className="pointer-events-none absolute inset-x-0 top-0 h-px transition-opacity duration-300" + style={{ + background: `linear-gradient(90deg, transparent, rgba(${colorDef.rgb}, ${isHovered ? 0.5 : 0}), transparent)`, + }} + /> + {/* Gradient background */} <div className={cn( @@ -177,7 +243,7 @@ export function TldrToolCard({ className="pointer-events-none absolute -inset-px transition-opacity duration-500" style={{ opacity: spotlightOpacity, - background: `radial-gradient(400px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(${spotlightRgb}, 0.15), transparent 40%)`, + background: `radial-gradient(500px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(${colorDef.rgb}, 0.15), transparent 40%)`, }} aria-hidden="true" /> @@ -191,11 +257,19 @@ export function TldrToolCard({ <div className="flex items-start gap-3 sm:gap-4"> <div className={cn( - "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg sm:h-12 sm:w-12", + "relative flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br sm:h-12 sm:w-12", tool.color )} + style={{ + boxShadow: isHovered + ? `0 0 25px -5px rgba(${colorDef.rgb}, 0.5), 0 8px 20px -5px rgba(0, 0, 0, 0.3)` + : "0 4px 15px -3px rgba(0, 0, 0, 0.3)", + transition: "box-shadow 0.3s ease", + }} > - <DynamicIcon name={tool.icon} className="h-5 w-5 text-white sm:h-6 sm:w-6" /> + <DynamicIcon name={tool.icon} className="h-5 w-5 text-white drop-shadow sm:h-6 sm:w-6" /> + {/* Inner highlight */} + <div className="pointer-events-none absolute inset-0 rounded-xl bg-gradient-to-t from-transparent to-white/20" /> </div> <div className="min-w-0 flex-1"> <div className="flex flex-wrap items-center gap-2"> @@ -203,7 +277,12 @@ export function TldrToolCard({ {tool.shortName} </h3> {tool.category === "core" && ( - <span className="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-bold uppercase tracking-wider text-primary sm:text-xs"> + <span + className="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-bold uppercase tracking-wider text-primary ring-1 ring-primary/30" + style={{ + boxShadow: "0 0 10px -2px rgba(var(--primary-rgb, 139, 92, 246), 0.3)", + }} + > Core </span> )} @@ -216,7 +295,10 @@ export function TldrToolCard({ <div className="flex shrink-0 items-center gap-1.5 sm:gap-2"> {tool.stars && ( <span - className="relative inline-flex items-center gap-1 overflow-hidden rounded-full bg-gradient-to-r from-accent/20 via-accent/15 to-accent/20 px-2 py-1 text-xs font-bold text-accent shadow-lg shadow-accent/10 ring-1 ring-inset ring-accent/30 transition-all duration-300 hover:ring-accent/50 hover:shadow-accent/20 sm:gap-1.5 sm:px-3 sm:py-1.5 sm:text-xs" + className="relative inline-flex items-center gap-1 overflow-hidden rounded-full bg-gradient-to-r from-accent/20 via-accent/15 to-accent/20 px-2 py-1 text-xs font-bold text-accent ring-1 ring-inset ring-accent/30 transition-all duration-300 hover:ring-accent/50 sm:gap-1.5 sm:px-3 sm:py-1.5 sm:text-xs" + style={{ + boxShadow: "0 0 15px -3px rgba(251, 191, 36, 0.3)", + }} aria-label={`${formatStarCountFull(tool.stars)} GitHub stars`} title={`${formatStarCountFull(tool.stars)} stars`} > @@ -230,7 +312,7 @@ export function TldrToolCard({ href={tool.href} target="_blank" rel="noopener noreferrer" - className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/5 text-muted-foreground transition-colors hover:bg-white/10 hover:text-white sm:h-11 sm:w-11" + className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/5 text-muted-foreground ring-1 ring-white/10 transition-all duration-200 hover:bg-white/10 hover:text-white hover:ring-white/20 sm:h-11 sm:w-11" aria-label={`View ${tool.name} on GitHub`} > <ExternalLink className="h-4 w-4" /> @@ -249,8 +331,10 @@ export function TldrToolCard({ <div className="space-y-4 p-4 sm:space-y-5 sm:p-5"> {/* Why it's useful */} <div> - <h4 className="mb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <h4 className="mb-1.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <span className="h-px flex-1 bg-gradient-to-r from-border to-transparent" /> Why It's Useful + <span className="h-px flex-1 bg-gradient-to-l from-border to-transparent" /> </h4> <p className="text-xs leading-relaxed text-foreground/80 sm:text-sm"> {tool.whyItsUseful} @@ -259,35 +343,28 @@ export function TldrToolCard({ {/* Key features */} <div> - <h4 className="mb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <h4 className="mb-1.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <span className="h-px flex-1 bg-gradient-to-r from-border to-transparent" /> Key Features + <span className="h-px flex-1 bg-gradient-to-l from-border to-transparent" /> </h4> - <ul className="space-y-1 sm:space-y-1.5"> + <ul className="space-y-1.5 sm:space-y-2"> {tool.keyFeatures.map((feature) => ( - <li - key={feature} - className="flex items-start gap-2 text-xs text-foreground/80 sm:text-sm" - > - <ArrowUpRight className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground sm:h-4 sm:w-4" aria-hidden="true" /> - {feature} - </li> + <FeatureItem key={feature} feature={feature} /> ))} </ul> </div> {/* Tech stack */} <div> - <h4 className="mb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <h4 className="mb-1.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <span className="h-px flex-1 bg-gradient-to-r from-border to-transparent" /> Tech Stack + <span className="h-px flex-1 bg-gradient-to-l from-border to-transparent" /> </h4> <div className="flex flex-wrap gap-1.5 sm:gap-2"> {tool.techStack.map((tech) => ( - <span - key={tech} - className="rounded-md bg-white/5 px-1.5 py-0.5 text-xs font-medium text-muted-foreground sm:px-2 sm:py-1 sm:text-xs" - > - {tech} - </span> + <TechBadge key={tech} tech={tech} /> ))} </div> </div> @@ -295,8 +372,10 @@ export function TldrToolCard({ {/* Synergies */} {tool.synergies.length > 0 && ( <div> - <h4 className="mb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <h4 className="mb-1.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground sm:mb-2 sm:text-xs"> + <span className="h-px flex-1 bg-gradient-to-r from-border to-transparent" /> Synergies + <span className="h-px flex-1 bg-gradient-to-l from-border to-transparent" /> </h4> <div className="grid gap-1.5 sm:gap-2 sm:grid-cols-2"> {tool.synergies.map((synergy) => ( diff --git a/apps/web/components/tldr/tldr-tool-grid.tsx b/apps/web/components/tldr/tldr-tool-grid.tsx index 25ef0168..d4810188 100644 --- a/apps/web/components/tldr/tldr-tool-grid.tsx +++ b/apps/web/components/tldr/tldr-tool-grid.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useRef, useEffect, memo } from "react"; import { motion, useReducedMotion, AnimatePresence } from "framer-motion"; -import { Layers, Wrench, Search, X } from "lucide-react"; +import { Layers, Wrench, Search, X, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { TldrToolCard } from "./tldr-tool-card"; import type { TldrFlywheelTool } from "@/lib/tldr-content"; @@ -17,7 +17,7 @@ interface TldrToolGridProps { } // ============================================================================= -// SEARCH BAR COMPONENT +// SEARCH BAR COMPONENT - Enhanced with focus glow and animations // ============================================================================= const ToolSearchBar = memo(function ToolSearchBar({ @@ -35,6 +35,8 @@ const ToolSearchBar = memo(function ToolSearchBar({ inputRef: React.RefObject<HTMLInputElement | null>; reducedMotion: boolean; }) { + const [isFocused, setIsFocused] = useState(false); + return ( <motion.div initial={reducedMotion ? {} : { opacity: 0, y: -10 }} @@ -44,10 +46,39 @@ const ToolSearchBar = memo(function ToolSearchBar({ > <div className="relative mx-auto max-w-2xl"> {/* Glass morphism search container */} - <div className="relative rounded-xl border border-border/50 bg-card/50 backdrop-blur-sm sm:rounded-2xl"> + <div + className={cn( + "relative rounded-2xl border bg-card/50 backdrop-blur-md transition-all duration-300", + isFocused + ? "border-primary/50 bg-card/70" + : "border-border/50 hover:border-border" + )} + style={{ + boxShadow: isFocused + ? "0 0 40px -10px rgba(139, 92, 246, 0.3), 0 8px 30px -10px rgba(0, 0, 0, 0.3)" + : "0 4px 20px -5px rgba(0, 0, 0, 0.2)", + }} + > + {/* Focus glow ring */} + <div + className={cn( + "pointer-events-none absolute -inset-px rounded-2xl transition-opacity duration-300", + isFocused ? "opacity-100" : "opacity-0" + )} + style={{ + background: "linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.2), transparent)", + }} + /> + {/* Search icon */} - <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 sm:pl-4"> - <Search className="h-4 w-4 text-muted-foreground sm:h-5 sm:w-5" aria-hidden="true" /> + <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4 sm:pl-5"> + <Search + className={cn( + "h-5 w-5 transition-colors duration-200", + isFocused ? "text-primary" : "text-muted-foreground" + )} + aria-hidden="true" + /> </div> {/* Input field */} @@ -56,23 +87,30 @@ const ToolSearchBar = memo(function ToolSearchBar({ type="text" value={query} onChange={(e) => onQueryChange(e.target.value)} - placeholder="Search tools..." + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder="Search tools by name, feature, or technology..." aria-label="Search flywheel tools" - className="w-full rounded-xl bg-transparent py-3 pl-10 pr-16 text-sm text-white placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 sm:rounded-2xl sm:py-4 sm:pl-12 sm:pr-20" + className="w-full rounded-2xl bg-transparent py-4 pl-12 pr-20 text-sm text-white placeholder-muted-foreground focus:outline-none sm:py-5 sm:pl-14 sm:pr-24 sm:text-base" /> {/* Clear button and keyboard hint */} - <div className="absolute inset-y-0 right-0 flex items-center gap-1.5 pr-3 sm:gap-2 sm:pr-4"> - {query && ( - <button - onClick={() => onQueryChange("")} - className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-white/10 hover:text-white" - aria-label="Clear search" - > - <X className="h-4 w-4" /> - </button> - )} - <kbd className="hidden rounded-md border border-border bg-card/50 px-2 py-1 text-xs font-medium text-muted-foreground sm:inline-block"> + <div className="absolute inset-y-0 right-0 flex items-center gap-2 pr-4 sm:gap-3 sm:pr-5"> + <AnimatePresence> + {query && ( + <motion.button + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.8 }} + onClick={() => onQueryChange("")} + className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground ring-1 ring-white/10 transition-all duration-200 hover:bg-white/10 hover:text-white hover:ring-white/20" + aria-label="Clear search" + > + <X className="h-4 w-4" /> + </motion.button> + )} + </AnimatePresence> + <kbd className="hidden rounded-lg border border-border bg-card/50 px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm sm:inline-block"> / </kbd> </div> @@ -85,15 +123,19 @@ const ToolSearchBar = memo(function ToolSearchBar({ initial={reducedMotion ? {} : { opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} exit={reducedMotion ? {} : { opacity: 0, y: -5 }} - className="mt-2 text-center sm:mt-3" + className="mt-3 text-center sm:mt-4" role="status" aria-live="polite" > - <span className="text-xs text-muted-foreground sm:text-sm"> + <span className="inline-flex items-center gap-2 rounded-full bg-card/50 px-4 py-2 text-xs text-muted-foreground ring-1 ring-border/50 backdrop-blur-sm sm:text-sm"> {resultCount === 0 ? ( - "No tools match your search" + <> + <X className="h-3.5 w-3.5 text-destructive" /> + No tools match your search + </> ) : ( <> + <Sparkles className="h-3.5 w-3.5 text-primary" /> Showing{" "} <span className="font-semibold text-white"> {resultCount} @@ -134,10 +176,15 @@ const EmptySearchState = memo(function EmptySearchState({ transition={{ duration: reducedMotion ? 0 : 0.3 }} className="py-16 text-center" > - <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10"> - <Search className="h-8 w-8 text-primary" /> + <div + className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-gradient-to-br from-primary/20 to-primary/5 ring-1 ring-primary/20" + style={{ + boxShadow: "0 0 40px -10px rgba(139, 92, 246, 0.3)", + }} + > + <Search className="h-10 w-10 text-primary" /> </div> - <h3 className="mt-6 text-lg font-semibold text-white"> + <h3 className="mt-6 text-xl font-bold text-white"> No tools match "{query}" </h3> <p className="mt-2 text-sm text-muted-foreground"> @@ -146,7 +193,7 @@ const EmptySearchState = memo(function EmptySearchState({ </p> <button onClick={onClear} - className="mt-6 inline-flex items-center gap-2 rounded-lg bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20" + className="mt-6 inline-flex items-center gap-2 rounded-xl bg-primary/10 px-5 py-2.5 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all duration-200 hover:bg-primary/20 hover:ring-primary/30" > <X className="h-4 w-4" /> Clear search @@ -156,7 +203,7 @@ const EmptySearchState = memo(function EmptySearchState({ }); // ============================================================================= -// SECTION HEADER COMPONENT +// SECTION HEADER COMPONENT - Enhanced with better visual hierarchy // ============================================================================= const SectionHeader = memo(function SectionHeader({ @@ -165,37 +212,85 @@ const SectionHeader = memo(function SectionHeader({ icon: Icon, count, reducedMotion, + accentColor = "primary", }: { title: string; description: string; icon: React.ComponentType<{ className?: string }>; count: number; reducedMotion: boolean; + accentColor?: "primary" | "accent" | "success"; }) { + const colorClasses = { + primary: "from-primary to-primary/60 text-primary bg-primary/20 ring-primary/30", + accent: "from-accent to-accent/60 text-accent bg-accent/20 ring-accent/30", + success: "from-success to-success/60 text-success bg-success/20 ring-success/30", + }; + + const colors = colorClasses[accentColor]; + const [iconBg] = colors.split(" ").slice(0, 2); + return ( <motion.div initial={reducedMotion ? {} : { opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-50px" }} transition={{ duration: reducedMotion ? 0 : 0.5 }} - className="mb-6 sm:mb-8" + className="mb-8 sm:mb-10" > - <div className="flex items-center gap-2 sm:gap-3"> - <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/20 text-primary sm:h-10 sm:w-10 sm:rounded-xl"> - <Icon className="h-4 w-4 sm:h-5 sm:w-5" /> + <div className="flex items-center gap-3 sm:gap-4"> + {/* Icon container with gradient and glow */} + <div + className={cn( + "relative flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br shadow-lg sm:h-14 sm:w-14", + iconBg + )} + style={{ + boxShadow: accentColor === "primary" + ? "0 0 30px -5px rgba(139, 92, 246, 0.4)" + : accentColor === "accent" + ? "0 0 30px -5px rgba(251, 191, 36, 0.4)" + : "0 0 30px -5px rgba(34, 197, 94, 0.4)", + }} + > + <Icon className={cn("h-6 w-6 text-white drop-shadow sm:h-7 sm:w-7")} /> + {/* Inner glow */} + <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-t from-transparent to-white/20" /> </div> - <div> - <h2 className="text-lg font-bold text-white sm:text-xl md:text-2xl"> - {title} - <span className="ml-1.5 text-xs font-normal text-muted-foreground sm:ml-2 sm:text-sm"> - ({count}) + + <div className="flex-1"> + <div className="flex flex-wrap items-center gap-2 sm:gap-3"> + <h2 className="text-xl font-bold text-white sm:text-2xl md:text-3xl"> + {title} + </h2> + <span + className={cn( + "rounded-full px-2.5 py-1 text-xs font-bold ring-1 sm:px-3 sm:text-sm", + colors.split(" ").slice(2).join(" ") + )} + > + {count} </span> - </h2> + </div> + <p className="mt-1 max-w-3xl text-sm leading-relaxed text-muted-foreground sm:mt-2 sm:text-base"> + {description} + </p> </div> </div> - <p className="mt-2 max-w-2xl text-xs leading-relaxed text-muted-foreground sm:mt-3 sm:text-sm"> - {description} - </p> + + {/* Decorative line */} + <div className="mt-6 flex items-center gap-4"> + <div + className="h-px flex-1" + style={{ + background: accentColor === "primary" + ? "linear-gradient(90deg, rgba(139, 92, 246, 0.5), transparent)" + : accentColor === "accent" + ? "linear-gradient(90deg, rgba(251, 191, 36, 0.5), transparent)" + : "linear-gradient(90deg, rgba(34, 197, 94, 0.5), transparent)", + }} + /> + </div> </motion.div> ); }); @@ -275,7 +370,7 @@ export function TldrToolGrid({ tools, className }: TldrToolGridProps) { const isSearching = searchQuery.trim().length > 0; return ( - <div className={cn("space-y-16", className)}> + <div className={cn("space-y-16 sm:space-y-20", className)}> {/* Search Bar */} <ToolSearchBar query={searchQuery} @@ -304,6 +399,7 @@ export function TldrToolGrid({ tools, className }: TldrToolGridProps) { icon={Layers} count={coreTools.length} reducedMotion={reducedMotion} + accentColor="primary" /> <div className="grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3"> <AnimatePresence mode="popLayout"> @@ -340,6 +436,7 @@ export function TldrToolGrid({ tools, className }: TldrToolGridProps) { icon={Wrench} count={supportingTools.length} reducedMotion={reducedMotion} + accentColor="accent" /> <div className="grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3"> <AnimatePresence mode="popLayout"> diff --git a/apps/web/components/ui/bottom-sheet.test.tsx b/apps/web/components/ui/bottom-sheet.test.tsx new file mode 100644 index 00000000..47755f88 --- /dev/null +++ b/apps/web/components/ui/bottom-sheet.test.tsx @@ -0,0 +1,32 @@ +/** + * BottomSheet Component Tests + * + * Structural tests for the BottomSheet component. + * Runtime rendering is covered by Playwright E2E tests (e2e/bottom-sheet.spec.ts). + * + * These tests verify: + * - Component export exists and is callable + * - Component interface types are correct + * - Props are properly typed + */ + +import { describe, test, expect } from "bun:test"; +import { BottomSheet } from "./bottom-sheet"; + +describe("BottomSheet component", () => { + test("BottomSheet is exported as a function", () => { + expect(typeof BottomSheet).toBe("function"); + }); + + test("BottomSheet is a valid React component (has name)", () => { + expect(BottomSheet.name).toBe("BottomSheet"); + }); +}); + +describe("BottomSheet props interface", () => { + test("BottomSheet function accepts expected parameters", () => { + // TypeScript validates the interface at compile time + // This test verifies the function signature at runtime + expect(BottomSheet.length).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/apps/web/components/ui/bottom-sheet.tsx b/apps/web/components/ui/bottom-sheet.tsx new file mode 100644 index 00000000..fdfb5a99 --- /dev/null +++ b/apps/web/components/ui/bottom-sheet.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useEffect, useCallback, useSyncExternalStore, useRef } from "react"; +import { createPortal } from "react-dom"; +import { m, AnimatePresence, useDragControls, type PanInfo } from "framer-motion"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; + +// Subscribe function for useSyncExternalStore (no-op since we don't need updates) +const emptySubscribe = () => () => {}; +// Snapshot functions for client/server detection +const getClientSnapshot = () => true; +const getServerSnapshot = () => false; + +// Spring config matching our motion module's smooth spring +const smoothSpring = { type: "spring" as const, stiffness: 200, damping: 25 }; + +interface BottomSheetProps { + /** Whether the sheet is open */ + open: boolean; + /** Callback when sheet should close */ + onClose: () => void; + /** Title for accessibility (aria-label) */ + title: string; + /** Content to render inside the sheet */ + children: React.ReactNode; + /** Maximum height (default: 80vh) */ + maxHeight?: string; + /** Whether to show the drag handle */ + showHandle?: boolean; + /** Whether to close on backdrop click (default: true) */ + closeOnBackdrop?: boolean; + /** Whether to enable swipe-to-close (default: true) */ + swipeable?: boolean; + /** Additional className for the sheet container */ + className?: string; +} + +/** + * Mobile-optimized bottom sheet component. + * + * Features: + * - Swipe-to-close gesture (drag down > 200px or velocity > 500) + * - Escape key dismissal + * - Backdrop click dismissal (configurable) + * - Body scroll lock when open + * - Safe area padding for notched devices + * - Reduced motion fallback (opacity instead of slide) + * - 44px close button touch target + * - ARIA attributes for accessibility + */ +export function BottomSheet({ + open, + onClose, + title, + children, + maxHeight = "80vh", + showHandle = true, + closeOnBackdrop = true, + swipeable = true, + className, +}: BottomSheetProps) { + const prefersReducedMotion = useReducedMotion(); + const dragControls = useDragControls(); + const sheetRef = useRef<HTMLDivElement>(null); + const previousActiveElement = useRef<HTMLElement | null>(null); + + // Client-side only mounting for portal (avoids setState in effect) + const isClient = useSyncExternalStore( + emptySubscribe, + getClientSnapshot, + getServerSnapshot + ); + + // Focus management: move focus to sheet when open, restore when closed + useEffect(() => { + if (open) { + // Store the currently focused element to restore later (only if it's an HTMLElement) + const activeEl = document.activeElement; + if (activeEl instanceof HTMLElement) { + previousActiveElement.current = activeEl; + } + // Focus the sheet after a short delay to allow animation to start + const timer = setTimeout(() => { + sheetRef.current?.focus(); + }, 50); + return () => clearTimeout(timer); + } else if (previousActiveElement.current) { + // Restore focus to the previously focused element (if still in DOM) + try { + if (document.body.contains(previousActiveElement.current)) { + previousActiveElement.current.focus(); + } + } catch { + // Element may have been removed, ignore + } + previousActiveElement.current = null; + } + }, [open]); + + // Escape key handling + useEffect(() => { + if (!open) return; + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [open, onClose]); + + // Lock body scroll when open + useEffect(() => { + if (open) { + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = originalOverflow; + }; + } + }, [open]); + + // Swipe to close handler + const handleDragEnd = useCallback( + (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { + // Close if velocity is high enough or offset is large enough + if (info.velocity.y > 500 || info.offset.y > 200) { + onClose(); + } + }, + [onClose] + ); + + // Don't render on server (portal requires document.body) + if (!isClient) return null; + + return createPortal( + <AnimatePresence> + {open && ( + <> + {/* Backdrop */} + <m.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + transition={prefersReducedMotion ? { duration: 0.1 } : { duration: 0.2 }} + className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" + onClick={closeOnBackdrop ? onClose : undefined} + aria-hidden="true" + /> + + {/* Sheet */} + <m.div + ref={sheetRef} + role="dialog" + aria-modal="true" + aria-label={title} + tabIndex={-1} + drag={swipeable && !prefersReducedMotion ? "y" : false} + dragControls={dragControls} + dragListener={!showHandle} + dragConstraints={{ top: 0 }} + dragElastic={{ top: 0, bottom: 0.5 }} + onDragEnd={handleDragEnd} + initial={prefersReducedMotion ? { opacity: 0 } : { y: "100%" }} + animate={prefersReducedMotion ? { opacity: 1 } : { y: 0 }} + exit={prefersReducedMotion ? { opacity: 0 } : { y: "100%" }} + transition={prefersReducedMotion ? { duration: 0.1 } : smoothSpring} + className={cn( + "fixed inset-x-0 bottom-0 z-50 flex flex-col", + "rounded-t-3xl border-t border-border/50", + "bg-card/98 shadow-2xl backdrop-blur-xl", + className + )} + style={{ maxHeight }} + > + {/* Drag handle */} + {showHandle && ( + <div + className={cn( + "flex shrink-0 justify-center pb-1 pt-3", + swipeable && !prefersReducedMotion + ? "cursor-grab active:cursor-grabbing" + : "cursor-default" + )} + onPointerDown={(e) => { + if (swipeable && !prefersReducedMotion) { + dragControls.start(e); + } + }} + > + <div className="h-1 w-10 rounded-full bg-muted-foreground/30" /> + </div> + )} + + {/* Close button - 44px touch target */} + <button + type="button" + onClick={onClose} + className={cn( + "absolute right-4 top-4 z-10", + "flex h-11 w-11 items-center justify-center", + "rounded-full bg-muted text-muted-foreground", + "transition-colors hover:bg-muted/80 hover:text-foreground", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + )} + aria-label="Close" + > + <X className="h-5 w-5" /> + </button> + + {/* Content - scrollable with safe area padding */} + <div + className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-safe pt-2" + style={{ WebkitOverflowScrolling: "touch" }} + > + {children} + </div> + </m.div> + </> + )} + </AnimatePresence>, + document.body + ); +} diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx index 2110367c..f4e53cae 100644 --- a/apps/web/components/ui/button.tsx +++ b/apps/web/components/ui/button.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; -import { m, type HTMLMotionProps } from "framer-motion"; +import { AnimatePresence, m, type HTMLMotionProps } from "framer-motion"; import { Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { springs } from "@/components/motion"; @@ -17,7 +17,7 @@ import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; * - xl: 56px (hero CTAs) */ const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", + "relative inline-flex items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded-xl text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", { variants: { variant: { @@ -32,6 +32,12 @@ const buttonVariants = cva( ghost: "hover:bg-accent/20 hover:text-accent-foreground active:bg-accent/30", link: "text-primary underline-offset-4 hover:underline", + // Premium gradient variant for hero CTAs - Stripe-style + gradient: + "bg-gradient-to-r from-primary via-[oklch(0.65_0.2_220)] to-[oklch(0.6_0.22_280)] text-white shadow-lg shadow-primary/30 hover:shadow-xl hover:shadow-primary/40 hover:brightness-110 active:brightness-95", + // Subtle gradient for secondary emphasis + "gradient-subtle": + "bg-gradient-to-r from-white/10 to-white/5 border border-white/20 text-white hover:from-white/15 hover:to-white/10 hover:border-white/30 active:from-white/20 active:to-white/15", }, size: { default: "h-11 px-5 py-2.5 text-sm", @@ -52,6 +58,28 @@ const buttonVariants = cva( type ButtonVariantProps = VariantProps<typeof buttonVariants>; +type LoadingSpinnerProps = { + className?: string; + reducedMotion: boolean; +}; + +function LoadingSpinner({ className, reducedMotion }: LoadingSpinnerProps) { + if (reducedMotion) { + return <Loader2 className={cn("h-4 w-4", className)} aria-hidden="true" />; + } + + return ( + <m.span + className={cn("inline-flex", className)} + animate={{ rotate: 360 }} + transition={{ duration: 1, repeat: Infinity, ease: "linear" }} + aria-hidden="true" + > + <Loader2 className="h-4 w-4" /> + </m.span> + ); +} + interface ButtonProps extends Omit<HTMLMotionProps<"button">, "children">, ButtonVariantProps { @@ -63,6 +91,8 @@ interface ButtonProps loading?: boolean; /** Optional text to show alongside spinner when loading (if omitted, only spinner shown) */ loadingText?: string; + /** Optional loading progress (0-100) for determinate loading states */ + loadingProgress?: number; } /** @@ -85,6 +115,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( disableMotion = false, loading = false, loadingText, + loadingProgress, children, disabled, ...props @@ -95,23 +126,40 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const prefersReducedMotion = useReducedMotion(); const shouldDisableMotion = disableMotion || prefersReducedMotion; - // Loading spinner component - const LoadingSpinner = () => ( - <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" /> - ); + const clampedProgress = + typeof loadingProgress === "number" + ? Math.max(0, Math.min(100, loadingProgress)) + : undefined; + const showShimmer = loading && !shouldDisableMotion && variant !== "link"; + const showProgress = loading && typeof clampedProgress === "number"; - // Render content with optional loading state - const renderContent = () => { - if (loading) { - return ( - <> - <LoadingSpinner /> - {loadingText && <span>{loadingText}</span>} - </> - ); - } - return children; - }; + const content = ( + <span className="relative z-10 inline-flex items-center justify-center gap-2"> + <m.span + className="inline-flex items-center gap-2" + aria-hidden={loading} + animate={{ opacity: loading ? 0 : 1, scale: loading ? 0.98 : 1 }} + transition={shouldDisableMotion ? { duration: 0 } : { duration: 0.15 }} + > + {children} + </m.span> + <AnimatePresence> + {loading && ( + <m.span + key="loading" + className="absolute inset-0 inline-flex items-center justify-center gap-2" + initial={shouldDisableMotion ? {} : { opacity: 0, scale: 0.96 }} + animate={{ opacity: 1, scale: 1 }} + exit={shouldDisableMotion ? {} : { opacity: 0, scale: 0.96 }} + transition={shouldDisableMotion ? { duration: 0 } : { duration: 0.15 }} + > + <LoadingSpinner reducedMotion={shouldDisableMotion} /> + {loadingText && <span>{loadingText}</span>} + </m.span> + )} + </AnimatePresence> + </span> + ); // For asChild, use Slot without motion if (asChild) { @@ -138,7 +186,22 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( className={cn(buttonVariants({ variant, size, className }))} {...(props as React.ComponentPropsWithoutRef<"button">)} > - {renderContent()} + {showShimmer && ( + <span className="pointer-events-none absolute inset-0 z-0"> + <span className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/15 to-transparent" /> + </span> + )} + {showProgress && ( + <span className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1 overflow-hidden rounded-b-xl bg-current/15"> + <m.span + className="block h-full bg-current/50" + initial={{ width: 0 }} + animate={{ width: `${clampedProgress}%` }} + transition={shouldDisableMotion ? { duration: 0 } : { duration: 0.3 }} + /> + </span> + )} + {content} </button> ); } @@ -157,7 +220,22 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( transition={springs.snappy} {...props} > - {renderContent()} + {showShimmer && ( + <span className="pointer-events-none absolute inset-0 z-0"> + <span className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/15 to-transparent" /> + </span> + )} + {showProgress && ( + <span className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1 overflow-hidden rounded-b-xl bg-current/15"> + <m.span + className="block h-full bg-current/50" + initial={{ width: 0 }} + animate={{ width: `${clampedProgress}%` }} + transition={shouldDisableMotion ? { duration: 0 } : { duration: 0.3 }} + /> + </span> + )} + {content} </m.button> ); } diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx index 681ad980..5a6ed0d0 100644 --- a/apps/web/components/ui/card.tsx +++ b/apps/web/components/ui/card.tsx @@ -7,7 +7,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) { <div data-slot="card" className={cn( - "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", + "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6", + // Layered shadow for depth - Stripe-style elevation + "shadow-[0_1px_3px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.24)]", + // Smooth transition for hover states + "transition-[box-shadow,transform] duration-200 ease-out", className )} {...props} diff --git a/apps/web/components/ui/code-block.tsx b/apps/web/components/ui/code-block.tsx index c79f8128..d09ec564 100644 --- a/apps/web/components/ui/code-block.tsx +++ b/apps/web/components/ui/code-block.tsx @@ -56,10 +56,14 @@ function CopyButton({ onClick={() => copy(text)} aria-label={copied ? "Copied!" : "Copy to clipboard"} className={cn( - "inline-flex items-center gap-1.5 rounded-lg text-xs font-medium transition-all duration-200", + "inline-flex items-center gap-1.5 rounded-lg text-xs font-medium", + "transition-all duration-200", + // Minimum touch target for mobile (44px when compact) compact - ? "p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted" - : "px-2.5 py-1.5 text-white/40 hover:text-white hover:bg-white/10", + ? "min-h-[44px] min-w-[44px] justify-center p-2 text-muted-foreground hover:text-foreground hover:bg-muted active:scale-95" + : "px-3 py-2 text-white/60 hover:text-white hover:bg-white/10 active:scale-95", + // Focus ring for keyboard navigation + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className, )} > @@ -67,7 +71,7 @@ function CopyButton({ <> <Check className={cn( - "h-3.5 w-3.5", + "h-4 w-4", compact ? "text-[oklch(0.72_0.19_145)]" : "text-emerald-400", )} /> @@ -75,7 +79,7 @@ function CopyButton({ </> ) : ( <> - <Copy className="h-3.5 w-3.5" /> + <Copy className="h-4 w-4" /> {!compact && <span>Copy</span>} </> )} @@ -152,9 +156,9 @@ export function CodeBlock({ <div className="w-3 h-3 rounded-full bg-green-500/80" /> </div> {filename ? ( - <span className="text-xs text-white/40 font-mono">{filename}</span> + <span className="text-xs text-white/60 font-mono">{filename}</span> ) : ( - <div className="flex items-center gap-1.5 text-white/40"> + <div className="flex items-center gap-1.5 text-white/60"> <Terminal className="h-3.5 w-3.5" /> <span className="text-xs font-mono">{language}</span> </div> @@ -167,9 +171,16 @@ export function CodeBlock({ <div className="relative p-5 overflow-x-auto"> <pre className="font-mono text-sm"> {lines.map((line, i) => ( - <div key={i} className="flex"> + <div + key={i} + className={cn( + "flex -mx-5 px-5 transition-colors duration-150", + // Line highlight on hover for better readability + "hover:bg-white/[0.03]" + )} + > {showLineNumbers && ( - <span className="select-none w-8 text-white/20 text-right pr-4"> + <span className="select-none w-8 text-white/20 text-right pr-4 shrink-0"> {i + 1} </span> )} @@ -180,7 +191,7 @@ export function CodeBlock({ <span className="text-white/90">{line.slice(1)}</span> </> ) : line.startsWith("#") ? ( - <span className="text-white/40">{line}</span> + <span className="text-white/50">{line}</span> ) : ( line )} diff --git a/apps/web/components/ui/empty-state.tsx b/apps/web/components/ui/empty-state.tsx new file mode 100644 index 00000000..1ee61f89 --- /dev/null +++ b/apps/web/components/ui/empty-state.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { motion, springs } from "@/components/motion"; +import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; +import type { LucideIcon } from "lucide-react"; + +interface EmptyStateProps { + /** Icon to display */ + icon: LucideIcon; + /** Main title */ + title: string; + /** Description text */ + description: string; + /** Optional action button/element */ + action?: React.ReactNode; + /** Variant for different contexts */ + variant?: "default" | "compact" | "inline"; + /** Additional className */ + className?: string; + /** Optional class overrides */ + iconContainerClassName?: string; + iconClassName?: string; + titleClassName?: string; + descriptionClassName?: string; +} + +/** + * EmptyState - A premium empty state component for when there's no content. + * + * Used for: + * - Zero search results + * - Empty lists/grids + * - No data states + * - First-time user experience + */ +export function EmptyState({ + icon: Icon, + title, + description, + action, + variant = "default", + className, + iconContainerClassName, + iconClassName, + titleClassName, + descriptionClassName, +}: EmptyStateProps) { + const prefersReducedMotion = useReducedMotion(); + const reducedMotion = prefersReducedMotion ?? false; + + const variantStyles = { + default: "py-16", + compact: "py-10", + inline: "py-6", + }; + + const iconSizes = { + default: "h-16 w-16", + compact: "h-12 w-12", + inline: "h-10 w-10", + }; + + const iconContainerSizes = { + default: "h-20 w-20", + compact: "h-16 w-16", + inline: "h-12 w-12", + }; + + return ( + <motion.div + className={cn( + "flex flex-col items-center justify-center text-center", + variantStyles[variant], + className + )} + initial={reducedMotion ? {} : { opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={reducedMotion ? { duration: 0 } : springs.smooth} + > + {/* Icon container with subtle gradient background */} + <motion.div + className={cn( + "mb-4 flex items-center justify-center rounded-2xl", + "bg-gradient-to-br from-muted/80 to-muted/40", + "shadow-inner", + iconContainerSizes[variant], + iconContainerClassName + )} + initial={reducedMotion ? {} : { scale: 0.9 }} + animate={{ scale: 1 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.snappy, delay: 0.1 }} + > + <Icon + className={cn( + "text-muted-foreground/60", + iconSizes[variant], + iconClassName + )} + strokeWidth={1.5} + /> + </motion.div> + + {/* Title */} + <motion.h3 + className={cn( + "font-semibold text-foreground", + variant === "default" && "text-lg", + variant === "compact" && "text-base", + variant === "inline" && "text-sm", + titleClassName + )} + initial={reducedMotion ? {} : { opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: 0.15 }} + > + {title} + </motion.h3> + + {/* Description */} + <motion.p + className={cn( + "mt-2 max-w-sm text-muted-foreground", + variant === "default" && "text-sm", + variant === "compact" && "text-sm", + variant === "inline" && "text-xs", + descriptionClassName + )} + initial={reducedMotion ? {} : { opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: 0.2 }} + > + {description} + </motion.p> + + {/* Action */} + {action && ( + <motion.div + className="mt-6" + initial={reducedMotion ? {} : { opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + transition={reducedMotion ? { duration: 0 } : { ...springs.smooth, delay: 0.25 }} + > + {action} + </motion.div> + )} + </motion.div> + ); +} diff --git a/apps/web/components/ui/form-field.tsx b/apps/web/components/ui/form-field.tsx new file mode 100644 index 00000000..96457a2e --- /dev/null +++ b/apps/web/components/ui/form-field.tsx @@ -0,0 +1,185 @@ +"use client"; + +import * as React from "react"; +import { AnimatePresence, m } from "framer-motion"; +import { cn } from "@/lib/utils"; +import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; + +export interface FormFieldProps { + /** Input name for form submission */ + name: string; + /** Label text (becomes floating label) */ + label: string; + /** Input type (text, email, password, etc.) */ + type?: "text" | "email" | "password" | "url" | "tel" | "number"; + /** Current value (controlled) */ + value: string; + /** Change handler */ + onChange: (value: string) => void; + /** Blur handler */ + onBlur?: () => void; + /** Error message (shows error state when truthy) */ + error?: string; + /** Helper text (shown below input when no error) */ + helperText?: string; + /** Whether field is required */ + required?: boolean; + /** Whether field is disabled */ + disabled?: boolean; + /** Placeholder (optional, label acts as placeholder when empty) */ + placeholder?: string; + /** Character count limit (shows counter when set) */ + maxLength?: number; + /** Additional className */ + className?: string; + /** Input ref for focus management */ + inputRef?: React.Ref<HTMLInputElement>; +} + +export function FormField({ + name, + label, + type = "text", + value, + onChange, + onBlur, + error, + helperText, + required, + disabled, + placeholder, + maxLength, + className, + inputRef, +}: FormFieldProps) { + const id = React.useId(); + const [isFocused, setIsFocused] = React.useState(false); + const prefersReducedMotion = useReducedMotion(); + + const hasValue = value.length > 0; + const isFloating = isFocused || hasValue; + const showError = Boolean(error); + + const handleFocus = () => setIsFocused(true); + const handleBlur = () => { + setIsFocused(false); + onBlur?.(); + }; + + const motionTransition = prefersReducedMotion + ? { duration: 0 } + : { duration: 0.15, ease: "easeOut" as const }; + + return ( + <div className={cn("relative", className)}> + <div + className={cn( + "relative rounded-xl border-2 transition-colors duration-200", + isFocused && !showError && "border-primary", + showError && "border-destructive", + !isFocused && !showError && "border-border/50 hover:border-border", + disabled && "cursor-not-allowed opacity-60" + )} + > + <m.label + htmlFor={id} + className={cn( + "absolute left-4 pointer-events-none origin-left", + "transition-colors duration-200", + isFloating ? "text-xs font-medium" : "text-base text-muted-foreground", + isFocused && !showError && "text-primary", + showError && "text-destructive", + !isFocused && !showError && isFloating && "text-muted-foreground" + )} + animate={ + prefersReducedMotion + ? {} + : { + y: isFloating ? -10 : 14, + scale: isFloating ? 0.85 : 1, + } + } + transition={motionTransition} + style={{ + top: isFloating ? "8px" : "50%", + transform: isFloating ? undefined : "translateY(-50%)", + }} + > + {label} + {required && <span className="ml-0.5 text-destructive">*</span>} + </m.label> + + <input + ref={inputRef} + id={id} + name={name} + type={type} + value={value} + onChange={(event) => onChange(event.target.value)} + onFocus={handleFocus} + onBlur={handleBlur} + disabled={disabled} + required={required} + maxLength={maxLength} + placeholder={isFloating ? placeholder : undefined} + aria-invalid={showError} + aria-describedby={ + error ? `${id}-error` : helperText ? `${id}-helper` : undefined + } + className={cn( + "w-full bg-transparent px-4 pt-6 pb-2 text-base", + "rounded-xl outline-none", + "placeholder:text-muted-foreground/50", + disabled && "cursor-not-allowed" + )} + /> + </div> + + <div className="mt-1.5 flex items-start justify-between px-1"> + <AnimatePresence mode="wait"> + {showError ? ( + <m.p + key="error" + id={`${id}-error`} + initial={prefersReducedMotion ? {} : { opacity: 0, y: -5 }} + animate={{ opacity: 1, y: 0 }} + exit={prefersReducedMotion ? {} : { opacity: 0, y: -5 }} + transition={motionTransition} + className="text-sm text-destructive" + role="alert" + > + {error} + </m.p> + ) : helperText ? ( + <m.p + key="helper" + id={`${id}-helper`} + initial={prefersReducedMotion ? {} : { opacity: 0 }} + animate={{ opacity: 1 }} + exit={prefersReducedMotion ? {} : { opacity: 0 }} + transition={motionTransition} + className="text-sm text-muted-foreground" + > + {helperText} + </m.p> + ) : ( + <span /> + )} + </AnimatePresence> + + {maxLength ? ( + <span + className={cn( + "text-sm tabular-nums", + value.length >= maxLength ? "text-destructive" : "text-muted-foreground" + )} + > + {value.length}/{maxLength} + </span> + ) : ( + <span /> + )} + </div> + </div> + ); +} diff --git a/apps/web/components/ui/skeleton.tsx b/apps/web/components/ui/skeleton.tsx index 38f1fb53..6aa1c60e 100644 --- a/apps/web/components/ui/skeleton.tsx +++ b/apps/web/components/ui/skeleton.tsx @@ -10,8 +10,16 @@ function Skeleton({ className, shimmer = true, ...props }: SkeletonProps) { <div data-slot="skeleton" className={cn( - "rounded-md bg-muted", - shimmer && "relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent", + "rounded-md bg-muted/80", + // Enhanced shimmer with glow effect for premium feel + shimmer && [ + "relative overflow-hidden", + "before:absolute before:inset-0 before:-translate-x-full", + "before:animate-[shimmer_2s_infinite]", + "before:bg-gradient-to-r before:from-transparent before:via-white/15 before:to-transparent", + // Subtle pulse animation layered with shimmer + "animate-pulse", + ], className )} {...props} diff --git a/apps/web/components/ui/theme-toggle.tsx b/apps/web/components/ui/theme-toggle.tsx index 4ee6f7db..50c7ad4a 100644 --- a/apps/web/components/ui/theme-toggle.tsx +++ b/apps/web/components/ui/theme-toggle.tsx @@ -32,13 +32,18 @@ export function ThemeToggle({ className }: ThemeToggleProps) { type="button" onClick={cycle} className={cn( - "flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", + // 44px touch target for Apple HIG compliance + "flex h-11 w-11 items-center justify-center rounded-xl", + "text-muted-foreground transition-all duration-200", + "hover:bg-muted hover:text-foreground hover:scale-105", + "active:scale-95", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className )} aria-label={label} title={label} > - <Icon className="h-4 w-4" /> + <Icon className="h-5 w-5" /> </button> ); } diff --git a/apps/web/components/wizard/HelpPanel.tsx b/apps/web/components/wizard/HelpPanel.tsx index b78d7a55..41aa1e95 100644 --- a/apps/web/components/wizard/HelpPanel.tsx +++ b/apps/web/components/wizard/HelpPanel.tsx @@ -101,7 +101,7 @@ export function HelpPanel({ currentStep }: HelpPanelProps) { </h2> <button onClick={closeDialog} - className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + className="flex h-10 w-10 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background outline-none" aria-label="Close" > <X className="h-4 w-4" /> @@ -122,7 +122,7 @@ export function HelpPanel({ currentStep }: HelpPanelProps) { key={issue.symptom} className="group rounded-lg border border-border/50 bg-muted/30" > - <summary className="flex cursor-pointer items-center gap-2 px-4 py-3 text-sm font-medium text-foreground"> + <summary className="flex cursor-pointer items-center gap-2 px-4 py-3 text-sm font-medium text-foreground rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"> <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" /> {issue.symptom} </summary> diff --git a/apps/web/components/wizard/VPSComparison.tsx b/apps/web/components/wizard/VPSComparison.tsx index 5dc83fb0..3cef5207 100644 --- a/apps/web/components/wizard/VPSComparison.tsx +++ b/apps/web/components/wizard/VPSComparison.tsx @@ -189,7 +189,7 @@ export function VPSComparison() { {provider.name} </span> {provider.isTopPick && ( - <span className="inline-flex items-center gap-0.5 rounded-full bg-primary/20 px-1.5 py-0.5 text-[11px] font-medium text-primary"> + <span className="inline-flex items-center gap-0.5 rounded-full bg-primary/20 px-1.5 py-0.5 text-xs font-medium text-primary"> <Star className="h-2.5 w-2.5" /> Top pick </span> diff --git a/apps/web/e2e/bottom-sheet.spec.ts b/apps/web/e2e/bottom-sheet.spec.ts new file mode 100644 index 00000000..60b0e226 --- /dev/null +++ b/apps/web/e2e/bottom-sheet.spec.ts @@ -0,0 +1,270 @@ +import { test, expect } from "@playwright/test"; + +/** + * BottomSheet Component E2E Tests + * + * Tests the BottomSheet component's interactive behavior including: + * - Opening and closing animations + * - Swipe-to-dismiss gesture + * - Escape key dismissal + * - Backdrop click dismissal + * - Touch target sizes (44px minimum) + * - Reduced motion behavior + * - ARIA attributes for accessibility + * + * Note: These tests require a page that uses the BottomSheet component. + * Once bd-3co7k.4.1 (Convert Jargon Modal to BottomSheet) is complete, + * update the test routes accordingly. + */ + +test.describe("BottomSheet Component", () => { + test.beforeEach(async ({ page }) => { + // Set mobile viewport for bottom sheet tests + await page.setViewportSize({ width: 375, height: 812 }); // iPhone X + console.log("[E2E] Set mobile viewport for bottom sheet tests"); + }); + + test("bottom sheet opens with correct ARIA attributes", async ({ page }) => { + // Navigate to a page with BottomSheet (glossary/jargon page once converted) + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + // Look for any element that could trigger a bottom sheet + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + console.log("[E2E] Clicked jargon term trigger"); + + // Verify dialog appears with correct ARIA attributes + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + await expect(dialog).toHaveAttribute("aria-modal", "true"); + console.log("[E2E] Bottom sheet visible with correct ARIA attributes"); + } else { + console.log( + "[E2E] No jargon triggers found - BottomSheet not yet integrated. Skipping." + ); + test.skip(); + } + }); + + test("escape key closes bottom sheet", async ({ page }) => { + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + console.log("[E2E] Bottom sheet opened"); + + // Press Escape + await page.keyboard.press("Escape"); + console.log("[E2E] Pressed Escape key"); + + // Sheet should close + await expect(dialog).not.toBeVisible({ timeout: 2000 }); + console.log("[E2E] Bottom sheet dismissed via Escape key"); + } else { + console.log("[E2E] No jargon triggers found - skipping Escape test"); + test.skip(); + } + }); + + test("backdrop click closes bottom sheet", async ({ page }) => { + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + console.log("[E2E] Bottom sheet opened"); + + // Click the backdrop (outside the dialog) + // Calculate position above the sheet dynamically + const dialogBox = await dialog.boundingBox(); + const viewport = page.viewportSize(); + if (dialogBox && viewport) { + // Click in the center horizontally, but above the sheet (in backdrop area) + const clickX = viewport.width / 2; + const clickY = dialogBox.y / 2; // Click halfway between top of screen and sheet + await page.mouse.click(clickX, clickY); + console.log(`[E2E] Clicked backdrop area at (${clickX}, ${clickY})`); + } else { + // Fallback to a position near top of screen + await page.mouse.click(187, 50); + console.log("[E2E] Clicked backdrop area (fallback coordinates)"); + } + + await expect(dialog).not.toBeVisible({ timeout: 2000 }); + console.log("[E2E] Bottom sheet dismissed via backdrop click"); + } else { + console.log("[E2E] No jargon triggers found - skipping backdrop test"); + test.skip(); + } + }); + + test("close button meets 44px touch target", async ({ page }) => { + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + + // Find close button + const closeButton = dialog.getByRole("button", { name: /close/i }); + await expect(closeButton).toBeVisible(); + + const box = await closeButton.boundingBox(); + if (box) { + console.log(`[E2E] Close button size: ${box.width}x${box.height}`); + expect(box.width).toBeGreaterThanOrEqual(44); + expect(box.height).toBeGreaterThanOrEqual(44); + console.log("[E2E] Close button meets 44px touch target requirement"); + } else { + console.log("[E2E] Could not get close button bounding box - skipping size check"); + test.skip(); + } + } else { + console.log("[E2E] No jargon triggers found - skipping touch target test"); + test.skip(); + } + }); + + test("swipe down gesture closes bottom sheet", async ({ page }) => { + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + console.log("[E2E] Bottom sheet opened"); + + // Get the sheet's bounding box + const box = await dialog.boundingBox(); + if (box) { + // Simulate swipe down gesture + const startX = box.x + box.width / 2; + const startY = box.y + 50; // Start near top of sheet + const endY = box.y + 350; // Swipe down 300px + + await page.mouse.move(startX, startY); + await page.mouse.down(); + // Move slowly to simulate a real swipe + for (let y = startY; y <= endY; y += 30) { + await page.mouse.move(startX, y); + await page.waitForTimeout(10); + } + await page.mouse.up(); + console.log("[E2E] Performed swipe down gesture"); + + // Sheet should close + await expect(dialog).not.toBeVisible({ timeout: 2000 }); + console.log("[E2E] Bottom sheet dismissed via swipe gesture"); + } else { + console.log( + "[E2E] Could not get dialog bounding box - skipping swipe test" + ); + test.skip(); + } + } else { + console.log("[E2E] No jargon triggers found - skipping swipe test"); + test.skip(); + } + }); + + test("body scroll is locked when bottom sheet is open", async ({ page }) => { + await page.goto("/learn"); + console.log("[E2E] Navigated to learn page"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + // Check initial scroll state + const initialOverflow = await page.evaluate( + () => document.body.style.overflow + ); + console.log(`[E2E] Initial body overflow: "${initialOverflow}"`); + + await jargonTrigger.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + + // Check scroll lock + const lockedOverflow = await page.evaluate( + () => document.body.style.overflow + ); + console.log(`[E2E] Body overflow when sheet open: "${lockedOverflow}"`); + expect(lockedOverflow).toBe("hidden"); + + // Close the sheet + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible({ timeout: 2000 }); + + // Check scroll restored + const restoredOverflow = await page.evaluate( + () => document.body.style.overflow + ); + console.log(`[E2E] Body overflow after close: "${restoredOverflow}"`); + expect(restoredOverflow).not.toBe("hidden"); + } else { + console.log("[E2E] No jargon triggers found - skipping scroll lock test"); + test.skip(); + } + }); +}); + +test.describe("BottomSheet Reduced Motion", () => { + test("respects prefers-reduced-motion", async ({ page }) => { + // Enable reduced motion preference + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.setViewportSize({ width: 375, height: 812 }); + console.log("[E2E] Enabled prefers-reduced-motion: reduce"); + + await page.goto("/learn"); + + const jargonTrigger = page.locator('[data-testid="jargon-term"]').first(); + const hasTrigger = (await jargonTrigger.count()) > 0; + + if (hasTrigger) { + await jargonTrigger.click(); + + // With reduced motion, the sheet should still appear + // but without slide animation (using opacity instead) + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5000 }); + console.log( + "[E2E] Bottom sheet visible with reduced motion preference respected" + ); + + // Escape should still work + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible({ timeout: 2000 }); + console.log("[E2E] Sheet closed correctly with reduced motion"); + } else { + console.log( + "[E2E] No jargon triggers found - skipping reduced motion test" + ); + test.skip(); + } + }); +}); diff --git a/apps/web/e2e/generated-data-pages.spec.ts b/apps/web/e2e/generated-data-pages.spec.ts index 2e2285b8..c487f21a 100644 --- a/apps/web/e2e/generated-data-pages.spec.ts +++ b/apps/web/e2e/generated-data-pages.spec.ts @@ -66,6 +66,14 @@ async function waitForPageSettled(page: Page): Promise<void> { await page.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {}); } +/** Wait for tools page hydration - stats text indicates client component has mounted */ +async function waitForToolsPageHydrated(page: Page): Promise<void> { + await waitForPageSettled(page); + // Wait for client component hydration by checking for dynamic content + // Use expect with auto-retry for more robustness + await expect(page.getByText(/Showing \d+ of \d+ tools/)).toBeVisible({ timeout: 15000 }); +} + test.describe("Flywheel Page (generated data)", () => { test("loads without JS errors or failed requests", async ({ page }) => { const { jsErrors, failedRequests } = setupErrorMonitoring(page); @@ -208,6 +216,68 @@ test.describe("Learn Dashboard (generated data)", () => { }); }); +test.describe("Tools Status Page (generated data)", () => { + test("loads without JS errors or failed requests", async ({ page }) => { + const { jsErrors, failedRequests } = setupErrorMonitoring(page); + + await page.goto("/tools"); + await waitForPageSettled(page); + + // Page has main heading + await expect(page.locator("h1").first()).toBeVisible(); + + // No errors + expect(failedRequests).toEqual([]); + expect(jsErrors).toEqual([]); + }); + + test("renders tool cards from generated manifest data", async ({ page }) => { + await page.goto("/tools"); + await waitForPageSettled(page); + + // Page should have substantial content from manifest tools + const textContent = await page.textContent("body"); + expect(textContent?.length).toBeGreaterThan(1000); + + // Should have multiple tool card elements + const elements = page.locator("div, section, article"); + const count = await elements.count(); + expect(count).toBeGreaterThan(10); + }); + + test("displays category filters", async ({ page }) => { + await page.goto("/tools"); + await waitForToolsPageHydrated(page); + + // Check for category filter buttons + const allButton = page.getByRole("button", { name: "All" }); + await expect(allButton).toBeVisible(); + + // Page should have filter buttons for categories + const buttons = page.locator("button"); + const buttonCount = await buttons.count(); + expect(buttonCount).toBeGreaterThan(1); + }); + + test("has search input element", async ({ page }) => { + await page.goto("/tools"); + await waitForToolsPageHydrated(page); + + // Find search input + const searchInput = page.locator("input[type='text'], input[placeholder*='earch']"); + await expect(searchInput.first()).toBeVisible(); + }); + + test("stats bar shows tool counts", async ({ page }) => { + await page.goto("/tools"); + await waitForToolsPageHydrated(page); + + // Stats bar should show "Showing X of Y tools" + const statsText = page.getByText(/Showing \d+ of \d+ tools/); + await expect(statsText).toBeVisible(); + }); +}); + test.describe("Tool Detail Pages (generated data)", () => { // Sample tool pages - these use hand-maintained tool-data.tsx // which is merged with manifest data via manifest-adapter diff --git a/apps/web/e2e/wizard-flow.spec.ts b/apps/web/e2e/wizard-flow.spec.ts index fa364647..173f4e9d 100644 --- a/apps/web/e2e/wizard-flow.spec.ts +++ b/apps/web/e2e/wizard-flow.spec.ts @@ -753,6 +753,94 @@ test.describe("Step 9: Run Installer Page", () => { // Warning message should be visible await expect(page.locator('text=/don.t close the terminal/i')).toBeVisible(); }); + + test("should have pin-ref toggle checkbox", async ({ page }) => { + await page.goto("/wizard/run-installer"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Pin checkbox should be visible + const pinCheckbox = page.locator('#pin-ref'); + await expect(pinCheckbox).toBeVisible(); + // Should be unchecked by default + await expect(pinCheckbox).not.toBeChecked(); + }); + + test("should show pinned ref input when toggle is enabled", async ({ page }) => { + await page.goto("/wizard/run-installer"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Initially, input should not be visible + const refInput = page.locator('input[placeholder*="main, v1.0.0"]'); + await expect(refInput).not.toBeVisible(); + + // Enable the pin toggle + await page.locator('#pin-ref').click(); + + // Now input should be visible + await expect(refInput).toBeVisible(); + // Default value should be "main" + await expect(refInput).toHaveValue("main"); + }); + + test("should update command when pinned ref is set", async ({ page }) => { + await page.goto("/wizard/run-installer"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Get the default command (without pinning) + const commandElement = page.locator('code').filter({ hasText: 'curl -fsSL' }).first(); + const defaultCommand = await commandElement.textContent(); + expect(defaultCommand).not.toContain('ACFS_REF='); + + // Enable pinning and set a custom ref + await page.locator('#pin-ref').click(); + const refInput = page.locator('input[placeholder*="main, v1.0.0"]'); + await refInput.clear(); + await refInput.fill("v1.2.3"); + await refInput.blur(); + + // Command should now include ACFS_REF + await expect(commandElement).toContainText('ACFS_REF="v1.2.3"'); + await expect(commandElement).toContainText('v1.2.3/install.sh'); + }); + + test("should include commit SHA in command when pinned", async ({ page }) => { + await page.goto("/wizard/run-installer"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Enable pinning with a commit SHA + await page.locator('#pin-ref').click(); + const refInput = page.locator('input[placeholder*="main, v1.0.0"]'); + await refInput.clear(); + await refInput.fill("abc123def456"); + await refInput.blur(); + + // Command should include the SHA + const commandElement = page.locator('code').filter({ hasText: 'curl -fsSL' }).first(); + await expect(commandElement).toContainText('ACFS_REF="abc123def456"'); + await expect(commandElement).toContainText('abc123def456/install.sh'); + }); + + test("should revert to default command when pin toggle is disabled", async ({ page }) => { + await page.goto("/wizard/run-installer"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Enable pinning + await page.locator('#pin-ref').click(); + const refInput = page.locator('input[placeholder*="main, v1.0.0"]'); + await refInput.clear(); + await refInput.fill("custom-ref"); + await refInput.blur(); + + // Verify pinned command + const commandElement = page.locator('code').filter({ hasText: 'curl -fsSL' }).first(); + await expect(commandElement).toContainText('ACFS_REF="custom-ref"'); + + // Disable pinning + await page.locator('#pin-ref').click(); + + // Command should no longer include ACFS_REF + await expect(commandElement).not.toContainText('ACFS_REF='); + }); }); // ============================================================================= @@ -1406,3 +1494,235 @@ test.describe("Accessibility", () => { await expect(checkboxes.first()).toHaveAttribute('aria-checked', 'true'); }); }); + +// ============================================================================= +// COMMAND BUILDER - E2E Tests (bd-31ps.4.3) +// ============================================================================= +test.describe("Command Builder Panel", () => { + test.beforeEach(async ({ page }) => { + await setupWizardState(page, { os: "mac", ip: "192.168.1.100" }); + }); + + test("should display command builder on launch-onboarding page", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Command builder panel should be visible + await expect(page.locator('text="Your Commands"')).toBeVisible(); + }); + + test("should show SSH root command with stored IP", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // SSH root command should include the stored IP + await expect(page.locator('text="ssh root@192.168.1.100"')).toBeVisible(); + }); + + test("should show installer command in vibe mode by default", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Installer command should include --mode vibe + await expect(page.locator('code').filter({ hasText: '--mode vibe' }).first()).toBeVisible(); + }); + + test("should update installer command when mode is changed to safe", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Click on Safe mode button + const safeModeBtn = page.locator('button:has-text("Safe")'); + await safeModeBtn.click(); + + // Installer command should now include --mode safe + await expect(page.locator('code').filter({ hasText: '--mode safe' }).first()).toBeVisible(); + }); + + test("should show advanced settings when clicked", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Advanced settings should be hidden initially + const usernameInput = page.locator('#cb-user'); + await expect(usernameInput).not.toBeVisible(); + + // Click Advanced toggle + await page.click('button:has-text("Advanced")'); + + // Now username and ref inputs should be visible + await expect(usernameInput).toBeVisible(); + await expect(page.locator('#cb-ref')).toBeVisible(); + }); + + test("should update SSH user command when username is changed", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Default should show ubuntu user + await expect(page.locator('text="SSH as ubuntu"')).toBeVisible(); + + // Open advanced settings + await page.click('button:has-text("Advanced")'); + + // Change username + const usernameInput = page.locator('#cb-user'); + await usernameInput.clear(); + await usernameInput.fill("devuser"); + await usernameInput.blur(); + + // Command label and command should update + await expect(page.locator('text="SSH as devuser"')).toBeVisible(); + await expect(page.locator('code').filter({ hasText: 'devuser@192.168.1.100' }).first()).toBeVisible(); + }); + + test("should include ACFS_REF in installer command when ref is set", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Open advanced settings + await page.click('button:has-text("Advanced")'); + + // Set a pinned ref + const refInput = page.locator('#cb-ref'); + await refInput.clear(); + await refInput.fill("v1.0.0"); + await refInput.blur(); + + // Installer command should include ACFS_REF + await expect(page.locator('code').filter({ hasText: 'ACFS_REF="v1.0.0"' }).first()).toBeVisible(); + }); + + test("should have share link button that copies URL", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Share button should be visible + const shareBtn = page.locator('button:has-text("Share link")'); + await expect(shareBtn).toBeVisible(); + + // Click share button + await shareBtn.click(); + + // Button text should change to "Copied!" + await expect(page.locator('button:has-text("Copied!")')).toBeVisible(); + }); + + test("should have copy buttons for each command", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Should have multiple copy buttons (one for each command) + const copyButtons = page.locator('button[aria-label*="Copy"]'); + const count = await copyButtons.count(); + expect(count).toBeGreaterThanOrEqual(4); // ssh-root, installer, ssh-user, doctor, onboard + }); + + test("should show checkmark after clicking copy button", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Click first copy button + const copyBtn = page.locator('button[aria-label*="Copy"]').first(); + await copyBtn.click(); + + // Check icon should appear briefly (indicating copied state) + // The button contains an SVG that changes from Copy to Check + await expect(copyBtn.locator('svg.text-\\[oklch\\(0\\.72_0\\.19_145\\)\\]')).toBeVisible(); + }); + + test("should restore state from URL query params", async ({ page }) => { + // Navigate with query params + await page.goto("/wizard/launch-onboarding?ip=10.20.30.40&mode=safe&user=admin&ref=v2.0.0"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Commands should reflect the URL params + // Note: IP might still use localStorage if set; this tests fresh load + await expect(page.locator('code').filter({ hasText: '--mode safe' }).first()).toBeVisible(); + }); + + test("should display IP input when no IP is stored", async ({ page }) => { + // Clear state and navigate + await page.goto("/"); + await page.evaluate(() => localStorage.clear()); + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // IP input should be visible when no IP is stored + const ipInput = page.locator('#cb-ip'); + await expect(ipInput).toBeVisible(); + + // Should show placeholder message + await expect(page.locator('text="Enter your VPS IP to generate personalized commands."')).toBeVisible(); + }); + + test("should validate IP input and show error for invalid IP", async ({ page }) => { + // Clear state + await page.goto("/"); + await page.evaluate(() => localStorage.clear()); + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Enter invalid IP + const ipInput = page.locator('#cb-ip'); + await ipInput.fill("not-an-ip"); + await ipInput.blur(); + + // Error message should appear + await expect(page.locator('text="Enter a valid IP (e.g., 203.0.113.42)"')).toBeVisible(); + }); + + test("should generate commands when valid IP is entered", async ({ page }) => { + // Clear state + await page.goto("/"); + await page.evaluate(() => localStorage.clear()); + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Enter valid IP + const ipInput = page.locator('#cb-ip'); + await ipInput.fill("203.0.113.42"); + await ipInput.blur(); + + // Commands should appear with the entered IP + await expect(page.locator('text="ssh root@203.0.113.42"')).toBeVisible(); + }); +}); + +// ============================================================================= +// COMMAND BUILDER - Mobile Tests (bd-31ps.4.3) +// ============================================================================= +test.describe("Command Builder Panel - Mobile", () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await setupWizardState(page, { os: "mac", ip: "192.168.1.100" }); + }); + + test("should display command builder on mobile", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Command builder should be visible on mobile + await expect(page.locator('text="Your Commands"')).toBeVisible(); + }); + + test("should have horizontally scrollable command text", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Code blocks should have overflow-x-auto for scrolling + const codeBlock = page.locator('code.overflow-x-auto').first(); + await expect(codeBlock).toBeVisible(); + }); + + test("should toggle mode on mobile", async ({ page }) => { + await page.goto("/wizard/launch-onboarding"); + await expect(page.locator("h1").first()).toBeVisible({ timeout: TIMEOUTS.PAGE_LOAD }); + + // Toggle to Safe mode + await page.click('button:has-text("Safe")'); + + // Command should update + await expect(page.locator('code').filter({ hasText: '--mode safe' }).first()).toBeVisible(); + }); +}); diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index 514aedc0..a87fb407 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -672,8 +672,8 @@ export const trackSessionStart = (): void => { is_first_visit: isFirstVisit, }); - // Check for returning user - const visitCount = parseInt(safeGetItem('acfs_visit_count') || '0', 10) + 1; + // Check for returning user (use || 0 to handle NaN from corrupted storage) + const visitCount = (parseInt(safeGetItem('acfs_visit_count') || '0', 10) || 0) + 1; safeSetItem('acfs_visit_count', String(visitCount)); // Set comprehensive user properties diff --git a/apps/web/lib/commands.ts b/apps/web/lib/commands.ts index 44e4db29..dfa3a38c 100644 --- a/apps/web/lib/commands.ts +++ b/apps/web/lib/commands.ts @@ -194,11 +194,11 @@ export const COMMANDS: CommandRef[] = [ example: "direnv allow", }, { - name: "bd", + name: "br", fullName: "Beads CLI", description: "Task graph management.", category: "stack", - example: "bd ready", + example: "br ready", }, { name: "bv", diff --git a/apps/web/lib/design-tokens.ts b/apps/web/lib/design-tokens.ts index a55ed55d..8223a004 100644 --- a/apps/web/lib/design-tokens.ts +++ b/apps/web/lib/design-tokens.ts @@ -115,8 +115,8 @@ export const radius = { * Section header typography */ export const typography = { - /** Section label (uppercase, small) */ - sectionLabel: "text-[11px] font-bold uppercase tracking-[0.25em] text-primary", + /** Section label (uppercase, small, min 12px for accessibility) */ + sectionLabel: "text-xs font-bold uppercase tracking-[0.25em] text-primary", /** Section heading */ sectionHeading: "font-mono text-3xl font-bold tracking-tight", /** Large section heading */ @@ -125,6 +125,32 @@ export const typography = { sectionDescription: "mx-auto max-w-2xl text-muted-foreground", } as const; +/** + * Display typography - fluid values for hero sections (used in CSS variables) + */ +export const displayTypography = { + /** Font sizes using CSS clamp for fluid scaling */ + fontSize: { + "5xl": "clamp(3rem, 2.5rem + 2.5vw, 5rem)", + "6xl": "clamp(3.5rem, 3rem + 3vw, 6rem)", + }, + /** Letter spacing for large display text */ + tracking: { + "5xl": "-0.035em", + "6xl": "-0.04em", + }, + /** Line heights for large display text */ + leading: { + "5xl": 1.05, + "6xl": 1, + }, + /** Tailwind utility classes for display text */ + classes: { + "5xl": "text-display-5xl", + "6xl": "text-display-6xl", + }, +} as const; + // ============================================================================= // ANIMATION CLASSES // ============================================================================= diff --git a/apps/web/lib/flywheel.ts b/apps/web/lib/flywheel.ts index e515a43c..4e04f353 100644 --- a/apps/web/lib/flywheel.ts +++ b/apps/web/lib/flywheel.ts @@ -449,37 +449,42 @@ const _flywheelTools: FlywheelTool[] = [ href: "https://github.com/Dicklesworthstone/ntm", icon: "LayoutGrid", color: "from-sky-400 to-blue-500", - tagline: "The agent cockpit", + tagline: "Multi-agent tmux command center", description: - "Transform tmux into a multi-agent command center. Spawn Claude, Codex, and Gemini agents in named panes. Broadcast prompts to specific agent types. Persistent sessions survive SSH disconnects.", + "Orchestrate multiple AI coding agents across tmux sessions. Spawn Claude, Codex, and Gemini agents in named panes. 80+ commands for session management, prompt broadcasting, file conflict detection, and context rotation. Persistent sessions survive SSH disconnects.", deepDescription: - "NTM is the orchestration layer that lets you run multiple AI agents in parallel. Spawn agents with type classification (cc/cod/gmi), broadcast prompts with filtering, use the command palette TUI for quick actions. Features include configurable hooks, robot mode for automation, and deep Agent Mail integration.", - connectsTo: ["slb", "mail", "cass", "caam", "ru", "srps"], + "NTM transforms tmux into a multi-agent command center with 80+ commands. Spawn agents with type classification (cc/cod/gmi), broadcast prompts with filtering, and use the command palette TUI for quick actions. Features context window monitoring with automatic compaction recovery, checkpoints for session state management, agent profiles/personas for specialized roles, and deep integrations with Agent Mail (file reservations, messaging), CASS (session search), and beads (--robot-bead-* commands). Robot mode (--robot-*) provides JSON output for automation.", + connectsTo: ["slb", "mail", "cass", "caam", "ru", "srps", "bv", "br", "dcg"], connectionDescriptions: { slb: "Routes dangerous commands through SLB safety checks", - mail: "Spawned agents auto-register with Mail for coordination", - cass: "All session history indexed for cross-agent search", + mail: "Agents auto-register with Mail; ntm mail commands for messaging; pre-commit guard for file reservations", + cass: "Direct integration via --robot-cass-search, --robot-cass-context, --robot-cass-status", caam: "Quick-switches credentials when spawning new agents", ru: "RU agent-sweep uses ntm robot mode for orchestration", srps: "SRPS keeps tmux sessions responsive when agents spawn heavy builds", + bv: "Graph analysis via --robot-plan, --robot-graph for dependency insights", + br: "Bead management via --robot-bead-create, --robot-bead-claim, --robot-bead-close", + dcg: "DCG hooks protect agents in NTM sessions from destructive commands", }, stars: 16, features: [ - "Spawn multiple agents: ntm spawn project --cc=3 --cod=2 --gmi=1", - "Broadcast to agent types: ntm send project --cc 'prompt'", - "Command palette TUI with fuzzy search and categories", - "Real-time dashboard with Catppuccin color themes", - "Robot mode for scripting: --robot-status, --robot-plan", - "Hooks: pre/post-spawn, pre/post-send, pre/post-shutdown", + "80+ commands: spawn, send, dashboard, palette, checkpoint, health, and more", + "Agent types: Claude (cc), Codex (cod), Gemini (gmi) with named panes", + "Context rotation: monitors usage, warns at 80%, auto-compaction recovery", + "Command palette TUI with fuzzy search, Catppuccin themes, pinned commands", + "Robot mode: --robot-status, --robot-snapshot, --robot-plan, --robot-mail", + "Hooks: pre/post-spawn, pre/post-send, pre/post-shutdown with env vars", ], cliCommands: [ "ntm spawn <session> --cc=N --cod=N --gmi=N", "ntm send <session> --cc 'prompt'", - "ntm palette [session]", - "ntm dashboard [session]", + "ntm --robot-status", + "ntm --robot-snapshot", + "ntm dashboard <session>", + "ntm checkpoint save <session> -m 'description'", ], installCommand: - "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/ntm/main/install.sh | bash", + "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/ntm/main/install.sh | bash -s -- --easy-mode", language: "Go", }, { @@ -491,9 +496,9 @@ const _flywheelTools: FlywheelTool[] = [ color: "from-violet-400 to-purple-500", tagline: "Gmail for your agents", description: - "A complete coordination system for multi-agent workflows. Agents register identities, send/receive messages, search conversations, and declare file reservations to prevent edit conflicts.", + "A complete coordination system for multi-agent workflows. Agents register identities, send/receive messages, search conversations, and declare file reservations to prevent edit conflicts. HTTP-only FastMCP server with static export and Web UI.", deepDescription: - "Agent Mail is the nervous system of the flywheel. It provides: agent identities (adjective+noun names like 'BlueLake'), threaded markdown messages, full-text search, and advisory file locks. SQLite-backed storage means complete audit trails. 20+ MCP tools for programmatic access.", + "Agent Mail is the nervous system of the flywheel. HTTP-only transport (Streamable HTTP) for modern MCP clients. Provides: agent identities (adjective+noun names like 'BlueLake'), threaded GFM messages, FTS5 full-text search, and advisory file reservations. SQLite + Git dual persistence means human-auditable artifacts. 30+ MCP tools including macros for common workflows. Static mailbox export with Ed25519 signing and age encryption for audits. Web UI for exploration and Human Overseer for human-to-agent messaging.", connectsTo: ["bv", "cm", "slb", "ntm", "ru"], connectionDescriptions: { bv: "Task IDs link conversations to Beads issues", @@ -505,18 +510,20 @@ const _flywheelTools: FlywheelTool[] = [ stars: 1015, demoUrl: "https://dicklesworthstone.github.io/cass-memory-system-agent-mailbox-viewer/viewer/", features: [ - "Agent identities with auto-generated names", + "Agent identities with adjective+noun names (BlueLake, GreenCastle)", "GitHub-flavored Markdown messages with threading", - "Advisory file reservations (exclusive/shared, TTL)", - "Full-text search across all conversations", - "Contact policies: open, auto, contacts_only, block_all", - "Macro helpers for common workflows", + "Advisory file reservations with pre-commit guard enforcement", + "FTS5 full-text search with boolean operators", + "Contact policies with auto-allow heuristics", + "Static export with Ed25519 signing and age encryption", + "Web UI and Human Overseer for human-to-agent messaging", + "Product bus for multi-repo coordination", ], cliCommands: [ - "ensure_project(human_key='/path/to/project')", - "register_agent(project_key, program='claude-code', model='opus-4.5')", - "send_message(project_key, sender, to=['Agent'], subject, body_md)", - "file_reservation_paths(project_key, agent, paths, ttl_seconds)", + "mcp-agent-mail serve-http --port 8765", + "mcp-agent-mail guard install <project> <repo>", + "mcp-agent-mail share wizard", + "mcp-agent-mail doctor check --verbose", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/mcp_agent_mail/main/scripts/install.sh" | bash -s -- --yes', @@ -529,34 +536,34 @@ const _flywheelTools: FlywheelTool[] = [ href: "https://github.com/Dicklesworthstone/ultimate_bug_scanner", icon: "Bug", color: "from-rose-400 to-red-500", - tagline: "AST-based pattern detection", + tagline: "1000+ bug patterns for AI workflows", description: - "Custom AST-grep patterns detecting subtle bugs across 7+ languages. Designed to have false positives for AI agents to evaluate. Sub-5-second feedback loops. Perfect as pre-commit hook or agent post-processor.", + "AST-grep patterns detecting 1000+ bug types across 8 languages. 18 detection categories from null safety to security vulnerabilities. Sub-5-second scans. Auto-wires into Claude Code, Codex, Cursor, Gemini, and Windsurf agents.", deepDescription: - "UBS uses ast-grep for AST-based pattern matching that tolerates false positives for AI agent review. 18 detection categories including null safety, async bugs, security vulnerabilities, and memory leaks. Zero configuration required. Unified JSON/JSONL/SARIF output for automation.", - connectsTo: ["bv", "slb"], + "UBS is a meta-runner that fans out per-language scanners (ubs-js.sh, ubs-python.sh, etc.) and merges results into unified output. Uses ast-grep for AST-based pattern matching with 18 detection categories: null safety, async/await bugs, security holes (XSS, injection), memory leaks, type coercion, and more. Supports --beads-jsonl for Beads integration. The installer auto-detects local coding agents and wires guardrails (on-file-write hooks for Claude Code, .cursorrules blocks).", + connectsTo: ["bv", "br"], connectionDescriptions: { - bv: "Creates Beads issues for discovered bugs", - slb: "Pre-validates code before risky operations", + bv: "Bug findings create blocking issues via --beads-jsonl output", + br: "Direct JSONL output for beads_rust issue creation", }, stars: 91, features: [ - "7 languages: JS/TS, Python, Go, Rust, C/C++, Java, Ruby", - "18 detection categories: security, async bugs, null safety", - "Sub-5-second feedback loops", - "Unified output: --format=json|jsonl|sarif", - "Baseline comparison for drift detection", - "On-file-write hooks for Claude Code, Cursor", + "8 languages: JS/TS, Python, Go, Rust, C/C++, Java, Ruby, Swift", + "18 detection categories: security, async, null safety, memory leaks", + "5 output formats: text, json, jsonl, sarif, toon", + "Agent guardrails: Claude Code hooks, .cursorrules, .codex/rules", + "Baseline comparison: --comparison for drift detection", + "Git-aware: --staged, --diff for targeted scans", ], cliCommands: [ "ubs . --format=json", - "ubs --ci --fail-on-warning .", + "ubs --staged --fail-on-warning", "ubs --only=python,js src/", - "ubs --comparison baseline.json .", + "ubs --beads-jsonl findings.jsonl .", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh" | bash -s -- --easy-mode', - language: "Python", + language: "Shell", }, { id: "bv", @@ -570,7 +577,7 @@ const _flywheelTools: FlywheelTool[] = [ "Transforms task tracking with DAG-based analysis. Nine graph metrics, robot protocol for AI, time-travel diffing. Agents use BV to figure out what to work on next.", deepDescription: "BV treats your project as a Directed Acyclic Graph. Computes PageRank, Betweenness Centrality, HITS, Critical Path, and more. Robot protocol (--robot-*) outputs structured JSON for agents. Time-travel lets you diff across git history.", - connectsTo: ["br", "mail", "ubs", "cass", "cm", "ru"], + connectsTo: ["br", "mail", "ubs", "cass", "cm", "ru", "ntm"], connectionDescriptions: { br: "Reads and visualizes issues created by beads_rust (br)", mail: "Task updates trigger notifications", @@ -578,16 +585,17 @@ const _flywheelTools: FlywheelTool[] = [ cass: "Search prior sessions for task context", cm: "Remembers successful approaches", ru: "RU integrates with beads for multi-repo task tracking", + ntm: "NTM uses --robot-plan, --robot-graph for dependency insights during agent orchestration", }, stars: 546, demoUrl: "https://dicklesworthstone.github.io/beads_viewer-pages/", features: [ - "9 graph metrics: PageRank, Betweenness, HITS, Critical Path", - "6 TUI views: list, kanban, graph, insights, history, flow", - "Robot protocol: --robot-triage, --robot-plan, --robot-insights", - "Time-travel: --as-of HEAD~30, --diff-since '30 days ago'", - "Export to Markdown, HTML (Cytoscape.js), SQLite", - "Live reload on beads.jsonl changes", + "9 graph metrics: PageRank, Betweenness, HITS, Critical Path, Eigenvector, Degree, Density, Cycles, Topo Sort", + "6 TUI views: list, kanban, graph, tree, insights, history", + "Robot protocol: --robot-triage, --robot-plan, --robot-insights, --robot-alerts, --robot-forecast", + "11 built-in recipes: actionable, high-impact, bottlenecks, stale, quick-wins", + "Export to Markdown, HTML (D3.js force-graph), SQLite static sites (--pages)", + "Live reload on beads.jsonl changes, TOON format for low-token output", ], cliCommands: [ "bv --robot-triage", @@ -608,44 +616,32 @@ const _flywheelTools: FlywheelTool[] = [ color: "from-amber-400 to-orange-500", tagline: "Rust-powered issue tracking CLI", description: - "Local-first issue tracking for AI agents. SQLite + JSONL hybrid: fast queries locally, git-friendly export for collaboration. Non-invasive - never auto-commits or touches source code.", - deepDescription: `beads_rust (br) is a ~20K-line Rust port of Steve Yegge's beads, frozen at the -"classic" SQLite + JSONL architecture. Issues live in .beads/ - they travel with your repo. - -Key architecture: -- SQLite for fast local queries (list, ready, blocked, search) -- JSONL export (br sync --flush-only) for git-friendly commits -- Non-invasive: never runs git commands or installs hooks automatically -- Agent-first: every command supports --json for machine consumption - -JSONL schema fields: id, title, description, status, priority (0-4), issue_type, -created_at, created_by, updated_at, close_reason, closed_at, source_repo, -compaction_level, dependencies, labels, owner - -40 commands including: create, list, ready, blocked, dep, label, epic, -defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph`, - connectsTo: ["bv", "mail", "ntm", "ru"], + "Local-first issue tracking for AI agents. SQLite primary storage with JSONL export for git. Dependencies, labels, priorities (P0-P4), blocking relationships. Non-invasive: never runs git commands automatically. The bd alias provides backward compatibility.", + deepDescription: + "beads_rust (br) is the ~20K line Rust port of the beads issue tracker. SQLite for fast local queries, JSONL for git-friendly collaboration. Full dependency graph, labels, priorities, comments. Agent-first design: all commands support --json. Explicit sync (flush-only/import-only). Works offline. Doctor diagnostics and schema output (--format toon/json).", + connectsTo: ["bv", "mail", "ntm", "ru", "ubs"], connectionDescriptions: { bv: "BV visualizes and analyzes beads from br", mail: "Task updates notify agents via mail", ntm: "NTM spawns agents that pick work from beads", ru: "RU syncs repos containing beads across projects", + ubs: "UBS --beads-jsonl outputs findings as importable beads", }, stars: 128, features: [ - "SQLite + JSONL hybrid: fast queries + git-friendly export", - "Non-invasive: never auto-commits or touches source code", - "Full dependency graph: blocks/blocked-by with br dep", - "br ready: shows unblocked, non-deferred work", - "br stats: lead time, activity, status breakdown", - "40 commands, all support --json for agents", + "SQLite + JSONL hybrid: fast queries, git-friendly export", + "Non-invasive: never runs git commands automatically", + "Full dependency graph: blocks/blocked-by, cycles detection", + "Labels, priorities (P0-P4), comments, assignees", + "Agent-first: all commands support --json/--robot output", + "Rich terminal output with auto TTY detection, doctor diagnostics", ], cliCommands: [ - "br create 'Fix bug' -p 1 --type bug", + "br create 'Fix bug' --priority 1 --type bug", + "br list --status open --priority 0-1 --json", "br ready --json", - "br dep add <child> <parent>", + "br dep add bd-child bd-parent", "br sync --flush-only", - "br stats", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/beads_rust/main/install.sh" | bash', @@ -660,9 +656,9 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-cyan-400 to-sky-500", tagline: "Instant search across all agents", description: - "Unified search for all AI coding sessions. Indexes Claude, Codex, Cursor, Gemini, ChatGPT, Cline, and more. Tantivy-powered <60ms prefix queries.", + "Unified search for all AI coding sessions. Indexes 11 agent formats: Claude Code, Codex, Cursor, Gemini, ChatGPT, Cline, Aider, Pi-Agent, Factory, OpenCode, Amp. Tantivy-powered <60ms queries with optional semantic search.", deepDescription: - "CASS unifies session history from 10 agent formats into a single searchable timeline. Edge n-gram indexing for instant prefix matching. Six ranking modes balance relevance, recency, and match quality. Robot mode with cursor pagination and token budgeting.", + "CASS unifies session history from 11 agent formats into a single searchable timeline. Three search modes: lexical (BM25 with edge n-grams), semantic (local MiniLM or hash embedder fallback), and hybrid (RRF fusion). Robot mode with cursor pagination, field selection, and token budgeting. HTML export with optional AES-256-GCM encryption. Multi-machine search via SSH/rsync with interactive setup wizard.", connectsTo: ["cm", "ntm", "bv"], connectionDescriptions: { cm: "Indexes stored memories for retrieval", @@ -671,18 +667,21 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` }, stars: 145, features: [ - "10 agent formats: Claude Code, Codex, Cursor, Gemini, ChatGPT", - "Tantivy search with <60ms prefix queries", - "6 ranking modes: RecentHeavy, Balanced, RelevanceHeavy", - "Three-pane TUI with 50+ keyboard shortcuts", - "Robot mode with cursor pagination", - "Remote sources via SSH/rsync", + "11 agent formats: Claude Code, Codex, Cursor, Gemini, ChatGPT, Cline, Aider, Pi-Agent, Factory", + "Three search modes: lexical (BM25), semantic (MiniLM/hash fallback), hybrid (RRF)", + "Sub-60ms queries with edge n-gram prefix matching", + "Aggregations for 99% token reduction (--aggregate agent,workspace)", + "Robot mode with cursor pagination and token budgeting", + "Context command finds related sessions for a source path", + "HTML export with optional AES-256-GCM encryption", + "Multi-machine search via SSH with interactive setup wizard", ], cliCommands: [ - 'cass search "query" --robot --limit 10', - "cass index --watch", - "cass sources add user@host --preset macos-defaults", - "cass timeline --today --json", + 'cass search "query" --robot --limit 10 --fields minimal', + 'cass search "*" --json --aggregate agent', + "cass context /path/to/file.ts --json", + "cass health --json", + "cass sources setup", ], installCommand: "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/coding_agent_session_search/main/install.sh | bash -s -- --easy-mode", @@ -695,35 +694,34 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` href: "https://github.com/Dicklesworthstone/cass_memory_system", icon: "Brain", color: "from-pink-400 to-fuchsia-500", - tagline: "Persistent agent memory", + tagline: "Cross-agent procedural memory", description: - "Human-like memory for AI agents. Procedural playbooks, episodic session logs, semantic facts. Agents learn from experience and never repeat mistakes.", + "Transforms scattered agent sessions into persistent, cross-agent memory. Patterns discovered in Cursor automatically help Claude Code on the next session. Three-layer cognitive architecture: Episodic → Working → Procedural.", deepDescription: - "CM implements the ACE (Agentic Context Engineering) framework. Four-stage pipeline: Generator → Reflector → Validator → Curator. Playbook bullets with 90-day decay half-life. Evidence validation against CASS history. The Curator has NO LLM to prevent context collapse.", - connectsTo: ["mail", "cass", "bv"], + "CM implements a three-layer memory system mirroring human expertise: raw sessions (episodic via CASS) → structured summaries (working via diary) → distilled playbook rules (procedural). Rules have 90-day decay half-life and 4× harmful weight. Scientific validation requires evidence from CASS history before rules are accepted. Bad rules auto-invert into anti-pattern warnings.", + connectsTo: ["cass", "mail", "bv"], connectionDescriptions: { - mail: "Stores conversation summaries", - cass: "Semantic search over memories", - bv: "Remembers successful approaches", + cass: "Primary dependency - mines sessions for episodic memory", + mail: "Shares memory context across agent conversations", + bv: "Task patterns and successful approaches remembered", }, stars: 71, - demoUrl: "https://dicklesworthstone.github.io/cass-memory-system-agent-mailbox-viewer/viewer/", features: [ - "ACE pipeline: Generator → Reflector → Validator → Curator", - "Playbook bullets with decay (90-day half-life)", - "5 MCP tools: cm_context, cm_feedback, memory_search", - "Multi-iteration reflection with deduplication", - "Evidence validation against session history", - "4× harmful weight for mistake avoidance", + "Three-layer architecture: Episodic → Working → Procedural", + "Cross-agent learning: all agent sessions feed unified playbook", + "Confidence decay (90-day half-life) prevents stale rules", + "Anti-pattern learning: bad rules become warnings", + "Scientific validation against CASS history", + "Agent-native onboarding with gap analysis", ], cliCommands: [ 'cm context "task description" --json', - "cm reflect --days 7 --max-sessions 20", - "cm feedback --bullet-id b-123 --helpful", - "cm serve", + "cm onboard status --json", + "cm playbook list --json", + "cm stats --json", ], installCommand: - "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/cass_memory_system/main/install.sh | bash -s -- --easy-mode", + "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/cass_memory_system/main/install.sh | bash -s -- --easy-mode --verify", language: "TypeScript", }, { @@ -735,27 +733,33 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-amber-400 to-orange-500", tagline: "Instant auth switching", description: - "Manage multiple API keys for Claude, Codex, and Gemini. Sub-100ms account switching. Smart rotation with cooldown tracking. Encrypted credential bundles.", + "Manage multiple accounts for Claude Code, Codex CLI, and Gemini CLI with sub-100ms switching. Smart rotation algorithms, cooldown tracking, health scoring, and vault-based profile isolation for parallel agent sessions.", deepDescription: - "CAAM enables seamless multi-account workflows. Smart profile rotation considers cooldown state, health, recency, and plan type. Transparent failover with auto-retry. AES-256-GCM encryption with Argon2id key derivation for secure export.", - connectsTo: ["ntm"], + "CAAM enables seamless multi-account workflows for AI coding CLIs. Vault profiles store auth files for instant switching without browser flows. Smart rotation considers cooldown state, health status (healthy/warning/critical), recency, and plan type. Robot mode provides JSON output for agent automation. Features include profile isolation for parallel sessions, background token refresh daemon, and multi-machine vault sync. AES-256-GCM encrypted bundles with Argon2id key derivation for secure export/import.", + connectsTo: ["ntm", "slb", "mail"], connectionDescriptions: { - ntm: "Provides credentials when spawning agents", + ntm: "Provides credentials when spawning agents; enables parallel sessions with isolated profiles", + slb: "Account switching can be coordinated through SLB for team approval workflows", + mail: "Account switches can trigger Agent Mail notifications for coordination", }, stars: 12, features: [ - "Sub-100ms account switching", - "Smart rotation: cooldown, health, recency, plan type", - "Transparent failover with auto-retry", + "Sub-100ms account switching via vault profiles", "3 providers: Claude Code, Codex CLI, Gemini CLI", - "AES-256-GCM encryption for export bundles", - "Background daemon for proactive token refresh", + "Smart rotation: cooldown, health, recency, plan type", + "Health scoring: healthy (🟢), warning (🟡), critical (🔴)", + "caam run with automatic failover on rate limits", + "Project-profile associations (per-directory defaults)", + "Profile isolation for parallel agent sessions", + "Robot mode with JSON output for agent automation", ], cliCommands: [ - "caam activate claude alice@gmail.com", - "caam run claude -- 'your prompt'", - "caam cooldown set claude/alice --minutes 90", - "caam daemon start", + "caam pick claude # fzf-style profile picker", + "caam run claude -- 'prompt' # Auto-failover on limits", + "caam project set claude work # Per-directory profile", + "caam robot status # JSON status for agents", + "caam cooldown set claude/work # Mark rate-limited", + "caam exec codex work -- 'x' # Isolated session", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/coding_agent_account_manager/main/install.sh" | bash', @@ -770,30 +774,34 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-yellow-400 to-amber-500", tagline: "Two-person rule for agents", description: - "Safety friction for autonomous agents. Three-tier risk classification. Cryptographic command binding with SHA-256+HMAC. Dynamic quorum. Complete audit trails.", + "Nuclear-launch-style safety for AI agents. Four risk tiers (CRITICAL/DANGEROUS/CAUTION/SAFE) with 40+ regex patterns. CRITICAL commands require 2+ approvals from different agents. Cryptographic signing, rollback support, and outcome analytics.", deepDescription: - "SLB implements nuclear-launch-style safety for AI agents. CRITICAL commands need 2+ approvals from different models. Commands bound with SHA-256 hash. Reviews signed with HMAC. Self-review protection prevents agents from approving their own requests.", - connectsTo: ["mail", "ubs", "ntm", "srps"], + "SLB implements a two-person authorization rule for dangerous commands. Commands are classified by regex patterns: CRITICAL (rm -rf /, DROP DATABASE, terraform destroy) needs 2+ approvals, DANGEROUS (git reset --hard, rm -rf) needs 1, CAUTION auto-approves after 30s, SAFE skips entirely. Approvals are cryptographically signed with HMAC. Features include Claude Code hooks, Cursor rules generation, session management for agents, watch mode (NDJSON streaming) for reviewing agents, pre-execution state capture for rollback, and outcome recording for pattern improvement.", + connectsTo: ["dcg", "mail", "ntm", "caam"], connectionDescriptions: { - mail: "Approval requests sent as urgent messages", - ubs: "Pre-flight scans before execution", - ntm: "Coordinates quorum across agents", - srps: "SRPS prevents multi-agent sessions from overwhelming system resources", + dcg: "DCG blocks pre-execution, SLB validates with multi-agent approval", + mail: "Approval requests can be routed via Agent Mail for coordination", + ntm: "Coordinates approval quorum across NTM-managed agents", + caam: "Account switching can require SLB approval for team workflows", }, stars: 23, features: [ - "3-tier: CRITICAL (2+), DANGEROUS (1), CAUTION (auto-30s)", - "SHA-256 command binding (raw + cwd + argv)", - "HMAC-SHA256 review signatures", - "Different-model enforcement for CRITICAL", - "Self-review protection", - "SQLite audit trails", + "4-tier risk: CRITICAL (2+), DANGEROUS (1), CAUTION (30s), SAFE (skip)", + "40+ regex patterns: 24 critical, 15 dangerous, 6 safe", + "HMAC-SHA256 cryptographic approval signatures", + "Self-review protection (agents can't approve own requests)", + "Watch mode for reviewing agents (NDJSON streaming)", + "Rollback support with pre-execution state capture", + "Claude Code hooks and Cursor rules generation", + "Outcome recording for pattern analytics and learning", ], cliCommands: [ - 'slb run "rm -rf ./build"', - "slb approve <request-id>", - "slb reject <request-id> --reason '...'", - "slb tui", + 'slb run "rm -rf ./build" --reason "Clean artifacts"', + 'slb check "git push --force" # Test classification', + "slb approve <id> -s $SESSION_ID -k $KEY", + "slb watch --auto-approve-caution # Review agent mode", + "slb tui # Interactive dashboard", + "slb session start -a MyAgent # Start agent session", ], installCommand: "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/simultaneous_launch_button/main/scripts/install.sh | bash", @@ -808,9 +816,9 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-red-400 to-rose-500", tagline: "Pre-execution safety net", description: - "A Claude Code hook that blocks dangerous commands BEFORE they execute. Catches git resets, force pushes, rm -rf, DROP TABLE, and more. Fail-open design ensures you're never blocked by errors.", + "Claude Code PreToolUse hook blocking dangerous commands BEFORE execution. 50+ packs across 17 categories: git, filesystem, databases, Kubernetes, cloud providers, CI/CD, and more. Fail-open design ensures you're never blocked by errors.", deepDescription: - "DCG is the safety layer that protects your codebase from destructive operations. It intercepts commands as a PreToolUse hook in Claude Code, checking against 50+ protection packs covering git, filesystem, databases, Kubernetes, and cloud operations. When a dangerous command is detected, DCG blocks it and suggests safer alternatives. The allow-once workflow enables legitimate bypasses with time-limited short codes.", + "DCG protects your codebase from destructive operations. As a PreToolUse hook, it intercepts commands before execution with sub-millisecond latency. 50+ protection packs cover git (reset --hard, force push, branch -D), filesystem (rm -rf outside temp dirs), databases (DROP TABLE, TRUNCATE), Kubernetes (delete namespace, drain), and cloud providers (AWS, GCP, Azure). Commands are classified as blocked or allowed with safe directory exceptions (/tmp, /var/tmp, $TMPDIR). Output formats include text, JSON, and SARIF for security tooling integration.", connectsTo: ["slb", "ntm", "mail", "srps"], connectionDescriptions: { slb: "DCG and SLB form a two-layer safety system - DCG blocks pre-execution, SLB validates post-execution", @@ -820,20 +828,22 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` }, stars: 50, features: [ - "Pre-execution blocking: Catches commands before damage", - "50+ protection packs: git, database, k8s, cloud, filesystem", - "Allow-once workflow: Legitimate bypasses with short codes", - "Fail-open design: Never blocks on errors or timeouts", - "Pack configuration: Enable packs relevant to your workflow", - "Explain mode: Understand why commands are blocked", + "Sub-millisecond SIMD-accelerated PreToolUse hook", + "Heredoc/inline script scanning (python -c, bash -c, etc.)", + "Smart context detection: data vs execution contexts", + "49+ packs: git, filesystem, database, k8s, cloud, cicd", + "Agent-specific trust profiles (claude-code, gemini, etc.)", + "MCP server mode for direct agent integration", + "Fail-open design: never blocks on errors/timeouts", + "Output: text, JSON, SARIF for security tooling", ], cliCommands: [ - "dcg test 'command' # Test if command would be blocked", - "dcg test 'command' --explain # Detailed explanation", - "dcg packs # List available packs", - "dcg doctor # Check installation health", - "dcg install # Register Claude Code hook", - "dcg allow-once CODE # Bypass for legitimate use", + "dcg test 'rm -rf /' # Test if command blocked", + "dcg explain 'command' # Decision trace with reasons", + "dcg scan src/ # CI/pre-commit integration", + "dcg packs --verbose # List packs with counts", + "dcg mcp-server # Start MCP server", + "dcg doctor # Health check + verification", ], installCommand: "curl --proto '=https' --proto-redir '=https' -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh | bash", @@ -846,36 +856,71 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` href: "https://github.com/Dicklesworthstone/repo_updater", icon: "GitMerge", color: "from-indigo-400 to-blue-500", - tagline: "Multi-repo sync + AI automation", + tagline: "Multi-repo sync + AI review orchestration", description: - "Synchronize dozens of GitHub repos with one command. AI-driven commit automation. Parallel workers, resume support, zero string parsing.", - deepDescription: - "RU solves the repo sprawl problem. Pure Bash with git plumbing (no locale issues). Parallel work-stealing sync with portable locking. Agent Sweep: three-phase AI workflow (understand → plan → execute) commits dirty repos intelligently. Review system orchestrates code reviews via ntm.", - connectsTo: ["ntm", "mail", "bv"], + "Synchronize 100+ GitHub repos with one command. AI-assisted code review with priority scoring. Dependency updates across package managers. Pure Bash with git plumbing.", + deepDescription: `RU is a multi-repo management system with AI orchestration. Pure Bash using git plumbing commands +(rev-list, status --porcelain) - never parses human-readable output, so it's locale-independent. + +**Core sync:** Clone missing repos, pull updates, detect conflicts. Parallel work-stealing queue with +portable locking. Resume interrupted syncs from checkpoint. Actionable resolution commands for every +conflict type (dirty tree, diverged branches, auth failures). + +**AI-Assisted Review (ru review):** +- GraphQL batch queries discover issues/PRs across all repos +- Multi-factor priority scoring: type (+20 PRs), labels (+50 security), age, staleness +- Git worktree isolation for parallel sessions (main directory untouched) +- Session drivers: auto, ntm (robot mode API), local (raw tmux) +- Two-phase workflow: --plan (discover) → --apply --push (execute) +- Quality gates: ShellCheck, tests, lint before push + +**Agent Sweep (ru agent-sweep):** +- Three-phase AI workflow: understand → plan → execute +- Parallel execution with phase timeouts +- Secret scanning (none/warn/block modes) + +**Dependency Updates (ru dep-update):** +- Supported: npm, pip, cargo, go, composer +- Per-dependency test verification with auto-fix attempts +- Major version filtering, regex include/exclude patterns + +**Automation & Scripting:** +- Output modes: text, TOON, JSON for CI integration +- Meaningful exit codes: 0=ok, 1=partial, 2=conflicts, 3=system, 4=bad args, 5=interrupted +- Bulk import from GitHub/GitLab/Bitbucket/Gitea (ru import) +- Orphan repo cleanup with ru prune`, + connectsTo: ["ntm", "mail", "bv", "gh"], connectionDescriptions: { ntm: "Uses ntm robot mode for AI-assisted reviews and agent sweep", mail: "Can coordinate repo claims across agents", bv: "Integrates with beads for multi-repo task tracking", + gh: "GraphQL API for batched issue/PR discovery", }, - stars: 50, + stars: 78, features: [ - "Parallel sync: ru sync -j4 (work-stealing queue)", + "Parallel sync with work-stealing queue (ru sync -j4)", + "AI code review with priority scoring (ru review)", + "Dependency updates across package managers (ru dep-update)", + "Agent sweep for multi-repo automation", + "Git worktree isolation for parallel sessions", "Resume from checkpoint: ru sync --resume", - "Agent Sweep: ru agent-sweep --parallel 4", - "AI code review: ru review --plan", - "Repo spec syntax: owner/repo@branch as local-name", - "JSON output: ru sync --json for automation", + "Quality gates: ShellCheck, tests, lint", + "TOON/JSON output modes for CI integration", + "Bulk import from GitHub/GitLab/Bitbucket (ru import)", ], cliCommands: [ "ru sync # Clone missing + pull updates", "ru sync -j4 --autostash # Parallel with auto-stash", - "ru status --fetch # Check ahead/behind state", - "ru agent-sweep --dry-run # Preview AI commit plan", - "ru agent-sweep --parallel 4 --with-release", + "ru sync --resume # Resume interrupted sync", + "ru status --no-fetch # Quick local status check", "ru review --plan # AI-assisted code review", + "ru review --apply --push # Apply approved changes", + "ru agent-sweep -j4 # Parallel AI commit automation", + "ru import github user/org # Bulk import repos", + "ru prune # Remove orphan repos", ], installCommand: - 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/install.sh" | bash', + 'curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/install.sh" | bash', language: "Bash", }, { @@ -885,36 +930,61 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` href: "https://github.com/Dicklesworthstone/meta_skill", icon: "Sparkles", color: "from-teal-500 to-emerald-600", - tagline: "Complete skill management platform", + tagline: "Local-first skill management platform", description: - "Store skills, search them, track effectiveness, package for sharing, and integrate with AI agents via MCP server. Skills come from hand-written files, CASS mining, bundles, or guided workflows.", - deepDescription: - "MS is the skill management layer for AI agents. Thompson sampling learns which skills work best over time. The MCP server exposes 6 native tools (search, load, evidence, list, show, doctor) so any AI agent can query skills directly. Multi-layer security: ACIP detects prompt injection, DCG classifies command safety, path policy prevents escapes, secret scanner redacts sensitive data.", - connectsTo: ["cass", "cm", "bv", "br"], + "Dual persistence (SQLite + Git), hybrid search (BM25 + semantic + RRF), UCB bandit optimization, multi-layer security (ACIP + DCG), graph analysis via bv, MCP server for AI agents.", + deepDescription: `MS is a local-first skill management platform with comprehensive capabilities: + +**Persistence:** Dual storage - SQLite for fast queries, Git for audit trails. Neither is privileged; +they serve different needs. Database corrupts? Rebuild from Git. Git unavailable? SQLite still works. + +**Search:** Hybrid BM25 + hash embeddings (deterministic, no external models) fused with RRF. +Same text = same vector on any machine. Combines keyword precision with semantic recall. + +**Suggestions:** UCB bandit optimization that learns from feedback. Each signal (BM25, semantic, +recency, feedback) is an arm with beta distributions. Context modifiers adjust for project type. +Over time, the system learns which signals matter for your workflow. + +**Security:** Multi-layer defense: +- ACIP: Classifies content by trust boundary, quarantines prompt injections +- DCG: Command safety tiers (Safe/Caution/Danger/Critical) with approval gates +- Path Policy: Prevents symlink escapes and directory traversal +- Secret Scanner: Redacts credentials and API keys + +**Graph Analysis:** Converts skills to JSONL and delegates to bv for PageRank, betweenness, +cycles, critical path analysis, HITS algorithm. Use the right tool for each job. + +**MCP Server:** Exposes 12 tools (search, load, evidence, list, show, doctor, lint, suggest, +feedback, index, validate, config) so Claude, Codex, and other MCP-aware agents can use ms +as a native tool, not string-parsing.`, + connectsTo: ["cass", "cm", "bv", "br", "jfp"], connectionDescriptions: { cass: "One input source for skill extraction (among several)", cm: "Skills and CM memories are complementary knowledge layers", bv: "Graph analysis via bv for PageRank, betweenness, cycles", br: "Skill workflows generate beads for execution tracking", + jfp: "JFP downloads remote prompts, MS manages local skills", }, - stars: 10, + stars: 68, features: [ - "MCP server: Native AI agent integration (6 tools)", - "Thompson sampling: Learns from usage to optimize suggestions", - "ACIP: Prompt-injection detection with quarantine", - "DCG: Command safety tiers (Safe/Caution/Danger/Critical)", - "Hybrid search: BM25 + hash embeddings with RRF fusion", - "Token packing: Optimize skill loading for context budgets", + "Dual persistence: SQLite + Git archive", + "Hybrid search: BM25 + hash embeddings + RRF", + "UCB bandit: Learns from feedback to optimize suggestions", + "MCP server: 12 native tools for AI agent integration", + "ACIP + DCG: Multi-layer security with quarantine", + "Token packing with pack contracts (debug/refactor/learn)", + "Auto-loading: Context-aware skill suggestions", + "Graph analysis: PageRank, bottlenecks, cycles via bv", ], cliCommands: [ - "ms mcp serve # Start MCP server for AI agents", - "ms search 'error handling' # Hybrid search", - "ms load <skill> --pack 2000 # Token-packed loading", - "ms security scan <file> # ACIP prompt injection check", - "ms graph insights # Dependency analysis via bv", + "ms doctor # Health checks and repairs", + "ms search 'error handling' # Hybrid BM25 + semantic search", + "ms suggest # Bandit-optimized suggestions", + "ms mcp serve # Start MCP server (12 tools)", + "ms build --from-cass <query> # Build skills from CASS", + "ms sync # Multi-machine Git sync", ], - installCommand: - 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/meta_skill/main/scripts/install.sh" | bash', + installCommand: "cargo install --git https://github.com/Dicklesworthstone/meta_skill", language: "Rust", }, { @@ -926,9 +996,9 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-blue-500 to-indigo-600", tagline: "Offload Rust builds to remote workers", description: - "Transparently offloads cargo builds to powerful remote machines. Your local machine stays cool while remote workers handle compilation. Seamless rsync integration.", + "Claude Code PreToolUse hook that offloads cargo builds to remote workers. Intercepts build commands, syncs source via rsync + zstd, compiles on server-grade hardware, and streams artifacts back.", deepDescription: - "RCH intercepts cargo commands and offloads compilation to remote workers via SSH. File sync via rsync keeps source and artifacts in sync. Works with any cargo command. Configurable worker pools for load balancing. Dramatically speeds up builds on laptops by using server-grade hardware.", + "RCH runs as a PreToolUse hook intercepting cargo commands before execution. Workers are managed via `rch workers` with health probes and priority scheduling. The daemon mode maintains persistent SSH connections for low-latency builds. Agent detection (`rch agents`) finds running Claude Code, Codex, and Gemini sessions to coordinate multi-agent builds. Doctor command validates workers, daemon, and hook configuration.", connectsTo: ["ntm", "ru", "br"], connectionDescriptions: { ntm: "NTM can spawn agents on same machines RCH uses as workers", @@ -937,16 +1007,20 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` }, stars: 45, features: [ - "Transparent cargo interception", - "rsync-based file synchronization", - "Configurable remote worker pools", - "SSH multiplexing for low latency", - "Works with cargo build, test, clippy, etc.", + "PreToolUse hook intercepts cargo commands automatically", + "rsync + zstd sync with incremental artifact streaming", + "Worker pool with health probes and priority scheduling", + "Daemon mode with persistent SSH connections", + "Agent detection for Claude Code, Codex, Gemini CLI", + "Doctor command validates entire configuration", ], cliCommands: [ - "rch --help # Show usage", - "rch cargo build --release # Build remotely", - "rch status # Check worker status", + "rch doctor # Comprehensive diagnostics", + "rch workers list # Show configured workers", + "rch workers probe --all # Test worker connectivity", + "rch daemon start # Start persistent daemon", + "rch hook install # Install PreToolUse hook", + "rch agents # Detect running AI agents", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/remote_compilation_helper/master/install.sh" | bash', @@ -959,28 +1033,52 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` href: "https://github.com/Dicklesworthstone/wezterm_automata", icon: "Terminal", color: "from-purple-500 to-violet-600", - tagline: "Scriptable terminal automation", + tagline: "Terminal hypervisor for AI agents", description: - "Automates WezTerm terminal interactions. Send keystrokes, read screen content, manage panes and tabs programmatically. Perfect for agent orchestration.", - deepDescription: - "WA exposes WezTerm's Lua API for external automation. Agents can spawn terminal sessions, send commands, read output, and manage window layouts. Works with NTM for multi-agent coordination. Enables sophisticated agent workflows that require terminal control.", - connectsTo: ["ntm", "br"], + "A terminal hypervisor that captures pane output in real-time, detects AI agent state transitions via pattern matching, and enables event-driven automation across multi-agent swarms.", + deepDescription: `WA is a terminal hypervisor - not just an automation tool. It runs a daemon that continuously +observes WezTerm panes with sub-50ms latency, capturing output deltas and detecting state +transitions in AI coding agents (Claude Code, Codex, Gemini). + +The pattern detection engine recognizes agent-specific states: ready for input, thinking, +rate limited, awaiting approval, idle timeout. When states change, WA can trigger automated +responses via callbacks or Robot Mode. + +Robot Mode provides a JSON API for external orchestration: +- wa robot state: Get current state of all observed panes +- wa robot get-text: Extract screen content from specific panes +- wa robot send: Inject keystrokes with configurable delays +- wa robot wait-for: Block until a pattern matches +- wa robot search: Query the FTS5-indexed capture history + +The policy engine allows capability gates (e.g., "agent X can only send to its own pane") +to prevent runaway automation. All captured content is stored in SQLite with FTS5 for +full-text search across sessions - invaluable for debugging agent behavior.`, + connectsTo: ["ntm", "mail", "br"], connectionDescriptions: { - ntm: "NTM uses WA for terminal session management", - br: "Terminal automation tasks tracked via beads", + ntm: "WA observes agents spawned by NTM sessions", + mail: "State changes can trigger Agent Mail notifications", + br: "Task completions can update bead status", }, - stars: 32, + stars: 42, features: [ - "Send keystrokes to any pane", - "Read screen buffer content", - "Manage panes and tabs programmatically", - "Lua scripting integration", - "Event-driven automation hooks", + "Real-time delta capture (sub-50ms latency)", + "Multi-agent pattern detection engine", + "Robot Mode JSON API for orchestration", + "FTS5-powered search with BM25 ranking", + "Policy engine with capability gates", + "Workflow automation triggered by pattern matches", + "TOON output format for token-efficient AI consumption", + "Explainability via 'wa why' command", ], cliCommands: [ - "wa send 'git status' # Send command to pane", - "wa read-screen # Get current screen content", - "wa split --direction right # Split pane", + "wa daemon start # Start background observer", + "wa robot state # JSON state of all panes", + "wa robot get-text --pane 1 # Extract pane content", + "wa robot send --pane 1 'cmd' # Inject keystrokes", + "wa robot wait-for 0 'pattern' # Event-driven wait", + "wa robot events # Recent detection events", + "wa why deny.alt_screen # Explain policy denials", ], installCommand: "cargo install --git https://github.com/Dicklesworthstone/wezterm_automata", language: "Rust", @@ -992,34 +1090,102 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` href: "https://github.com/Dicklesworthstone/brenner_bot", icon: "Bot", color: "from-rose-500 to-red-600", - tagline: "Research methodology automation", + tagline: "Multi-agent scientific research orchestration", description: - "Automates Brenner research methodology. Manages hypothesis slates, triangulated analysis, corpus mining, and systematic inquiry workflows.", - deepDescription: - "Brenner Bot encapsulates the Brenner operators for systematic research: Problem Selection, Hypothesis Slate, Third Alternative, Iterative Refinement, Ruthless Kill, Quickie Pilot, Materialization, and Inner Truth extraction. Agents use it to run methodical research sessions that converge on validated insights.", - connectsTo: ["cass", "cm", "br", "ms"], + "Operationalizes Sydney Brenner's scientific methodology. Orchestrates multi-agent research sessions with hypothesis lifecycle tracking, discriminative test design, anomaly management, and evidence pack integration.", + deepDescription: `Brenner Bot is a research orchestration platform that turns AI agents into a collaborative research group. +Built around Sydney Brenner's methodology (Nobel laureate, discoverer of mRNA), it provides a curated corpus (236 transcript +sections with §n anchors), multi-model syntheses (Opus, GPT, Gemini), and full artifact lifecycle management. + +**Research Artifact Lifecycle:** +- Hypothesis management: proposed → active → under_attack → killed/validated/dormant +- Discriminative tests: designed → pending → completed/blocked (exclusion beats accumulation) +- Anomaly tracking: active → resolved/deferred/paradigm_shifting (can spawn new hypotheses) +- Critique system: adversarial attacks with severity levels and response tracking +- Evidence packs: import papers, datasets, prior sessions with stable EV-NNN citations + +**Cockpit Runtime:** +- Multi-agent sessions via ntm with role-specific prompts (hypothesis_generator, test_designer, adversarial_critic) +- Thread ID is global join key: ties Agent Mail, ntm sessions, artifacts, beads +- Session state machine with phase detection (awaiting_responses → partially_complete → awaiting_compilation) +- Artifact compiler with 50+ validation rules: third alternative checks, potency controls, citation anchors + +**The Brenner Approach:** +- Two axioms: Reality has a generative grammar, Understanding = Reconstruction +- "Exclusion is always a tremendously good thing" - design for sharp discriminative experiments +- Third alternative check: "Both could be wrong" - always consider misspecification`, + connectsTo: ["mail", "ntm", "cass", "br"], connectionDescriptions: { - cass: "Searches past sessions for research context", - cm: "Research findings become procedural memories", - br: "Research tasks tracked via beads", - ms: "Research patterns become skills", + mail: "Research sessions coordinate via Agent Mail threads with acknowledgment tracking", + ntm: "Cockpit runtime spawns parallel research agents with role-specific prompts", + cass: "Research session history is searchable for prior solutions", + br: "Research tasks and session artifacts tracked via beads", }, stars: 28, features: [ - "Brenner operator automation", - "Hypothesis slate management", - "Corpus mining and triangulation", - "Convergence detection", - "Research session archival", + "Hypothesis lifecycle: proposed → active → killed/validated with discriminative tests", + "Evidence packs: import papers, datasets, prior sessions with stable EV-NNN citations", + "Anomaly tracking with paradigm_shifting status and hypothesis spawning", + "Critique system for adversarial review with severity levels", + "Cockpit runtime: multi-agent sessions with role-specific prompts", + "236-section transcript corpus with §n citations and quote bank", + "Artifact compiler with 50+ Brenner-style validation rules", + "Session state machine with phase detection and progress tracking", + "Experiment capture with result encoding and Agent Mail posting", ], cliCommands: [ - "brenner session start # Start research session", - "brenner hypothesis add # Add to hypothesis slate", - "brenner converge # Check convergence state", + "brenner corpus search 'query' # Full-text corpus search with §n anchors", + "brenner hypothesis create --statement 'H' # Create hypothesis with category", + "brenner evidence add --type paper --title 'T' # Import external evidence", + "brenner anomaly create --observation 'O' # Track unexpected results", + "brenner session start --to A,B --question 'Q' # Orchestrate multi-agent session", + "brenner cockpit start --role-map 'A=hypothesis_generator' # Full cockpit", + "brenner critique create --target H-001 --attack 'A' # Adversarial critique", + "brenner doctor --json # Verify installation with JSON output", ], installCommand: 'curl --proto \'=https\' --proto-redir \'=https\' -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/brenner_bot/main/install.sh" | bash', - language: "Rust", + language: "TypeScript", + }, + { + id: "jfp", + name: "JeffreysPrompts", + shortName: "JFP", + href: "https://jeffreysprompts.com", + icon: "BookOpen", + color: "from-amber-400 to-yellow-500", + tagline: "Curated prompt library + skill installer", + description: + "Browse a curated library of battle-tested prompts and install them directly as Claude Code skills. Works via CLI or web UI with interactive fzf-style picker.", + deepDescription: + "JFP (JeffreysPrompts.com CLI) is the fastest way to discover prompts that actually work in production agent workflows. It mirrors the website library in a CLI: search, preview, and install prompts as Claude Code skills in seconds. Features include an interactive fzf-style picker (jfp i), task-based suggestion engine (jfp suggest), and workflow bundles for team standardization. Premium features include collections, sync across machines, and a skills marketplace.", + connectsTo: ["ms", "apr", "cm"], + connectionDescriptions: { + ms: "Use JFP to discover prompts, then manage/curate them locally with MS", + apr: "Feed high-quality prompts into APR to refine and harden specifications", + cm: "Prompts that work become reusable memory artifacts", + }, + stars: 50, + features: [ + "Interactive fzf-style prompt picker (jfp i)", + "Task-based prompt suggestions (jfp suggest)", + "Install prompts as Claude Code skills", + "Workflow bundles for team standardization", + "MCP server mode for agent integration (jfp serve)", + "Variable rendering with placeholder fill (jfp render --fill)", + "Premium: collections, sync, notes, marketplace", + "Shell completion and auto-updates", + ], + cliCommands: [ + "jfp i", + "jfp suggest \"write unit tests\"", + "jfp search \"code review\"", + "jfp install idea-wizard", + "jfp bundles", + "jfp serve", + ], + installCommand: "curl -fsSL https://jeffreysprompts.com/install-cli.sh | bash", + language: "TypeScript (Bun)", }, { id: "srps", @@ -1030,37 +1196,42 @@ defer/undefer, search, stats, doctor, changelog, orphans, audit, history, graph` color: "from-yellow-400 to-orange-500", tagline: "Keep your workstation responsive under heavy agent load", description: - "Installs ananicy-cpp with 1700+ rules to automatically deprioritize background processes, plus sysmoni TUI for real-time monitoring.", + "Installs ananicy-cpp with curated rules to automatically deprioritize background processes. Includes sysmoni Go TUI with per-process IO throughput, FD counts, and JSON export. Works on Linux and WSL2.", deepDescription: `When AI coding agents run cargo build, npm install, or spawn multiple parallel processes, your system can become unresponsive. SRPS solves this by automatically lowering the priority of known resource hogs (compilers, bundlers, test runners) while keeping your terminal and IDE snappy. -Built on ananicy-cpp (a C++ replacement for the original ananicy) with curated rules -for developer workloads. The sysmoni TUI shows real-time CPU/memory per process with -ananicy rule status, so you can see exactly what's being managed. +Built on ananicy-cpp with curated rules for developer workloads. The sysmoni Go TUI +(Bubble Tea) shows real-time CPU/memory per process, IO throughput (read/write kB/s), +open FD counts, per-core sparklines, and JSON/NDJSON export. -Key capabilities: -- 1700+ pre-configured rules for compilers, browsers, IDEs, and common dev tools -- Custom rule support for any process (add your own in /etc/ananicy.d/) -- Sysctl kernel tweaks for better responsiveness under memory pressure -- Zero configuration needed - just install and forget`, - connectsTo: ["ntm", "dcg", "slb"], +Helper tools: check-throttled, cursor-guard (log/renice-only), srps-doctor, srps-reload-rules. +Safety-first: no automated process killing. Aliases: limited, cargo-limited, make-limited. + +Supports Linux (Debian/Ubuntu) and WSL2. Idempotent installer with --plan dry-run.`, + connectsTo: ["ntm", "dcg", "slb", "pt"], connectionDescriptions: { ntm: "SRPS ensures tmux sessions stay responsive even during heavy builds - no frozen terminals", dcg: "Combined safety: DCG prevents destructive commands, SRPS prevents resource exhaustion from runaway processes", slb: "When SLB launches multiple agents, SRPS keeps them from starving each other for CPU/memory", + pt: "PT identifies stuck processes, SRPS deprioritizes resource hogs - complementary approaches", }, stars: 50, features: [ - "ananicy-cpp daemon with 1700+ process priority rules", - "sysmoni Go TUI for real-time resource monitoring", - "Auto-deprioritizes compilers, bundlers, test runners, browsers", - "Sysctl tweaks for better responsiveness under memory pressure", - "Custom rules: add any process to /etc/ananicy.d/", - "Zero-config: install once, benefits forever", + "ananicy-cpp daemon with curated process priority rules", + "sysmoni Go TUI: CPU/MEM gauges, IO throughput, FD counts", + "Per-core sparklines, JSON/NDJSON export, GPU monitoring", + "Sysctl tweaks + systemd limits for WSL2 compatibility", + "Helper tools: check-throttled, srps-doctor, cursor-guard", + "Idempotent installer with --plan dry-run, --uninstall", + ], + cliCommands: [ + "sysmoni", + "check-throttled", + "srps-doctor", + "sysmoni --json", ], - cliCommands: ["sysmoni"], installCommand: "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/system_resource_protection_script/main/install.sh | bash -s -- --install", language: "Go + C++ + Bash", @@ -1072,26 +1243,18 @@ Key capabilities: href: "https://github.com/Dicklesworthstone/automated_plan_reviser_pro", icon: "FileText", color: "from-amber-500 to-yellow-600", - tagline: "Automated iterative spec refinement with extended AI reasoning", + tagline: "Iterative spec refinement via GPT Pro Extended Reasoning + Oracle", description: - "Takes rough plans and runs multiple review cycles using GPT Pro 5.2 Extended Reasoning via Oracle to identify architectural issues, edge cases, and security flaws.", - deepDescription: `Complex specifications require multiple review cycles to catch all issues. -Instead of manually running 15-20 AI review rounds, APR automates the refinement process. -Early rounds fix major issues, middle rounds refine structure, and later rounds polish -abstractions until you have a production-ready specification. - -APR uses GPT Pro 5.2 Extended Reasoning via Oracle for deep analysis. Each pass adds: -- Better structure and organization -- Identified dependencies between components -- Edge cases and error scenarios -- Security considerations -- More actionable implementation steps + "Automates multi-round specification review using GPT Pro 5.2 Extended Reasoning. Document bundling, convergence analytics, session management, and robot mode for coding agents.", + deepDescription: `APR (v1.2.2) automates iterative specification refinement like numerical optimization converging on a steady state. Rounds 1-3 fix major architectural issues and security gaps, rounds 4-7 refine interfaces, rounds 8-12 handle edge cases, rounds 13+ polish abstractions. -Key capabilities: -- Multi-pass iterative refinement with configurable depth -- Markdown plan file processing with preserved formatting -- Output comparison between versions to see improvements -- Integration with beads for converting refined specs to tasks`, +Workflow: apr setup (interactive wizard) → apr run <N> (execute round) → apr integrate <N> (Claude Code prompt). Document bundling automatically combines README + spec + implementation (every 3-4 rounds). Background execution with 10-60 minute GPT Pro reasoning and desktop notifications on completion. + +Convergence analytics: apr stats shows weighted score (output_trend 35% + change_velocity 35% + similarity_trend 30%). Score ≥0.75 = approaching stability. apr diff compares rounds with delta/diff. apr dashboard provides full-screen analytics. + +Reliability: Pre-flight validation before expensive Oracle runs. Auto-retry with exponential backoff (10s → 30s → 90s). Session locking prevents concurrent runs. Identity validation for GPT Pro Extended Thinking stability (minStableMs=30s, stableCycles=12). + +Robot mode (JSON API): apr robot validate <N> → apr robot run <N> → apr robot history. Semantic error codes: ok, usage_error, not_configured, config_error, validation_failed, dependency_missing, busy. TOON format support via tru.`, connectsTo: ["jfp", "cm", "bv"], connectionDescriptions: { jfp: "Battle-tested prompts from JFP can be refined into comprehensive specifications via APR", @@ -1100,66 +1263,29 @@ Key capabilities: }, stars: 85, features: [ - "Automated multi-pass specification refinement", - "Extended AI reasoning via GPT Pro 5.2 + Oracle", - "Markdown-based plan processing", - "Progressive structure and detail improvement", - "Dependency identification and edge case detection", - "Robot mode for AI agent integration", - ], - cliCommands: ["apr refine <file>", "apr --help", "apr --version"], - installCommand: - "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/automated_plan_reviser_pro/main/install.sh | bash --easy-mode", - language: "Bash", - }, - { - id: "jfp", - name: "JeffreysPrompts CLI", - shortName: "JFP", - href: "https://jeffreysprompts.com", - icon: "Sparkles", - color: "from-pink-500 to-rose-600", - tagline: "Battle-tested prompts as installable Claude Code skills", - description: - "Official CLI for jeffreysprompts.com - browse curated prompts and install them as Claude Code skills with one command.", - deepDescription: `JFP connects your terminal directly to jeffreysprompts.com - a curated collection -of battle-tested AI coding prompts organized by use case. Instead of writing prompts from scratch -or copying them manually, JFP installs them directly to your Claude Code skills folder. - -The prompts on jeffreysprompts.com are real-world patterns extracted from thousands of hours -of AI-assisted coding sessions. They cover everything from exploration and planning to -review and execution workflows. - -Key capabilities: -- Browse prompts by category (exploration, review, planning, execution) -- One-command skill installation: jfp install <prompt-name> -- Offline browsing of downloaded prompts -- MCP server mode for agent integration: jfp serve -- JSON output for programmatic access: jfp --json list`, - connectsTo: ["ms", "apr", "cm"], - connectionDescriptions: { - ms: "JFP downloads from remote, MS manages local - together they're your complete skill system", - apr: "Downloaded prompts can be refined into comprehensive specifications via APR", - cm: "Effective prompts become retrievable memories for future sessions", - }, - stars: 120, - features: [ - "One-command prompt installation to Claude Code skills", - "Curated prompt categories: exploration, review, planning, execution", - "MCP server mode for agent integration", - "Offline browsing of installed prompts", - "JSON output for programmatic access", - "Built-in update command", + "Iterative convergence: architecture → refinement → polish", + "Document bundling (README + spec + implementation)", + "Background processing with session management", + "Convergence analytics with weighted scoring", + "Pre-flight validation and auto-retry", + "Session locking prevents concurrent runs", + "Robot mode JSON API with semantic error codes", + "Claude Code integration prompts (apr integrate)", + "Round diff comparison with delta support", ], cliCommands: [ - "jfp install <prompt>", - "jfp list", - "jfp search <query>", - "jfp serve", + "apr setup", + "apr run <N>", + "apr run <N> --include-impl", + "apr status", + "apr diff <N> [M]", + "apr integrate <N> --copy", + "apr stats", + "apr robot validate <N>", ], installCommand: - "git clone https://github.com/Dicklesworthstone/jeffreysprompts.com.git ~/.local/share/jeffreysprompts.com && cd ~/.local/share/jeffreysprompts.com && bun install && bun run build:cli && cp jfp ~/.local/bin/", - language: "TypeScript/Bun", + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/automated_plan_reviser_pro/main/install.sh | bash", + language: "Bash", }, { id: "pt", @@ -1168,22 +1294,18 @@ Key capabilities: href: "https://github.com/Dicklesworthstone/process_triage", icon: "Activity", color: "from-red-500 to-orange-600", - tagline: "Find and kill stuck/zombie processes with intelligent scoring", + tagline: "Bayesian-inference zombie/abandoned process detection and cleanup", description: - "Rust-based process manager with Bayesian scoring to identify and terminate problematic processes. TUI for interactive selection.", - deepDescription: `When builds hang, test runners go rogue, or processes zombie out, you need to find -and terminate them quickly. PT uses intelligent Bayesian scoring to prioritize truly problematic -processes - not just those using resources, but those that are actually stuck or misbehaving. + "Four-state classification model (Useful, Useful-but-bad, Abandoned, Zombie) with evidence-based posterior inference. Interactive TUI via gum, agent/robot mode for automation.", + deepDescription: `PT identifies abandoned processes using Bayesian posterior inference. Every process is classified into one of four states: Useful (productive work), Useful-but-bad (stuck/leaking), Abandoned (forgotten), or Zombie (terminated but not reaped). -The scoring algorithm considers CPU usage patterns, memory growth rate, file descriptor counts, -and process state to identify processes that are genuinely problematic vs. those that are just -doing heavy work. +Evidence sources: process type (test runner? dev server?), age vs expected lifetime, parent PID (orphaned?), CPU activity, I/O activity, TTY state, memory usage, and past decisions (learns from your patterns). Confidence levels: very_high (>0.99), high (>0.95), medium (>0.80), low (<0.80). -Key capabilities: -- Bayesian scoring identifies stuck processes, not just heavy ones -- Interactive TUI for selecting processes to terminate -- Robot mode for automation: pt --robot list -- Integration with SRPS for proactive resource management`, +Safety model: Identity validation (boot_id:start_time_ticks:pid) prevents PID reuse attacks. Protected processes (systemd, sshd, docker, postgres, etc.) are never flagged. Staged kill signals: SIGTERM → wait → SIGKILL. Blast radius assessment includes memory freed, CPU released, and child process impact. + +Agent/robot mode safety gates: min_posterior (0.95 default), max_kills (10/session), max_blast_radius (4GB), fdr_budget (0.05). Output formats: json, toon, md, jsonl, summary, metrics, slack, exitcode, prose. + +Tech stack: Rust pt-core inference engine + Bash wrapper + gum TUI. Session bundles (.ptb) enable sharing and reproducibility with optional encryption.`, connectsTo: ["srps", "ntm"], connectionDescriptions: { srps: "PT terminates stuck processes, SRPS prevents them from starving the system", @@ -1191,16 +1313,27 @@ Key capabilities: }, stars: 45, features: [ - "Bayesian process scoring algorithm", - "Interactive TUI for process selection", - "Robot mode for automation", - "Resource usage trend analysis", - "Zombie process detection", - "Safe termination with confirmation", + "Four-state Bayesian classification (Useful/Bad/Abandoned/Zombie)", + "Evidence-based posterior inference with confidence levels", + "Protected process lists (systemd, sshd, docker, postgres)", + "Identity validation prevents PID reuse attacks", + "Staged kill signals (SIGTERM → wait → SIGKILL)", + "Blast radius assessment before termination", + "Interactive gum TUI for process selection", + "Agent/robot mode with safety gates", + "Session bundles (.ptb) for sharing/reproducibility", ], - cliCommands: ["pt", "pt --robot list", "pt --help"], - installCommand: "cargo install --git https://github.com/Dicklesworthstone/process_triage", - language: "Rust", + cliCommands: [ + "pt", + "pt scan", + "pt deep", + "pt agent plan --format json", + "pt robot plan --format toon", + "pt history", + ], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/process_triage/master/install.sh | bash", + language: "Rust/Bash", }, { id: "xf", @@ -1218,16 +1351,13 @@ lexical (BM25 keyword matching), and semantic (vector similarity via hash embedd optional MiniLM with --semantic flag). Key capabilities: -- Tantivy-powered BM25 search with phrase queries, boolean operators, and wildcards -- Reciprocal Rank Fusion combines keyword and semantic results optimally -- Hash-based embeddings by default (zero dependencies, ~0ms per embedding) -- Optional MiniLM embeddings for true semantic synonym matching -- Parses window.YTD.* JavaScript format from X data exports -- Data types: tweet, like, dm, grok, follower, following, block, mute -- SQLite storage with FTS5, memory-mapped Tantivy index -- SIMD-accelerated vector operations, F16 quantization (50% storage reduction) -- All data stays local - no network calls during search -- 13 commands: import, index, search, stats, tweet, list, export, config, doctor, shell, benchmark`, +- Rust + Tantivy for sub-millisecond lexical search (<10ms typical) +- Hybrid BM25 + semantic search with RRF fusion +- Zero-dependency hash embedder (default) or optional MiniLM embeddings (--semantic) +- SIMD-accelerated vector search with F16 quantization +- Fully local, privacy-preserving processing (no network calls) +- DM context search: view full conversation threads with matches highlighted +- Parses all X archive formats: tweets, likes, DMs, Grok chats, followers`, connectsTo: ["cass", "cm"], connectionDescriptions: { cass: "Similar search architecture - hybrid retrieval patterns", @@ -1235,26 +1365,426 @@ Key capabilities: }, stars: 156, features: [ - "Sub-millisecond lexical search (<10ms hybrid)", - "Three search modes: hybrid, lexical, semantic", - "Reciprocal Rank Fusion scoring", - "Zero external API dependencies (hash mode)", - "DM context view with full conversation threads", - "Parses tweets, likes, DMs, Grok chats", - "Interactive REPL shell for exploration", + "Sub-millisecond lexical search (<10ms typical)", + "Hybrid BM25 + semantic search with RRF fusion", + "Hash embedder (default) or MiniLM (--semantic)", + "SIMD-accelerated vector search, F16 quantization", + "DM context search with full conversation threads", + "Parses tweets, likes, DMs, Grok chats, followers", ], cliCommands: [ - "xf index ~/x-archive", - "xf search 'query' --format json", - "xf search 'term' --mode semantic", - "xf stats", - "xf doctor", - "xf shell", + "xf index ~/x-archive # Index archive", + "xf search 'query' # Hybrid search (default)", + "xf search 'query' --mode semantic # Vector similarity", + "xf search 'topic' --types dm --context # DM threads", + "xf stats --format json # Archive stats", ], installCommand: "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/xf/main/install.sh | bash", language: "Rust", }, + // ============================================================ + // UTILITY TOOLS - Supporting tools for the flywheel + // ============================================================ + { + id: "giil", + name: "Get Image from Internet Link", + shortName: "GIIL", + href: "https://github.com/Dicklesworthstone/giil", + icon: "Image", + color: "from-slate-500 to-slate-600", + tagline: "Download full-resolution cloud images via four-tier capture", + description: + "Download images from iCloud, Dropbox, Google Photos, and Google Drive share links with intelligent four-tier capture strategy and MozJPEG compression.", + deepDescription: `GIIL (v3.1.0 Hybrid Edition) solves the remote image retrieval problem for AI-assisted debugging. +When users share screenshots via cloud links, agents can download full-resolution originals directly. + +Four-tier capture strategy ensures maximum quality: +1. Download button detection (e.g., Dropbox "Download" button click) +2. Network CDN interception (catches direct image URLs from requests) +3. Element screenshot (targets largest image element) +4. Viewport screenshot (final fallback) + +Supports iCloud (share.icloud.com), Dropbox (dropbox.com/s/, dl.dropbox.com), Google Photos (photos.google.com), +and Google Drive (drive.google.com). Album mode (--all flag) extracts all images from multi-image shares. + +Processing: MozJPEG compression (default 85% quality), HEIC/AVIF to JPEG conversion via Sharp. +Output formats: default (file + path), JSON ({"file","format","size"}), TOON (formatted log), base64. + +Exit codes enable scripting: 0=success, 10=network error, 11=auth required, 12=not found, 13=unsupported platform. +Tech stack: Bash wrapper orchestrating Node.js/Playwright/Chromium/Sharp for headless browser automation.`, + connectsTo: ["cass", "cm"], + connectionDescriptions: { + cass: "Downloaded images become part of session context for future reference", + cm: "Visual debugging patterns become searchable memories", + }, + stars: 24, + features: [ + "Four-tier capture strategy (download→CDN→element→viewport)", + "iCloud, Dropbox, Google Photos, Google Drive support", + "Album mode (--all) for multi-image shares", + "MozJPEG compression with configurable quality", + "HEIC/AVIF to JPEG conversion", + "JSON/TOON/base64 output formats", + "Structured exit codes for scripting", + "Headless Chromium via Playwright", + ], + cliCommands: [ + 'giil "https://share.icloud.com/..."', + 'giil --all "https://photos.google.com/share/..."', + "giil --format json --quality 90 URL", + "giil --help", + ], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/giil/main/install.sh | bash", + language: "Bash/Node.js", + }, + { + id: "csctf", + name: "Chat Shared Conversation to File", + shortName: "CSCTF", + href: "https://github.com/Dicklesworthstone/csctf", + icon: "FileText", + color: "from-emerald-500 to-teal-600", + tagline: "Convert AI chat share links to Markdown/HTML", + description: + "Archive AI conversations from ChatGPT, Claude, and Gemini share links into clean Markdown and HTML files.", + deepDescription: `AI conversations are ephemeral - share links expire, contexts get lost. CSCTF converts +share links from major AI platforms into permanent Markdown and HTML archives. + +Perfect for: +- Building a searchable knowledge base of solved problems +- Sharing AI-assisted solutions with team members +- Documenting debugging sessions for future reference +- Creating training data from real conversations + +Key capabilities: +- ChatGPT share link conversion +- Claude share link conversion +- Gemini share link conversion +- Clean Markdown with preserved code blocks +- Static HTML with syntax highlighting +- Batch processing of multiple links`, + connectsTo: ["cass", "cm"], + connectionDescriptions: { + cass: "Archived conversations become searchable in CASS", + cm: "Converted conversations feed into procedural memory", + }, + stars: 45, + features: [ + "ChatGPT share link support", + "Claude share link support", + "Gemini share link support", + "Clean Markdown output", + "Static HTML with syntax highlighting", + "Batch processing support", + ], + cliCommands: ['csctf "https://chatgpt.com/share/..."', "csctf --md-only", "csctf --help"], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/csctf/main/install.sh | bash", + language: "Rust", + }, + { + id: "tru", + name: "TOON Rust", + shortName: "TRU", + href: "https://github.com/Dicklesworthstone/toon_rust", + icon: "Minimize2", + color: "from-violet-500 to-purple-600", + tagline: "Token-optimized notation for LLM context efficiency", + description: + "Compress structured data into token-efficient TOON format, reducing LLM context usage by 30-50%.", + deepDescription: `LLM context windows are precious. TOON (Token-Optimized Object Notation) is a format +designed specifically to minimize token usage while preserving semantic meaning. + +Instead of verbose JSON, TOON uses abbreviations, removes redundant structure, and employs +format-aware compression. The result: 30-50% fewer tokens for the same information. + +Key capabilities: +- JSON to TOON conversion with automatic optimization +- TOON to JSON restoration for downstream processing +- Token count estimation and comparison +- Streaming support for large documents +- LLM-aware compression heuristics`, + connectsTo: ["cm", "cass"], + connectionDescriptions: { + cm: "Compressed memories use fewer tokens in agent context", + cass: "Search results can be TOON-compressed before inclusion", + }, + stars: 67, + features: [ + "30-50% token reduction", + "JSON to TOON conversion", + "TOON to JSON restoration", + "Token count estimation", + "Streaming support", + "LLM-aware compression", + ], + cliCommands: ["tru compress file.json", "tru expand file.toon", "tru --help"], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/toon_rust/master/install.sh | bash", + language: "Rust", + }, + { + id: "rano", + name: "Request/Response Network Observer", + shortName: "RANO", + href: "https://github.com/Dicklesworthstone/rano", + icon: "Wifi", + color: "from-cyan-500 to-blue-600", + tagline: "Network observer for AI CLIs with request/response logging", + description: + "Transparent proxy that logs all HTTP traffic from AI coding agents for debugging and analysis.", + deepDescription: `When AI agents make API calls, understanding what's being sent and received is crucial +for debugging and optimization. RANO acts as a transparent proxy, logging all HTTP traffic +from Claude Code, Codex, and other AI CLIs. + +Use cases: +- Debug failed API calls to understand error responses +- Analyze token usage across different prompts +- Monitor rate limiting and retry behavior +- Audit AI agent network activity + +Key capabilities: +- Transparent proxy requiring no code changes +- Request/response body logging with formatting +- Token usage extraction from API responses +- Filtering by host, path, or content type`, + connectsTo: ["caut", "cm"], + connectionDescriptions: { + caut: "Network logs feed into usage tracking for cost analysis", + cm: "Patterns in API usage become procedural memories", + }, + stars: 32, + features: [ + "Transparent proxy operation", + "Request/response logging", + "Token usage extraction", + "No code changes required", + "Filtering by host/path", + "Formatted output", + ], + cliCommands: ["rano start", "rano logs", "rano --help"], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bash", + language: "Rust", + }, + { + id: "mdwb", + name: "Markdown Web Browser", + shortName: "MDWB", + href: "https://github.com/Dicklesworthstone/markdown_web_browser", + icon: "Globe", + color: "from-orange-500 to-amber-600", + tagline: "Convert websites to Markdown for LLM consumption", + description: + "Fetch web pages and convert them to clean Markdown, perfect for including documentation in LLM context.", + deepDescription: `AI agents often need to reference documentation, but web pages are full of navigation, +ads, and formatting that waste precious context tokens. MDWB fetches pages and converts them +to clean, readable Markdown. + +Perfect for: +- Including API documentation in agent context +- Referencing library docs without leaving the terminal +- Creating offline documentation archives +- Feeding web content to AI for analysis + +Key capabilities: +- Intelligent content extraction (removes nav, ads, footers) +- Code block preservation with language detection +- Link and image handling with optional resolution +- Recursive crawling for documentation sites`, + connectsTo: ["tru", "cm"], + connectionDescriptions: { + tru: "Converted markdown can be TOON-compressed to save tokens", + cm: "Web content becomes searchable procedural memory", + }, + stars: 89, + features: [ + "Intelligent content extraction", + "Code block preservation", + "Language detection", + "Link and image handling", + "Recursive crawling", + "Clean Markdown output", + ], + cliCommands: ["mdwb fetch https://docs.example.com", "mdwb --recursive", "mdwb --help"], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/markdown_web_browser/main/install.sh | bash", + language: "Rust", + }, + { + id: "s2p", + name: "Source to Prompt TUI", + shortName: "S2P", + href: "https://github.com/Dicklesworthstone/source_to_prompt_tui", + icon: "FileCode", + color: "from-green-500 to-emerald-600", + tagline: "World-class TUI for combining source code into LLM-ready prompts", + description: + "Terminal UI for selecting code files and generating structured, token-counted prompts with XML-like output format optimized for LLM parsing.", + deepDescription: `S2P solves the code-to-context problem for AI-assisted development. Features a tree explorer with file sizes and line counts, vim-style navigation (j/k/h/l), quick file-type selects (1-9,0,r for JS/React/TS/JSON/MD/Python/Go/Java/Ruby/PHP/Rust), live syntax preview, and real-time token estimation using tiktoken (cl100k_base). + +Structured XML-like output with <preamble>, <goal>, <project_structure>, and <files> tags for reliable LLM parsing. Context window bar shows usage against 128K limit with cost estimates. + +Processing options: JS/TS minification via Terser, CSS via csso, comment stripping for multiple languages. Presets save file selections and options to ~/.source2prompt.json for reproducible workflows. + +Single compiled binary via Bun with zero runtime dependencies. Respects .gitignore recursively including nested gitignores. Handles large projects (10K+ files) with lazy content loading and virtualized rendering.`, + connectsTo: ["cass", "cm"], + connectionDescriptions: { + cass: "Generated prompts become part of session history for later search", + cm: "Effective prompt patterns stored as procedural memories", + }, + stars: 78, + features: [ + "Tree file explorer with sizes and line counts", + "Vim-style navigation (j/k/h/l)", + "Quick file-type shortcuts (1-9,0,r)", + "Live syntax-highlighted preview", + "Real-time tiktoken token counting", + "Context window usage bar with cost estimate", + "Code minification (Terser/csso)", + "Comment stripping (C-style, hash-style, HTML)", + "Preset save/load to ~/.source2prompt.json", + "Recursive .gitignore support", + "Clipboard integration (Ctrl+G, y to copy)", + ], + cliCommands: ["s2p", "s2p /path/to/project", "s2p --help"], + installCommand: + "curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/source_to_prompt_tui/main/install.sh | bash", + language: "TypeScript", + }, + { + id: "rust_proxy", + name: "Rust Proxy", + shortName: "RustProxy", + href: "https://github.com/Dicklesworthstone/rust_proxy", + icon: "Network", + color: "from-gray-500 to-zinc-600", + tagline: "Transparent proxy routing for debugging network traffic", + description: + "High-performance transparent proxy for routing and debugging network traffic in development environments.", + deepDescription: `When debugging network issues or analyzing API traffic, a transparent proxy is invaluable. +Rust Proxy provides a lightweight, high-performance proxy that can intercept, log, and modify +HTTP/HTTPS traffic. + +Use cases: +- Debugging microservice communication +- Analyzing third-party API behavior +- Testing error handling with injected failures +- Capturing traffic for replay testing + +Key capabilities: +- Transparent HTTP/HTTPS proxying +- Request/response modification +- Traffic capture and replay +- Minimal performance overhead +- Configuration via TOML`, + connectsTo: ["rano"], + connectionDescriptions: { + rano: "Rust Proxy handles routing, RANO handles logging and analysis", + }, + stars: 28, + features: [ + "Transparent HTTP/HTTPS proxy", + "Request/response modification", + "Traffic capture and replay", + "Minimal performance overhead", + "TOML configuration", + "TLS support", + ], + cliCommands: ["rust_proxy start", "rust_proxy --config proxy.toml", "rust_proxy --help"], + installCommand: + "cargo install --git https://github.com/Dicklesworthstone/rust_proxy", + language: "Rust", + }, + { + id: "aadc", + name: "ASCII Diagram Corrector", + shortName: "AADC", + href: "https://github.com/Dicklesworthstone/aadc", + icon: "BoxSelect", + color: "from-indigo-500 to-blue-600", + tagline: "Fix malformed ASCII art diagrams", + description: + "Automatically correct alignment and connection issues in ASCII diagrams generated by AI or written by hand.", + deepDescription: `AI-generated ASCII diagrams often have subtle alignment issues - boxes that don't quite +connect, arrows that are off by one character, or inconsistent spacing. AADC automatically +detects and fixes these issues. + +Common fixes: +- Box corner alignment +- Arrow endpoint connection +- Consistent spacing and padding +- Line continuation across breaks +- Unicode box-drawing character normalization + +Perfect for: +- Cleaning up AI-generated architecture diagrams +- Fixing documentation ASCII art +- Standardizing diagram styles across a codebase`, + connectsTo: ["cm"], + connectionDescriptions: { + cm: "Diagram correction patterns become procedural memory", + }, + stars: 34, + features: [ + "Automatic alignment correction", + "Box corner detection and fixing", + "Arrow endpoint connection", + "Unicode normalization", + "Consistent spacing", + "Multiple diagram styles", + ], + cliCommands: ["aadc fix diagram.txt", "aadc --style unicode", "aadc --help"], + installCommand: "cargo install --git https://github.com/Dicklesworthstone/aadc", + language: "Rust", + }, + { + id: "caut", + name: "Coding Agent Usage Tracker", + shortName: "CAUT", + href: "https://github.com/Dicklesworthstone/coding_agent_usage_tracker", + icon: "BarChart3", + color: "from-pink-500 to-rose-600", + tagline: "Track LLM provider usage and costs", + description: + "Monitor token usage, API costs, and rate limits across Claude, OpenAI, and other LLM providers.", + deepDescription: `Running multiple AI agents across projects quickly adds up in API costs. CAUT tracks +usage across all your LLM providers in one place, helping you understand where tokens go +and optimize spending. + +Tracks: +- Token usage per provider, project, and session +- Estimated costs based on current pricing +- Rate limit hits and retry patterns +- Usage trends over time + +Key capabilities: +- Multi-provider support (Anthropic, OpenAI, Google, etc.) +- Project and session attribution +- Cost estimation with customizable pricing +- Export to CSV/JSON for analysis +- Integration with rano for automatic tracking`, + connectsTo: ["rano", "cm"], + connectionDescriptions: { + rano: "RANO captures the traffic, CAUT calculates the costs", + cm: "Usage patterns become memories for optimizing future sessions", + }, + stars: 42, + features: [ + "Multi-provider usage tracking", + "Cost estimation", + "Project attribution", + "Rate limit monitoring", + "Usage trend analysis", + "CSV/JSON export", + ], + cliCommands: ["caut status", "caut report --days 7", "caut --help"], + installCommand: + "cargo install --git https://github.com/Dicklesworthstone/coding_agent_usage_tracker", + language: "Rust", + }, ]; // Merge basic metadata from manifest (source of truth for names, taglines, diff --git a/apps/web/lib/generated/manifest-commands.ts b/apps/web/lib/generated/manifest-commands.ts index 70a6924c..d9e12496 100644 --- a/apps/web/lib/generated/manifest-commands.ts +++ b/apps/web/lib/generated/manifest-commands.ts @@ -22,9 +22,7 @@ export const manifestCommands: ManifestCommand[] = [ { moduleId: "stack.beads_rust", cliName: "br", - cliAliases: [ - "bd", - ], + cliAliases: [], description: "beads_rust (br) - Rust issue tracker with graph-aware dependencies", commandExample: "br ready --json", }, diff --git a/apps/web/lib/generated/manifest-tools.ts b/apps/web/lib/generated/manifest-tools.ts index b57e294f..7dba2cee 100644 --- a/apps/web/lib/generated/manifest-tools.ts +++ b/apps/web/lib/generated/manifest-tools.ts @@ -88,9 +88,7 @@ export const manifestTools: ManifestWebTool[] = [ language: "Rust", stars: 128, cliName: "br", - cliAliases: [ - "bd", - ], + cliAliases: [], commandExample: "br ready --json", }, { diff --git a/apps/web/lib/jargon.ts b/apps/web/lib/jargon.ts index abf12f78..cb54070d 100644 --- a/apps/web/lib/jargon.ts +++ b/apps/web/lib/jargon.ts @@ -485,7 +485,7 @@ export const jargonDictionary: Record<string, JargonTerm> = { short: "A task tracking system designed specifically for AI coding agents", long: "Beads is a task management system that solves a critical problem: AI agents lose their memory between sessions. When you close a session and come back later, the AI doesn't remember what was done, what's left to do, or what depends on what. Beads provides that memory. It stores tasks in a structured format within your project (in a .beads/ folder that's saved with your code). Each task has a unique ID, can list other tasks it depends on, and tracks its status. Crucially, Beads understands dependencies: if Task B depends on Task A, it won't show Task B as 'ready to work on' until Task A is complete. This makes complex, multi-step projects manageable across many sessions and multiple AI agents.", analogy: "Beads is like a project manager who never forgets anything and never goes home. They know every task, every dependency, every completion status. When a new AI agent shows up and asks 'what should I work on?', Beads can instantly answer: 'Task 14 and 17 are ready because their dependencies are complete, but Task 15 is blocked until Task 12 finishes.' This coordination happens automatically, without requiring humans to track everything manually.", - why: "Beads is central to how the Agent Flywheel workflow operates. You start by planning (perhaps using ChatGPT 5.2 Pro for deep thinking), then break that plan into tasks tracked by Beads. AI agents check Beads to find available work. They mark tasks complete when done. Everything persists in your project's version control, so work is never lost. Commands like 'bd ready' (show tasks ready to work on), 'bd create' (add a new task), and 'bd close' (mark a task done) make it easy to interact with.", + why: "Beads is central to how the Agent Flywheel workflow operates. You start by planning (perhaps using ChatGPT 5.2 Pro for deep thinking), then break that plan into tasks tracked by Beads. AI agents check Beads to find available work. They mark tasks complete when done. Everything persists in your project's version control, so work is never lost. Commands like 'br ready' (show tasks ready to work on), 'br create' (add a new task), and 'br close' (mark a task done) make it easy to interact with.", related: ["ai-agents", "ntm", "agent-mail", "git"], }, diff --git a/apps/web/lib/markdown-components.tsx b/apps/web/lib/markdown-components.tsx index a90276d0..e510308a 100644 --- a/apps/web/lib/markdown-components.tsx +++ b/apps/web/lib/markdown-components.tsx @@ -181,7 +181,7 @@ function Pre({ children, ...props }: MarkdownProps) { {/* Copy button */} <button onClick={handleCopy} - className="absolute right-3 top-3 z-10 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground" + className="absolute right-3 top-3 z-10 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 transition-opacity p-1.5 rounded-md bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" aria-label="Copy code" > {copied ? ( diff --git a/apps/web/lib/tldr-content.ts b/apps/web/lib/tldr-content.ts index 8d6e8237..3258c22c 100644 --- a/apps/web/lib/tldr-content.ts +++ b/apps/web/lib/tldr-content.ts @@ -42,14 +42,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 1400, whatItDoes: - "A mail-like coordination layer for multi-agent workflows. Agents send messages, read threads, and reserve files asynchronously via MCP tools - like Gmail for AI coding agents.", + "A mail-like coordination layer for multi-agent workflows. Agents send messages, read threads, and reserve files asynchronously via MCP tools - like Gmail for AI coding agents. HTTP-only FastMCP transport with static export.", whyItsUseful: - "Critical for multi-agent setups. When 5+ Claude Code instances work the same codebase, they need to coordinate who's editing what. Agent Mail prevents merge conflicts and builds an audit trail of all agent decisions.", + "Critical for multi-agent setups. When 5+ Claude Code instances work the same codebase, they need to coordinate who's editing what. Agent Mail prevents merge conflicts via advisory file reservations with pre-commit guard enforcement, and builds an audit trail of all agent decisions via SQLite + Git dual persistence.", implementationHighlights: [ - "FastMCP server implementation for universal agent compatibility", - "SQLite-backed storage for complete audit trails", - "Advisory file locking to prevent edit conflicts", - "GitHub-flavored Markdown support in messages", + "HTTP-only FastMCP server (Streamable HTTP transport)", + "SQLite + Git dual persistence for human-auditable artifacts", + "FTS5 full-text search with boolean operators", + "Pre-commit guard for file reservation enforcement", + "Static export with Ed25519 signing and age encryption", ], synergies: [ { @@ -62,20 +63,27 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ }, { toolId: "slb", - description: "Launched agents coordinate via mail threads", + description: "Two-person approval requests delivered via agent inboxes", + }, + { + toolId: "ntm", + description: "NTM-spawned agents auto-register with Agent Mail", }, ], - techStack: ["Python 3.11+", "FastMCP", "FastAPI", "SQLite"], + techStack: ["Python 3.14+", "FastMCP", "SQLAlchemy async", "SQLite + FTS5", "LiteLLM"], keyFeatures: [ - "Threaded messaging between AI agents", - "Advisory file reservations", - "SQLite-backed persistent storage", - "MCP integration for any compatible agent", + "Threaded GFM messages with importance levels", + "Advisory file reservations with pre-commit guard", + "SQLite + Git dual persistence (human-auditable)", + "Contact policies with auto-allow heuristics", + "Static export with Ed25519 signing and age encryption", + "Web UI and Human Overseer for human-to-agent messaging", ], useCases: [ "Coordinating file ownership across parallel agents", "Passing context between session restarts", "Building audit trails of agent decisions", + "Exporting encrypted archives for security audits", ], }, { @@ -98,6 +106,10 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ "60fps TUI rendering with vim keybindings", ], synergies: [ + { + toolId: "br", + description: "Reads and visualizes issues from beads_rust (.beads/*.jsonl)", + }, { toolId: "mail", description: "Task updates trigger notifications via Agent Mail", @@ -110,13 +122,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ toolId: "cass", description: "Search prior sessions for task context", }, + { + toolId: "ntm", + description: "NTM uses --robot-plan for dependency analysis during multi-agent orchestration", + }, ], techStack: ["Go", "Bubble Tea", "Lip Gloss", "Graph algorithms"], keyFeatures: [ - "PageRank-based issue prioritization", - "Critical path analysis", - "Robot mode for AI agent integration", - "Interactive TUI with vim keybindings", + "9 graph metrics: PageRank, Betweenness, HITS, Eigenvector, Critical Path", + "6 TUI views with recipe system (11 built-in recipes)", + "Robot protocol with TOON format for low-token output", + "Static site export with SQLite FTS5 search", ], useCases: [ "Identifying which task unblocks the most other work", @@ -134,14 +150,14 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 128, whatItDoes: - "Local-first issue tracking for AI agents. SQLite + JSONL hybrid: fast queries locally, git-friendly export for collaboration. Non-invasive - never auto-commits or touches source code.", + "Local-first issue tracking for AI agents. SQLite for fast local queries, JSONL export for git-friendly collaboration. Full dependency graph with blocking/blocked-by relationships, priorities P0-P4.", whyItsUseful: - "Your issues travel with your repo - no external service required. ~20K lines of Rust focused on one thing: tracking issues without getting in your way. br ready shows actionable work; br sync --flush-only exports for git commit.", + "Your issues travel with your repo - no external service required. Non-invasive design: never runs git commands automatically. Agents can create, update, and close issues with simple CLI commands. The bd alias provides backward compatibility.", implementationHighlights: [ - "SQLite for fast queries, JSONL for git-friendly export", - "Non-invasive: never runs git commands automatically", - "40 commands, all support --json for agents", - "br ready/blocked for dependency-aware work queues", + "~20K lines of Rust (vs 276K in original Go)", + "SQLite primary storage + JSONL export (hybrid architecture)", + "Non-invasive: explicit sync, never runs git automatically", + "Full dependency graph with cycles detection", ], synergies: [ { @@ -156,13 +172,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ toolId: "ntm", description: "NTM spawns agents that pick work from beads", }, + { + toolId: "ubs", + description: "UBS --beads-jsonl outputs findings as importable beads", + }, ], - techStack: ["Rust", "SQLite", "JSONL", "Serde"], + techStack: ["Rust", "SQLite", "Serde", "JSONL"], keyFeatures: [ - "SQLite + JSONL hybrid architecture", - "br ready: unblocked, non-deferred work", - "br dep: full dependency graph management", - "br stats: lead time and activity metrics", + "SQLite + JSONL hybrid: fast queries, git-friendly export", + "Dependency graph with cycles detection", + "Labels, priorities (P0-P4), comments, assignees", + "Agent-first: --json/--robot output, doctor diagnostics", ], useCases: [ "Tracking tasks that travel with the code", @@ -180,14 +200,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 307, whatItDoes: - "Blazing-fast search across all your past AI coding agent sessions. Indexes conversations from Claude Code, Codex, Cursor, Gemini, ChatGPT and more with sub-millisecond query times.", + "Blazing-fast search across all your past AI coding agent sessions. Indexes 11 agent formats: Claude Code, Codex, Cursor, Gemini, ChatGPT, Cline, Aider, Pi-Agent, Factory, OpenCode, Amp. Sub-60ms queries with optional semantic search.", whyItsUseful: - "You've solved this problem before - but which session? CASS lets you search 'how did I fix that React hydration error' and instantly find the exact conversation. Also supports semantic search and multi-machine sync via SSH.", + "You've solved this problem before - but which session? CASS lets you search 'how did I fix that React hydration error' and instantly find the exact conversation. Three search modes (lexical, semantic, hybrid), HTML export with encryption, and multi-machine sync via SSH.", implementationHighlights: [ - "Rust + Tantivy for sub-millisecond full-text search", - "Hybrid semantic + keyword search with RRF fusion", - "Robot mode (--robot) for AI agent integration", - "Multi-machine sync via SSH", + "Rust + Tantivy BM25 with edge n-gram prefix indexing", + "Three search modes: lexical, semantic (MiniLM/hash fallback), hybrid (RRF)", + "Aggregations for 99% token reduction (--aggregate agent,workspace)", + "Context command finds related sessions for source paths", + "Multi-machine sync via SSH with interactive setup wizard", ], synergies: [ { @@ -203,17 +224,19 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Links search results to related Beads tasks", }, ], - techStack: ["Rust", "Tantivy", "Ratatui", "JSONL parsing"], + techStack: ["Rust", "Tantivy", "Ratatui", "SQLite FTS5", "FastEmbed"], keyFeatures: [ - "Unified search across all agent types", - "Sub-second search over millions of messages", - "Robot mode for AI agent integration", - "TUI for interactive exploration", + "Unified search across 11 agent formats", + "Aggregations for 99% token reduction", + "Context command for path-based session discovery", + "Robot mode with cursor pagination and token budgeting", + "Hash embedder fallback for deterministic searches", ], useCases: [ "Finding how a similar bug was fixed before", - "Retrieving context from past project work", - "Building on previous agent conversations", + "Aggregating session stats across agents/workspaces", + "Path-based context discovery for related sessions", + "Multi-machine search across laptop, desktop, and servers", ], }, { @@ -226,14 +249,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 234, whatItDoes: - "One-command bootstrap that transforms a fresh Ubuntu VPS into a fully-configured agentic coding environment with all flywheel tools installed.", + "One-command bootstrap that transforms a fresh Ubuntu VPS into a fully-configured agentic coding environment. CLI provides doctor (47+ health checks), update (category-specific), cheatsheet (50+ aliases), and session management.", whyItsUseful: - "Setting up a new development environment takes hours. ACFS does it in 30 minutes, installing 30+ tools, three AI agents, and all the flywheel tooling automatically.", + "Setting up a new development environment takes hours. ACFS does it in 30 minutes, installing 30+ tools, three AI agents, and all flywheel tooling. Post-install CLI provides `acfs doctor` for health checks and `acfs update` for maintenance.", implementationHighlights: [ - "Single curl | bash installation", - "Idempotent (safe to re-run)", - "Manifest-driven architecture", - "SHA256 checksum verification for security", + "Single curl | bash installation with SHA256 verification", + "Idempotent and resumable installation", + "Manifest-driven architecture (acfs.manifest.yaml)", + "47+ doctor checks + --deep for auth/DB functional tests", + "Category-specific updates with --dry-run preview and logging", ], synergies: [ { @@ -251,14 +275,16 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ ], techStack: ["Bash", "YAML manifest", "Next.js wizard"], keyFeatures: [ - "30-minute zero-to-hero setup", - "Installs Claude Code, Codex, Gemini CLI", - "All flywheel tools pre-configured", - "Step-by-step wizard for beginners", + "acfs doctor: 47+ health checks across 7 categories", + "acfs doctor --deep: Functional tests (auth, DB connectivity)", + "acfs update: Category-specific with --dry-run preview", + "acfs cheatsheet: 50+ aliases for modern CLI tools", + "acfs dashboard: Static HTML dashboard generation", + "Update logging to ~/.acfs/logs/updates/", ], useCases: [ "Setting up new development VPS", - "Onboarding team members", + "Ongoing maintenance with acfs doctor and acfs update", "Reproducible environment provisioning", ], }, @@ -272,36 +298,36 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 132, whatItDoes: - "Custom pattern-based bug scanner with 1,000+ detection rules across multiple languages. Catches common bugs, security issues, and code smells before they become problems.", + "A meta-runner that fans out per-language scanners across 8 languages (JS/TS, Python, Go, Rust, C/C++, Java, Ruby, Swift). Uses ast-grep for AST-based pattern matching with 18 detection categories and 1000+ bug patterns.", whyItsUseful: - "Instead of managing dozens of separate linters, UBS provides comprehensive bug detection in a single tool. Its pattern-based approach catches issues that traditional static analyzers miss.", + "AI coding agents move 10-100x faster than humans. UBS keeps pace with sub-5-second scans and auto-wires guardrails into Claude Code, Codex, Cursor, Gemini, and Windsurf agents. The --beads-jsonl output creates Beads issues directly from findings.", implementationHighlights: [ - "1,000+ custom detection patterns across languages", - "Pattern-based scanning with extensible rule definitions", - "Consistent JSON output across all languages", - "Exit codes designed for CI/CD integration", + "Shell meta-runner with per-language modules (ubs-js.sh, ubs-python.sh, etc.)", + "ast-grep for syntax-aware pattern matching (not regex)", + "18 detection categories: null safety, async bugs, XSS, memory leaks", + "5 output formats: text, json, jsonl, sarif, toon", ], synergies: [ { toolId: "bv", - description: "Bug findings become blocking issues in Beads", + description: "Bug findings become blocking issues via --beads-jsonl", }, { - toolId: "slb", - description: "Pre-flight scans before risky operations", + toolId: "br", + description: "Direct JSONL output for beads_rust issue tracking", }, ], - techStack: ["Bash", "Pattern matching", "JSON output"], + techStack: ["Bash", "ast-grep", "Per-language scanners", "ripgrep"], keyFeatures: [ - "1,000+ built-in detection patterns", - "Consistent JSON output format", - "Multi-language support", - "Perfect for pre-commit hooks", + "1000+ bug patterns across 8 languages", + "18 detection categories with severity levels", + "Agent guardrails: Claude Code hooks, .cursorrules", + "Git-aware: --staged, --diff for targeted scans", ], useCases: [ - "Pre-commit validation across polyglot repos", - "CI/CD pipeline integration", - "Catching AI-generated code errors", + "Pre-commit quality gate for AI-generated code", + "CI/CD pipeline integration with --fail-on-warning", + "Baseline comparison for regression detection", ], }, { @@ -314,14 +340,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 89, whatItDoes: - "Intercepts dangerous shell commands (rm -rf, git reset --hard, etc.) before execution. Requires confirmation for destructive operations.", + "Claude Code PreToolUse hook that blocks dangerous commands BEFORE execution. 50+ packs across 17 categories: git (reset --hard, force push), filesystem (rm -rf), databases (DROP TABLE), Kubernetes, cloud providers, and more.", whyItsUseful: - "AI agents can and will run 'rm -rf /' if they think it solves your problem. DCG is the safety net that catches catastrophic commands before they execute.", + "AI agents can and will run 'rm -rf /' if they think it solves your problem. DCG catches catastrophic commands before they execute with sub-millisecond latency. Safe directory exceptions (/tmp, /var/tmp, $TMPDIR) allow temp operations without friction.", implementationHighlights: [ - "Rust implementation with SIMD-accelerated pattern matching", - "Sub-microsecond command analysis overhead", - "Configurable risk levels and bypass rules", - "Logging of all intercepted commands", + "SIMD-accelerated sub-millisecond PreToolUse hook", + "Heredoc/inline script scanning (python -c, bash -c, node -e)", + "Smart context detection: data vs execution contexts", + "Agent-specific trust profiles with configurable permissions", + "MCP server mode for direct agent integration", ], synergies: [ { @@ -333,17 +360,19 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Guards all commands in NTM-managed sessions", }, ], - techStack: ["Rust", "SIMD", "Shell integration"], + techStack: ["Rust", "Claude Code hooks", "SARIF output", "MCP"], keyFeatures: [ - "Intercepts rm -rf, git reset --hard, etc.", - "SIMD-accelerated pattern matching", - "Configurable allowlists", - "Command audit logging", + "Heredoc/inline script AST scanning", + "49+ packs: git, filesystem, database, k8s, cloud", + "Agent-specific trust levels and profiles", + "dcg scan for CI/pre-commit integration", + "MCP server for direct agent access", ], useCases: [ - "Protecting against accidental data loss", - "Auditing dangerous commands from agents", - "Training wheels for new AI agent setups", + "Pre-execution safety for AI coding agents", + "Catching hidden destructive ops in inline scripts", + "CI integration via dcg scan command", + "MCP server mode for agent workflows", ], }, { @@ -354,38 +383,48 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ icon: "RefreshCw", color: "from-orange-500 to-amber-600", category: "core", - stars: 67, + stars: 78, whatItDoes: - "Keeps dozens (or hundreds) of Git repositories in sync with a single command. Clones missing repos, pulls updates, detects conflicts.", + "Multi-repo management system: sync 100+ repos, AI-assisted code review with priority scoring, dependency updates across package managers, and agent-driven commit automation.", whyItsUseful: - "Managing many repos across machines is painful. 'ru sync' handles everything: cloning what's missing, pulling what's stale, and reporting conflicts with resolution commands.", + "Managing 100+ repos manually is impossible. 'ru sync' handles clone/pull in parallel. 'ru review' discovers issues/PRs via GraphQL batch queries, scores by priority (security+50, bugs+30, age), and spawns isolated Claude Code sessions in worktrees.", implementationHighlights: [ - "Pure Bash with git plumbing (no string parsing)", - "Parallel sync with configurable worker count", - "AI-assisted code review integration", - "Meaningful exit codes for CI/CD", + "Pure Bash with git plumbing (rev-list, status --porcelain)", + "Work-stealing queue for parallel sync with portable locking", + "GraphQL batch queries for efficient issue/PR discovery", + "Git worktree isolation for parallel AI review sessions", + "Meaningful exit codes: 0=ok, 1=partial, 2=conflicts, 3=system", + "TOON/JSON output modes for CI/automation integration", ], synergies: [ { - toolId: "ubs", - description: "Run bug scans across all synced repos", + toolId: "ntm", + description: "Uses ntm robot mode API for AI review session management", }, { - toolId: "ntm", - description: "NTM integration for agent-driven sweeps", + toolId: "mail", + description: "Coordinates repo claims across parallel agents", + }, + { + toolId: "bv", + description: "Multi-repo task tracking via beads integration", }, ], - techStack: ["Bash 4.0+", "Git plumbing", "GitHub CLI"], + techStack: ["Bash 4.0+", "Git plumbing", "GitHub CLI GraphQL", "ntm robot mode"], keyFeatures: [ - "One-command multi-repo sync", - "Parallel operations", - "Conflict detection with resolution hints", - "AI code review integration", + "Parallel sync with work-stealing queue (-j4)", + "AI code review with priority scoring (ru review)", + "Dependency updates (npm, pip, cargo, go, composer)", + "Agent sweep for multi-repo automation", + "Bulk import from GitHub/GitLab/Bitbucket (ru import)", + "Orphan cleanup with ru prune", + "Resume from checkpoint (--resume)", ], useCases: [ - "Keeping development machines in sync", - "CI/CD repo management", - "Automated codebase maintenance", + "Syncing 100+ repos across development machines", + "AI-assisted code review at scale", + "Automated dependency updates with testing", + "Bulk onboarding repos from multiple Git providers", ], }, { @@ -398,41 +437,41 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 152, whatItDoes: - "A memory system built on top of CASS. Implements three-layer cognitive architecture: Episodic (experiences), Working (active context), and Procedural (skills and lessons learned).", + "Cross-agent procedural memory system. Transforms scattered sessions from all your AI agents into persistent, unified knowledge. Three-layer cognitive architecture: Episodic (raw sessions via CASS) → Working (diary summaries) → Procedural (playbook rules with confidence tracking).", whyItsUseful: - "Without persistent memory, every agent session starts from scratch. CM lets agents learn from past sessions - remembering what worked, what failed, and extracting reusable playbook rules for future work.", + "A debugging technique discovered in Cursor is immediately available to Claude Code. Rules have 90-day decay half-life and 4× harmful weight for mistakes. Bad rules auto-invert into anti-pattern warnings. Every agent learns from every other agent's experience.", implementationHighlights: [ - "Three-layer cognitive architecture (Episodic, Working, Procedural)", - "MCP tools for cross-session context persistence", - "Memory consolidation and summarization", - "Hierarchical memory organization", + "Cross-agent learning: Claude Code, Codex, Cursor, Aider sessions unified", + "Confidence decay system with 90-day half-life", + "Scientific validation: rules require CASS evidence before acceptance", + "Anti-pattern learning: harmful rules become warnings", ], synergies: [ { - toolId: "mail", + toolId: "cass", description: - "Conversation summaries from mail threads stored as memories", + "Primary dependency - provides episodic memory via session search", }, { - toolId: "cass", - description: "Semantic search over stored memories", + toolId: "mail", + description: "Memory context shared across agent conversations", }, { toolId: "bv", - description: "Task patterns and solutions remembered", + description: "Task patterns and successful approaches remembered", }, ], - techStack: ["TypeScript", "Bun", "MCP Protocol", "SQLite"], + techStack: ["TypeScript", "Bun", "SQLite"], keyFeatures: [ - "Three memory layers: episodic, working, procedural", - "MCP integration for any compatible agent", - "Automatic memory consolidation", - "Cross-session context persistence", + "Cross-agent learning from all AI coding tools", + "Confidence decay prevents stale rules", + "Agent-native onboarding with gap analysis", + "cm context returns rules, anti-patterns, and history snippets", ], useCases: [ - "Remembering project conventions across sessions", - "Learning from past debugging sessions", - "Building institutional knowledge over time", + "Cross-pollinating debugging knowledge between agents", + "Building institutional memory that persists across tools", + "Learning from past mistakes with anti-pattern warnings", ], }, { @@ -445,14 +484,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 69, whatItDoes: - "Manages named tmux sessions with project-specific persistence. Creates organized workspaces for multi-agent development with typed panes.", + "A multi-agent tmux orchestration tool with 80+ commands. Spawns Claude, Codex, and Gemini agents in named panes with type classification (cc/cod/gmi). Monitors context windows, detects file conflicts, and provides robot mode for automation.", whyItsUseful: - "When running multiple AI coding agents simultaneously, keeping track of which agent is working on what becomes impossible without organization. NTM solves this by providing persistent, named sessions that survive reboots.", + "Running multiple AI agents simultaneously creates chaos without orchestration. NTM provides the command center: spawn agents with one command, broadcast prompts to specific types, monitor context usage, and coordinate via Agent Mail. Sessions persist across SSH disconnects and system reboots.", implementationHighlights: [ - "Go implementation with Bubble Tea TUI framework", - "Project-aware persistence using XDG conventions", - "Support for agent type classification (claude, codex, cursor, etc.)", - "Real-time dashboard showing all active agent panes", + "Go implementation with Bubble Tea TUI and Catppuccin themes", + "Context rotation monitors usage, warns at 80%, triggers compaction recovery", + "Robot mode (--robot-*) outputs JSON for agent automation", + "Direct CASS integration: --robot-cass-search, --robot-cass-context", + "Bead management: --robot-bead-create, --robot-bead-claim, --robot-bead-close", ], synergies: [ { @@ -463,24 +503,35 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ { toolId: "mail", description: - "Agents in different NTM panes communicate via Agent Mail threads", + "Agents auto-register with Mail; ntm mail commands for messaging; pre-commit guard enforces file reservations", }, { toolId: "cass", - description: "Session history from all NTM panes is indexed for search", + description: "Direct integration via --robot-cass-search and --robot-cass-context commands", + }, + { + toolId: "bv", + description: "Graph analysis via --robot-plan and --robot-graph for dependency insights", + }, + { + toolId: "br", + description: "Bead management via --robot-bead-* commands for issue tracking", }, ], - techStack: ["Go 1.22+", "Bubble Tea", "tmux 3.0+"], + techStack: ["Go 1.25+", "Bubble Tea", "tmux 3.0+", "Catppuccin themes"], keyFeatures: [ - "Spawn named agent panes with type classification", - "Broadcast prompts to specific agent types", - "Session persistence across reboots", - "Dashboard view of all active agents", + "80+ commands: spawn, send, dashboard, checkpoint, health, and more", + "Agent type classification with named panes (cc, cod, gmi)", + "Context window monitoring with automatic compaction recovery", + "Command palette TUI with fuzzy search and pinned commands", + "Robot mode for scripting and agent automation", + "Hooks: pre/post-spawn, pre/post-send, pre/post-shutdown", ], useCases: [ - "Running 5+ Claude Code agents on different features simultaneously", - "Organizing development environments by project", - "Managing long-running agent sessions for complex refactors", + "Running 10+ agents across multiple projects simultaneously", + "Broadcasting prompts to all Claude agents: ntm send proj --cc 'prompt'", + "Monitoring context window usage to prevent agent context exhaustion", + "Checkpointing session state before risky operations", ], }, { @@ -493,40 +544,49 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 49, whatItDoes: - "Two-person rule CLI for approving dangerous shell commands. Requires a second human or AI reviewer to approve risky operations before execution.", + "Nuclear-launch-style two-person rule for dangerous commands. Four risk tiers classify commands via 40+ regex patterns: CRITICAL (2+ approvals), DANGEROUS (1 approval), CAUTION (30s auto-approve), SAFE (skip). Cryptographic signing, rollback support, and outcome analytics.", whyItsUseful: - "AI agents can accidentally run destructive commands. SLB implements a 'two-person rule' where dangerous commands require explicit approval from another party before executing, preventing catastrophic mistakes.", + "AI agents can and will run destructive commands if they think it solves your problem. SLB intercepts commands like 'rm -rf /', 'DROP DATABASE', and 'terraform destroy' requiring explicit approval from another agent or human reviewer before execution. Watch mode lets reviewing agents stream pending requests.", implementationHighlights: [ - "Go implementation with Bubble Tea TUI", - "SQLite-backed persistent command queue", - "Configurable risk detection patterns", - "Real-time approval/rejection notifications", + "Go implementation with Bubble Tea TUI dashboard", + "40+ regex patterns: 24 critical, 15 dangerous, 6 safe", + "HMAC-SHA256 cryptographic approval signatures", + "Watch mode streams NDJSON events for reviewing agents", + "Pre-execution state capture for rollback", + "Outcome recording for pattern improvement", ], synergies: [ { - toolId: "ntm", - description: "Protects NTM-managed sessions from dangerous commands", + toolId: "dcg", + description: "DCG blocks pre-execution, SLB validates with multi-agent approval", }, { - toolId: "dcg", - description: "Works alongside DCG for layered command safety", + toolId: "ntm", + description: "Coordinates approval quorum across NTM-managed agents", }, { toolId: "mail", - description: "Approval requests can be sent via Agent Mail", + description: "Approval requests can be routed via Agent Mail", + }, + { + toolId: "caam", + description: "Account switching can require SLB approval for team workflows", }, ], - techStack: ["Go", "Bubble Tea", "SQLite"], + techStack: ["Go 1.24+", "Bubble Tea", "SQLite", "HMAC-SHA256"], keyFeatures: [ - "Two-person rule enforcement", - "Command queue with approval workflow", - "Pattern-based risk detection", - "SQLite persistence", + "4-tier risk: CRITICAL (2+), DANGEROUS (1), CAUTION (30s), SAFE (skip)", + "40+ regex patterns for command classification", + "Self-review protection (agents can't approve own requests)", + "Watch mode for reviewing agents (NDJSON streaming)", + "Claude Code hooks and Cursor rules generation", + "Session management with cryptographic signing", ], useCases: [ - "Requiring approval for rm -rf and git reset operations", - "Adding safety gates to autonomous agent workflows", - "Audit trail of dangerous command approvals", + "Two-person approval for rm -rf, DROP DATABASE, terraform destroy", + "Agent coordination for dangerous operations", + "Audit trail of all dangerous command approvals", + "Rollback support when commands cause problems", ], }, { @@ -537,17 +597,18 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ icon: "Sparkles", color: "from-teal-500 to-emerald-600", category: "core", - stars: 10, + stars: 68, whatItDoes: - "Complete skill management platform: store, search, track effectiveness, package for sharing, and integrate with AI agents via MCP. Skills come from hand-written files, CASS mining, bundles, or guided workflows.", + "Local-first skill management platform: dual persistence (SQLite + Git), hybrid search (BM25 + semantic + RRF), UCB bandit optimization, multi-layer security (ACIP + DCG), graph analysis via bv, MCP server for AI agents.", whyItsUseful: - "AI agents need reusable context to be effective. MS doesn't just store skills—it learns which ones work. Thompson sampling optimizes suggestions over time, security systems (ACIP, DCG) keep content safe, and the MCP server makes skills native to any AI agent.", + "AI agents need reusable context to be effective. MS doesn't just store skills—it learns which ones work via UCB bandit optimization. Context-aware auto-loading suggests skills based on project type. Pack contracts optimize token budgets. The MCP server makes skills native tools for any AI agent.", implementationHighlights: [ - "Dual persistence: SQLite for queries + Git for audit trails", - "Thompson sampling bandit learns from usage to optimize suggestions", - "MCP server exposes 6 native tools for AI agent integration", - "ACIP prompt-injection detection with quarantine system", - "DCG command safety tiers (Safe/Caution/Danger/Critical)", + "Dual persistence: SQLite for queries + Git for audit trails (neither privileged)", + "UCB bandit learns from feedback to optimize suggestions", + "Hybrid search: BM25 + deterministic hash embeddings + RRF fusion", + "MCP server exposes 12 native tools (search, load, evidence, list, show, doctor, lint, suggest, feedback, index, validate, config)", + "ACIP prompt-injection quarantine + DCG command safety tiers", + "Graph analysis via bv: PageRank, betweenness, cycles, critical path", ], synergies: [ { @@ -560,21 +621,27 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ }, { toolId: "bv", - description: "Graph analysis via bv for skill dependency insights", + description: "Graph analysis via bv for PageRank, bottlenecks, cycles", + }, + { + toolId: "jfp", + description: "JFP downloads remote prompts, MS manages local skills", }, ], - techStack: ["Rust", "SQLite", "Tantivy", "Git", "MCP"], + techStack: ["Rust", "SQLite + FTS5", "Git archive", "MCP stdio/HTTP"], keyFeatures: [ - "MCP server for native AI agent integration", - "Thompson sampling optimizes suggestions over time", + "MCP server: 12 native tools for AI agent integration", + "UCB bandit optimization learns from feedback", + "Context-aware auto-loading (ms load --auto)", + "Pack contracts: debug/refactor/learn/quickref/codegen", "Multi-layer security (ACIP, DCG, path policy, secrets)", - "Hybrid search: BM25 + hash embeddings with RRF", - "Token packing for context budget optimization", + "Hybrid search: BM25 + hash embeddings + RRF", ], useCases: [ "AI agents querying skills via MCP during sessions", - "Building team-wide skill libraries with effectiveness tracking", - "Packaging and sharing skills via signed bundles", + "Context-aware skill suggestions based on project type", + "Token-optimized loading with pack contracts", + "Graph analysis of skill dependencies via bv", ], }, { @@ -587,14 +654,14 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 35, whatItDoes: - "Offloads Rust compilation to remote workers via transparent cargo interception. Syncs source via rsync, builds remotely, and streams artifacts back.", + "Claude Code PreToolUse hook that offloads Rust compilation to remote workers. Intercepts cargo commands, syncs source via rsync + zstd, compiles on server-grade hardware, streams artifacts back.", whyItsUseful: - "Multi-agent swarms trigger many concurrent builds. RCH transparently routes cargo commands to powerful remote machines, preventing local CPU bottlenecks and dramatically reducing build times.", + "Multi-agent swarms trigger many concurrent builds. RCH intercepts commands before execution and routes them to remote workers with health probes and priority scheduling. Agent detection coordinates builds across Claude Code, Codex, and Gemini sessions.", implementationHighlights: [ - "Claude Code hook intercepts cargo commands", - "rsync + zstd for fast source synchronization", - "Worker pool with priority-based scheduling", - "Artifact streaming with incremental updates", + "PreToolUse hook intercepts cargo before execution", + "rsync + zstd with incremental artifact streaming", + "Worker health probes and priority scheduling", + "Agent detection for multi-agent coordination", ], synergies: [ { @@ -612,10 +679,10 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ ], techStack: ["Rust", "rsync", "zstd", "SSH", "Claude Code hooks"], keyFeatures: [ - "Transparent cargo interception", - "Multi-worker pool with priority scheduling", - "Incremental artifact sync", - "Daemon mode with status monitoring", + "PreToolUse hook intercepts cargo automatically", + "Worker pool with health probes and priorities", + "Daemon mode with persistent SSH connections", + "Agent detection: Claude Code, Codex, Gemini", ], useCases: [ "Offloading builds during multi-agent sessions", @@ -623,6 +690,56 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ "Distributing builds across powerful remote servers", ], }, + { + id: "caam", + name: "Coding Agent Account Manager", + shortName: "CAAM", + href: "https://github.com/Dicklesworthstone/coding_agent_account_manager", + icon: "KeyRound", + color: "from-amber-500 to-orange-600", + category: "core", + stars: 12, + whatItDoes: + "Manages multiple accounts for Claude Code, Codex CLI, and Gemini CLI with sub-100ms switching. Vault profiles store auth files for instant activation without browser flows. Smart rotation algorithms automatically select the best profile based on cooldown state, health, and usage patterns.", + whyItsUseful: + "When running multiple agents, you'll hit rate limits. CAAM lets you switch accounts instantly - no browser login, no waiting. Profile isolation enables parallel sessions where each agent uses its own credentials. Health scoring (🟢/🟡/🔴) shows which profiles are ready vs. cooling down.", + implementationHighlights: [ + "Go implementation with 50+ commands", + "Vault-based profile storage for instant switching", + "Robot mode with JSON output for agent integration", + "AES-256-GCM encrypted bundles with Argon2id key derivation", + "Background daemon for proactive token refresh", + ], + synergies: [ + { + toolId: "ntm", + description: "NTM spawns agents with isolated CAAM profiles for parallel sessions", + }, + { + toolId: "mail", + description: "Account switches can trigger Agent Mail notifications", + }, + { + toolId: "slb", + description: "Team approval workflows for account switching", + }, + ], + techStack: ["Go", "SQLite", "OAuth", "AES-256-GCM", "Argon2id"], + keyFeatures: [ + "Sub-100ms switching via vault profiles", + "caam run: automatic failover on rate limits", + "Project-profile associations (per-directory defaults)", + "Smart rotation: cooldown, health, recency, plan type", + "Health scoring: healthy/warning/critical status", + "Robot mode with JSON output for agents", + ], + useCases: [ + "caam run with automatic rate limit failover", + "Per-directory profile defaults for projects", + "Running parallel agents with isolated credentials", + "Automated rotation for long-running sessions", + ], + }, { id: "wa", name: "WezTerm Automata", @@ -640,7 +757,8 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ "Real-time delta extraction (sub-50ms latency)", "Multi-agent pattern detection engine", "FTS5-powered full-text search with BM25 ranking", - "Safety policy engine with capability gates", + "TOON output format for token-efficient AI consumption", + "Workflow automation triggered by pattern matches", ], synergies: [ { @@ -658,15 +776,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ ], techStack: ["Rust", "WezTerm API", "SQLite FTS5", "Pattern matching"], keyFeatures: [ - "Real-time terminal observation", - "Intelligent pattern detection", - "Robot Mode JSON API", - "Event-driven automation", + "Real-time terminal observation (<50ms latency)", + "Multi-agent pattern detection (Claude, Codex, Gemini)", + "Robot Mode JSON/TOON API", + "Event-driven wait-for automation", + "Explainability via 'wa why' command", ], useCases: [ "Detecting agent rate limits and errors", "Coordinating multi-agent workflows", "Searching across captured terminal sessions", + "Triggering automated responses on state changes", ], }, { @@ -679,40 +799,44 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "core", stars: 28, whatItDoes: - "Research orchestration platform inspired by Sydney Brenner's scientific methodology. Coordinates multi-agent AI research sessions with systematic problem formulation and rigorous constraint-based reasoning.", + "Multi-agent scientific research orchestration platform based on Sydney Brenner's methodology. Manages full research artifact lifecycle: hypotheses, discriminative tests, anomalies, critiques, and evidence packs with cockpit runtime for parallel agent sessions.", whyItsUseful: - "Complex research problems need structured approaches. Brenner Bot combines a curated corpus with multi-model AI syntheses to enable collaborative scientific research conversations with proper citation tracking.", + "Transforms AI agents into a collaborative research group with rigorous scientific discipline. The Brenner approach emphasizes exclusion over accumulation, third-alternative thinking, and discriminative experiments that collapse hypothesis space fast.", implementationHighlights: [ - "236-section transcript corpus with stable §n citations", - "Multi-model syntheses from Claude, GPT, Gemini", - "Artifact compiler with 50+ validation rules", - "Agent Mail integration for research sessions", + "Hypothesis lifecycle management: proposed → active → killed/validated with discriminative tests", + "Evidence packs: import papers, datasets, prior sessions with stable EV-NNN citations", + "Anomaly tracking with paradigm_shifting status and hypothesis spawning capability", + "Cockpit runtime: multi-agent sessions with role-specific prompts (hypothesis_generator, test_designer, adversarial_critic)", + "Session state machine with phase detection and artifact compiler (50+ validation rules)", ], synergies: [ { toolId: "mail", - description: "Research sessions coordinate via Agent Mail threads", + description: "Research sessions coordinate via Agent Mail threads with acknowledgment tracking", }, { toolId: "ntm", - description: "NTM spawns parallel research agents", + description: "Cockpit runtime spawns parallel research agents with role-specific prompts", }, { toolId: "cass", - description: "Research session history is searchable", + description: "Research session history searchable for prior solutions and patterns", }, ], - techStack: ["TypeScript", "Bun", "Agent Mail", "Multi-model AI"], + techStack: ["TypeScript", "Bun", "Agent Mail", "ntm", "Multi-model AI"], keyFeatures: [ - "Primary source corpus with citations", - "Multi-agent research sessions", - "Discriminative test ranking", - "Adversarial critique generation", + "Hypothesis lifecycle: create, activate, kill, validate with test evidence", + "Evidence packs with stable EV-NNN citations for papers, datasets, prior sessions", + "Anomaly management: track, defer, resolve, spawn new hypotheses", + "Critique system: adversarial attacks with severity levels and responses", + "Cockpit runtime: orchestrate multi-agent sessions with role assignments", + "Corpus search with 236 transcript sections and §n anchors", ], useCases: [ - "Structured hypothesis generation", - "Multi-model research synthesis", - "Scientific methodology workflows", + "Running structured multi-agent research sessions with hypothesis tracking", + "Managing evidence from external sources with citation anchors", + "Designing discriminative experiments that eliminate rather than confirm", + "Orchestrating parallel AI agents as a collaborative research group", ], }, // =========================================================================== @@ -728,14 +852,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 24, whatItDoes: - "Downloads images from iCloud public share links for use in remote debugging sessions. Converts iCloud URLs to direct image downloads.", + "Downloads full-resolution images from iCloud, Dropbox, Google Photos, and Google Drive share links using a four-tier capture strategy with headless Chromium automation.", whyItsUseful: - "When debugging remotely via SSH, you can't easily share screenshots. GIIL lets you upload to iCloud, share the link, and the remote machine downloads the image directly for AI agents to analyze.", + "When debugging remotely, users share cloud links but you're SSH'd into a headless server. GIIL's four-tier capture (download button → CDN interception → element screenshot → viewport fallback) ensures maximum quality retrieval for AI agent analysis.", implementationHighlights: [ - "iCloud public share URL parsing", - "Direct image download without browser", - "Automatic file naming from URL metadata", - "Works over SSH without GUI", + "Four-tier capture: download→CDN→element→viewport", + "Playwright/Chromium headless browser automation", + "MozJPEG compression with configurable quality", + "Album mode (--all) extracts all images from shares", + "Structured exit codes (0/10/11/12/13) for scripting", ], synergies: [ { @@ -747,17 +872,19 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Image analysis sessions are searchable", }, ], - techStack: ["Bash", "curl", "iCloud API"], + techStack: ["Bash", "Node.js", "Playwright", "Chromium", "Sharp", "MozJPEG"], keyFeatures: [ - "iCloud share link support", - "CLI-based image download", - "No browser required", - "Works over SSH", + "iCloud, Dropbox, Google Photos, Google Drive support", + "Four-tier intelligent capture strategy", + "Album mode for multi-image shares", + "JSON/TOON/base64 output formats", + "HEIC/AVIF to JPEG conversion", ], useCases: [ - "Sharing screenshots for remote debugging", - "Getting images to headless servers", - "AI agent image analysis workflows", + "Retrieving user screenshots for remote debugging", + "Extracting full albums from cloud shares", + "AI agent visual analysis workflows", + "Scripted image collection with exit code handling", ], }, { @@ -770,14 +897,14 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 50, whatItDoes: - "Auto-deprioritizes background processes to keep your terminal responsive during heavy builds and multi-agent sessions.", + "Installs ananicy-cpp with curated rules to auto-deprioritize background processes. Includes sysmoni Go TUI (Bubble Tea) with IO throughput, FD counts, per-core sparklines, JSON export. Works on Linux and WSL2.", whyItsUseful: - "When running cargo build, npm install, or multiple AI agents simultaneously, SRPS prevents your system from becoming unresponsive by automatically lowering the priority of known resource hogs.", + "When running cargo build, npm install, or multiple AI agents, SRPS prevents unresponsive systems by lowering priority of known resource hogs. Safety-first: no automated process killing. Helper tools for diagnostics.", implementationHighlights: [ - "ananicy-cpp daemon with 1700+ process rules", - "sysmoni Go TUI for real-time monitoring", - "Sysctl tweaks for better responsiveness", - "Custom rule support for any process", + "ananicy-cpp daemon with curated process rules", + "sysmoni Go TUI: CPU/MEM, IO throughput, FD counts", + "Per-core sparklines, JSON/NDJSON export, GPU monitoring", + "Helper tools: check-throttled, srps-doctor, cursor-guard", ], synergies: [ { @@ -792,13 +919,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ toolId: "dcg", description: "Combined safety: resource protection + command protection", }, + { + toolId: "pt", + description: "PT identifies stuck processes, SRPS deprioritizes resource hogs", + }, ], - techStack: ["Go", "C++", "ananicy-cpp", "systemd"], + techStack: ["Go", "Bubble Tea", "C++", "ananicy-cpp", "systemd"], keyFeatures: [ - "Automatic process deprioritization", - "Real-time TUI monitoring", - "1700+ pre-configured rules", - "Custom rule creation", + "Automatic process deprioritization via ananicy-cpp", + "sysmoni TUI with IO and FD monitoring", + "WSL2-compatible systemd limits", + "Idempotent installer with --plan dry-run", ], useCases: [ "Multi-agent coding sessions", @@ -817,14 +948,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 156, whatItDoes: - "Ultra-fast search over X/Twitter data archives. Indexes tweets, likes, DMs, and Grok chats with hybrid BM25 + semantic search using Reciprocal Rank Fusion.", + "Ultra-fast search over X/Twitter data archives with sub-millisecond latency. Uses hybrid BM25 + semantic search with Reciprocal Rank Fusion. Indexes tweets, likes, DMs, and Grok conversations.", whyItsUseful: - "Your X archive is a goldmine of bookmarks, threads, and ideas. XF makes your archive instantly searchable with three modes: hybrid (default), lexical (BM25), and semantic (vector similarity).", + "Your X archive is a goldmine of bookmarks, threads, and ideas, but Twitter's search is terrible. XF makes your archive instantly searchable (<10ms) with both keyword and semantic matching. DM context search shows full conversation threads.", implementationHighlights: [ - "Tantivy-powered BM25 with phrase queries and boolean operators", - "Hash-based embeddings (zero deps) or optional MiniLM semantic", - "SIMD-accelerated vector ops with F16 quantization", - "Parses window.YTD.* JavaScript format from X exports", + "Rust + Tantivy for sub-millisecond lexical search", + "Hybrid BM25 + semantic search with RRF fusion", + "Hash embedder (default) or optional MiniLM (--semantic)", + "SIMD-accelerated vector search with F16 quantization", + "Privacy-first, fully local processing (no network calls)", ], synergies: [ { @@ -836,17 +968,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Found tweets can become memories", }, ], - techStack: ["Rust", "Tantivy", "SQLite", "FNV-1a hash embeddings"], + techStack: ["Rust", "Tantivy", "SQLite", "SIMD", "F16 quantization"], keyFeatures: [ - "Sub-millisecond lexical, <10ms hybrid search", - "DM context view with full conversation threads", - "JSON/CSV/compact output formats", - "Interactive REPL shell (xf shell)", + "Sub-millisecond lexical search (<10ms typical)", + "Hybrid BM25 + semantic with RRF fusion", + "DM context search with full threads", + "Indexes tweets, likes, DMs, Grok chats", ], useCases: [ "Finding that thread you bookmarked months ago", - "Searching DMs with full conversation context", - "Exporting tweets to JSON for analysis pipelines", + "Searching DM conversations with full context", + "Researching past discussions on a topic", ], }, { @@ -859,36 +991,44 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 78, whatItDoes: - "Terminal UI for combining source code files into LLM-ready prompts. Select files, preview output, copy to clipboard with token counting.", + "World-class terminal UI for combining source code files into LLM-ready prompts. Tree explorer with vim-style navigation, live syntax preview, token counting, and structured XML-like output optimized for AI parsing.", whyItsUseful: - "Crafting prompts with code context is tedious. s2p lets you interactively select files, see the combined output, and track token count, all in a beautiful TUI.", + "Crafting prompts with code context is tedious and error-prone. S2P provides visual file selection with sizes and line counts, real-time token/cost estimation, quick file-type shortcuts (1-9,0,r), and produces structured output that LLMs parse reliably.", implementationHighlights: [ - "Bun single-binary distribution", - "React/Ink terminal UI framework", - "tiktoken-accurate token counting", - "Gitignore-aware file filtering", + "Bun single-binary with zero runtime dependencies", + "React/Ink terminal UI with virtualized rendering", + "tiktoken cl100k_base encoding (GPT-4 compatible)", + "Structured XML output: <preamble>, <goal>, <project_structure>, <files>", + "JS/TS minification via Terser, CSS via csso", + "Recursive .gitignore support including nested gitignores", ], synergies: [ { toolId: "cass", - description: "Generated prompts can be searched later", + description: "Generated prompts become searchable session history", }, { toolId: "cm", - description: "Effective prompts stored as memories", + description: "Effective prompt patterns stored as procedural memories", }, ], - techStack: ["TypeScript", "Bun", "React", "Ink", "tiktoken"], + techStack: ["TypeScript", "Bun", "React", "Ink", "tiktoken", "Terser", "csso"], keyFeatures: [ - "Interactive file selection", - "Real-time token counting", - "Clipboard integration", - "Gitignore-aware filtering", + "Tree file explorer with sizes and line counts", + "Vim-style navigation (j/k/h/l)", + "Quick file-type shortcuts (1-9,0,r)", + "Live syntax-highlighted preview", + "Real-time token count and cost estimate", + "Context window usage bar (128K limit)", + "Preset save/load (~/.source2prompt.json)", + "Code minification and comment stripping", ], useCases: [ - "Preparing code context for Claude/GPT", - "Creating reproducible prompt templates", - "Managing context window budget", + "Preparing code context for Claude Code, Codex, or GPT", + "Creating reproducible prompt templates with presets", + "Managing context window budget visually", + "Generating documentation or code review prompts", + "Sharing code context in structured format", ], }, { @@ -901,14 +1041,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 85, whatItDoes: - "Automated iterative specification refinement using extended AI reasoning. Takes rough plans and runs multiple review cycles to identify architectural issues, edge cases, and security flaws.", + "Iterative specification refinement via GPT Pro 5.2 Extended Reasoning + Oracle. Document bundling (README + spec + impl), convergence analytics with weighted scoring, session management, and robot mode JSON API for coding agents.", whyItsUseful: - "Complex specifications need 15-20 review cycles to catch all issues. Instead of manually prompting each round, APR automates the refinement loop, progressively improving structure and detail.", + "Complex specs need 15-20 review cycles. APR automates the loop: rounds 1-3 fix architecture, 4-7 refine interfaces, 8-12 handle edge cases, 13+ polish abstractions. Convergence score (≥0.75 = stable) tells you when to stop.", implementationHighlights: [ - "Uses GPT Pro 5.2 Extended Reasoning via Oracle", - "Iterative multi-pass refinement algorithm", - "Markdown plan file processing", - "Configurable refinement depth", + "GPT Pro 5.2 Extended Reasoning via Oracle browser automation", + "Convergence analytics (output_trend + change_velocity + similarity)", + "Pre-flight validation and auto-retry with exponential backoff", + "Session locking prevents concurrent runs", + "Robot mode: apr robot validate/run/history with semantic error codes", ], synergies: [ { @@ -924,18 +1065,19 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Refined specs generate well-structured beads", }, ], - techStack: ["Bash", "Oracle CLI", "GPT Pro 5.2", "Markdown"], + techStack: ["Bash", "Oracle", "Node.js", "gum", "GPT Pro 5.2"], keyFeatures: [ - "Automated multi-pass refinement", - "Extended AI reasoning integration", - "Markdown-based plan processing", - "Progressive structure improvement", + "Document bundling (README + spec + implementation)", + "Convergence analytics with weighted scoring", + "Background processing with session management", + "Claude Code integration prompts (apr integrate)", + "Robot mode JSON API with semantic error codes", ], useCases: [ - "Turning rough ideas into detailed specifications", - "Catching architectural flaws early", - "Creating implementation-ready plans for agents", - "Iterative requirement refinement", + "Multi-round spec refinement converging on stable design", + "Background 10-60 minute reviews with desktop notifications", + "Automated agent workflows via robot mode", + "Tracking convergence to know when specs are ready", ], }, { @@ -948,14 +1090,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 120, whatItDoes: - "Official CLI for jeffreysprompts.com - browse, search, and install battle-tested prompts as Claude Code skills with one command.", + "Official CLI for jeffreysprompts.com - browse, search, and install battle-tested prompts as Claude Code skills. Features interactive fzf-style picker and task-based suggestion engine.", whyItsUseful: - "Instead of writing prompts from scratch, install proven patterns. JFP connects to jeffreysprompts.com to discover prompts categorized by use case, then installs them directly to your Claude Code skills folder.", + "Instead of writing prompts from scratch, install proven patterns. The interactive mode (jfp i) lets you fuzzy-search the entire library, while jfp suggest recommends prompts based on your task description. Premium features include collections, cross-machine sync, and a skills marketplace.", implementationHighlights: [ "TypeScript/Bun compiled to standalone binary", - "Direct integration with Claude Code skills system", - "MCP server mode for agent integration", - "Offline browsing of downloaded prompts", + "Interactive fzf-style picker (jfp i) for browsing", + "Task-based suggestions (jfp suggest) using semantic matching", + "MCP server mode (jfp serve) for agent integration", + "Variable rendering with placeholder fill (jfp render --fill)", ], synergies: [ { @@ -973,16 +1116,17 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ ], techStack: ["TypeScript", "Bun", "Claude Code Skills API"], keyFeatures: [ - "One-command skill installation", - "Browsable prompt categories", - "Claude Code skills integration", - "MCP server for agent access", + "Interactive fzf-style prompt picker (jfp i)", + "Task-based suggestions (jfp suggest)", + "Workflow bundles for team patterns", + "MCP server mode for agent workflows", + "Premium: collections, sync, marketplace", ], useCases: [ "Bootstrapping a new project with proven prompts", - "Discovering prompts for specific domains", - "Sharing effective prompts across teams", - "Building a personal prompt library", + "Task-based discovery with jfp suggest", + "Running as MCP server for agent access", + "Syncing prompt libraries across machines", ], }, { @@ -995,14 +1139,15 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ category: "supporting", stars: 45, whatItDoes: - "Find and terminate stuck or zombie processes with intelligent Bayesian scoring. Identifies resource hogs and helps clean up runaway processes.", + "Bayesian-inference zombie/abandoned process detection using four-state classification (Useful, Useful-but-bad, Abandoned, Zombie) with evidence-based posterior probability scoring.", whyItsUseful: - "When cargo build hangs, or a test runner goes rogue, pt helps you identify and terminate the offending processes. Uses intelligent scoring to prioritize truly problematic processes.", + "When builds hang or test runners go rogue, PT computes P(state|evidence) using process type, age, CPU/IO activity, memory, and past decisions. Confidence levels (very_high >0.99 to low <0.80) guide safe termination with identity validation and staged kill signals.", implementationHighlights: [ - "Rust implementation for speed", - "Bayesian scoring for process prioritization", - "Interactive TUI for process selection", - "Robot mode for automation", + "Rust pt-core inference engine + Bash wrapper", + "Four-state Bayesian posterior classification", + "Identity validation (boot_id:start_time:pid) prevents PID reuse", + "Protected process lists (systemd, sshd, docker, postgres)", + "Agent/robot mode with safety gates (min_posterior, max_kills, fdr_budget)", ], synergies: [ { @@ -1014,18 +1159,275 @@ const _tldrFlywheelTools: TldrFlywheelTool[] = [ description: "Clean up runaway processes in tmux sessions", }, ], - techStack: ["Rust", "Bayesian inference", "procfs"], + techStack: ["Rust", "Bash", "gum", "procfs", "Bayesian inference"], + keyFeatures: [ + "Four-state classification with posterior probabilities", + "Evidence-based scoring (process type, age, CPU, IO, memory)", + "Protected processes and identity validation", + "Interactive gum TUI for process selection", + "Session bundles (.ptb) for sharing/reproducibility", + ], + useCases: [ + "Identifying and killing abandoned dev servers", + "Cleaning up zombie processes with confidence scores", + "Automated triage via agent/robot mode with safety gates", + "Sharing reproducible triage sessions via .ptb bundles", + ], + }, + { + id: "tru", + name: "TOON Rust", + shortName: "TRU", + href: "https://github.com/Dicklesworthstone/toon_rust", + icon: "FileJson", + color: "from-violet-500 to-purple-600", + category: "supporting", + stars: 32, + whatItDoes: + "Token-optimized notation format for efficient LLM context packing. Compresses structured data into a dense format that maximizes information per token.", + whyItsUseful: + "LLM context windows are precious. TRU compresses JSON, YAML, and other structured data into a compact notation that conveys the same information in fewer tokens, letting you fit more context into each request.", + implementationHighlights: [ + "Rust implementation for speed", + "Lossless compression of structured data", + "Multiple output formats (TOON, JSON, YAML)", + "CLI and library modes", + ], + synergies: [ + { + toolId: "s2p", + description: "Compress source prompts for maximum context efficiency", + }, + { + toolId: "cass", + description: "Compact session data for storage and search", + }, + ], + techStack: ["Rust", "Serde", "Token optimization"], + keyFeatures: [ + "Token-optimized notation format", + "Structured data compression", + "Multiple format support", + "Fast Rust implementation", + ], + useCases: [ + "Fitting more context into LLM requests", + "Compressing structured data for agents", + "Optimizing token usage in prompts", + ], + }, + { + id: "rust_proxy", + name: "Rust Proxy", + shortName: "RustProxy", + href: "https://github.com/Dicklesworthstone/rust_proxy", + icon: "Network", + color: "from-slate-500 to-zinc-600", + category: "supporting", + stars: 18, + whatItDoes: + "Transparent HTTP/HTTPS proxy for debugging and inspecting network traffic. Routes requests through a local proxy for analysis.", + whyItsUseful: + "When debugging API integrations or AI agent network calls, you need visibility into what's being sent and received. Rust Proxy provides transparent interception without modifying your code.", + implementationHighlights: [ + "Rust implementation with async I/O", + "HTTPS interception with certificate generation", + "Request/response logging", + "Minimal latency overhead", + ], + synergies: [ + { + toolId: "rano", + description: "Complementary network debugging - proxy vs observer", + }, + { + toolId: "cass", + description: "Log network calls alongside session history", + }, + ], + techStack: ["Rust", "Tokio", "TLS", "HTTP proxy"], + keyFeatures: [ + "Transparent HTTP/HTTPS proxy", + "Request/response inspection", + "Certificate generation", + "Low latency overhead", + ], + useCases: [ + "Debugging API integrations", + "Inspecting AI agent network calls", + "Analyzing third-party API traffic", + ], + }, + { + id: "rano", + name: "RANO", + shortName: "RANO", + href: "https://github.com/Dicklesworthstone/rano", + icon: "Radio", + color: "from-cyan-500 to-blue-600", + category: "supporting", + stars: 25, + whatItDoes: + "Network observer for AI CLI tools that logs requests and responses without proxying. Passive monitoring of LLM API traffic.", + whyItsUseful: + "Understanding what your AI agents are actually sending to APIs helps with debugging, cost tracking, and optimization. RANO passively observes network traffic without adding proxy overhead.", + implementationHighlights: [ + "Rust implementation for performance", + "Passive network observation (no proxy)", + "LLM-specific request/response parsing", + "JSON output for analysis", + ], + synergies: [ + { + toolId: "caut", + description: "Network observations feed usage tracking", + }, + { + toolId: "cass", + description: "Correlate network calls with session history", + }, + ], + techStack: ["Rust", "pcap", "Network monitoring"], + keyFeatures: [ + "Passive network observation", + "LLM API traffic parsing", + "Request/response logging", + "Zero proxy overhead", + ], + useCases: [ + "Debugging AI agent API calls", + "Tracking LLM API usage", + "Analyzing request patterns", + ], + }, + { + id: "mdwb", + name: "Markdown Web Browser", + shortName: "MDWB", + href: "https://github.com/Dicklesworthstone/markdown_web_browser", + icon: "Globe", + color: "from-emerald-500 to-teal-600", + category: "supporting", + stars: 42, + whatItDoes: + "Converts websites to clean Markdown for LLM consumption. Strips ads, navigation, and boilerplate to extract just the content.", + whyItsUseful: + "AI agents need web content in a format they can understand. MDWB fetches pages and converts them to clean Markdown, perfect for feeding into LLM context windows.", + implementationHighlights: [ + "Rust implementation with async fetching", + "Intelligent content extraction (reader mode)", + "Configurable output formatting", + "Handles JavaScript-rendered pages", + ], + synergies: [ + { + toolId: "tru", + description: "Compress fetched content for maximum token efficiency", + }, + { + toolId: "cm", + description: "Store fetched content as memories", + }, + ], + techStack: ["Rust", "HTML parsing", "Markdown", "HTTP client"], + keyFeatures: [ + "Website to Markdown conversion", + "Content extraction (reader mode)", + "JavaScript rendering support", + "Clean output formatting", + ], + useCases: [ + "Feeding web content to AI agents", + "Research automation", + "Documentation scraping", + ], + }, + { + id: "aadc", + name: "ASCII Art Diagram Corrector", + shortName: "AADC", + href: "https://github.com/Dicklesworthstone/aadc", + icon: "PenTool", + color: "from-amber-500 to-yellow-600", + category: "supporting", + stars: 15, + whatItDoes: + "Fixes malformed ASCII art diagrams generated by AI. Corrects alignment, box characters, and connection lines.", + whyItsUseful: + "AI models often generate ASCII diagrams with alignment issues, broken lines, or inconsistent characters. AADC automatically detects and fixes these problems.", + implementationHighlights: [ + "Rust implementation for speed", + "Pattern detection for common diagram types", + "Character alignment correction", + "Box-drawing character normalization", + ], + synergies: [ + { + toolId: "s2p", + description: "Clean up diagrams in generated prompts", + }, + { + toolId: "cm", + description: "Store corrected diagrams as memories", + }, + ], + techStack: ["Rust", "Pattern matching", "Text processing"], + keyFeatures: [ + "ASCII diagram detection", + "Alignment correction", + "Box-drawing normalization", + "Line connection repair", + ], + useCases: [ + "Fixing AI-generated diagrams", + "Cleaning up documentation", + "Preparing diagrams for version control", + ], + }, + { + id: "caut", + name: "Coding Agent Usage Tracker", + shortName: "CAUT", + href: "https://github.com/Dicklesworthstone/coding_agent_usage_tracker", + icon: "BarChart", + color: "from-rose-500 to-pink-600", + category: "supporting", + stars: 28, + whatItDoes: + "Tracks LLM provider usage across multiple coding agents. Monitors API calls, token consumption, and costs.", + whyItsUseful: + "When running multiple AI agents simultaneously, costs can spiral. CAUT provides visibility into which agents are using how many tokens and at what cost.", + implementationHighlights: [ + "Rust implementation with SQLite storage", + "Multi-provider support (Anthropic, OpenAI, Google)", + "Real-time usage monitoring", + "Cost estimation and alerts", + ], + synergies: [ + { + toolId: "rano", + description: "Network observations feed usage data", + }, + { + toolId: "ntm", + description: "Track usage per NTM-managed session", + }, + { + toolId: "mail", + description: "Usage alerts via Agent Mail", + }, + ], + techStack: ["Rust", "SQLite", "API monitoring"], keyFeatures: [ - "Intelligent process scoring", - "Interactive TUI selection", - "Robot mode for automation", - "Resource usage analysis", + "Multi-provider usage tracking", + "Token consumption monitoring", + "Cost estimation", + "Usage alerts and reporting", ], useCases: [ - "Killing stuck build processes", - "Cleaning up zombie processes", - "Identifying memory hogs", - "Automated process cleanup", + "Tracking AI agent costs", + "Budget monitoring for teams", + "Identifying expensive operations", ], }, ]; @@ -1056,9 +1458,9 @@ export const tldrPageData = { title: "The Agentic Coding Flywheel", subtitle: "TL;DR Edition", description: - "15 core tools and 7 supporting utilities that transform multi-agent AI coding workflows. Each tool makes the others more powerful - the more you use it, the faster it spins. While others argue about agentic coding, we're just over here building as fast as we can.", + "16 core tools and 13 supporting utilities that transform multi-agent AI coding workflows. Each tool makes the others more powerful - the more you use it, the faster it spins. While others argue about agentic coding, we're just over here building as fast as we can.", stats: [ - { label: "Ecosystem Tools", value: "22" }, + { label: "Ecosystem Tools", value: "29" }, { label: "GitHub Stars", value: "3,600+" }, { label: "Languages", value: "5" }, ], @@ -1066,7 +1468,7 @@ export const tldrPageData = { coreDescription: "The core flywheel tools form the backbone: Agent Mail for coordination, BV for graph-based prioritization, CASS for instant session search, CM for persistent memory, UBS for bug detection, MS for skill management with MCP integration, plus session management, safety guards, and automated setup.", supportingDescription: - "Supporting tools extend the ecosystem: GIIL for remote image debugging, SRPS for system responsiveness under heavy load, XF for searching your X archive, S2P for crafting prompts from source code, APR for spec refinement, JFP for curated prompt discovery, and PT for process triage.", + "Supporting tools extend the ecosystem: GIIL for remote image debugging, SRPS for system responsiveness under heavy load, XF for searching your X archive, S2P for crafting prompts from source code, APR for spec refinement, JFP for curated prompt discovery, PT for process triage, TRU for token-optimized notation, RANO for network observation, MDWB for website-to-Markdown conversion, AADC for ASCII diagram correction, and CAUT for usage tracking.", flywheelExplanation: { title: "Why a Flywheel?", paragraphs: [ diff --git a/bun.lock b/bun.lock index 7af94faa..5aa37287 100644 --- a/bun.lock +++ b/bun.lock @@ -375,7 +375,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], @@ -1423,6 +1423,8 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "protobufjs/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/checksums.yaml b/checksums.yaml index e0cef4ee..b040eb75 100644 --- a/checksums.yaml +++ b/checksums.yaml @@ -1,10 +1,10 @@ -# checksums.yaml - Auto-generated 2026-01-30T21:10:00+00:00 +# checksums.yaml - Auto-generated 2026-02-16T18:11:02+00:00 # Run: ./scripts/lib/security.sh --update-checksums installers: zoxide: url: "https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh" - sha256: "c5d94701cfdd87d19f3989c1239e921d4ff95d4c0742b16beca061618c937bf0" + sha256: "0c25aed6cce9d56ee0596c9a5df8cbe99ffd0f7a7054de0262234dd268b61404" rano: url: "https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh" @@ -24,19 +24,19 @@ installers: cass: url: "https://raw.githubusercontent.com/Dicklesworthstone/coding_agent_session_search/main/install.sh" - sha256: "744f39ccd11c1a9be372d5b559d21cb77e6bcfe25905e227caa6ba7c801a5933" + sha256: "616223707fe05a86c24ed9ef1bf437009d42ee1af8cc10495cb19876b85bff3e" mcp_agent_mail: url: "https://raw.githubusercontent.com/Dicklesworthstone/mcp_agent_mail/main/scripts/install.sh" - sha256: "92fb23cc1ecdfaa30fb351d601125fd48e8c2f6fcea0df7a5ddf48e15e9631e2" + sha256: "5b64dab0cf64125535f14ec787bc94a35a7039cf57baba7ebead28a9b96f795c" dcg: url: "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh" - sha256: "8a1cd1c91f40b1561a73f904d4809ee93e9ea9415f5cccaf8f996f095916f8c1" + sha256: "a976cd77182563a1c6bc28a306eb9462678f5784caa011559c0b5b07edab04a3" claude: url: "https://claude.ai/install.sh" - sha256: "363382bed8849f78692bd2f15167a1020e1f23e7da1476ab8808903b6bebae05" + sha256: "a27f0c75029d86eab7313ce4d5a2464e4e68dcce76905a1462a76ab4f19937de" atuin: url: "https://setup.atuin.sh" @@ -60,15 +60,15 @@ installers: uv: url: "https://astral.sh/uv/install.sh" - sha256: "2206437df06d0fff515d0e95193cfc2f4c2719d4c82f569d70057bbf5c4caba7" + sha256: "ffd252b87a59c4dbf792a4580f8c248130ade93d24a1f15d178815871e864dfc" pt: url: "https://raw.githubusercontent.com/Dicklesworthstone/process_triage/master/install.sh" - sha256: "9bd53fc4f77441b79ed897e1029f962df8e89476653f46064a6dd7702ab232c5" + sha256: "84160d2d429cee0b98f7ed49b58daa09a7848ea08c0fa770ab49efa01e832074" tru: url: "https://raw.githubusercontent.com/Dicklesworthstone/toon_rust/master/install.sh" - sha256: "4996301850402a0e18eb4f0f501e42c32b4871cb7f745e53aaec356180ac4989" + sha256: "19b9e7d1de4509e54c4ba1646fc0d18c5e39e6b2e1c8b6775dbdda6d268dc316" ntm: url: "https://raw.githubusercontent.com/Dicklesworthstone/ntm/main/install.sh" @@ -84,7 +84,7 @@ installers: rch: url: "https://raw.githubusercontent.com/Dicklesworthstone/remote_compilation_helper/master/install.sh" - sha256: "85df586c025260ad3a75a11f44a40314c30f3f6aaeed728d7d76927bb9d1fce4" + sha256: "50bd627e954e9eeadeccaf475432b040f9b6fe3b325d80dbebe86a63fa449030" caam: url: "https://raw.githubusercontent.com/Dicklesworthstone/coding_agent_account_manager/main/install.sh" @@ -108,7 +108,7 @@ installers: jfp: url: "https://jeffreysprompts.com/install-cli.sh" - sha256: "97db629240b4065349e63601f05cd483fcec4b1e16008e986d6eed3c3a0367b7" + sha256: "FETCH_FAILED" rust: url: "https://sh.rustup.rs" diff --git a/docs/manifest-gap-analysis.md b/docs/manifest-gap-analysis.md index 87ef68d5..2927682b 100644 --- a/docs/manifest-gap-analysis.md +++ b/docs/manifest-gap-analysis.md @@ -220,7 +220,7 @@ verify: | Install services-setup.sh | ❌ No | | Install checksums.yaml + VERSION | ❌ No | | Install acfs CLI (doctor.sh) | `acfs.doctor` (partial) | -| Install Claude Git Safety Guard hook | ❌ No | +| Install DCG (Destructive Command Guard) hook | ❌ No | | Create state.json | ❌ No (orchestration) | **Gap:** Most finalize actions are orchestration-level, not module-level. @@ -242,7 +242,7 @@ These are things the installer does that the manifest doesn't specify: 9. **Tmux configuration** linking 10. **ACFS scripts/lib/** installation 11. **acfs-update wrapper** installation -12. **Claude Git Safety Guard** hook installation +12. **DCG (Destructive Command Guard)** hook installation 13. **State file** creation and management 14. **Smoke test** verification diff --git a/docs/tui-research.md b/docs/tui-research.md index 40725cdd..f85b617f 100644 --- a/docs/tui-research.md +++ b/docs/tui-research.md @@ -106,7 +106,7 @@ declare -A WIZARD_STATE=( [project_name]="" [project_dir]="" [tech_stack]="" # Space-separated: "nodejs typescript docker" - [enable_bd]="true" + [enable_br]="true" [enable_claude]="true" [enable_agents]="true" [enable_ubsignore]="true" @@ -239,7 +239,7 @@ select_features() { gum choose --no-limit \ --cursor.foreground "$ACFS_ACCENT" \ --selected.foreground "$ACFS_SUCCESS" \ - "Beads issue tracking (bd)" \ + "Beads issue tracking (br)" \ "Claude Code settings" \ "AGENTS.md template" \ "UBS ignore patterns" diff --git a/docs/tui-wizard-design.md b/docs/tui-wizard-design.md index 6e3cf74a..55ba39a7 100644 --- a/docs/tui-wizard-design.md +++ b/docs/tui-wizard-design.md @@ -87,7 +87,7 @@ Introduce the wizard and set expectations. │ │ │ This wizard will help you create a new project with: │ │ • Git repository with .gitignore │ -│ • Beads issue tracking (bd) │ +│ • Beads issue tracking (br) │ │ • Claude Code settings │ │ • AGENTS.md tailored to your tech stack │ │ • UBS ignore patterns │ @@ -290,7 +290,7 @@ Select which ACFS features to enable. Which features do you want to enable? - [✓] Beads issue tracking (bd) + [✓] Beads issue tracking (br) Track work items with dependencies [✓] Claude Code settings @@ -309,7 +309,7 @@ Select which ACFS features to enable. ``` ### State Changes -- Sets `WIZARD_STATE[enable_bd]` +- Sets `WIZARD_STATE[enable_br]` - Sets `WIZARD_STATE[enable_claude]` - Sets `WIZARD_STATE[enable_agents]` - Sets `WIZARD_STATE[enable_ubsignore]` @@ -385,7 +385,7 @@ Review all choices before creating the project. │ Tech Stack: Node.js, TypeScript, Docker │ │ │ │ Features: │ - │ ✓ Beads (bd) │ + │ ✓ Beads (br) │ │ ✓ Claude Code settings │ │ ✓ AGENTS.md │ │ ✓ UBS ignore patterns │ @@ -436,7 +436,7 @@ Show creation progress with status indicators. ✓ Initializing git repository ✓ Creating .gitignore ✓ Creating .ubsignore - ⠋ Initializing beads (bd)... + ⠋ Initializing beads (br)... ○ Creating Claude settings ○ Generating AGENTS.md ○ Creating README.md @@ -451,8 +451,8 @@ Show creation progress with status indicators. ``` ✓ Creating directory ✓ Initializing git repository - ✖ Initializing beads (bd) - Error: bd command not found + ✖ Initializing beads (br) + Error: br command not found ┌─────────────────────────────────────────────────────────────┐ │ Some steps failed. What would you like to do? │ @@ -497,7 +497,7 @@ Celebrate completion and show next steps. Next steps: cd /data/projects/my-awesome-project claude . # Start Claude Code - bd ready # Check available work + br ready # Check available work ► Open in Claude Code Exit @@ -525,7 +525,7 @@ Celebrate completion and show next steps. │ │ WIZARD_STATE[project_name] = "" │ │ │ │ WIZARD_STATE[project_dir] = "" │ │ │ │ WIZARD_STATE[tech_stack] = "" │ │ -│ │ WIZARD_STATE[enable_bd] = "true" │ │ +│ │ WIZARD_STATE[enable_br] = "true" │ │ │ │ WIZARD_STATE[enable_claude] = "true" │ │ │ │ WIZARD_STATE[enable_agents] = "true" │ │ │ │ WIZARD_STATE[enable_ubsignore] = "true" │ │ @@ -595,13 +595,13 @@ Options: - Exit ``` -### 3. bd init fails +### 3. br init fails ``` -bd init fails +br init fails │ ▼ Show warning (not fatal): - "bd initialization failed. You can run 'bd init' later." + "br initialization failed. You can run 'br init' later." │ ▼ Continue with remaining steps (graceful degradation) @@ -688,7 +688,7 @@ WIZARD_STATE=( [project_name]="my-awesome-project" [project_dir]="/data/projects/my-awesome-project" [tech_stack]="nodejs typescript docker" - [enable_bd]="true" + [enable_br]="true" [enable_claude]="true" [enable_agents]="true" [enable_ubsignore]="true" diff --git a/install.sh b/install.sh index b19029a4..f0464c43 100755 --- a/install.sh +++ b/install.sh @@ -129,10 +129,12 @@ SKIP_UBUNTU_UPGRADE=false TARGET_UBUNTU_VERSION="25.10" # Target user configuration -# Default: install for the "ubuntu" user (typical VPS images). -# Advanced: override with env vars (see README): -# TARGET_USER=myuser TARGET_HOME=/home/myuser ... -TARGET_USER="${TARGET_USER:-ubuntu}" +# Default: detect the current user (or SUDO_USER if running under sudo). +# Override with env var: TARGET_USER=myuser +# Note: Previously defaulted to "ubuntu" which broke non-ubuntu VPS installs. +_ACFS_DETECTED_USER="${SUDO_USER:-$(whoami)}" +TARGET_USER="${TARGET_USER:-$_ACFS_DETECTED_USER}" +unset _ACFS_DETECTED_USER # Leave TARGET_HOME unset by default; init_target_paths will derive it from: # - $HOME when running as TARGET_USER # - /home/$TARGET_USER otherwise @@ -651,7 +653,9 @@ acfs_log_init() { # If tee logging fails, we fall back to simple file redirection. local tee_logging_ok=false if command -v tee >/dev/null 2>&1; then - # Test if process substitution works before committing to it + # Test if process substitution works before committing to it. + # On bash 5.3+, bare `exec` under set -e can exit the script + # before `if` catches the failure, so we test in a subshell. # shellcheck disable=SC2261 if (exec 3>&1; echo test > >(cat >/dev/null)) 2>/dev/null; then # Process substitution works - set up tee logging @@ -659,8 +663,9 @@ acfs_log_init() { exec 3>&2 || true # Now redirect stderr to tee (which sends to both log and original stderr) # shellcheck disable=SC2261 - if exec 2> >(tee -a "$ACFS_LOG_FILE" >&3); then - tee_logging_ok=true + # Use subshell test first to prevent exec from exiting under bash 5.3+ + if (set +e; exec 2> >(tee -a "$ACFS_LOG_FILE" >&3)) 2>/dev/null; then + exec 2> >(tee -a "$ACFS_LOG_FILE" >&3) && tee_logging_ok=true fi fi fi @@ -950,6 +955,14 @@ cleanup() { log_error "" # Emit failure summary (best-effort) acfs_summary_emit "failure" 0 2>/dev/null || true + # Send webhook notification for failure (bd-2zqr) + if type -t webhook_notify &>/dev/null; then + webhook_notify "failure" "${ACFS_SUMMARY_FILE:-}" 2>/dev/null || true + fi + # Send ntfy.sh notification for failure (bd-2igt6) + if type -t acfs_notify_install_failure &>/dev/null; then + acfs_notify_install_failure 2>/dev/null || true + fi fi # Finalize log file (restore stderr, strip colors, add footer) acfs_log_close 2>/dev/null || true @@ -1126,6 +1139,20 @@ parse_args() { NO_DEPS=true shift ;; + --webhook|--webhook=*) + # Webhook URL for install completion notification (bd-2zqr) + if [[ "$1" == "--webhook" ]]; then + if [[ -z "${2:-}" ]]; then + log_fatal "--webhook requires a URL (e.g., --webhook https://hooks.slack.com/...)" + fi + export ACFS_WEBHOOK_URL="$2" + shift 2 + else + # Handle --webhook=https://... format + export ACFS_WEBHOOK_URL="${1#*=}" + shift + fi + ;; *) log_warn "Unknown option: $1" shift @@ -1280,7 +1307,9 @@ detect_environment() { ACFS_CHECKSUMS_YAML="$SCRIPT_DIR/checksums.yaml" ACFS_MANIFEST_YAML="$SCRIPT_DIR/acfs.manifest.yaml" else - # Fallback: current directory + # Fallback: current directory (only valid for testing from repo root) + # This should NOT be reached in curl-pipe mode since bootstrap_repo_archive + # sets ACFS_BOOTSTRAP_DIR. If we reach here without SCRIPT_DIR, something is wrong. ACFS_LIB_DIR="./scripts/lib" ACFS_GENERATED_DIR="./scripts/generated" ACFS_ASSETS_DIR="./acfs" @@ -1290,12 +1319,80 @@ detect_environment() { export ACFS_LIB_DIR ACFS_GENERATED_DIR ACFS_ASSETS_DIR ACFS_CHECKSUMS_YAML ACFS_MANIFEST_YAML + # Validate that library directory exists - if not, fail early with a clear message + if [[ ! -d "$ACFS_LIB_DIR" ]]; then + local abs_lib_dir="$ACFS_LIB_DIR" + # Try to show absolute path for better debugging + if [[ "$ACFS_LIB_DIR" == ./* ]]; then + abs_lib_dir="$(pwd)/${ACFS_LIB_DIR#./}" + fi + echo "ERROR: Library directory not found: $abs_lib_dir" >&2 + echo "This typically means bootstrap failed or the script is being run from an unexpected location." >&2 + echo "For curl|bash installation, ensure network connectivity to GitHub." >&2 + echo "For local installation, run from the repository root directory." >&2 + exit 1 + fi + # Source minimal libs in correct order (logging, then helpers) if [[ -f "$ACFS_LIB_DIR/logging.sh" ]]; then # shellcheck source=scripts/lib/logging.sh source "$ACFS_LIB_DIR/logging.sh" fi + # Verify internal script integrity before sourcing (bd-3tpl.5) + # Fail-closed: abort if any tracked script has been modified. + # Gracefully skips if checksums file is missing (pre-migration compat). + if [[ -f "$ACFS_GENERATED_DIR/internal_checksums.sh" ]]; then + # shellcheck source=scripts/generated/internal_checksums.sh + source "$ACFS_GENERATED_DIR/internal_checksums.sh" + if declare -p ACFS_INTERNAL_CHECKSUMS &>/dev/null; then + local _ics_base + if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]]; then + _ics_base="$ACFS_BOOTSTRAP_DIR" + elif [[ -n "${SCRIPT_DIR:-}" ]]; then + _ics_base="$SCRIPT_DIR" + else + _ics_base="." + fi + local _ics_fail=0 + for _ics_path in "${!ACFS_INTERNAL_CHECKSUMS[@]}"; do + local _ics_expected="${ACFS_INTERNAL_CHECKSUMS[$_ics_path]}" + local _ics_file="$_ics_base/$_ics_path" + if [[ -f "$_ics_file" ]]; then + local _ics_actual + _ics_actual=$(sha256sum "$_ics_file" | awk '{print $1}') + if [[ "$_ics_actual" != "$_ics_expected" ]]; then + _ics_fail=$((_ics_fail + 1)) + if declare -f log_error &>/dev/null; then + log_error "INTEGRITY: $_ics_path checksum mismatch (expected ${_ics_expected:0:12}… got ${_ics_actual:0:12}…)" + else + echo "ERROR: INTEGRITY: $_ics_path checksum mismatch" >&2 + fi + fi + else + _ics_fail=$((_ics_fail + 1)) + if declare -f log_error &>/dev/null; then + log_error "INTEGRITY: $_ics_path missing (expected checksum ${_ics_expected:0:12}…)" + else + echo "ERROR: INTEGRITY: $_ics_path missing" >&2 + fi + fi + done + if [[ "$_ics_fail" -gt 0 ]]; then + local _msg="Internal script integrity check failed: $_ics_fail file(s) modified. Run 'bun run generate' to regenerate checksums." + if declare -f log_error &>/dev/null; then + log_error "$_msg" + else + echo "ERROR: $_msg" >&2 + fi + exit 1 + fi + if declare -f log_success &>/dev/null; then + log_success "Internal script integrity verified (${ACFS_INTERNAL_CHECKSUMS_COUNT:-?} scripts)" + fi + fi + fi + if [[ -f "$ACFS_LIB_DIR/security.sh" ]]; then # shellcheck source=scripts/lib/security.sh source "$ACFS_LIB_DIR/security.sh" @@ -1368,6 +1465,17 @@ detect_environment() { source "$ACFS_LIB_DIR/autofix_existing.sh" fi + # Source webhook notification library (bd-2zqr) + if [[ -f "$ACFS_LIB_DIR/webhook.sh" ]]; then + # shellcheck source=scripts/lib/webhook.sh + source "$ACFS_LIB_DIR/webhook.sh" + fi + # Source ntfy.sh notification library (bd-2igt6) + if [[ -f "$ACFS_LIB_DIR/notify.sh" ]]; then + # shellcheck source=scripts/lib/notify.sh + source "$ACFS_LIB_DIR/notify.sh" + fi + # Source manifest index (data-only, safe to source) if [[ -f "$ACFS_GENERATED_DIR/manifest_index.sh" ]]; then # shellcheck source=scripts/generated/manifest_index.sh @@ -1748,9 +1856,9 @@ acfs_curl_with_retry() { if acfs_curl -o "$output_path" "$url"; then return 0 + else + exit_code=$? fi - - exit_code=$? if ! acfs_is_retryable_curl_exit_code "$exit_code"; then return "$exit_code" fi @@ -2578,10 +2686,18 @@ acfs_chown_tree() { fi # GNU coreutils: -h = do not dereference symlinks; -R = recursive. - if ! $SUDO chown -hR "$owner_group" "$resolved"; then - log_error "acfs_chown_tree: chown failed for $resolved" - return 1 - fi + # Transient files (SSH control sockets, etc.) may vanish during the + # recursive walk of a live home directory. Only fail on non-transient errors. + local _chown_err="" + _chown_err=$($SUDO chown -hR "$owner_group" "$resolved" 2>&1) || { + local _real_err + _real_err=$(printf '%s\n' "$_chown_err" | grep -v "No such file or directory" || true) + if [[ -n "$_real_err" ]]; then + log_error "acfs_chown_tree: chown failed for $resolved" + return 1 + fi + log_detail "acfs_chown_tree: transient file warnings during chown (safe to ignore)" + } } confirm_or_exit() { @@ -2611,8 +2727,8 @@ confirm_or_exit() { # Set up target-specific paths # Must be called after ensure_root init_target_paths() { - # If running as ubuntu, use ubuntu's home - # If running as root, install for ubuntu user + # If running as the target user, use their $HOME directly. + # If running as root (or another user), derive TARGET_HOME from TARGET_USER. if [[ "$(whoami)" == "$TARGET_USER" ]]; then TARGET_HOME="${TARGET_HOME:-$HOME}" else @@ -2628,8 +2744,8 @@ init_target_paths() { fi # ACFS directories for target user - ACFS_HOME="$TARGET_HOME/.acfs" - ACFS_STATE_FILE="$ACFS_HOME/state.json" + ACFS_HOME="${ACFS_HOME:-$TARGET_HOME/.acfs}" + ACFS_STATE_FILE="${ACFS_STATE_FILE:-$ACFS_HOME/state.json}" # Basic hardening: refuse to use a symlinked ACFS_HOME when running with # elevated privileges (prevents clobbering arbitrary paths via symlink tricks). @@ -2862,7 +2978,13 @@ run_ubuntu_upgrade_phase() { log_info "Automatically rebooting to clear pending updates..." # Initialize state file early for tracking - mkdir -p "${ACFS_RESUME_DIR:-/var/lib/acfs}" + # Try without sudo first, fall back to sudo for system directories + if ! mkdir -p "${ACFS_RESUME_DIR:-/var/lib/acfs}" 2>/dev/null; then + if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then + sudo mkdir -p "${ACFS_RESUME_DIR:-/var/lib/acfs}" + sudo chown "$(id -u):$(id -g)" "${ACFS_RESUME_DIR:-/var/lib/acfs}" 2>/dev/null || true + fi + fi if type -t state_ensure_valid &>/dev/null; then state_ensure_valid || true fi @@ -3081,8 +3203,13 @@ normalize_user() { # Print password for the operator (important for safe mode) echo "" >&2 - log_warn "Generated password for '$TARGET_USER': $user_password" - log_warn "Save this password! You may need it for sudo access (safe mode)." + if declare -f log_sensitive >/dev/null; then + log_sensitive "Generated password for '$TARGET_USER': $user_password" + log_sensitive "Save this password! You may need it for sudo access (safe mode)." + else + log_warn "Generated password for '$TARGET_USER': $user_password" + log_warn "Save this password! You may need it for sudo access (safe mode)." + fi echo "" >&2 else log_warn "Failed to generate password for $TARGET_USER" @@ -3494,12 +3621,17 @@ install_cli_tools() { try_step "Configuring git-lfs" run_as_target git lfs install --skip-repo || true fi - # Install optional apt packages individually to prevent one failure from blocking others + # Install optional apt packages - batch install for speed (14→1 apt-get calls) log_detail "Installing optional apt packages" local optional_pkgs=(lsd eza bat fd-find btop dust neovim htop tree ncdu httpie entr mtr pv docker.io docker-compose-plugin) - for pkg in "${optional_pkgs[@]}"; do - $SUDO apt-get install -y "$pkg" >/dev/null 2>&1 || log_detail "$pkg not available (optional)" - done + # First attempt: batch install all at once (fastest path) + if ! $SUDO apt-get install -y "${optional_pkgs[@]}" >/dev/null 2>&1; then + # Fallback: some packages failed, install individually to get what we can + log_detail "Batch install failed, trying packages individually" + for pkg in "${optional_pkgs[@]}"; do + $SUDO apt-get install -y "$pkg" >/dev/null 2>&1 || log_detail "$pkg not available (optional)" + done + fi # Robust lazygit install (apt or binary fallback) if ! command_exists lazygit; then @@ -3740,13 +3872,21 @@ install_languages_legacy_tools() { try_step "Installing Atuin" acfs_run_verified_upstream_script_as_target "atuin" "sh" || return 1 fi - # Zoxide (install as target user) + # Zoxide - prefer apt to avoid GitHub API rate limits in CI # Check multiple possible locations if [[ -x "$TARGET_HOME/.local/bin/zoxide" ]] || [[ -x "/usr/local/bin/zoxide" ]] || command -v zoxide &>/dev/null; then log_detail "Zoxide already installed" else log_detail "Installing Zoxide for $TARGET_USER" - try_step "Installing Zoxide" acfs_run_verified_upstream_script_as_target "zoxide" "sh" || return 1 + # Prefer apt (avoids GitHub API rate limits), fall back to upstream script + if apt-cache show zoxide &>/dev/null; then + try_step "Installing Zoxide (apt)" $SUDO apt-get install -y zoxide || { + log_detail "apt install failed, falling back to upstream script" + try_step "Installing Zoxide (upstream)" acfs_run_verified_upstream_script_as_target "zoxide" "sh" || return 1 + } + else + try_step "Installing Zoxide" acfs_run_verified_upstream_script_as_target "zoxide" "sh" || return 1 + fi fi } @@ -4459,11 +4599,32 @@ NTM_CONFIG_EOF fi # SLB (Simultaneous Launch Button) + # The upstream install script calls GitHub API for latest version, which hits rate limits in CI. + # We install via .deb package directly to avoid this. if binary_installed "slb"; then log_detail "SLB already installed" else log_detail "Installing SLB" - try_step "Installing SLB" acfs_run_verified_upstream_script_as_target "slb" "bash" || log_warn "SLB installation may have failed" + local slb_version="0.2.0" + local slb_arch="amd64" + [[ "$(uname -m)" == "aarch64" ]] && slb_arch="arm64" + local slb_deb="slb_${slb_version}_linux_${slb_arch}.deb" + local slb_url="https://github.com/Dicklesworthstone/slb/releases/download/v${slb_version}/${slb_deb}" + local slb_tmp + slb_tmp="$(mktemp -d "${TMPDIR:-/tmp}/acfs-slb.XXXXXX" 2>/dev/null)" || slb_tmp="" + if [[ -n "$slb_tmp" ]] && [[ -d "$slb_tmp" ]]; then + if acfs_curl -o "${slb_tmp}/${slb_deb}" "$slb_url" && \ + $SUDO dpkg -i "${slb_tmp}/${slb_deb}"; then + log_success "SLB installed via .deb" + else + log_warn "SLB .deb install failed, trying upstream script" + try_step "Installing SLB (upstream)" acfs_run_verified_upstream_script_as_target "slb" "bash" || log_warn "SLB installation may have failed" + fi + rm -rf "$slb_tmp" + else + log_warn "Failed to create temp directory for SLB, trying upstream script" + try_step "Installing SLB (upstream)" acfs_run_verified_upstream_script_as_target "slb" "bash" || log_warn "SLB installation may have failed" + fi fi # RU (Repo Updater) @@ -4585,6 +4746,9 @@ finalize() { try_step "Installing continue.sh" install_asset "scripts/lib/continue.sh" "$ACFS_HOME/scripts/lib/continue.sh" || return 1 try_step "Installing info.sh" install_asset "scripts/lib/info.sh" "$ACFS_HOME/scripts/lib/info.sh" || return 1 try_step "Installing cheatsheet.sh" install_asset "scripts/lib/cheatsheet.sh" "$ACFS_HOME/scripts/lib/cheatsheet.sh" || return 1 + try_step "Installing webhook.sh" install_asset "scripts/lib/webhook.sh" "$ACFS_HOME/scripts/lib/webhook.sh" || return 1 + try_step "Installing notify.sh" install_asset "scripts/lib/notify.sh" "$ACFS_HOME/scripts/lib/notify.sh" || return 1 + try_step "Installing notifications.sh" install_asset "scripts/lib/notifications.sh" "$ACFS_HOME/scripts/lib/notifications.sh" || return 1 try_step "Installing dashboard.sh" install_asset "scripts/lib/dashboard.sh" "$ACFS_HOME/scripts/lib/dashboard.sh" || return 1 # Install acfs-update wrapper command @@ -4593,6 +4757,17 @@ finalize() { try_step "Setting acfs-update ownership" $SUDO chown "$TARGET_USER:$TARGET_USER" "$ACFS_HOME/bin/acfs-update" || return 1 try_step "Linking acfs-update command" run_as_target ln -sf "$ACFS_HOME/bin/acfs-update" "$TARGET_HOME/.local/bin/acfs-update" || return 1 + # Install root AGENTS.md generator (if available) and generate /AGENTS.md once + if [[ -n "${SCRIPT_DIR:-}" ]] && [[ -f "$SCRIPT_DIR/scripts/generate-root-agents-md.sh" ]]; then + try_step "Installing flywheel-update-agents-md" install_asset "scripts/generate-root-agents-md.sh" "$ACFS_HOME/bin/flywheel-update-agents-md" || return 1 + try_step "Setting flywheel-update-agents-md permissions" $SUDO chmod 755 "$ACFS_HOME/bin/flywheel-update-agents-md" || return 1 + try_step "Setting flywheel-update-agents-md ownership" $SUDO chown "$TARGET_USER:$TARGET_USER" "$ACFS_HOME/bin/flywheel-update-agents-md" || return 1 + try_step "Linking flywheel-update-agents-md command" $SUDO ln -sf "$ACFS_HOME/bin/flywheel-update-agents-md" "/usr/local/bin/flywheel-update-agents-md" || return 1 + try_step "Generating /AGENTS.md" $SUDO /usr/local/bin/flywheel-update-agents-md || true + else + log_warn "Root AGENTS.md generator not found; skipping /AGENTS.md generation" + fi + # Install services-setup wizard try_step "Installing services-setup.sh" install_asset "scripts/services-setup.sh" "$ACFS_HOME/scripts/services-setup.sh" || return 1 try_step "Setting scripts permissions" $SUDO chmod 755 "$ACFS_HOME/scripts/services-setup.sh" || return 1 @@ -5076,7 +5251,19 @@ main() { ACFS_RAW="https://raw.githubusercontent.com/${ACFS_REPO_OWNER}/${ACFS_REPO_NAME}/${ACFS_REF}" export ACFS_REF ACFS_RAW fi - bootstrap_repo_archive + # Download and extract the repo archive for curl-pipe mode. + # This sets ACFS_BOOTSTRAP_DIR and related paths. If it fails, we cannot continue + # because the library files (install_helpers.sh, etc.) won't be available. + if ! bootstrap_repo_archive; then + log_error "Bootstrap failed. Cannot continue without library files." + log_error "Try again, or run from a local checkout instead of curl|bash." + exit 1 + fi + # Verify bootstrap succeeded - ACFS_BOOTSTRAP_DIR must be set for curl-pipe mode + if [[ -z "${ACFS_BOOTSTRAP_DIR:-}" ]]; then + log_error "Bootstrap did not set ACFS_BOOTSTRAP_DIR. This is a bug." + exit 1 + fi fi # Detect environment and source manifest index (mjt.5.3) @@ -5091,12 +5278,15 @@ main() { local _acfs_lock_dir="${ACFS_HOME:-$HOME/.acfs}" mkdir -p "$_acfs_lock_dir" 2>/dev/null || true local _acfs_lock_file="$_acfs_lock_dir/.install.lock" - # NOTE: exec with high FDs can fail on some bash versions (5.3+). - # We try FD 199, then 198 as fallback, and skip locking if both fail. + # NOTE: On bash 5.3+, `exec N>file` under set -e exits the script + # before `if` can catch the failure. We test in a subshell first, + # then only exec in the main shell if the subshell succeeded. local _acfs_lock_fd="" - if exec 199>"$_acfs_lock_file" 2>/dev/null; then + if (exec 199>"$_acfs_lock_file") 2>/dev/null; then + exec 199>"$_acfs_lock_file" _acfs_lock_fd=199 - elif exec 198>"$_acfs_lock_file" 2>/dev/null; then + elif (exec 198>"$_acfs_lock_file") 2>/dev/null; then + exec 198>"$_acfs_lock_file" _acfs_lock_fd=198 fi if [[ -n "$_acfs_lock_fd" ]]; then @@ -5271,7 +5461,7 @@ main() { # ============================================================ # Initialize state file location (uses TARGET_USER's home) ACFS_HOME="${ACFS_HOME:-/home/${TARGET_USER}/.acfs}" - ACFS_STATE_FILE="$ACFS_HOME/state.json" + ACFS_STATE_FILE="${ACFS_STATE_FILE:-$ACFS_HOME/state.json}" export ACFS_HOME ACFS_STATE_FILE # Validate and handle existing state file @@ -5402,6 +5592,15 @@ main() { # Emit install summary JSON (bd-31ps.3.2) acfs_summary_emit "success" "$total_seconds" 2>/dev/null || true + # Send webhook notification if configured (bd-2zqr) + if type -t webhook_notify &>/dev/null; then + webhook_notify "success" "${ACFS_SUMMARY_FILE:-}" 2>/dev/null || true + fi + # Send ntfy.sh notification if configured (bd-2igt6) + if type -t acfs_notify_install_success &>/dev/null; then + acfs_notify_install_success 2>/dev/null || true + fi + SMOKE_TEST_FAILED=false if ! run_smoke_test; then SMOKE_TEST_FAILED=true diff --git a/onboard/docs/ntm/command_palette.md b/onboard/docs/ntm/command_palette.md index aa259299..7139b90d 100644 --- a/onboard/docs/ntm/command_palette.md +++ b/onboard/docs/ntm/command_palette.md @@ -116,7 +116,7 @@ Pick the next bead you can actually do usefully now and start coding on it immed OK, so start systematically and methodically and meticulously and diligently executing those remaining beads tasks that you created in the optimal logical order! Don't forget to mark beads as you work on them. ### do_all_of_it | Do All Of It -OK, please do ALL of that now. Track work via bd beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work. +OK, please do ALL of that now. Track work via br beads (no markdown TODO lists): create/claim/update/close beads as you go so nothing gets lost, and keep communicating via Agent Mail when you start/finish work. ## Git & Operations diff --git a/packages/manifest/package.json b/packages/manifest/package.json index 4960a5e8..31fa8f9a 100644 --- a/packages/manifest/package.json +++ b/packages/manifest/package.json @@ -31,8 +31,8 @@ "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2" + "@types/node": "^22.19.11", + "typescript": "^5.9.3" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/packages/manifest/src/generate.ts b/packages/manifest/src/generate.ts index 6135216b..f092ba74 100644 --- a/packages/manifest/src/generate.ts +++ b/packages/manifest/src/generate.ts @@ -142,6 +142,38 @@ const MANIFEST_INDEX_HEADER = `#!/usr/bin/env bash # Data-only manifest index. Safe to source. `; +const INTERNAL_CHECKSUMS_HEADER = `#!/usr/bin/env bash +# shellcheck disable=SC2034 +# ============================================================ +# AUTO-GENERATED internal script checksums - DO NOT EDIT +# Regenerate: bun run generate (from packages/manifest) +# ============================================================ +# SHA256 checksums for critical internal scripts (bd-3tpl). +# Used by check-manifest-drift.sh to detect unauthorized changes. +`; + +/** + * Critical internal scripts that should be checksummed. + * Paths are relative to PROJECT_ROOT. + */ +const INTERNAL_SCRIPTS_TO_CHECKSUM = [ + 'scripts/lib/security.sh', + 'scripts/lib/agents.sh', + 'scripts/lib/update.sh', + 'scripts/lib/doctor.sh', + 'scripts/lib/install_helpers.sh', + 'scripts/lib/logging.sh', + 'scripts/lib/state.sh', + 'scripts/lib/session.sh', + 'scripts/lib/os_detect.sh', + 'scripts/lib/errors.sh', + 'scripts/lib/user.sh', + 'scripts/lib/tools.sh', + 'scripts/lib/export-config.sh', + 'scripts/acfs-global', + 'scripts/acfs-update', +] as const; + // ============================================================ // Security Constants // ============================================================ @@ -912,6 +944,34 @@ function generateManifestIndex(manifest: Manifest, manifestSha256: string): stri lines.push(')'); lines.push(''); + // Module descriptions for progress display (bd-21kh) + lines.push('declare -gA ACFS_MODULE_DESC=('); + for (const module of orderedModules) { + lines.push(` [${module.id}]="${escapeBash(module.description || module.id)}"`); + } + lines.push(')'); + lines.push(''); + + // Installed check commands for skip-if-present logic (bd-1eop) + lines.push('declare -gA ACFS_MODULE_INSTALLED_CHECK=('); + for (const module of orderedModules) { + if (module.installed_check?.command) { + lines.push(` [${module.id}]="${escapeBash(module.installed_check.command)}"`); + } + } + lines.push(')'); + lines.push(''); + + // Installed check run_as context (bd-1eop) + lines.push('declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=('); + for (const module of orderedModules) { + if (module.installed_check?.run_as) { + lines.push(` [${module.id}]="${escapeBash(module.installed_check.run_as)}"`); + } + } + lines.push(')'); + lines.push(''); + // Mark that the index is fully loaded (used by acfs_resolve_selection) lines.push('ACFS_MANIFEST_INDEX_LOADED=true'); lines.push(''); @@ -919,6 +979,34 @@ function generateManifestIndex(manifest: Manifest, manifestSha256: string): stri return lines.join('\n'); } +/** + * Generate internal script checksums file (bd-3tpl). + * Computes SHA256 for critical internal scripts and emits a bash associative array. + */ +function generateInternalChecksums(): string { + const lines: string[] = [INTERNAL_CHECKSUMS_HEADER]; + + lines.push('declare -gA ACFS_INTERNAL_CHECKSUMS=('); + for (const relPath of INTERNAL_SCRIPTS_TO_CHECKSUM) { + const absPath = join(PROJECT_ROOT, relPath); + if (existsSync(absPath)) { + const content = readFileSync(absPath); + const hash = createHash('sha256').update(content).digest('hex'); + lines.push(` [${relPath}]="${hash}"`); + } else { + lines.push(` # MISSING: ${relPath}`); + } + } + lines.push(')'); + lines.push(''); + + lines.push(`ACFS_INTERNAL_CHECKSUMS_COUNT=${INTERNAL_SCRIPTS_TO_CHECKSUM.length}`); + lines.push(`ACFS_INTERNAL_CHECKSUMS_GENERATED="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)"`); + lines.push(''); + + return lines.join('\n'); +} + /** * Generate a category install script */ @@ -1009,7 +1097,8 @@ function generateDoctorChecks(manifest: Manifest): string { for (let i = 0; i < module.verify.length; i++) { const verify = module.verify[i]; - const isOptional = /\|\|\s*true\s*$/.test(verify); + // Module is optional if: the module itself is marked optional OR the command ends with || true + const isOptional = module.optional || /\|\|\s*true\s*$/.test(verify); const cleanCmd = verify.replace(/\s*\|\|\s*true\s*$/, '').trim(); const suffix = module.verify.length > 1 ? `.${i + 1}` : ''; const description = escapeBash(module.description); @@ -1040,14 +1129,15 @@ function generateDoctorChecks(manifest: Manifest): string { // Run the command string in a fresh bash so quoted commands remain intact. // Use `bash -o pipefail -c "$cmd"` (NOT `bash -c "… $cmd"`) to avoid breaking // when `$cmd` itself contains quotes. + // Use ${ACFS_*-default} to respect NO_COLOR (empty preserves empty). Related: bd-39ye lines.push(' if bash -o pipefail -c "$cmd" &>/dev/null; then'); - lines.push(' echo -e "\\033[0;32m[ok]\\033[0m $id - $desc"'); + lines.push(' echo -e "${ACFS_GREEN-\\033[0;32m}[ok]${ACFS_NC-\\033[0m} $id - $desc"'); lines.push(' ((passed += 1))'); lines.push(' elif [[ "$optional" = "optional" ]]; then'); - lines.push(' echo -e "\\033[0;33m[skip]\\033[0m $id - $desc"'); + lines.push(' echo -e "${ACFS_YELLOW-\\033[0;33m}[skip]${ACFS_NC-\\033[0m} $id - $desc"'); lines.push(' ((skipped += 1))'); lines.push(' else'); - lines.push(' echo -e "\\033[0;31m[fail]\\033[0m $id - $desc"'); + lines.push(' echo -e "${ACFS_RED-\\033[0;31m}[fail]${ACFS_NC-\\033[0m} $id - $desc"'); lines.push(' ((failed += 1))'); lines.push(' fi'); lines.push(' done'); @@ -1592,6 +1682,13 @@ async function main(): Promise<void> { filesToGenerate.set(filepath, { content, mode: 0o644 }); } + // Internal script checksums (bd-3tpl) + { + const filepath = join(OUTPUT_DIR, 'internal_checksums.sh'); + const content = generateInternalChecksums(); + filesToGenerate.set(filepath, { content, mode: 0o644 }); + } + // Web data: TypeScript modules for apps/web { const toolsPath = join(WEB_OUTPUT_DIR, 'manifest-tools.ts'); diff --git a/packages/manifest/src/parser.test.ts b/packages/manifest/src/parser.test.ts index 4bdd511a..323d6d95 100644 --- a/packages/manifest/src/parser.test.ts +++ b/packages/manifest/src/parser.test.ts @@ -472,7 +472,7 @@ defaults: modules: - id: agents.claude description: Claude Code - install: ["bun install -g --trust @anthropic-ai/claude-code@stable"] + install: ["bun install -g --trust @anthropic-ai/claude-code@latest"] verify: ["claude --version"] tags: - recommended diff --git a/packages/manifest/src/types.ts b/packages/manifest/src/types.ts index 1218d5d0..c7c4989c 100644 --- a/packages/manifest/src/types.ts +++ b/packages/manifest/src/types.ts @@ -86,7 +86,7 @@ export interface ModuleWebMetadata { stars?: number; /** CLI command name (e.g., "br") */ cli_name?: string; - /** CLI command aliases (e.g., ["bd"]) */ + /** CLI command aliases */ cli_aliases?: string[]; /** CLI usage example (e.g., "br ready --json") */ command_example?: string; diff --git a/packages/onboard/onboard.sh b/packages/onboard/onboard.sh index 70d90049..1e07fd7f 100755 --- a/packages/onboard/onboard.sh +++ b/packages/onboard/onboard.sh @@ -85,6 +85,7 @@ declare -gA LESSON_SUMMARIES=( [8]="Keeping tools updated|Staying current with AI agents|Community resources" [9]="Multi-repo sync with ru sync|AI-driven commits via agent-sweep|Parallel workflow automation" [10]="DCG command safety|Protection packs|Allow-once workflow" + [21]="Single-branch model for agent swarms|File reservations replace branches|Preventing conflicts with Agent Mail" ) # Number of lessons (derived from array length for maintainability) diff --git a/scripts/check-manifest-drift.sh b/scripts/check-manifest-drift.sh new file mode 100755 index 00000000..e84dc6cd --- /dev/null +++ b/scripts/check-manifest-drift.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +# check-manifest-drift.sh - Detect and auto-fix ACFS manifest/script SHA256 drift +# +# This script verifies that scripts/generated/manifest_index.sh has the correct +# SHA256 hash for acfs.manifest.yaml, AND that internal library scripts match +# their recorded checksums in scripts/generated/internal_checksums.sh. +# If drift is detected, it can regenerate all generated scripts, commit, and push. +# +# Usage: +# ./scripts/check-manifest-drift.sh [--fix] [--json] [--quiet] +# +# Options: +# --fix Auto-regenerate, commit, and push if drift detected (default: check only) +# --json Output results as JSON +# --quiet Suppress non-error output +# +# Exit codes: +# 0 No drift (or drift was auto-fixed with --fix) +# 1 Drift detected (check-only mode) +# 2 Auto-fix failed +# 3 Missing prerequisites + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Defaults +FIX_MODE=false +JSON_MODE=false +QUIET=false + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --fix) FIX_MODE=true; shift ;; + --json) JSON_MODE=true; shift ;; + --quiet) QUIET=true; shift ;; + --help|-h) + head -20 "$0" | grep '^#' | sed 's/^# \?//' + exit 0 + ;; + *) echo "Unknown arg: $1" >&2; exit 3 ;; + esac +done + +log() { $QUIET || echo "[manifest-drift] $*" >&2; } +log_error() { echo "[manifest-drift] ERROR: $*" >&2; } + +# Verify prerequisites +MANIFEST="$REPO_ROOT/acfs.manifest.yaml" +INDEX="$REPO_ROOT/scripts/generated/manifest_index.sh" + +if [[ ! -f "$MANIFEST" ]]; then + log_error "Manifest not found: $MANIFEST" + exit 3 +fi +if [[ ! -f "$INDEX" ]]; then + log_error "Generated index not found: $INDEX" + exit 3 +fi + +# Compute actual hash +ACTUAL_SHA256=$(sha256sum "$MANIFEST" | awk '{print $1}') + +# Extract recorded hash from generated index +RECORDED_SHA256=$(grep -oP 'ACFS_MANIFEST_SHA256="\K[a-f0-9]+' "$INDEX" | head -1) + +if [[ -z "$RECORDED_SHA256" ]]; then + log_error "Could not extract ACFS_MANIFEST_SHA256 from $INDEX" + exit 3 +fi + +# Count SHA256 lines (detect duplicate) +SHA_LINE_COUNT=$(grep -c 'ACFS_MANIFEST_SHA256=' "$INDEX" || true) + +# Count modules in manifest vs generated index +MANIFEST_MODULE_COUNT=$(grep -c '^\s*- id:' "$MANIFEST" || true) +INDEX_MODULE_COUNT=$(awk '/^ACFS_MODULES_IN_ORDER=/,/^\)/' "$INDEX" | grep -c '"' || true) + +DRIFT_DETECTED=false +DRIFT_REASONS=() + +if [[ "$ACTUAL_SHA256" != "$RECORDED_SHA256" ]]; then + DRIFT_DETECTED=true + DRIFT_REASONS+=("SHA256 mismatch: actual=$ACTUAL_SHA256 recorded=$RECORDED_SHA256") +fi + +if [[ "$SHA_LINE_COUNT" -gt 1 ]]; then + DRIFT_DETECTED=true + DRIFT_REASONS+=("Duplicate ACFS_MANIFEST_SHA256 lines: $SHA_LINE_COUNT found") +fi + +# ============================================================ +# Internal script checksum verification (bd-3tpl) +# ============================================================ +INTERNAL_CHECKSUMS_FILE="$REPO_ROOT/scripts/generated/internal_checksums.sh" +INTERNAL_DRIFT_COUNT=0 +INTERNAL_DRIFT_FILES=() +INTERNAL_CHECKED=0 + +if [[ -f "$INTERNAL_CHECKSUMS_FILE" ]]; then + # Source the checksums file to get ACFS_INTERNAL_CHECKSUMS associative array + # shellcheck source=generated/internal_checksums.sh + source "$INTERNAL_CHECKSUMS_FILE" + + if declare -p ACFS_INTERNAL_CHECKSUMS &>/dev/null; then + for rel_path in "${!ACFS_INTERNAL_CHECKSUMS[@]}"; do + expected="${ACFS_INTERNAL_CHECKSUMS[$rel_path]}" + abs_path="$REPO_ROOT/$rel_path" + if [[ -f "$abs_path" ]]; then + actual=$(sha256sum "$abs_path" | awk '{print $1}') + INTERNAL_CHECKED=$((INTERNAL_CHECKED + 1)) + if [[ "$actual" != "$expected" ]]; then + INTERNAL_DRIFT_COUNT=$((INTERNAL_DRIFT_COUNT + 1)) + INTERNAL_DRIFT_FILES+=("$rel_path") + DRIFT_DETECTED=true + DRIFT_REASONS+=("Internal script checksum mismatch: $rel_path") + fi + else + INTERNAL_DRIFT_COUNT=$((INTERNAL_DRIFT_COUNT + 1)) + INTERNAL_DRIFT_FILES+=("$rel_path (MISSING)") + DRIFT_DETECTED=true + DRIFT_REASONS+=("Internal script missing: $rel_path") + fi + done + log "Internal checksums: $INTERNAL_CHECKED checked, $INTERNAL_DRIFT_COUNT drifted" + else + log "Warning: ACFS_INTERNAL_CHECKSUMS not defined in $INTERNAL_CHECKSUMS_FILE" + fi +else + log "Internal checksums file not found (pre-migration), skipping" +fi + +# Output results +if $JSON_MODE; then + reasons_json="[]" + if [[ ${#DRIFT_REASONS[@]} -gt 0 ]]; then + reasons_json=$(printf '%s\n' "${DRIFT_REASONS[@]}" | jq -R . | jq -s .) + fi + internal_drift_json="[]" + if [[ ${#INTERNAL_DRIFT_FILES[@]} -gt 0 ]]; then + internal_drift_json=$(printf '%s\n' "${INTERNAL_DRIFT_FILES[@]}" | jq -R . | jq -s .) + fi + jq -nc \ + --argjson drift "$DRIFT_DETECTED" \ + --arg actual "$ACTUAL_SHA256" \ + --arg recorded "$RECORDED_SHA256" \ + --argjson sha_lines "$SHA_LINE_COUNT" \ + --argjson manifest_modules "$MANIFEST_MODULE_COUNT" \ + --argjson index_modules "$INDEX_MODULE_COUNT" \ + --argjson internal_checked "$INTERNAL_CHECKED" \ + --argjson internal_drifted "$INTERNAL_DRIFT_COUNT" \ + --argjson internal_drift_files "$internal_drift_json" \ + --argjson reasons "$reasons_json" \ + '{ + drift_detected: $drift, + manifest: { + actual_sha256: $actual, + recorded_sha256: $recorded, + sha256_line_count: $sha_lines, + manifest_modules: $manifest_modules, + index_modules: $index_modules + }, + internal_scripts: { + checked: $internal_checked, + drifted: $internal_drifted, + drift_files: $internal_drift_files + }, + reasons: $reasons + }' + if ! $FIX_MODE; then + if $DRIFT_DETECTED; then + exit 1 + else + exit 0 + fi + fi +fi + +if ! $DRIFT_DETECTED; then + log "No drift detected. SHA256=$ACTUAL_SHA256 (${INDEX_MODULE_COUNT} modules)" + exit 0 +fi + +# Drift detected +for reason in "${DRIFT_REASONS[@]}"; do + log_error "$reason" +done + +if ! $FIX_MODE; then + log "Drift detected but --fix not specified. Run with --fix to auto-repair." + exit 1 +fi + +# Auto-fix: regenerate, commit, push +log "Auto-fixing manifest drift..." + +# Check prerequisites for fix +if ! command -v bun &>/dev/null; then + log_error "bun not found - cannot regenerate" + exit 2 +fi + +# Regenerate +cd "$REPO_ROOT/packages/manifest" +if ! bun run generate 2>&1; then + log_error "bun run generate failed" + exit 2 +fi + +# Verify manifest fix +NEW_RECORDED=$(grep -oP 'ACFS_MANIFEST_SHA256="\K[a-f0-9]+' "$INDEX" | head -1) +ACTUAL_NOW=$(sha256sum "$MANIFEST" | awk '{print $1}') + +if [[ "$NEW_RECORDED" != "$ACTUAL_NOW" ]]; then + log_error "Regeneration did not fix manifest mismatch! recorded=$NEW_RECORDED actual=$ACTUAL_NOW" + exit 2 +fi + +log "Manifest SHA256 now matches: $ACTUAL_NOW" + +# Verify internal checksums fix (if file was regenerated) +if [[ -f "$INTERNAL_CHECKSUMS_FILE" ]] && [[ "$INTERNAL_DRIFT_COUNT" -gt 0 ]]; then + log "Verifying internal script checksums after regeneration..." + unset ACFS_INTERNAL_CHECKSUMS + source "$INTERNAL_CHECKSUMS_FILE" + post_fix_drift=0 + for rel_path in "${!ACFS_INTERNAL_CHECKSUMS[@]}"; do + expected="${ACFS_INTERNAL_CHECKSUMS[$rel_path]}" + abs_path="$REPO_ROOT/$rel_path" + if [[ -f "$abs_path" ]]; then + actual=$(sha256sum "$abs_path" | awk '{print $1}') + if [[ "$actual" != "$expected" ]]; then + post_fix_drift=$((post_fix_drift + 1)) + log_error "Still drifted after fix: $rel_path" + fi + fi + done + if [[ "$post_fix_drift" -gt 0 ]]; then + log_error "Internal checksum drift persists after regeneration ($post_fix_drift files)" + exit 2 + fi + log "Internal script checksums verified clean after regeneration" +fi + +# Commit and push +cd "$REPO_ROOT" + +if git diff --quiet scripts/generated/; then + log "No changes in generated scripts after regeneration (already up to date)" + exit 0 +fi + +git add scripts/generated/ +git commit -m "$(cat <<'COMMIT_MSG' +fix(manifest): auto-fix SHA256 drift in generated scripts + +Detected by check-manifest-drift.sh (scheduled systemd timer). +Regenerated all scripts via `bun run generate` to sync with current +acfs.manifest.yaml hash. + +Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> +COMMIT_MSG +)" + +# Push (main:master for compat) +if git push origin main:master 2>&1; then + log "Fix committed and pushed successfully." +else + log_error "Push failed - fix committed locally but not pushed" + exit 2 +fi + +exit 0 diff --git a/scripts/completions/_acfs b/scripts/completions/_acfs index 315e773e..b5b19286 100644 --- a/scripts/completions/_acfs +++ b/scripts/completions/_acfs @@ -20,7 +20,7 @@ _acfs() { _arguments \ '-i[Launch TUI wizard for guided project setup]' \ '--interactive[Launch TUI wizard for guided project setup]' \ - '--no-bd[Skip beads (bd) initialization]' \ + '--no-br[Skip beads (br) initialization]' \ '--no-claude[Skip Claude settings creation]' \ '--no-agents[Skip AGENTS.md template creation]' \ '-h[Show help message]' \ @@ -105,7 +105,7 @@ _acfs() { _acfs_commands() { local commands commands=( - 'newproj:Create new project (git, bd, AGENTS.md, Claude settings)' + 'newproj:Create new project (git, br, AGENTS.md, Claude settings)' 'new:Create new project (alias for newproj)' 'services-setup:Configure AI agents and cloud services' 'services:Configure services (alias for services-setup)' diff --git a/scripts/completions/acfs.bash b/scripts/completions/acfs.bash index 2c7901ce..80c7db4c 100644 --- a/scripts/completions/acfs.bash +++ b/scripts/completions/acfs.bash @@ -11,7 +11,7 @@ _acfs_completions() { local commands="newproj new services-setup services setup doctor check session sessions update status continue progress info i cheatsheet cs dashboard dash support-bundle bundle version help" # Subcommand-specific flags - local newproj_flags="-i --interactive --no-bd --no-claude --no-agents -h --help" + local newproj_flags="-i --interactive --no-br --no-claude --no-agents -h --help" local doctor_flags="--json --deep --no-cache --fix --dry-run -h --help" local info_flags="--json --html --minimal" local cheatsheet_flags="--json" diff --git a/scripts/generate-root-agents-md.sh b/scripts/generate-root-agents-md.sh new file mode 100755 index 00000000..a6c3a1ba --- /dev/null +++ b/scripts/generate-root-agents-md.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# generate-root-agents-md.sh - Generate /AGENTS.md for Flywheel VPS +# +# Creates a comprehensive AGENTS.md at /AGENTS.md that documents all +# installed flywheel tools, common workflows, and agent guidelines. +# +# Usage: +# ./scripts/generate-root-agents-md.sh [--output PATH] [--dry-run] +# +# Options: +# --output PATH Write to PATH instead of /AGENTS.md (default: /AGENTS.md) +# --dry-run Print to stdout instead of writing file +# +# Exit codes: +# 0 Success +# 1 Write failed +# 2 Missing prerequisites + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +OUTPUT="/AGENTS.md" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --output) OUTPUT="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + -h|--help) echo "Usage: $0 [--output PATH] [--dry-run]"; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; + esac +done + +TIMESTAMP=$(date -Iseconds) + +# --- Tool version detection --- +get_version() { + local tool="$1" + local path + path=$(command -v "$tool" 2>/dev/null) || { echo "not installed"; return; } + local ver + ver=$("$tool" --version 2>/dev/null | head -1 | grep -oP '[\d]+\.[\d]+\.[\d]+' | head -1) || ver="" + if [[ -n "$ver" ]]; then + echo "$ver" + else + echo "installed (version unknown)" + fi +} + +# --- Build tool table --- +build_tool_table() { + local tools=( + "ntm:Named Tmux Manager:Multi-agent session orchestration (spawn, kill, send, list)" + "br:Beads Rust:Local-first issue tracker with dependency graphs and JSONL sync" + "bv:Beads Viewer:Graph-aware task triage and dependency visualization" + "ru:Repo Updater:Multi-repo git sync, status, and maintenance" + "cass:Coding Agent Session Search:Full-text search across past agent sessions" + "cm:CASS Memory:Procedural memory system for AI coding agents" + "caam:CLI Account Manager:Sub-100ms switching between AI coding CLI accounts" + "slb:Simultaneous Launch Button:Two-person rule for destructive commands" + "dcg:Destructive Command Guard:Safety net for dangerous shell commands" + "ubs:Ultimate Bug Scanner:Automated code review and bug detection" + "ms:Meta Skill:Claude Code skill management and generation" + "pt:Prompt Tools:Prompt engineering utilities" + "apr:APR:Automated PR review and management" + "rch:Remote Compilation Helper:Offload compilation to worker fleet" + ) + + echo "| Tool | Version | Description |" + echo "|------|---------|-------------|" + for entry in "${tools[@]}"; do + IFS=':' read -r cmd name desc <<< "$entry" + local ver + ver=$(get_version "$cmd") + echo "| \`$cmd\` | $ver | $desc |" + done +} + +# --- Generate content --- +generate() { +cat << 'HEADER' +# Flywheel VPS - Agent Guidelines + +> This file documents the tools, workflows, and conventions for AI coding agents +> operating on this VPS. All agents should read this before starting work. + +HEADER + +echo "> Auto-generated: $TIMESTAMP" +echo '> Regenerate: `sudo /data/projects/agentic_coding_flywheel_setup/scripts/generate-root-agents-md.sh`' +echo "" + +cat << 'SECTION1' +## Project Layout + +All projects live under `/data/projects/`. Each project has its own git repo, +AGENTS.md, and `.beads/` directory for local issue tracking. + +``` +/data/projects/ + ntm/ # Named Tmux Manager (Go) + beads_rust/ # Issue tracker CLI (Rust) + coding_agent_session_search/ # Session search (Rust) + agentic_coding_flywheel_setup/ # VPS setup & scripts (Bash/TS) + mcp_agent_mail/ # Agent coordination server (Python) + ... +``` + +## Installed Tools + +SECTION1 + +build_tool_table +echo "" + +cat << 'SECTION2' +## Common Workflows + +### Starting Work on a Project + +```bash +cd /data/projects/PROJECT_NAME +cat AGENTS.md # Read project-specific guidelines +br list --status open # See open tasks +br show BEAD_ID # Get task details +br update BEAD_ID --status in_progress # Claim task +``` + +### Multi-Agent Session Management + +```bash +ntm spawn PROJECT [--label LABEL] [--cc N] # Start agent session +ntm list [--project PROJECT] # List sessions +ntm send SESSION "message" # Send to session +ntm kill SESSION # Kill session +``` + +### Issue Tracking with Beads + +```bash +br list --status open --status in_progress # Active work +br show BEAD_ID # Full details +br update BEAD_ID --status in_progress # Start working +br close BEAD_ID --reason "description" # Complete task +br sync --flush-only # Export to JSONL +``` + +### Multi-Repo Maintenance + +```bash +ru status # Check all repos +ru sync -j4 # Pull all repos in parallel +``` + +### Session Archaeology + +```bash +cass search "keyword" # Search past sessions +cm recall TOPIC # Recall procedural memory +``` + +## Agent Coordination + +Agents coordinate via **Agent Mail** (MCP server). Key concepts: +- Each agent registers with a project and gets a unique identity +- Messages are sent between named agents within a project +- File reservations prevent edit conflicts on shared files + +## Safety Rules + +1. **Never force-push to main/master** without explicit user approval +2. **Never commit secrets** (.env, *.key, credentials.json) +3. **Use `dcg`** — destructive commands are guarded automatically +4. **Read AGENTS.md** in each project before making changes +5. **Mark beads** as you work on them (in_progress -> closed) +6. **Run tests** before committing (go test, cargo test, etc.) + +## Git Conventions + +- Commit messages: `type(scope): description` (feat, fix, chore, docs, test, refactor) +- Always include: `Co-Authored-By: Claude <noreply@anthropic.com>` +- Push after committing — don't leave unpushed work +- Never amend published commits + +SECTION2 +} + +# --- Output --- +if $DRY_RUN; then + generate + exit 0 +fi + +content=$(generate) + +if [[ "$OUTPUT" == "/AGENTS.md" ]]; then + # Writing to root requires sudo + echo "$content" | sudo tee "$OUTPUT" > /dev/null + sudo chmod 644 "$OUTPUT" + sudo chown root:root "$OUTPUT" +else + echo "$content" > "$OUTPUT" +fi + +echo "Generated $OUTPUT ($(echo "$content" | wc -l) lines)" diff --git a/scripts/generated/doctor_checks.sh b/scripts/generated/doctor_checks.sh index 27f318c6..645dc3a9 100755 --- a/scripts/generated/doctor_checks.sh +++ b/scripts/generated/doctor_checks.sh @@ -126,8 +126,8 @@ declare -a MANIFEST_CHECKS=( "tools.lazydocker Lazydocker (binary install) lazydocker --version required" "network.tailscale.1 Zero-config mesh VPN for secure remote VPS access tailscale version required" "network.tailscale.2 Zero-config mesh VPN for secure remote VPS access systemctl is-enabled tailscaled required" - "network.ssh_keepalive.1 Configure SSH server keepalive to prevent VPN/NAT disconnects grep -E '^ClientAliveInterval[[:space:]]+60' /etc/ssh/sshd_config required" - "network.ssh_keepalive.2 Configure SSH server keepalive to prevent VPN/NAT disconnects grep -E '^ClientAliveCountMax[[:space:]]+3' /etc/ssh/sshd_config required" + "network.ssh_keepalive.1 Configure SSH server keepalive to prevent VPN/NAT disconnects grep -E '^ClientAliveInterval[[:space:]]+60' /etc/ssh/sshd_config optional" + "network.ssh_keepalive.2 Configure SSH server keepalive to prevent VPN/NAT disconnects grep -E '^ClientAliveCountMax[[:space:]]+3' /etc/ssh/sshd_config optional" "lang.bun Bun runtime for JS tooling and global CLIs ~/.bun/bin/bun --version required" "lang.uv uv Python tooling (fast venvs) ~/.local/bin/uv --version required" "lang.rust.1 Rust nightly + cargo ~/.cargo/bin/cargo --version required" @@ -140,21 +140,21 @@ declare -a MANIFEST_CHECKS=( "agents.claude Claude Code ~/.local/bin/claude --version || ~/.local/bin/claude --help required" "agents.codex OpenAI Codex CLI ~/.local/bin/codex --version || ~/.local/bin/codex --help required" "agents.gemini Google Gemini CLI ~/.local/bin/gemini --version || ~/.local/bin/gemini --help required" - "tools.vault HashiCorp Vault CLI vault --version required" - "db.postgres18.1 PostgreSQL 18 psql --version required" + "tools.vault HashiCorp Vault CLI vault --version optional" + "db.postgres18.1 PostgreSQL 18 psql --version optional" "db.postgres18.2 PostgreSQL 18 systemctl status postgresql --no-pager optional" - "cloud.wrangler Cloudflare Wrangler CLI wrangler --version required" - "cloud.supabase Supabase CLI supabase --version required" - "cloud.vercel Vercel CLI vercel --version required" + "cloud.wrangler Cloudflare Wrangler CLI wrangler --version optional" + "cloud.supabase Supabase CLI supabase --version optional" + "cloud.vercel Vercel CLI vercel --version optional" "stack.ntm Named tmux manager (agent cockpit) ntm --help required" "stack.mcp_agent_mail Like gmail for coding agents; MCP HTTP server + token; installs beads tools command -v am required" "stack.meta_skill.1 Local-first knowledge management with hybrid semantic search (ms) ms --version required" "stack.meta_skill.2 Local-first knowledge management with hybrid semantic search (ms) ms doctor --json optional" - "stack.automated_plan_reviser.1 Automated iterative spec refinement with extended AI reasoning (apr) apr --help required" + "stack.automated_plan_reviser.1 Automated iterative spec refinement with extended AI reasoning (apr) apr --help optional" "stack.automated_plan_reviser.2 Automated iterative spec refinement with extended AI reasoning (apr) apr --version optional" - "stack.jeffreysprompts.1 Curated battle-tested prompts for AI agents - browse and install as skills (jfp) jfp --version required" + "stack.jeffreysprompts.1 Curated battle-tested prompts for AI agents - browse and install as skills (jfp) jfp --version optional" "stack.jeffreysprompts.2 Curated battle-tested prompts for AI agents - browse and install as skills (jfp) jfp doctor optional" - "stack.process_triage.1 Find and terminate stuck/zombie processes with intelligent scoring (pt) pt --help required" + "stack.process_triage.1 Find and terminate stuck/zombie processes with intelligent scoring (pt) pt --help optional" "stack.process_triage.2 Find and terminate stuck/zombie processes with intelligent scoring (pt) pt --version optional" "stack.ultimate_bug_scanner.1 UBS bug scanning (easy-mode) ubs --help required" "stack.ultimate_bug_scanner.2 UBS bug scanning (easy-mode) ubs doctor optional" @@ -165,29 +165,30 @@ declare -a MANIFEST_CHECKS=( "stack.cm.1 Procedural memory for agents (cass-memory) cm --version required" "stack.cm.2 Procedural memory for agents (cass-memory) cm doctor --json optional" "stack.caam Instant auth switching for agent CLIs caam status || caam --help required" - "stack.slb Two-person rule for dangerous commands (optional guardrails) export PATH=\"\$HOME/go/bin:\$PATH\" && slb >/dev/null 2>&1 || slb --help >/dev/null 2>&1 required" + "stack.slb Two-person rule for dangerous commands (optional guardrails) export PATH=\"\$HOME/go/bin:\$PATH\" && slb >/dev/null 2>&1 || slb --help >/dev/null 2>&1 optional" "stack.dcg.1 Destructive Command Guard - Claude Code hook blocking dangerous git/fs commands dcg --version required" "stack.dcg.2 Destructive Command Guard - Claude Code hook blocking dangerous git/fs commands settings=\"\$HOME/.claude/settings.json\"\\nalt_settings=\"\$HOME/.config/claude/settings.json\"\\nif [[ -f \"\$settings\" ]]; then\\n grep -q \"dcg\" \"\$settings\"\\nelif [[ -f \"\$alt_settings\" ]]; then\\n grep -q \"dcg\" \"\$alt_settings\"\\nelse\\n exit 1\\nfi required" "stack.ru Repo Updater - multi-repo sync + AI-driven commit automation ru --version required" - "stack.brenner_bot Brenner Bot - research session manager with hypothesis tracking brenner --version || brenner --help required" - "stack.rch Remote Compilation Helper - transparent build offloading for AI coding agents rch --version || rch --help required" - "stack.wezterm_automata WezTerm Automata (wa) - terminal automation and orchestration for AI agents wa --version || wa --help required" - "stack.srps.1 System Resource Protection Script - ananicy-cpp rules + TUI monitor for responsive dev workstations sysmoni --version || sysmoni --help required" - "stack.srps.2 System Resource Protection Script - ananicy-cpp rules + TUI monitor for responsive dev workstations systemctl is-active ananicy-cpp required" - "utils.giil Get Image from Internet Link - download cloud images for visual debugging giil --help || giil --version required" - "utils.csctf Chat Shared Conversation to File - convert AI share links to Markdown/HTML csctf --help || csctf --version required" - "utils.xf xf - Ultra-fast X/Twitter archive search with Tantivy xf --help || xf --version required" - "utils.toon_rust toon_rust (tru) - Token-optimized notation format for LLM context efficiency tru --help || tru --version required" - "utils.rano rano - Network observer for AI CLIs with request/response logging rano --help || rano --version required" - "utils.mdwb markdown_web_browser (mdwb) - Convert websites to Markdown for LLM consumption mdwb --help || mdwb --version required" - "utils.s2p source_to_prompt_tui (s2p) - Code to LLM prompt generator with TUI s2p --help || s2p --version required" - "utils.rust_proxy rust_proxy - Transparent proxy routing for debugging network traffic rust_proxy --help || rust_proxy --version required" - "utils.aadc aadc - ASCII diagram corrector for fixing malformed ASCII art aadc --help || aadc --version required" - "utils.caut coding_agent_usage_tracker (caut) - LLM provider usage tracker caut --help || caut --version required" + "stack.brenner_bot Brenner Bot - research session manager with hypothesis tracking brenner --version || brenner --help optional" + "stack.rch Remote Compilation Helper - transparent build offloading for AI coding agents rch --version || rch --help optional" + "stack.wezterm_automata WezTerm Automata (wa) - terminal automation and orchestration for AI agents wa --version || wa --help optional" + "stack.srps.1 System Resource Protection Script - ananicy-cpp rules + TUI monitor for responsive dev workstations sysmoni --version || sysmoni --help optional" + "stack.srps.2 System Resource Protection Script - ananicy-cpp rules + TUI monitor for responsive dev workstations systemctl is-active ananicy-cpp optional" + "utils.giil Get Image from Internet Link - download cloud images for visual debugging giil --help || giil --version optional" + "utils.csctf Chat Shared Conversation to File - convert AI share links to Markdown/HTML csctf --help || csctf --version optional" + "utils.xf xf - Ultra-fast X/Twitter archive search with Tantivy xf --help || xf --version optional" + "utils.toon_rust toon_rust (tru) - Token-optimized notation format for LLM context efficiency tru --help || tru --version optional" + "utils.rano rano - Network observer for AI CLIs with request/response logging rano --help || rano --version optional" + "utils.mdwb markdown_web_browser (mdwb) - Convert websites to Markdown for LLM consumption mdwb --help || mdwb --version optional" + "utils.s2p source_to_prompt_tui (s2p) - Code to LLM prompt generator with TUI s2p --help || s2p --version optional" + "utils.rust_proxy rust_proxy - Transparent proxy routing for debugging network traffic rust_proxy --help || rust_proxy --version optional" + "utils.aadc aadc - ASCII diagram corrector for fixing malformed ASCII art aadc --help || aadc --version optional" + "utils.caut coding_agent_usage_tracker (caut) - LLM provider usage tracker caut --help || caut --version optional" "acfs.workspace.1 Agent workspace with tmux session and project folder test -d /data/projects/my_first_project required" "acfs.workspace.2 Agent workspace with tmux session and project folder grep -q \"alias agents=\" ~/.zshrc.local || grep -q \"alias agents=\" ~/.zshrc required" "acfs.onboard Onboarding TUI tutorial onboard --help || command -v onboard required" "acfs.update ACFS update command wrapper command -v acfs-update required" + "acfs.nightly Nightly auto-update timer (systemd) systemctl --user is-enabled acfs-nightly-update.timer optional" "acfs.doctor ACFS doctor command for health checks acfs doctor --help || command -v acfs required" ) @@ -203,13 +204,13 @@ run_manifest_checks() { cmd="$(printf '%b' "$cmd")" if bash -o pipefail -c "$cmd" &>/dev/null; then - echo -e "\033[0;32m[ok]\033[0m $id - $desc" + echo -e "${ACFS_GREEN-\033[0;32m}[ok]${ACFS_NC-\033[0m} $id - $desc" ((passed += 1)) elif [[ "$optional" = "optional" ]]; then - echo -e "\033[0;33m[skip]\033[0m $id - $desc" + echo -e "${ACFS_YELLOW-\033[0;33m}[skip]${ACFS_NC-\033[0m} $id - $desc" ((skipped += 1)) else - echo -e "\033[0;31m[fail]\033[0m $id - $desc" + echo -e "${ACFS_RED-\033[0;31m}[fail]${ACFS_NC-\033[0m} $id - $desc" ((failed += 1)) fi done diff --git a/scripts/generated/install_acfs.sh b/scripts/generated/install_acfs.sh index 1c49ee2c..cbe06413 100755 --- a/scripts/generated/install_acfs.sh +++ b/scripts/generated/install_acfs.sh @@ -93,7 +93,7 @@ acfs_security_init() { } # Category: acfs -# Modules: 4 +# Modules: 5 # Agent workspace with tmux session and project folder install_acfs_workspace() { @@ -342,6 +342,153 @@ INSTALL_ACFS_UPDATE log_success "acfs.update installed" } +# Nightly auto-update timer (systemd) +install_acfs_nightly() { + local module_id="acfs.nightly" + acfs_require_contract "module:${module_id}" || return 1 + log_step "Installing acfs.nightly" + + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: mkdir -p ~/.acfs/scripts ~/.config/systemd/user (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +mkdir -p ~/.acfs/scripts ~/.config/systemd/user +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: install command failed: mkdir -p ~/.acfs/scripts ~/.config/systemd/user" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "install command failed: mkdir -p ~/.acfs/scripts ~/.config/systemd/user" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: # Install nightly update wrapper script (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +# Install nightly update wrapper script +if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/lib/nightly_update.sh" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/lib/nightly_update.sh" ~/.acfs/scripts/nightly-update.sh +elif [[ -f "scripts/lib/nightly_update.sh" ]]; then + cp "scripts/lib/nightly_update.sh" ~/.acfs/scripts/nightly-update.sh +else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/lib/nightly_update.sh" -o ~/.acfs/scripts/nightly-update.sh +fi +chmod +x ~/.acfs/scripts/nightly-update.sh +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: install command failed: # Install nightly update wrapper script" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "install command failed: # Install nightly update wrapper script" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: # Install systemd timer unit (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +# Install systemd timer unit +if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.timer" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.timer" ~/.config/systemd/user/acfs-nightly-update.timer +elif [[ -f "scripts/templates/acfs-nightly-update.timer" ]]; then + cp "scripts/templates/acfs-nightly-update.timer" ~/.config/systemd/user/acfs-nightly-update.timer +else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/templates/acfs-nightly-update.timer" -o ~/.config/systemd/user/acfs-nightly-update.timer +fi +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: install command failed: # Install systemd timer unit" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "install command failed: # Install systemd timer unit" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: # Install systemd service unit (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +# Install systemd service unit +if [[ -n "${ACFS_BOOTSTRAP_DIR:-}" ]] && [[ -f "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.service" ]]; then + cp "${ACFS_BOOTSTRAP_DIR}/scripts/templates/acfs-nightly-update.service" ~/.config/systemd/user/acfs-nightly-update.service +elif [[ -f "scripts/templates/acfs-nightly-update.service" ]]; then + cp "scripts/templates/acfs-nightly-update.service" ~/.config/systemd/user/acfs-nightly-update.service +else + ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" + CURL_ARGS=(-fsSL) + if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) + fi + curl "${CURL_ARGS[@]}" "${ACFS_RAW}/scripts/templates/acfs-nightly-update.service" -o ~/.config/systemd/user/acfs-nightly-update.service +fi +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: install command failed: # Install systemd service unit" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "install command failed: # Install systemd service unit" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: # Reload systemd and enable the timer (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +# Reload systemd and enable the timer +systemctl --user daemon-reload +systemctl --user enable --now acfs-nightly-update.timer +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: install command failed: # Reload systemd and enable the timer" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "install command failed: # Reload systemd and enable the timer" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + + # Verify + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: verify: systemctl --user is-enabled acfs-nightly-update.timer (target_user)" + else + if ! run_as_target_shell <<'INSTALL_ACFS_NIGHTLY' +systemctl --user is-enabled acfs-nightly-update.timer +INSTALL_ACFS_NIGHTLY + then + log_warn "acfs.nightly: verify failed: systemctl --user is-enabled acfs-nightly-update.timer" + if type -t record_skipped_tool >/dev/null 2>&1; then + record_skipped_tool "acfs.nightly" "verify failed: systemctl --user is-enabled acfs-nightly-update.timer" + elif type -t state_tool_skip >/dev/null 2>&1; then + state_tool_skip "acfs.nightly" + fi + return 0 + fi + fi + + log_success "acfs.nightly installed" +} + # ACFS doctor command for health checks install_acfs_doctor() { local module_id="acfs.doctor" @@ -406,6 +553,7 @@ install_acfs() { install_acfs_workspace install_acfs_onboard install_acfs_update + install_acfs_nightly install_acfs_doctor } diff --git a/scripts/generated/install_agents.sh b/scripts/generated/install_agents.sh index 2593ac9b..1f274ca4 100755 --- a/scripts/generated/install_agents.sh +++ b/scripts/generated/install_agents.sh @@ -124,7 +124,7 @@ install_agents_claude() { fi if [[ -n "$url" ]] && [[ -n "$expected_sha256" ]]; then - if verify_checksum "$url" "$expected_sha256" "$tool" | run_as_target_runner 'bash' '-s' '--' 'stable'; then + if verify_checksum "$url" "$expected_sha256" "$tool" | run_as_target_runner 'bash' '-s' '--' 'latest'; then install_success=true else log_error "agents.claude: verify_checksum or installer execution failed" diff --git a/scripts/generated/install_all.sh b/scripts/generated/install_all.sh index effe3024..44c189f7 100755 --- a/scripts/generated/install_all.sh +++ b/scripts/generated/install_all.sh @@ -186,6 +186,7 @@ install_all() { install_acfs_workspace install_acfs_onboard install_acfs_update + install_acfs_nightly install_acfs_doctor log_success "All modules installed!" diff --git a/scripts/generated/install_shell.sh b/scripts/generated/install_shell.sh index 31577fbb..30a7ba5f 100755 --- a/scripts/generated/install_shell.sh +++ b/scripts/generated/install_shell.sh @@ -250,6 +250,26 @@ INSTALL_SHELL_OMZ return 1 fi fi + if [[ "${DRY_RUN:-false}" = "true" ]]; then + log_info "dry-run: install: # Install ACFS shell completions (zsh) (target_user)" + else + if ! run_as_target_shell <<'INSTALL_SHELL_OMZ' +# Install ACFS shell completions (zsh) +ACFS_RAW="${ACFS_RAW:-https://raw.githubusercontent.com/Dicklesworthstone/agentic_coding_flywheel_setup/main}" +mkdir -p ~/.acfs/completions +CURL_ARGS=(-fsSL) +if curl --help all 2>/dev/null | grep -q -- '--proto'; then + CURL_ARGS=(--proto '=https' --proto-redir '=https' -fsSL) +fi +curl "${CURL_ARGS[@]}" -o ~/.acfs/completions/_acfs "${ACFS_RAW}/scripts/completions/_acfs" +# Also install bash completions for users who switch shells +curl "${CURL_ARGS[@]}" -o ~/.acfs/completions/acfs.bash "${ACFS_RAW}/scripts/completions/acfs.bash" +INSTALL_SHELL_OMZ + then + log_error "shell.omz: install command failed: # Install ACFS shell completions (zsh)" + return 1 + fi + fi if [[ "${DRY_RUN:-false}" = "true" ]]; then log_info "dry-run: install: # Install pre-configured Powerlevel10k settings (prevents config wizard on first login) (target_user)" else diff --git a/scripts/generated/internal_checksums.sh b/scripts/generated/internal_checksums.sh new file mode 100644 index 00000000..a2cf0b31 --- /dev/null +++ b/scripts/generated/internal_checksums.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034 +# ============================================================ +# AUTO-GENERATED internal script checksums - DO NOT EDIT +# Regenerate: bun run generate (from packages/manifest) +# ============================================================ +# SHA256 checksums for critical internal scripts (bd-3tpl). +# Used by check-manifest-drift.sh to detect unauthorized changes. + +declare -gA ACFS_INTERNAL_CHECKSUMS=( + [scripts/lib/security.sh]="1648e29171cf0ab04255a25cc186ecae9060b6d6efad16a076b98c1ff91fea2a" + [scripts/lib/agents.sh]="6451e29a96ac776c78854a69ffe3a688e564226a6fd78f1e8d02dd00e024d348" + [scripts/lib/update.sh]="4d82fa1a5bc8429b6f88127c24905fc12f07afce7ae9b3276ce7ee151c89b92f" + [scripts/lib/doctor.sh]="15a2a936642726d81261c1d968834145f092d7f7c201c16c69c01e3987e903ba" + [scripts/lib/install_helpers.sh]="e5334330a9b733f44ef481afa11833c79f39544466a1fe5b478f2d610ec50c8d" + [scripts/lib/logging.sh]="2c59b18646afd9b5413ac2b7956f6dea2299fe2f108913c2d998bdd8c5abcb97" + [scripts/lib/state.sh]="2aa67b11352abe8067e6e7c029543ef1dbcbc908ae6b420262e7eab8cbf414f0" + [scripts/lib/session.sh]="f008658acc15c08929013d46af33f62ca4306cafaf5cf3344bd0626e3e41f137" + [scripts/lib/os_detect.sh]="841c65c20ac86d45c59c852a537c3fc17a7a6923914ba07e5cddf57a45ae4223" + [scripts/lib/errors.sh]="66b949934dd903b7b89e1641ac8002cbb53dfb2aee10d0948ceec4131e9deec7" + [scripts/lib/user.sh]="767910abfa2d59c6ef1147a91e9571395f481b2dd0135d983e576b64f25da0bc" + [scripts/lib/tools.sh]="325f8ad08a07fec4c489092a925ac9d6009ab88d3d71cff4ccf5d41acaaf94e9" + [scripts/lib/export-config.sh]="731732f60e13169554488f154e81225d1a73f3521ce28cae0ebc00e78f69f85f" + [scripts/acfs-global]="ac2d5dc4f19e4ebfa9b354c7c22f96ec9b82e1975abe89249d07b0610197931b" + [scripts/acfs-update]="e8b756ecb2b16c36dec0f52f4e8d18c893b2d6f36d1c7d7c0716dfe51db9f63f" +) + +ACFS_INTERNAL_CHECKSUMS_COUNT=15 +ACFS_INTERNAL_CHECKSUMS_GENERATED="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)" diff --git a/scripts/generated/manifest_index.sh b/scripts/generated/manifest_index.sh index b074f3c4..e83f1bc5 100644 --- a/scripts/generated/manifest_index.sh +++ b/scripts/generated/manifest_index.sh @@ -6,7 +6,7 @@ # ============================================================ # Data-only manifest index. Safe to source. -ACFS_MANIFEST_SHA256="31d310a8aa38aaff6425b80d7e54b2f2a0f0e9d4e2000631366c858afa6d7fc9" +ACFS_MANIFEST_SHA256="21585f1725dbfbc1c3b86f1a466aa2d31c2a2cd6919deb12461d5acb3d84d869" ACFS_MODULES_IN_ORDER=( "base.system" @@ -67,6 +67,7 @@ ACFS_MODULES_IN_ORDER=( "acfs.workspace" "acfs.onboard" "acfs.update" + "acfs.nightly" "acfs.doctor" ) @@ -129,6 +130,7 @@ declare -gA ACFS_MODULE_PHASE=( [acfs.workspace]="10" [acfs.onboard]="10" [acfs.update]="10" + [acfs.nightly]="10" [acfs.doctor]="10" ) @@ -191,6 +193,7 @@ declare -gA ACFS_MODULE_DEPS=( [acfs.workspace]="agents.claude,agents.codex,agents.gemini,cli.modern" [acfs.onboard]="" [acfs.update]="" + [acfs.nightly]="acfs.update" [acfs.doctor]="" ) @@ -253,6 +256,7 @@ declare -gA ACFS_MODULE_FUNC=( [acfs.workspace]="install_acfs_workspace" [acfs.onboard]="install_acfs_onboard" [acfs.update]="install_acfs_update" + [acfs.nightly]="install_acfs_nightly" [acfs.doctor]="install_acfs_doctor" ) @@ -315,6 +319,7 @@ declare -gA ACFS_MODULE_CATEGORY=( [acfs.workspace]="acfs" [acfs.onboard]="acfs" [acfs.update]="acfs" + [acfs.nightly]="acfs" [acfs.doctor]="acfs" ) @@ -377,6 +382,7 @@ declare -gA ACFS_MODULE_TAGS=( [acfs.workspace]="workspace,agents" [acfs.onboard]="orchestration" [acfs.update]="orchestration" + [acfs.nightly]="orchestration,maintenance" [acfs.doctor]="orchestration" ) @@ -439,7 +445,189 @@ declare -gA ACFS_MODULE_DEFAULT=( [acfs.workspace]="1" [acfs.onboard]="1" [acfs.update]="1" + [acfs.nightly]="1" [acfs.doctor]="1" ) +declare -gA ACFS_MODULE_DESC=( + [base.system]="Base packages + sane defaults" + [users.ubuntu]="Ensure ubuntu user + passwordless sudo + ssh keys" + [base.filesystem]="Create workspace and ACFS directories" + [shell.zsh]="Zsh shell package" + [shell.omz]="Oh My Zsh + Powerlevel10k + plugins + ACFS config" + [cli.modern]="Modern CLI tools referenced by the zshrc intent" + [tools.lazygit]="Lazygit (apt or binary fallback)" + [tools.lazydocker]="Lazydocker (binary install)" + [network.tailscale]="Zero-config mesh VPN for secure remote VPS access" + [network.ssh_keepalive]="Configure SSH server keepalive to prevent VPN/NAT disconnects" + [lang.bun]="Bun runtime for JS tooling and global CLIs" + [lang.uv]="uv Python tooling (fast venvs)" + [lang.rust]="Rust nightly + cargo" + [lang.go]="Go toolchain" + [lang.nvm]="nvm + latest Node.js" + [tools.atuin]="Atuin shell history (Ctrl-R superpowers)" + [tools.zoxide]="Zoxide (better cd)" + [tools.ast_grep]="ast-grep (used by UBS for syntax-aware scanning)" + [agents.claude]="Claude Code" + [agents.codex]="OpenAI Codex CLI" + [agents.gemini]="Google Gemini CLI" + [tools.vault]="HashiCorp Vault CLI" + [db.postgres18]="PostgreSQL 18" + [cloud.wrangler]="Cloudflare Wrangler CLI" + [cloud.supabase]="Supabase CLI" + [cloud.vercel]="Vercel CLI" + [stack.ntm]="Named tmux manager (agent cockpit)" + [stack.mcp_agent_mail]="Like gmail for coding agents; MCP HTTP server + token; installs beads tools" + [stack.meta_skill]="Local-first knowledge management with hybrid semantic search (ms)" + [stack.automated_plan_reviser]="Automated iterative spec refinement with extended AI reasoning (apr)" + [stack.jeffreysprompts]="Curated battle-tested prompts for AI agents - browse and install as skills (jfp)" + [stack.process_triage]="Find and terminate stuck/zombie processes with intelligent scoring (pt)" + [stack.ultimate_bug_scanner]="UBS bug scanning (easy-mode)" + [stack.beads_rust]="beads_rust (br) - Rust issue tracker with graph-aware dependencies" + [stack.beads_viewer]="bv TUI for Beads tasks" + [stack.cass]="Unified search across agent session history" + [stack.cm]="Procedural memory for agents (cass-memory)" + [stack.caam]="Instant auth switching for agent CLIs" + [stack.slb]="Two-person rule for dangerous commands (optional guardrails)" + [stack.dcg]="Destructive Command Guard - Claude Code hook blocking dangerous git/fs commands" + [stack.ru]="Repo Updater - multi-repo sync + AI-driven commit automation" + [stack.brenner_bot]="Brenner Bot - research session manager with hypothesis tracking" + [stack.rch]="Remote Compilation Helper - transparent build offloading for AI coding agents" + [stack.wezterm_automata]="WezTerm Automata (wa) - terminal automation and orchestration for AI agents" + [stack.srps]="System Resource Protection Script - ananicy-cpp rules + TUI monitor for responsive dev workstations" + [utils.giil]="Get Image from Internet Link - download cloud images for visual debugging" + [utils.csctf]="Chat Shared Conversation to File - convert AI share links to Markdown/HTML" + [utils.xf]="xf - Ultra-fast X/Twitter archive search with Tantivy" + [utils.toon_rust]="toon_rust (tru) - Token-optimized notation format for LLM context efficiency" + [utils.rano]="rano - Network observer for AI CLIs with request/response logging" + [utils.mdwb]="markdown_web_browser (mdwb) - Convert websites to Markdown for LLM consumption" + [utils.s2p]="source_to_prompt_tui (s2p) - Code to LLM prompt generator with TUI" + [utils.rust_proxy]="rust_proxy - Transparent proxy routing for debugging network traffic" + [utils.aadc]="aadc - ASCII diagram corrector for fixing malformed ASCII art" + [utils.caut]="coding_agent_usage_tracker (caut) - LLM provider usage tracker" + [acfs.workspace]="Agent workspace with tmux session and project folder" + [acfs.onboard]="Onboarding TUI tutorial" + [acfs.update]="ACFS update command wrapper" + [acfs.nightly]="Nightly auto-update timer (systemd)" + [acfs.doctor]="ACFS doctor command for health checks" +) + +declare -gA ACFS_MODULE_INSTALLED_CHECK=( + [base.system]="command -v curl && command -v git && command -v jq" + [base.filesystem]="test -d /data/projects && test -d ~/.acfs" + [shell.zsh]="command -v zsh" + [shell.omz]="test -d ~/.oh-my-zsh && test -f ~/.acfs/zsh/acfs.zshrc" + [cli.modern]="command -v rg && command -v tmux && command -v fzf" + [tools.lazygit]="command -v lazygit" + [tools.lazydocker]="command -v lazydocker" + [network.tailscale]="command -v tailscale" + [network.ssh_keepalive]="# Check if ClientAliveInterval is configured (non-zero)\ngrep -qE '^ClientAliveInterval[[:space:]]+[1-9]' /etc/ssh/sshd_config 2>/dev/null\n" + [lang.bun]="test -x ~/.bun/bin/bun" + [lang.uv]="test -x ~/.local/bin/uv" + [lang.rust]="test -x ~/.cargo/bin/cargo" + [lang.go]="command -v go" + [lang.nvm]="test -d ~/.nvm && ls ~/.nvm/versions/node/ 2>/dev/null | grep -q ." + [tools.atuin]="test -x ~/.atuin/bin/atuin" + [tools.zoxide]="command -v zoxide" + [tools.ast_grep]="command -v sg" + [agents.claude]="test -x ~/.local/bin/claude" + [agents.codex]="test -x ~/.local/bin/codex" + [agents.gemini]="test -x ~/.local/bin/gemini" + [tools.vault]="command -v vault" + [db.postgres18]="command -v psql" + [cloud.wrangler]="command -v wrangler" + [cloud.supabase]="command -v supabase" + [cloud.vercel]="command -v vercel" + [stack.ntm]="command -v ntm" + [stack.mcp_agent_mail]="command -v am" + [stack.meta_skill]="command -v ms" + [stack.automated_plan_reviser]="command -v apr" + [stack.jeffreysprompts]="command -v jfp" + [stack.process_triage]="command -v pt" + [stack.ultimate_bug_scanner]="command -v ubs" + [stack.beads_rust]="command -v br" + [stack.beads_viewer]="command -v bv" + [stack.cass]="command -v cass" + [stack.cm]="command -v cm" + [stack.caam]="command -v caam" + [stack.slb]="command -v slb" + [stack.dcg]="command -v dcg" + [stack.ru]="command -v ru" + [stack.brenner_bot]="command -v brenner" + [stack.rch]="command -v rch" + [stack.wezterm_automata]="command -v wa" + [stack.srps]="command -v sysmoni && systemctl is-active ananicy-cpp >/dev/null 2>&1" + [utils.giil]="command -v giil" + [utils.csctf]="command -v csctf" + [utils.xf]="command -v xf" + [utils.toon_rust]="command -v tru" + [utils.rano]="command -v rano" + [utils.mdwb]="command -v mdwb" + [utils.s2p]="command -v s2p" + [utils.rust_proxy]="command -v rust_proxy" + [utils.aadc]="command -v aadc" + [utils.caut]="command -v caut" + [acfs.workspace]="test -d /data/projects/my_first_project" + [acfs.nightly]="systemctl --user is-enabled acfs-nightly-update.timer 2>/dev/null" +) + +declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( + [base.system]="current" + [base.filesystem]="target_user" + [shell.zsh]="current" + [shell.omz]="target_user" + [cli.modern]="current" + [tools.lazygit]="current" + [tools.lazydocker]="current" + [network.tailscale]="current" + [network.ssh_keepalive]="current" + [lang.bun]="target_user" + [lang.uv]="target_user" + [lang.rust]="target_user" + [lang.go]="current" + [lang.nvm]="target_user" + [tools.atuin]="target_user" + [tools.zoxide]="target_user" + [tools.ast_grep]="target_user" + [agents.claude]="target_user" + [agents.codex]="target_user" + [agents.gemini]="target_user" + [tools.vault]="current" + [db.postgres18]="current" + [cloud.wrangler]="target_user" + [cloud.supabase]="target_user" + [cloud.vercel]="target_user" + [stack.ntm]="target_user" + [stack.mcp_agent_mail]="target_user" + [stack.meta_skill]="target_user" + [stack.automated_plan_reviser]="target_user" + [stack.jeffreysprompts]="target_user" + [stack.process_triage]="target_user" + [stack.ultimate_bug_scanner]="target_user" + [stack.beads_rust]="target_user" + [stack.beads_viewer]="target_user" + [stack.cass]="target_user" + [stack.cm]="target_user" + [stack.caam]="target_user" + [stack.slb]="target_user" + [stack.dcg]="target_user" + [stack.ru]="target_user" + [stack.brenner_bot]="target_user" + [stack.rch]="target_user" + [stack.wezterm_automata]="target_user" + [stack.srps]="target_user" + [utils.giil]="target_user" + [utils.csctf]="target_user" + [utils.xf]="target_user" + [utils.toon_rust]="target_user" + [utils.rano]="target_user" + [utils.mdwb]="target_user" + [utils.s2p]="target_user" + [utils.rust_proxy]="target_user" + [utils.aadc]="target_user" + [utils.caut]="target_user" + [acfs.workspace]="target_user" + [acfs.nightly]="target_user" +) + ACFS_MANIFEST_INDEX_LOADED=true diff --git a/scripts/lib/agents.sh b/scripts/lib/agents.sh index db1f5310..a3556e8f 100755 --- a/scripts/lib/agents.sh +++ b/scripts/lib/agents.sh @@ -18,7 +18,7 @@ fi # ============================================================ # NPM package names for each agent -CLAUDE_PACKAGE="@anthropic-ai/claude-code@stable" +CLAUDE_PACKAGE="@anthropic-ai/claude-code@latest" CODEX_PACKAGE="${CODEX_PACKAGE:-@openai/codex@latest}" CODEX_FALLBACK_VERSION="${CODEX_FALLBACK_VERSION:-0.87.0}" CODEX_FALLBACK_PACKAGE="" @@ -145,7 +145,7 @@ install_claude_code() { local url="${KNOWN_INSTALLERS[claude]}" local sha="${LOADED_CHECKSUMS[claude]}" if [[ -n "$url" && -n "$sha" ]]; then - if _agent_run_as_user "source '$AGENTS_SCRIPT_DIR/security.sh'; verify_checksum '$url' '$sha' 'claude' | bash -s -- stable"; then + if _agent_run_as_user "source '$AGENTS_SCRIPT_DIR/security.sh'; verify_checksum '$url' '$sha' 'claude' | bash -s -- latest"; then log_success "Claude Code installed (verified)" return 0 fi @@ -167,7 +167,7 @@ upgrade_claude_code() { if [[ -x "$claude_bin" ]]; then log_detail "Upgrading Claude Code (native)..." - if _agent_run_as_user "\"$claude_bin\" update"; then + if _agent_run_as_user "\"$claude_bin\" update --channel latest"; then log_success "Claude Code upgraded" return 0 fi diff --git a/scripts/lib/autofix.sh b/scripts/lib/autofix.sh index 27614b9f..cfdf2007 100755 --- a/scripts/lib/autofix.sh +++ b/scripts/lib/autofix.sh @@ -25,6 +25,7 @@ declare -ga ACFS_CHANGE_ORDER # Ordered list of change IDs (global) # Session management ACFS_SESSION_ID="" ACFS_AUTOFIX_INITIALIZED=false +ACFS_AUTOFIX_LOCK_FD="" # ============================================================================= # Logging Helpers (avoid dependency on logging.sh) @@ -376,10 +377,24 @@ start_autofix_session() { log_info "[AUTO-FIX] Starting session: $ACFS_SESSION_ID" # Acquire lock (prevent concurrent modifications) - exec 200>"$ACFS_LOCK_FILE" - if ! flock -n 200; then - log_error "Another ACFS process is running auto-fix operations" - return 1 + # NOTE: On bash 5.3+, `exec N>file` under set -e exits the script + # before `if` can catch the failure. We test in a subshell first, + # then only exec in the main shell if the subshell succeeded. + ACFS_AUTOFIX_LOCK_FD="" + if (exec 200>"$ACFS_LOCK_FILE") 2>/dev/null; then + exec 200>"$ACFS_LOCK_FILE" + ACFS_AUTOFIX_LOCK_FD=200 + elif (exec 199>"$ACFS_LOCK_FILE") 2>/dev/null; then + exec 199>"$ACFS_LOCK_FILE" + ACFS_AUTOFIX_LOCK_FD=199 + fi + if [[ -n "$ACFS_AUTOFIX_LOCK_FD" ]]; then + if ! flock -n "$ACFS_AUTOFIX_LOCK_FD"; then + log_error "Another ACFS process is running auto-fix operations" + return 1 + fi + else + log_warn "Could not acquire autofix lock (continuing anyway)" fi # Write session start marker @@ -402,8 +417,11 @@ end_autofix_session() { # Remove session marker rm -f "$ACFS_STATE_DIR/.session" - # Release lock - flock -u 200 2>/dev/null || true + # Release lock (use whichever FD was acquired in start_autofix_session) + if [[ -n "${ACFS_AUTOFIX_LOCK_FD:-}" ]]; then + flock -u "$ACFS_AUTOFIX_LOCK_FD" 2>/dev/null || true + ACFS_AUTOFIX_LOCK_FD="" + fi } # ============================================================================= diff --git a/scripts/lib/doctor.sh b/scripts/lib/doctor.sh index adb32f4f..420d1fea 100644 --- a/scripts/lib/doctor.sh +++ b/scripts/lib/doctor.sh @@ -272,7 +272,7 @@ print_acfs_help() { echo " cheatsheet Command reference (aliases, shortcuts)" echo " continue [options] View installation/upgrade progress" echo " dashboard <command> Generate/view a static HTML dashboard" - echo " newproj <name> Create new project with git, bd, claude settings" + echo " newproj <name> Create new project with git, br, claude settings" echo " update [options] Update ACFS tools to latest versions" echo " services-setup Configure AI agents and cloud services" echo " session <command> Export/import/share agent sessions" @@ -1112,8 +1112,32 @@ check_stack() { version=$(get_version_line "ms") check "stack.meta_skill" "meta_skill ($version)" "pass" "installed" else - check "stack.meta_skill" "meta_skill (ms)" "warn" "not installed" \ - "Re-run: curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/meta_skill/main/scripts/install.sh | bash" + # Detect architecture to give the right install advice + local _ms_arch _ms_os _ms_fix + _ms_arch="$(uname -m 2>/dev/null || echo unknown)" + _ms_os="$(uname -s 2>/dev/null || echo unknown)" + _ms_fix="Re-run: curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/meta_skill/main/scripts/install.sh | bash" + + # Pre-built binaries exist for: x86_64-linux, aarch64-darwin, x86_64-darwin + # ARM64 Linux (aarch64-Linux) does NOT have a pre-built binary yet (GH#1) + case "${_ms_arch}-${_ms_os}" in + aarch64-Linux|arm64-Linux) + # ARM64 Linux binary is not yet published; the install script will 404 + check "stack.meta_skill" "meta_skill (ms)" "warn" \ + "ARM64 Linux binary not yet available (see meta_skill GH#1)" \ + "Build from source: cargo install --git https://github.com/Dicklesworthstone/meta_skill" + ;; + x86_64-Linux|x86_64-Darwin|arm64-Darwin|aarch64-Darwin) + # These platforms have pre-built binaries + check "stack.meta_skill" "meta_skill (ms)" "warn" "not installed" \ + "$_ms_fix" + ;; + *) + _ms_fix="meta_skill has no pre-built binary for ${_ms_arch}-${_ms_os}. Build from source: cargo install --git https://github.com/Dicklesworthstone/meta_skill" + check "stack.meta_skill" "meta_skill (ms)" "warn" "not installed" \ + "$_ms_fix" + ;; + esac fi # Check rch (Remote Compilation Helper) @@ -1169,6 +1193,100 @@ check_stack() { blank_line } +# ============================================================ +# Utility Tools Health Checks (bd-2gog) +# ============================================================ +# Optional utility tools from the Dicklesworthstone ecosystem. +# These are non-fatal checks (skip status) since utilities are optional. +# ============================================================ + +check_utilities() { + section "Utility tools" + + # tru (Token-Optimized Notation) + if command -v tru &>/dev/null; then + local version + version=$(get_version_line "tru") + check "util.tru" "tru ($version)" "pass" "installed" + else + check "util.tru" "tru (token notation)" "skip" "not installed (optional)" + fi + + # rust_proxy (Transparent Proxy Routing) + if command -v rust_proxy &>/dev/null; then + local version + version=$(get_version_line "rust_proxy") + check "util.rust_proxy" "rust_proxy ($version)" "pass" "installed" + else + check "util.rust_proxy" "rust_proxy (proxy routing)" "skip" "not installed (optional)" + fi + + # rano (Network Observer for AI CLIs) + if command -v rano &>/dev/null; then + local version + version=$(get_version_line "rano") + check "util.rano" "rano ($version)" "pass" "installed" + else + check "util.rano" "rano (network observer)" "skip" "not installed (optional)" + fi + + # xf (X/Twitter Archive Search) + if command -v xf &>/dev/null; then + local version + version=$(get_version_line "xf") + check "util.xf" "xf ($version)" "pass" "installed" + else + check "util.xf" "xf (X archive search)" "skip" "not installed (optional)" + fi + + # mdwb (Markdown Web Browser) + if command -v mdwb &>/dev/null; then + local version + version=$(get_version_line "mdwb") + check "util.mdwb" "mdwb ($version)" "pass" "installed" + else + check "util.mdwb" "mdwb (markdown browser)" "skip" "not installed (optional)" + fi + + # pt (Process Triage) + if command -v pt &>/dev/null; then + local version + version=$(get_version_line "pt") + check "util.pt" "pt ($version)" "pass" "installed" + else + check "util.pt" "pt (process triage)" "skip" "not installed (optional)" + fi + + # aadc (ASCII Diagram Corrector) + if command -v aadc &>/dev/null; then + local version + version=$(get_version_line "aadc") + check "util.aadc" "aadc ($version)" "pass" "installed" + else + check "util.aadc" "aadc (ASCII diagram corrector)" "skip" "not installed (optional)" + fi + + # s2p (Source to Prompt TUI) + if command -v s2p &>/dev/null; then + local version + version=$(get_version_line "s2p") + check "util.s2p" "s2p ($version)" "pass" "installed" + else + check "util.s2p" "s2p (source to prompt)" "skip" "not installed (optional)" + fi + + # caut (Coding Agent Usage Tracker) + if command -v caut &>/dev/null; then + local version + version=$(get_version_line "caut") + check "util.caut" "caut ($version)" "pass" "installed" + else + check "util.caut" "caut (usage tracker)" "skip" "not installed (optional)" + fi + + blank_line +} + # ============================================================ # Manifest Supplemental Checks (bd-31ps.5.1) # ============================================================ @@ -1204,6 +1322,8 @@ _is_bespoke_covered() { stack.beads_rust.*|stack.cass|stack.cm.*|stack.caam) return 0 ;; stack.dcg.*|stack.ru|stack.meta_skill.*) return 0 ;; stack.brenner_bot|stack.rch|stack.wezterm_automata) return 0 ;; + # check_utilities (bd-2gog) + util.*) return 0 ;; esac return 1 } @@ -1432,6 +1552,9 @@ run_deep_checks() { # Network health checks (bead bd-31ps.7.2) deep_check_network + # Notification (ntfy.sh) connectivity check (GitHub issue #131) + deep_check_notifications + # Calculate deep check specific counts DEEP_PASS_COUNT=$((PASS_COUNT - pre_pass)) DEEP_WARN_COUNT=$((WARN_COUNT - pre_warn)) @@ -1933,6 +2056,61 @@ check_network_apt_mirror() { fi } +# deep_check_notifications - Verify ntfy.sh notification configuration and connectivity +# Related: GitHub issue #131 +deep_check_notifications() { + local config_file="${HOME}/.config/acfs/config.yaml" + local enabled="" topic="" server="" + + # Read config (same logic as notify.sh) + if [[ -f "$config_file" ]]; then + enabled=$(grep -E '^\s*ntfy_enabled\s*:' "$config_file" 2>/dev/null | head -1 | \ + sed -E 's/^\s*ntfy_enabled\s*:\s*//; s/^["'"'"']//; s/["'"'"']$//' | \ + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true) + topic=$(grep -E '^\s*ntfy_topic\s*:' "$config_file" 2>/dev/null | head -1 | \ + sed -E 's/^\s*ntfy_topic\s*:\s*//; s/^["'"'"']//; s/["'"'"']$//' | \ + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true) + server=$(grep -E '^\s*ntfy_server\s*:' "$config_file" 2>/dev/null | head -1 | \ + sed -E 's/^\s*ntfy_server\s*:\s*//; s/^["'"'"']//; s/["'"'"']$//' | \ + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true) + fi + + # Allow env overrides + enabled="${ACFS_NTFY_ENABLED:-$enabled}" + topic="${ACFS_NTFY_TOPIC:-$topic}" + server="${ACFS_NTFY_SERVER:-$server}" + server="${server:-https://ntfy.sh}" + + # Check configuration state + if [[ "$enabled" != "true" ]]; then + check "deep.notifications.ntfy" "ntfy.sh notifications" "warn" "not enabled" "acfs notifications enable" + return + fi + + if [[ -z "$topic" ]]; then + check "deep.notifications.ntfy" "ntfy.sh notifications" "warn" "enabled but no topic set" "acfs notifications enable" + return + fi + + # Topic and enabled are set -- test server connectivity + if ! command -v curl &>/dev/null; then + check "deep.notifications.ntfy" "ntfy.sh notifications" "warn" "curl not available" "apt install curl" + return + fi + + # HEAD request against the server health endpoint (lightweight) + local http_code + http_code=$(curl -sL --max-time 5 --connect-timeout 3 -o /dev/null -w "%{http_code}" "${server}/v1/health" 2>/dev/null) || http_code="000" + + if [[ "$http_code" =~ ^2 ]]; then + check "deep.notifications.ntfy" "ntfy.sh notifications" "pass" "enabled, server reachable (${server})" + elif [[ "$http_code" == "000" ]]; then + check "deep.notifications.ntfy" "ntfy.sh notifications" "warn" "server unreachable (${server})" "Check network or acfs notifications set-server <url>" + else + check "deep.notifications.ntfy" "ntfy.sh notifications" "warn" "server returned HTTP ${http_code}" "Check server URL: ${server}" + fi +} + # check_vault_configured - Check if Vault is configured and reachable # Related: bead azw check_vault_configured() { @@ -2503,6 +2681,7 @@ $(gum style --foreground "$ACFS_MUTED" "OS:") $(gum style --foreground "$ACFS_TE check_agents check_cloud check_stack + check_utilities check_manifest_supplemental show_skipped_tools diff --git a/scripts/lib/error_tracking.sh b/scripts/lib/error_tracking.sh index 481dca2d..77058e48 100755 --- a/scripts/lib/error_tracking.sh +++ b/scripts/lib/error_tracking.sh @@ -745,8 +745,9 @@ try_step_with_backoff() { LAST_ERROR_CODE=0 LAST_ERROR_OUTPUT="" return 0 + else + exit_code=$? fi - exit_code=$? # Failure - error context already set by retry_with_backoff if type -t state_phase_fail &>/dev/null; then @@ -834,8 +835,9 @@ install_tool_tracked() { log_success "$tool_name installed successfully" fi return 0 + else + exit_code=$? fi - exit_code=$? track_failed_tool "$tool_name" "Exit code $exit_code" return 1 diff --git a/scripts/lib/gum_ui.sh b/scripts/lib/gum_ui.sh index cf7b6737..bbbd7fb1 100644 --- a/scripts/lib/gum_ui.sh +++ b/scripts/lib/gum_ui.sh @@ -11,6 +11,29 @@ if command -v gum &>/dev/null; then HAS_GUM=true fi +# ============================================================ +# NO_COLOR Support (https://no-color.org/) +# Fallback colors respect NO_COLOR env var and TTY status. +# Related: bd-39ye +# ============================================================ +if [[ -z "${NO_COLOR:-}" ]] && [[ -t 1 ]]; then + GUM_FB_BLUE='\033[0;34m' + GUM_FB_GREEN='\033[0;32m' + GUM_FB_YELLOW='\033[0;33m' + GUM_FB_RED='\033[0;31m' + GUM_FB_GRAY='\033[0;90m' + GUM_FB_PURPLE='\033[0;35m' + GUM_FB_NC='\033[0m' +else + GUM_FB_BLUE='' + GUM_FB_GREEN='' + GUM_FB_YELLOW='' + GUM_FB_RED='' + GUM_FB_GRAY='' + GUM_FB_PURPLE='' + GUM_FB_NC='' +fi + # ACFS Color scheme (Catppuccin Mocha inspired) ACFS_PRIMARY="#89b4fa" # Blue ACFS_SUCCESS="#a6e3a1" # Green @@ -44,7 +67,7 @@ print_banner() { --foreground "$ACFS_PRIMARY" \ --bold else - echo -e "\033[0;34m$banner\033[0m" + echo -e "${GUM_FB_BLUE}$banner${GUM_FB_NC}" fi } @@ -83,7 +106,7 @@ gum_step() { echo -n " " gum style "$message" else - echo -e "\033[0;34m[$step/$total]\033[0m $message" + echo -e "${GUM_FB_BLUE}[$step/$total]${GUM_FB_NC} $message" fi } @@ -97,7 +120,7 @@ gum_detail() { --margin "0 0 0 4" \ "→ $message" else - echo -e "\033[0;90m → $message\033[0m" + echo -e "${GUM_FB_GRAY} → $message${GUM_FB_NC}" fi } @@ -111,7 +134,7 @@ gum_success() { --bold \ "✓ $message" else - echo -e "\033[0;32m✓ $message\033[0m" + echo -e "${GUM_FB_GREEN}✓ $message${GUM_FB_NC}" fi } @@ -124,7 +147,7 @@ gum_warn() { --foreground "$ACFS_WARNING" \ "⚠ $message" else - echo -e "\033[0;33m⚠ $message\033[0m" + echo -e "${GUM_FB_YELLOW}⚠ $message${GUM_FB_NC}" fi } @@ -138,7 +161,7 @@ gum_error() { --bold \ "✖ $message" else - echo -e "\033[0;31m✖ $message\033[0m" + echo -e "${GUM_FB_RED}✖ $message${GUM_FB_NC}" fi } @@ -162,7 +185,7 @@ gum_spin() { --title "$message" \ -- "$@" else - echo -e "\033[0;90m⏳ $message...\033[0m" + echo -e "${GUM_FB_GRAY}⏳ $message...${GUM_FB_NC}" "$@" fi } @@ -291,7 +314,7 @@ gum_section() { else echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo -e "\033[0;35m $title\033[0m" + echo -e "${GUM_FB_PURPLE} $title${GUM_FB_NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" fi } @@ -347,7 +370,7 @@ $content" else echo "" echo "╔═══════════════════════════════════════════╗" - echo -e "║ \033[0;32m$title\033[0m" + echo -e "║ ${GUM_FB_GREEN}$title${GUM_FB_NC}" echo "╠═══════════════════════════════════════════╣" # shellcheck disable=SC2001 echo "$content" | sed 's/^/║ /' diff --git a/scripts/lib/info.sh b/scripts/lib/info.sh index 7c8bcba0..ac9476bd 100644 --- a/scripts/lib/info.sh +++ b/scripts/lib/info.sh @@ -48,8 +48,10 @@ _INFO_SHOW_STATS=false # ============================================================ # Color Constants (for terminal output) +# Respects NO_COLOR standard: https://no-color.org/ +# Related: bd-39ye # ============================================================ -if [[ -t 1 ]]; then +if [[ -z "${NO_COLOR:-}" ]] && [[ -t 1 ]]; then C_RESET='\033[0m' C_BOLD='\033[1m' C_DIM='\033[2m' diff --git a/scripts/lib/install_helpers.sh b/scripts/lib/install_helpers.sh index 69faae90..6005aea4 100644 --- a/scripts/lib/install_helpers.sh +++ b/scripts/lib/install_helpers.sh @@ -16,6 +16,12 @@ if [[ -z "${ACFS_BLUE:-}" ]]; then source "$INSTALL_HELPERS_DIR/logging.sh" 2>/dev/null || true fi +# Source progress bar library (bd-21kh) +if [[ -z "${ACFS_PROGRESS_TOTAL:-}" ]]; then + # shellcheck source=progress.sh + source "$INSTALL_HELPERS_DIR/progress.sh" 2>/dev/null || true +fi + # ------------------------------------------------------------ # Selection state (populated by parse_args or manifest selection) # ------------------------------------------------------------ @@ -879,8 +885,20 @@ acfs_run_generated_category_phase() { local module="" local key="" local func="" + local desc="" local ran_any=false + # Count modules for progress tracking (bd-21kh) + local module_count=0 + if declare -f progress_count_modules >/dev/null 2>&1; then + module_count=$(progress_count_modules "$category" "$phase") + fi + + # Initialize progress bar if we have modules + if [[ "$module_count" -gt 0 ]] && declare -f progress_init >/dev/null 2>&1; then + progress_init "$module_count" + fi + for module in "${ACFS_EFFECTIVE_PLAN[@]}"; do key="$module" if [[ "${ACFS_MODULE_CATEGORY[$key]:-}" != "$category" ]]; then @@ -890,29 +908,47 @@ acfs_run_generated_category_phase() { continue fi func="${ACFS_MODULE_FUNC[$key]:-}" + desc="${ACFS_MODULE_DESC[$key]:-$module}" if [[ -z "$func" ]]; then log_error "Missing generated function for $module" + if declare -f progress_finish >/dev/null 2>&1; then progress_finish; fi return 1 fi if ! declare -f "$func" >/dev/null 2>&1; then log_error "Generated function not found: $func (module $module)" + if declare -f progress_finish >/dev/null 2>&1; then progress_finish; fi return 1 fi # Skip-if-installed check (bd-1eop) if acfs_should_skip_module "$module"; then log_info "Skipping $module (already installed)" + # Still update progress bar to show skip + if declare -f progress_update >/dev/null 2>&1; then + progress_update "$module" "$desc [skipped]" + fi ran_any=true continue fi + # Update progress bar before installing (bd-21kh) + if declare -f progress_update >/dev/null 2>&1; then + progress_update "$module" "$desc" + fi + if ! "$func"; then log_error "Generated module failed: $module" + if declare -f progress_finish >/dev/null 2>&1; then progress_finish; fi return 1 fi ran_any=true done + # Finish progress bar + if [[ "$module_count" -gt 0 ]] && declare -f progress_finish >/dev/null 2>&1; then + progress_finish + fi + if [[ "$ran_any" != "true" ]]; then log_detail "No generated modules selected for $category (phase $phase)" fi diff --git a/scripts/lib/logging.sh b/scripts/lib/logging.sh index 14def933..2bbdb136 100644 --- a/scripts/lib/logging.sh +++ b/scripts/lib/logging.sh @@ -45,8 +45,31 @@ if ! declare -f acfs_log_init >/dev/null 2>&1; then # Tee stderr: all stderr output goes to both terminal and log file. # fd 3 = original stderr (preserved for terminal output). - exec 3>&2 - exec 2> >(tee -a "$ACFS_LOG_FILE" >&3) + # + # NOTE: Process substitution >(tee ...) can fail on some systems + # (especially Ubuntu 25.04 with bash 5.3+). We test first and + # fall back to simple file logging if it fails. + local tee_logging_ok=false + if command -v tee >/dev/null 2>&1; then + # Test if process substitution works before committing to it. + # On bash 5.3+, bare `exec` under set -e can exit the script + # before `if` catches the failure, so we test in a subshell. + # shellcheck disable=SC2261 + if (exec 3>&1; echo test > >(cat >/dev/null)) 2>/dev/null; then + exec 3>&2 || true + # shellcheck disable=SC2261 + # Use set +e locally to prevent exec from exiting under bash 5.3+ + if (set +e; exec 2> >(tee -a "$ACFS_LOG_FILE" >&3)) 2>/dev/null; then + exec 2> >(tee -a "$ACFS_LOG_FILE" >&3) && tee_logging_ok=true + fi + fi + fi + + if [[ "$tee_logging_ok" != "true" ]]; then + # Fallback: rely on explicit logging calls instead of automatic tee + ACFS_LOG_FALLBACK=true + export ACFS_LOG_FALLBACK + fi } fi @@ -85,13 +108,48 @@ if [[ -n "${_ACFS_LOGGING_SH_LOADED:-}" ]]; then fi _ACFS_LOGGING_SH_LOADED=1 -# Colors -export ACFS_RED='\033[0;31m' -export ACFS_GREEN='\033[0;32m' -export ACFS_YELLOW='\033[0;33m' -export ACFS_BLUE='\033[0;34m' -export ACFS_GRAY='\033[0;90m' -export ACFS_NC='\033[0m' # No Color +# ============================================================ +# Color Support with NO_COLOR Standard (https://no-color.org/) +# ============================================================ +# +# Respects: +# - NO_COLOR env var (any value = disable colors) +# - Non-TTY output (pipes, redirects) +# +# Related: bd-39ye + +# Initialize colors based on environment +_acfs_init_colors() { + # Disable colors if NO_COLOR is set (any value) or output is not a TTY + if [[ -n "${NO_COLOR:-}" ]] || [[ ! -t 2 ]]; then + # No colors - empty strings + export ACFS_RED='' + export ACFS_GREEN='' + export ACFS_YELLOW='' + export ACFS_BLUE='' + export ACFS_GRAY='' + export ACFS_NC='' + export ACFS_COLORS_ENABLED=false + else + # Colors enabled + export ACFS_RED='\033[0;31m' + export ACFS_GREEN='\033[0;32m' + export ACFS_YELLOW='\033[0;33m' + export ACFS_BLUE='\033[0;34m' + export ACFS_GRAY='\033[0;90m' + export ACFS_NC='\033[0m' + export ACFS_COLORS_ENABLED=true + fi +} + +# Initialize colors on first load +_acfs_init_colors + +# Check if colors are enabled +# Usage: if acfs_colors_enabled; then echo "colors!"; fi +acfs_colors_enabled() { + [[ "${ACFS_COLORS_ENABLED:-true}" == "true" ]] +} # Log a major step (blue) # Usage: log_step "1/8" "Installing packages..." @@ -151,6 +209,29 @@ if ! declare -f log_warn >/dev/null; then } fi +# Log sensitive warning message (tries to bypass log tee to avoid storing secrets) +# Usage: log_sensitive "Generated password for user: ..." +if ! declare -f log_sensitive >/dev/null; then + log_sensitive() { + local message="$1" + + # If stderr is being tee'd, fd 3 is the original terminal stderr. + if { true >&3; } 2>/dev/null; then + printf "${ACFS_YELLOW}⚠ %s${ACFS_NC}\n" "$message" >&3 + return 0 + fi + + # Fall back to /dev/tty when available to avoid log capture. + if [[ -w /dev/tty ]]; then + printf "${ACFS_YELLOW}⚠ %s${ACFS_NC}\n" "$message" > /dev/tty + return 0 + fi + + # Last resort: stderr (may be logged). + printf "${ACFS_YELLOW}⚠ %s${ACFS_NC}\n" "$message" >&2 + } +fi + # Log error message (red with X) # Usage: log_error "Failed to install package" if ! declare -f log_error >/dev/null; then diff --git a/scripts/lib/newproj.sh b/scripts/lib/newproj.sh index f58e64e3..b390183e 100644 --- a/scripts/lib/newproj.sh +++ b/scripts/lib/newproj.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ============================================================ # ACFS newproj - Create a new project with full ACFS tooling -# Creates a project with git, beads (bd), Claude settings, and AGENTS.md +# Creates a project with git, beads (br), Claude settings, and AGENTS.md # Supports both CLI mode and interactive TUI wizard mode # ============================================================ @@ -84,7 +84,7 @@ print_help() { echo "Usage: acfs newproj [options] <project-name> [directory]" echo " acfs newproj --interactive" echo "" - echo "Create a new project with ACFS tooling (git, bd, claude settings, AGENTS.md)" + echo "Create a new project with ACFS tooling (git, br, claude settings, AGENTS.md)" echo "" echo "Arguments:" echo " project-name Name of the project (required in CLI mode)" @@ -95,7 +95,7 @@ print_help() { echo " (recommended for first-time users)" echo "" echo "CLI mode options:" - echo " --no-bd Skip beads (bd) initialization" + echo " --no-br Skip beads (br) initialization" echo " --no-claude Skip Claude settings creation" echo " --no-agents Skip AGENTS.md template creation" echo " -h, --help Show this help message" @@ -194,7 +194,7 @@ Example structure: PROJECT_NAME_PLACEHOLDER/ ├── README.md ├── AGENTS.md -├── .beads/ # Issue tracking (bd) +├── .beads/ # Issue tracking (br) ├── .claude/ # Claude Code settings │ └── src/ # Your source code @@ -308,13 +308,10 @@ Common pitfalls: All issue tracking goes through **Beads**. No other TODO systems. -**Note:** `br` is a convenience alias (installed by `acfs/zsh/acfs.zshrc`) for the real Beads CLI: `bd`. -If `br` is unavailable (CI / non-interactive shells), use `bd` directly. - Key invariants: - `.beads/` is authoritative state and **must always be committed** with code changes. -- Do not edit `.beads/*.jsonl` directly; only via `br` / `bd`. +- Do not edit `.beads/*.jsonl` directly; only via `br`. ### Basics @@ -524,7 +521,7 @@ run_interactive_mode() { main() { local project_name="" local project_dir="" - local skip_bd=false + local skip_br=false local skip_claude=false local skip_agents=false local interactive_mode=false @@ -540,8 +537,8 @@ main() { interactive_mode=true shift ;; - --no-bd) - skip_bd=true + --no-br) + skip_br=true shift ;; --no-claude) @@ -742,7 +739,7 @@ EOF fi # Initialize beads (br) if available and not skipped - if [[ "$skip_bd" == "false" ]]; then + if [[ "$skip_br" == "false" ]]; then if command -v br &>/dev/null; then if [[ ! -d .beads ]]; then echo -e "${GREEN}Initializing beads (br)...${NC}" @@ -807,7 +804,7 @@ EOF if [[ "$skip_agents" == "false" ]] && [[ -f AGENTS.md ]]; then echo " # Edit AGENTS.md to customize for your project" fi - if [[ "$skip_bd" == "false" ]] && command -v br &>/dev/null; then + if [[ "$skip_br" == "false" ]] && command -v br &>/dev/null; then echo " br ready # Check for work" echo " br create --title=\"...\" # Create tasks" fi diff --git a/scripts/lib/newproj_agents.sh b/scripts/lib/newproj_agents.sh index 05458ccc..f813bace 100644 --- a/scripts/lib/newproj_agents.sh +++ b/scripts/lib/newproj_agents.sh @@ -467,12 +467,9 @@ _section_issue_tracking() { All issue tracking goes through **Beads**. No other TODO systems. -**Note:** `br` is a convenience alias (installed by `acfs/zsh/acfs.zshrc`) for the real Beads CLI: `bd`. -If `br` is unavailable (CI / non-interactive shells), use `bd` directly. - Key invariants: - `.beads/` is authoritative state and **must always be committed** with code changes. -- Do not edit `.beads/*.jsonl` directly; only via `br` / `bd`. +- Do not edit `.beads/*.jsonl` directly; only via `br`. ### Basics @@ -626,14 +623,14 @@ get_sections_for_tech_stack() { # Generate AGENTS.md content # Usage: content=$(generate_agents_md "project_name" [tech_stack...]) # Options via environment: -# AGENTS_ENABLE_BD=true|false - Include bd issue tracking section +# AGENTS_ENABLE_BR=true|false - Include br issue tracking section # AGENTS_ENABLE_CONSOLE=true|false - Include console output section generate_agents_md() { local project_name="${1:-my-project}" shift local tech_stack=("$@") - local enable_bd="${AGENTS_ENABLE_BD:-false}" + local enable_br="${AGENTS_ENABLE_BR:-false}" local enable_console="${AGENTS_ENABLE_CONSOLE:-false}" local generated_at local tech_stack_list="none" @@ -656,7 +653,7 @@ generate_agents_md() { read -ra section_array <<< "$sections" # Add optional sections based on flags - if [[ "$enable_bd" == "true" ]]; then + if [[ "$enable_br" == "true" ]]; then section_array+=("issue_tracking") fi if [[ "$enable_console" == "true" ]]; then @@ -851,10 +848,10 @@ preview_agents_md() { sections=$(get_sections_for_tech_stack "${tech_stack[@]}") read -ra section_array <<< "$sections" - local enable_bd="${AGENTS_ENABLE_BD:-false}" + local enable_br="${AGENTS_ENABLE_BR:-false}" local enable_console="${AGENTS_ENABLE_CONSOLE:-false}" - if [[ "$enable_bd" == "true" ]]; then + if [[ "$enable_br" == "true" ]]; then section_array+=("issue_tracking") fi if [[ "$enable_console" == "true" ]]; then diff --git a/scripts/lib/newproj_errors.sh b/scripts/lib/newproj_errors.sh index 6760f0e0..3ca1bcc4 100644 --- a/scripts/lib/newproj_errors.sh +++ b/scripts/lib/newproj_errors.sh @@ -303,7 +303,7 @@ preflight_check() { done # Check optional commands - local optional_cmds=(bd gum glow) + local optional_cmds=(br gum glow) for cmd in "${optional_cmds[@]}"; do if ! command -v "$cmd" &>/dev/null; then warnings+=("Optional command not found: $cmd (some features may be limited)") @@ -420,14 +420,14 @@ try_git_init() { return 0 } -# Try to initialize beads (bd) -# Usage: try_bd_init "/path/to/dir" -try_bd_init() { +# Try to initialize beads (br) +# Usage: try_br_init "/path/to/dir" +try_br_init() { local dir="$1" log_debug "Initializing br in: $dir" 2>/dev/null || true - # Check if br is available (br is the binary, bd is the alias) + # Check if br is available if ! command -v br &>/dev/null; then log_warn "br not found - skipping beads initialization" 2>/dev/null || true echo -e "${NEWPROJ_YELLOW}Note: br not installed. Skipping beads setup.${NEWPROJ_NC}" diff --git a/scripts/lib/newproj_screens.sh b/scripts/lib/newproj_screens.sh index a1f03cb0..761c3a00 100644 --- a/scripts/lib/newproj_screens.sh +++ b/scripts/lib/newproj_screens.sh @@ -245,7 +245,7 @@ run_wizard_confirm_only() { local project_name="$1" local project_dir="$2" local tech_stack="$3" - local enable_bd="${4:-true}" + local enable_br="${4:-true}" local enable_claude="${5:-true}" local enable_agents="${6:-true}" local enable_ubsignore="${7:-true}" @@ -253,7 +253,7 @@ run_wizard_confirm_only() { state_set "project_name" "$project_name" state_set "project_dir" "$project_dir" state_set "tech_stack" "$tech_stack" - state_set "enable_bd" "$enable_bd" + state_set "enable_br" "$enable_br" state_set "enable_claude" "$enable_claude" state_set "enable_agents" "$enable_agents" state_set "enable_ubsignore" "$enable_ubsignore" diff --git a/scripts/lib/newproj_screens/screen_agents_preview.sh b/scripts/lib/newproj_screens/screen_agents_preview.sh index 92969ec6..d2fafdb7 100644 --- a/scripts/lib/newproj_screens/screen_agents_preview.sh +++ b/scripts/lib/newproj_screens/screen_agents_preview.sh @@ -46,7 +46,7 @@ generate_preview_content() { done # Set generation flags from state - export AGENTS_ENABLE_BD=$(state_get "enable_bd") + export AGENTS_ENABLE_BR=$(state_get "enable_br") export AGENTS_ENABLE_CONSOLE="false" # Generate content using newproj_agents.sh @@ -78,7 +78,7 @@ get_preview_summary() { done # Set generation flags from state - export AGENTS_ENABLE_BD=$(state_get "enable_bd") + export AGENTS_ENABLE_BR=$(state_get "enable_br") export AGENTS_ENABLE_CONSOLE="false" preview_agents_md "$project_name" "${tech_array[@]}" diff --git a/scripts/lib/newproj_screens/screen_confirmation.sh b/scripts/lib/newproj_screens/screen_confirmation.sh index bfc1b1af..e3805ba8 100644 --- a/scripts/lib/newproj_screens/screen_confirmation.sh +++ b/scripts/lib/newproj_screens/screen_confirmation.sh @@ -37,7 +37,7 @@ get_files_to_create() { files+=("$project_dir/AGENTS.md") fi - if [[ "$(state_get "enable_bd")" == "true" ]]; then + if [[ "$(state_get "enable_br")" == "true" ]]; then files+=("$project_dir/.beads/") files+=("$project_dir/.beads/beads.db") fi @@ -152,7 +152,7 @@ render_confirmation_screen() { echo -e "${TUI_BOLD}Features${TUI_NC}" draw_line 50 - local features=("bd:Beads tracking" "claude:Claude Code settings" "agents:AGENTS.md" "ubsignore:UBS ignore") + local features=("br:Beads tracking" "claude:Claude Code settings" "agents:AGENTS.md" "ubsignore:UBS ignore") for feat in "${features[@]}"; do local id="${feat%%:*}" local name="${feat#*:}" diff --git a/scripts/lib/newproj_screens/screen_features.sh b/scripts/lib/newproj_screens/screen_features.sh index b95fd945..473f318e 100644 --- a/scripts/lib/newproj_screens/screen_features.sh +++ b/scripts/lib/newproj_screens/screen_features.sh @@ -23,7 +23,7 @@ SCREEN_FEATURES_PREV="tech_stack" # Available features with descriptions declare -ga FEATURE_OPTIONS=( - "bd:Beads issue tracking (bd):Track work with dependencies and smart prioritization" + "br:Beads issue tracking (br):Track work with dependencies and smart prioritization" "claude:Claude Code settings:Project-specific Claude Code configuration" "agents:AGENTS.md template:Instructions for AI coding assistants" "ubsignore:UBS ignore patterns:Configure Ultimate Bug Scanner exclusions" diff --git a/scripts/lib/newproj_screens/screen_progress.sh b/scripts/lib/newproj_screens/screen_progress.sh index d5f0aaf1..498c28dc 100644 --- a/scripts/lib/newproj_screens/screen_progress.sh +++ b/scripts/lib/newproj_screens/screen_progress.sh @@ -48,9 +48,9 @@ init_creation_steps() { STEP_STATUS["create_agents"]="pending" fi - if [[ "$(state_get "enable_bd")" == "true" ]]; then - STEP_ORDER+=("init_bd") - STEP_STATUS["init_bd"]="pending" + if [[ "$(state_get "enable_br")" == "true" ]]; then + STEP_ORDER+=("init_br") + STEP_STATUS["init_br"]="pending" fi if [[ "$(state_get "enable_claude")" == "true" ]]; then @@ -77,7 +77,7 @@ get_step_name() { create_readme) echo "Creating README.md" ;; create_gitignore) echo "Creating .gitignore" ;; create_agents) echo "Generating AGENTS.md" ;; - init_bd) echo "Initializing Beads tracking" ;; + init_br) echo "Initializing Beads tracking" ;; create_claude) echo "Creating Claude Code settings" ;; create_ubsignore) echo "Creating .ubsignore" ;; finalize) echo "Finalizing project" ;; @@ -292,7 +292,7 @@ venv/ esac done - export AGENTS_ENABLE_BD=$(state_get "enable_bd") + export AGENTS_ENABLE_BR=$(state_get "enable_br") agents_content=$(generate_agents_md "$project_name" "${tech_array[@]}") fi @@ -305,8 +305,8 @@ venv/ fi ;; - init_bd) - if try_bd_init "$project_dir"; then + init_br) + if try_br_init "$project_dir"; then update_step "$step" "success" return 0 else diff --git a/scripts/lib/newproj_screens/screen_success.sh b/scripts/lib/newproj_screens/screen_success.sh index efc7bfb6..41851ea7 100644 --- a/scripts/lib/newproj_screens/screen_success.sh +++ b/scripts/lib/newproj_screens/screen_success.sh @@ -62,7 +62,7 @@ EOF echo -e " ${TUI_SUCCESS}${BOX_CHECK}${TUI_NC} AGENTS.md for AI assistants" fi - if [[ "$(state_get "enable_bd")" == "true" ]]; then + if [[ "$(state_get "enable_br")" == "true" ]]; then echo -e " ${TUI_SUCCESS}${BOX_CHECK}${TUI_NC} Beads issue tracking (.beads/)" fi @@ -89,9 +89,9 @@ EOF echo -e " ${TUI_CYAN}claude${TUI_NC}" echo "" - if [[ "$(state_get "enable_bd")" == "true" ]]; then + if [[ "$(state_get "enable_br")" == "true" ]]; then echo " 3. Create your first task:" - echo -e " ${TUI_CYAN}bd create \"First feature\" -t feature${TUI_NC}" + echo -e " ${TUI_CYAN}br create \"First feature\" -t feature${TUI_NC}" echo "" fi diff --git a/scripts/lib/newproj_tui.sh b/scripts/lib/newproj_tui.sh index b07e89d7..8f4f6207 100644 --- a/scripts/lib/newproj_tui.sh +++ b/scripts/lib/newproj_tui.sh @@ -174,7 +174,7 @@ declare -gA WIZARD_STATE=( [project_name]="" [project_dir]="" [tech_stack]="" - [enable_bd]="true" + [enable_br]="true" [enable_claude]="true" [enable_agents]="true" [enable_ubsignore]="true" @@ -213,7 +213,7 @@ state_reset() { [project_name]="" [project_dir]="" [tech_stack]="" - [enable_bd]="true" + [enable_br]="true" [enable_claude]="true" [enable_agents]="true" [enable_ubsignore]="true" diff --git a/scripts/lib/nightly_update.sh b/scripts/lib/nightly_update.sh new file mode 100755 index 00000000..ff245b6e --- /dev/null +++ b/scripts/lib/nightly_update.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# ============================================================ +# ACFS Nightly Update - Pre-flight wrapper +# +# Called by systemd timer at 4am. Checks system health before +# running acfs-update to avoid updating under adverse conditions. +# +# Pre-flight checks: +# 1. Load average - skip if system is overloaded +# 2. Disk space - skip if critically low (<2GB) +# 3. Low-risk cleanup if disk is tight (<5GB) +# 4. Run acfs-update --yes --quiet +# +# Logs to: ~/.acfs/logs/updates/nightly-YYYY-MM-DD-HHMMSS.log +# ============================================================ + +set -euo pipefail + +# Resolve home directory (systemd %h may not set HOME reliably) +HOME="${HOME:-$(getent passwd "$(id -un)" | cut -d: -f6)}" +export HOME + +TIMESTAMP="$(date '+%Y-%m-%d-%H%M%S')" +LOG_DIR="$HOME/.acfs/logs/updates" +LOG_FILE="$LOG_DIR/nightly-${TIMESTAMP}.log" + +mkdir -p "$LOG_DIR" + +# Redirect all output to log file AND journal (stdout/stderr already go to journal via systemd) +exec > >(tee -a "$LOG_FILE") 2>&1 + +log() { echo "[$(date '+%H:%M:%S')] $*"; } + +log "=== ACFS Nightly Update starting ===" +log "Date: $(date)" +log "Host: $(hostname)" + +# ── Source notification library (best-effort, non-fatal) ───── +_ACFS_NOTIFY_LIB="" +for _candidate in \ + "$HOME/.acfs/scripts/lib/notify.sh" \ + "/data/projects/agentic_coding_flywheel_setup/scripts/lib/notify.sh"; do + if [[ -f "$_candidate" ]]; then + _ACFS_NOTIFY_LIB="$_candidate" + break + fi +done +if [[ -n "$_ACFS_NOTIFY_LIB" ]]; then + # shellcheck source=scripts/lib/notify.sh + source "$_ACFS_NOTIFY_LIB" 2>/dev/null || true +fi + +# ── Pre-flight 1: Load average check ────────────────────── +NPROC="$(nproc)" +LOAD_5MIN="$(awk '{print $2}' /proc/loadavg)" + +# Compare as integers (bash can't do float comparison natively) +LOAD_INT="${LOAD_5MIN%%.*}" +if [[ "$LOAD_INT" -ge "$NPROC" ]]; then + log "SKIP: 5-min load average ($LOAD_5MIN) >= nproc ($NPROC). System overloaded." + exit 0 +fi +log "OK: Load average $LOAD_5MIN < $NPROC cores" + +# ── Pre-flight 2: Disk space check ──────────────────────── +# Get available space on root filesystem in GB +ROOT_AVAIL_KB="$(df --output=avail / | tail -1 | tr -d ' ')" +ROOT_AVAIL_GB="$((ROOT_AVAIL_KB / 1048576))" + +if [[ "$ROOT_AVAIL_GB" -lt 2 ]]; then + log "SKIP: Root filesystem has only ${ROOT_AVAIL_GB}GB free (need >= 2GB). Critically low." + exit 0 +fi +log "OK: Root filesystem has ${ROOT_AVAIL_GB}GB free" + +# ── Pre-flight 3: Low-risk cleanup if tight on space ────── +if [[ "$ROOT_AVAIL_GB" -lt 5 ]]; then + log "WARN: Disk below 5GB free (${ROOT_AVAIL_GB}GB). Running safe cleanup..." + FREED=0 + + # Clean old /tmp build artifacts (>7 days) + # Note: || true after du pipeline guards against set -eo pipefail + # killing the script if a file disappears between find and du (race). + for pattern in "cargo-install*" "rustc*" "npm-*" "bun-*"; do + while IFS= read -r -d '' dir; do + sz="$(du -sk "$dir" 2>/dev/null | cut -f1 || true)" + sz="${sz:-0}" + rm -rf "$dir" 2>/dev/null && FREED=$((FREED + sz)) && log " Cleaned: $dir (${sz}KB)" + done < <(find /tmp -maxdepth 1 -name "$pattern" -mtime +7 -print0 2>/dev/null || true) + done + + # Clean old nightly logs (>30 days) + while IFS= read -r -d '' f; do + sz="$(du -sk "$f" 2>/dev/null | cut -f1 || true)" + sz="${sz:-0}" + rm -f "$f" 2>/dev/null && FREED=$((FREED + sz)) && log " Cleaned: $f (${sz}KB)" + done < <(find "$LOG_DIR" -name "nightly-*.log" -mtime +30 -print0 2>/dev/null || true) + + # Cargo registry cache if > 500MB + CARGO_REGISTRY="$HOME/.cargo/registry/cache" + if [[ -d "$CARGO_REGISTRY" ]]; then + REG_SIZE_KB="$(du -sk "$CARGO_REGISTRY" 2>/dev/null | cut -f1 || true)" + REG_SIZE_KB="${REG_SIZE_KB:-0}" + if [[ "$REG_SIZE_KB" -gt 512000 ]]; then + rm -rf "$CARGO_REGISTRY" 2>/dev/null || true + FREED=$((FREED + REG_SIZE_KB)) + log " Cleaned: cargo registry cache (${REG_SIZE_KB}KB)" + fi + fi + + # Bun install cache if > 500MB + BUN_CACHE="$HOME/.bun/install/cache" + if [[ -d "$BUN_CACHE" ]]; then + BUN_SIZE_KB="$(du -sk "$BUN_CACHE" 2>/dev/null | cut -f1 || true)" + BUN_SIZE_KB="${BUN_SIZE_KB:-0}" + if [[ "$BUN_SIZE_KB" -gt 512000 ]]; then + rm -rf "$BUN_CACHE" 2>/dev/null || true + FREED=$((FREED + BUN_SIZE_KB)) + log " Cleaned: bun install cache (${BUN_SIZE_KB}KB)" + fi + fi + + log "Cleanup freed ~$((FREED / 1024))MB" +fi + +# ── Run acfs-update ─────────────────────────────────────── +ACFS_UPDATE="" +for candidate in \ + "$HOME/.local/bin/acfs-update" \ + "$HOME/.acfs/scripts/lib/update.sh" \ + "/data/projects/agentic_coding_flywheel_setup/scripts/acfs-update"; do + if [[ -x "$candidate" ]]; then + ACFS_UPDATE="$candidate" + break + fi +done + +if [[ -z "$ACFS_UPDATE" ]]; then + log "ERROR: acfs-update not found in any expected location" + exit 1 +fi + +log "Running: $ACFS_UPDATE --yes --quiet" +log "---" + +# Run update; capture exit code but don't fail the whole script +set +e +"$ACFS_UPDATE" --yes --quiet +UPDATE_RC=$? +set -e + +log "---" +if [[ "$UPDATE_RC" -eq 0 ]]; then + log "=== Nightly update completed successfully ===" + if type -t acfs_notify_update_success &>/dev/null; then + acfs_notify_update_success 2>/dev/null || true + fi +else + log "=== Nightly update finished with exit code $UPDATE_RC ===" + if type -t acfs_notify_update_failure &>/dev/null; then + acfs_notify_update_failure "exit code $UPDATE_RC" 2>/dev/null || true + fi +fi + +exit "$UPDATE_RC" diff --git a/scripts/lib/notifications.sh b/scripts/lib/notifications.sh new file mode 100644 index 00000000..ce80ebc8 --- /dev/null +++ b/scripts/lib/notifications.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env bash +# ============================================================ +# ACFS CLI - Notification Management Subcommand +# +# Manages ntfy.sh push notifications for ACFS events. +# Usage: acfs notifications <enable|disable|test|status|topic|set-server> +# +# Config stored at: ~/.config/acfs/config.yaml +# Related bead: bd-2igt6 +# ============================================================ + +set -euo pipefail + +# ============================================================ +# Constants +# ============================================================ +ACFS_CONFIG_DIR="${HOME}/.config/acfs" +ACFS_CONFIG_FILE="${ACFS_CONFIG_DIR}/config.yaml" +ACFS_NTFY_SERVER_DEFAULT="https://ntfy.sh" + +# ============================================================ +# Helpers +# ============================================================ + +# Read a key from config.yaml (simple YAML parser) +_notif_config_read() { + local key="$1" + if [[ ! -f "$ACFS_CONFIG_FILE" ]]; then + return 0 + fi + local val + val=$(grep -E "^\s*${key}\s*:" "$ACFS_CONFIG_FILE" 2>/dev/null | head -1 | \ + sed -E "s/^\s*${key}\s*:\s*//; s/^[\"']//; s/[\"']\s*$//" | \ + sed 's/^[[:space:]]*//; s/[[:space:]]*$//') || true + printf '%s' "$val" +} + +# Write a key to config.yaml (upsert) +_notif_config_write() { + local key="$1" + local value="$2" + + # Ensure config dir exists + mkdir -p "$ACFS_CONFIG_DIR" + + if [[ ! -f "$ACFS_CONFIG_FILE" ]]; then + # Create new config file + printf '%s: %s\n' "$key" "$value" > "$ACFS_CONFIG_FILE" + return 0 + fi + + # Check if key already exists + if grep -qE "^\s*${key}\s*:" "$ACFS_CONFIG_FILE" 2>/dev/null; then + # Update existing key in-place + sed -i -E "s|^\s*${key}\s*:.*|${key}: ${value}|" "$ACFS_CONFIG_FILE" + else + # Append new key + printf '%s: %s\n' "$key" "$value" >> "$ACFS_CONFIG_FILE" + fi +} + +# Generate a random topic string: acfs-HOSTNAME-RANDOM8 +_notif_generate_topic() { + local hostname + hostname=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "host") + # Sanitize hostname: lowercase, alphanumeric + hyphens only + hostname=$(printf '%s' "$hostname" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-') + # Generate 8 random hex chars + local random_part + if [[ -r /dev/urandom ]]; then + random_part=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') + else + random_part=$(printf '%04x%04x' $((RANDOM % 65536)) $((RANDOM % 65536))) + fi + printf 'acfs-%s-%s' "$hostname" "$random_part" +} + +# ============================================================ +# Subcommands +# ============================================================ + +cmd_enable() { + local topic + topic=$(_notif_config_read "ntfy_topic") + + # Generate new topic if none exists + if [[ -z "$topic" ]]; then + topic=$(_notif_generate_topic) + fi + + local server + server=$(_notif_config_read "ntfy_server") + if [[ -z "$server" ]]; then + server="$ACFS_NTFY_SERVER_DEFAULT" + fi + + # Write config + _notif_config_write "ntfy_enabled" "true" + _notif_config_write "ntfy_topic" "$topic" + _notif_config_write "ntfy_server" "$server" + + echo "Notifications enabled!" + echo "" + echo "Subscribe on your phone or browser:" + echo " ${server}/${topic}" + echo "" + echo "Mobile apps:" + echo " Android: https://play.google.com/store/apps/details?id=io.heckel.ntfy" + echo " iOS: https://apps.apple.com/us/app/ntfy/id1625396347" + echo "" + echo "Or open the URL above in any browser." + echo "" + echo "Test it with: acfs notifications test" +} + +cmd_disable() { + _notif_config_write "ntfy_enabled" "false" + echo "Notifications disabled." +} + +cmd_test() { + local enabled + enabled=$(_notif_config_read "ntfy_enabled") + if [[ "$enabled" != "true" ]]; then + echo "Notifications are not enabled. Run 'acfs notifications enable' first." + return 1 + fi + + local topic server + topic=$(_notif_config_read "ntfy_topic") + server=$(_notif_config_read "ntfy_server") + server="${server:-$ACFS_NTFY_SERVER_DEFAULT}" + + if [[ -z "$topic" ]]; then + echo "Error: No topic configured. Run 'acfs notifications enable' first." + return 1 + fi + + echo "Sending test notification to ${server}/${topic} ..." + + local http_code + http_code=$(curl -s -o /dev/null -w '%{http_code}' \ + --max-time 10 \ + -H "Title: ACFS Test Notification" \ + -H "Priority: default" \ + -H "Tags: white_check_mark,acfs" \ + -d "If you see this, ACFS notifications are working! ($(hostname 2>/dev/null || echo 'unknown'))" \ + "${server}/${topic}" 2>/dev/null) || http_code="000" + + if [[ "$http_code" =~ ^2 ]]; then + echo "Test notification sent successfully (HTTP ${http_code})." + echo "Check your subscribed device." + else + echo "Failed to send test notification (HTTP ${http_code})." + echo "Check your network connection and server URL." + return 1 + fi +} + +cmd_status() { + local enabled topic server priority + + if [[ ! -f "$ACFS_CONFIG_FILE" ]]; then + echo "Notifications: not configured" + echo "Config file: ${ACFS_CONFIG_FILE} (not found)" + echo "" + echo "Run 'acfs notifications enable' to set up." + return 0 + fi + + enabled=$(_notif_config_read "ntfy_enabled") + topic=$(_notif_config_read "ntfy_topic") + server=$(_notif_config_read "ntfy_server") + server="${server:-$ACFS_NTFY_SERVER_DEFAULT}" + priority=$(_notif_config_read "ntfy_priority") + priority="${priority:-default}" + + echo "Notifications: ${enabled:-not set}" + echo "Topic: ${topic:-not set}" + echo "Server: ${server}" + echo "Priority: ${priority}" + echo "Config file: ${ACFS_CONFIG_FILE}" + + if [[ "$enabled" == "true" ]] && [[ -n "$topic" ]]; then + echo "" + echo "Subscribe URL: ${server}/${topic}" + fi +} + +cmd_topic() { + local enabled topic server + + enabled=$(_notif_config_read "ntfy_enabled") + topic=$(_notif_config_read "ntfy_topic") + server=$(_notif_config_read "ntfy_server") + server="${server:-$ACFS_NTFY_SERVER_DEFAULT}" + + if [[ -z "$topic" ]]; then + echo "No topic configured. Run 'acfs notifications enable' first." + return 1 + fi + + echo "${server}/${topic}" +} + +cmd_set_server() { + local new_server="${1:-}" + + if [[ -z "$new_server" ]]; then + echo "Usage: acfs notifications set-server <url>" + echo "Example: acfs notifications set-server https://ntfy.example.com" + return 1 + fi + + # Basic URL validation + if [[ ! "$new_server" =~ ^https?:// ]]; then + echo "Error: Server URL must start with http:// or https://" + return 1 + fi + + # Strip trailing slash + new_server="${new_server%/}" + + _notif_config_write "ntfy_server" "$new_server" + echo "ntfy server set to: ${new_server}" + + local topic + topic=$(_notif_config_read "ntfy_topic") + if [[ -n "$topic" ]]; then + echo "Subscribe URL: ${new_server}/${topic}" + fi +} + +cmd_set_priority() { + local new_priority="${1:-}" + + if [[ -z "$new_priority" ]]; then + echo "Usage: acfs notifications set-priority <priority>" + echo "Options: min, low, default, high, urgent (or 1-5)" + return 1 + fi + + # Validate priority + case "$new_priority" in + min|low|default|high|urgent|1|2|3|4|5) + ;; + *) + echo "Error: Invalid priority '$new_priority'" + echo "Options: min, low, default, high, urgent (or 1-5)" + return 1 + ;; + esac + + _notif_config_write "ntfy_priority" "$new_priority" + echo "Default notification priority set to: ${new_priority}" +} + +cmd_set_topic() { + local new_topic="${1:-}" + + if [[ -z "$new_topic" ]]; then + echo "Usage: acfs notifications set-topic <topic>" + echo "Example: acfs notifications set-topic acfs-myserver-secret123" + return 1 + fi + + _notif_config_write "ntfy_topic" "$new_topic" + echo "ntfy topic set to: ${new_topic}" + + local server + server=$(_notif_config_read "ntfy_server") + server="${server:-$ACFS_NTFY_SERVER_DEFAULT}" + echo "Subscribe URL: ${server}/${new_topic}" +} + +cmd_send() { + local title="${1:-}" + local body="${2:-}" + local priority="${3:-}" + + if [[ -z "$title" ]]; then + echo "Usage: acfs notifications send <title> [body] [priority]" + echo "" + echo "Send an ad-hoc notification via ntfy.sh." + echo "" + echo "Examples:" + echo " acfs notifications send 'Build done' 'All tests passed'" + echo " acfs notifications send 'Deploy failed' 'See logs' high" + return 1 + fi + + local enabled + enabled=$(_notif_config_read "ntfy_enabled") + if [[ "$enabled" != "true" ]]; then + echo "Notifications are not enabled. Run 'acfs notifications enable' first." + return 1 + fi + + local topic server + topic=$(_notif_config_read "ntfy_topic") + server=$(_notif_config_read "ntfy_server") + server="${server:-$ACFS_NTFY_SERVER_DEFAULT}" + + if [[ -z "$topic" ]]; then + echo "Error: No topic configured. Run 'acfs notifications enable' first." + return 1 + fi + + # Resolve priority (arg > config > default) + if [[ -z "$priority" ]]; then + priority=$(_notif_config_read "ntfy_priority") + fi + priority="${priority:-default}" + + echo "Sending notification to ${server}/${topic} ..." + + local http_code + http_code=$(curl -s -o /dev/null -w '%{http_code}' \ + --max-time 10 \ + -H "Title: ${title}" \ + -H "Priority: ${priority}" \ + -H "Tags: computer,acfs" \ + -d "${body:-$title}" \ + "${server}/${topic}" 2>/dev/null) || http_code="000" + + if [[ "$http_code" =~ ^2 ]]; then + echo "Notification sent (HTTP ${http_code})." + else + echo "Failed to send notification (HTTP ${http_code})." + return 1 + fi +} + +# ============================================================ +# Usage / Help +# ============================================================ + +show_help() { + echo "ACFS Notifications - Push notifications via ntfy.sh" + echo "" + echo "Usage: acfs notifications <command>" + echo "" + echo "Setup:" + echo " enable Enable notifications (generates random topic)" + echo " disable Disable notifications" + echo " status Show current notification config" + echo "" + echo "Configuration:" + echo " set-server URL Use a custom ntfy server" + echo " set-topic TOPIC Set a custom topic name" + echo " set-priority PRIO Set default priority (min/low/default/high/urgent)" + echo " topic Print the subscribe URL" + echo "" + echo "Actions:" + echo " test Send a test notification" + echo " send TITLE [BODY] [PRIORITY] Send a custom notification" + echo "" + echo "Config: ${ACFS_CONFIG_FILE}" + echo "" + echo "How it works:" + echo " 1. Run 'acfs notifications enable'" + echo " 2. Subscribe to the topic URL on your phone (ntfy app) or browser" + echo " 3. ACFS sends notifications for:" + echo " - Install success/failure" + echo " - Agent task completion/failure" + echo " - Human attention needed (urgent)" + echo " - Nightly update results" + echo "" + echo "Scripting (source scripts/lib/notify.sh):" + echo " acfs_notify <title> [body] [priority] [tags]" + echo " acfs_notify_task_complete <task> [agent] [detail]" + echo " acfs_notify_task_failed <task> [error] [agent]" + echo " acfs_notify_human_needed <reason> [context] [agent]" + echo " acfs_notify_debounced <key> <title> [body] [priority] [tags]" + echo "" + echo "Environment overrides:" + echo " ACFS_NTFY_ENABLED=true|false" + echo " ACFS_NTFY_TOPIC=<topic>" + echo " ACFS_NTFY_SERVER=<url>" + echo " ACFS_NTFY_PRIORITY=<priority>" + echo " ACFS_NTFY_DEBOUNCE_SECONDS=<seconds> (default: 30)" +} + +# ============================================================ +# Dispatcher +# ============================================================ + +main() { + local subcmd="${1:-help}" + shift 1 2>/dev/null || true + + case "$subcmd" in + enable) + cmd_enable "$@" + ;; + disable) + cmd_disable "$@" + ;; + test) + cmd_test "$@" + ;; + status) + cmd_status "$@" + ;; + topic) + cmd_topic "$@" + ;; + set-server) + cmd_set_server "$@" + ;; + set-priority) + cmd_set_priority "$@" + ;; + set-topic) + cmd_set_topic "$@" + ;; + send) + cmd_send "$@" + ;; + help|-h|--help) + show_help + ;; + *) + echo "Unknown command: $subcmd" + echo "Run 'acfs notifications help' for available commands." + return 1 + ;; + esac +} + +main "$@" diff --git a/scripts/lib/notify.sh b/scripts/lib/notify.sh new file mode 100644 index 00000000..02062e78 --- /dev/null +++ b/scripts/lib/notify.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash +# ============================================================ +# ACFS Installer - ntfy.sh Notification Library +# +# Provides lightweight push notifications via ntfy.sh for +# installation events, agent completions, and system alerts. +# Zero-config: silent no-op when disabled or unconfigured. +# +# Related: GitHub issue #131, bead bd-2igt6 +# +# Configuration (env vars override config.yaml values): +# ACFS_NTFY_TOPIC - required, the ntfy topic +# ACFS_NTFY_SERVER - optional, defaults to https://ntfy.sh +# ACFS_NTFY_PRIORITY - optional, default priority (min/low/default/high/urgent) +# ACFS_NTFY_ENABLED - optional, defaults to "true" if topic is set +# +# Config file: ~/.config/acfs/config.yaml +# Keys: ntfy_enabled, ntfy_topic, ntfy_server, ntfy_priority +# ============================================================ + +# Prevent multiple sourcing +if [[ -n "${_ACFS_NOTIFY_SH_LOADED:-}" ]]; then + return 0 +fi +_ACFS_NOTIFY_SH_LOADED=1 + +# ============================================================ +# Configuration +# ============================================================ + +# Default ntfy server +ACFS_NTFY_SERVER_DEFAULT="https://ntfy.sh" + +# Rate-limit state directory (per-user) +_ACFS_NOTIFY_STATE_DIR="${HOME}/.cache/acfs/notify" + +# Minimum seconds between notifications with the same debounce key. +# Override with ACFS_NTFY_DEBOUNCE_SECONDS (default: 30). +_ACFS_NOTIFY_DEBOUNCE_SECONDS="${ACFS_NTFY_DEBOUNCE_SECONDS:-30}" + +# ============================================================ +# Config Reader +# ============================================================ + +# Read a single key from ACFS config.yaml +# Usage: _acfs_notify_config_read <key> +# Returns: value on stdout, or empty string +_acfs_notify_config_read() { + local key="$1" + local config_file="${HOME}/.config/acfs/config.yaml" + + # Also check target user's config if running as root + if [[ "$(id -u)" -eq 0 ]] && [[ -n "${TARGET_HOME:-}" ]]; then + config_file="${TARGET_HOME}/.config/acfs/config.yaml" + fi + + if [[ ! -f "$config_file" ]]; then + return 0 + fi + + # Simple YAML parsing: key: "value" or key: 'value' or key: value + local val + val=$(grep -E "^\s*${key}\s*:" "$config_file" 2>/dev/null | head -1 | \ + sed -E "s/^\s*${key}\s*:\s*//; s/^[\"']//; s/[\"']\s*$//" | \ + sed 's/^[[:space:]]*//; s/[[:space:]]*$//') || true + + printf '%s' "$val" +} + +# ============================================================ +# Rate Limiting / Debounce +# ============================================================ + +# Check whether a notification with the given debounce key was sent +# recently (within _ACFS_NOTIFY_DEBOUNCE_SECONDS). Returns 0 if +# sending is allowed, 1 if the notification should be suppressed. +# When allowed, records the current timestamp. +# +# Usage: _acfs_notify_debounce_allowed <key> +_acfs_notify_debounce_allowed() { + local key="$1" + + # Debounce disabled (0 or negative) + if [[ "$_ACFS_NOTIFY_DEBOUNCE_SECONDS" -le 0 ]] 2>/dev/null; then + return 0 + fi + + mkdir -p "$_ACFS_NOTIFY_STATE_DIR" 2>/dev/null || return 0 + + # Sanitise the key so it is safe for a filename + local safe_key + safe_key=$(printf '%s' "$key" | tr -cd 'A-Za-z0-9_-') + local stamp_file="${_ACFS_NOTIFY_STATE_DIR}/${safe_key}.ts" + + local now last_ts + now=$(date +%s 2>/dev/null) || return 0 + + if [[ -f "$stamp_file" ]]; then + last_ts=$(cat "$stamp_file" 2>/dev/null) || last_ts=0 + if [[ "$last_ts" =~ ^[0-9]+$ ]]; then + local elapsed=$((now - last_ts)) + if [[ $elapsed -lt $_ACFS_NOTIFY_DEBOUNCE_SECONDS ]]; then + # Too soon -- suppress + return 1 + fi + fi + fi + + # Record timestamp and allow + printf '%s' "$now" > "$stamp_file" 2>/dev/null || true + return 0 +} + +# ============================================================ +# Core Notification Function +# ============================================================ + +# Send a notification via ntfy.sh (non-blocking, best-effort) +# +# Usage: acfs_notify <title> [body] [priority] [tags] +# title: Short notification title (required) +# body: Longer description (optional, default: "") +# priority: ntfy priority 1-5 or name (optional) +# 1=min, 2=low, 3=default, 4=high, 5=urgent +# Falls back to ACFS_NTFY_PRIORITY env / config, then "default". +# tags: Comma-separated ntfy tags/emoji shortcodes (optional) +# Falls back to "computer,acfs". +# +# Environment overrides: +# ACFS_NTFY_ENABLED=true|false Override config +# ACFS_NTFY_TOPIC=<topic> Override config +# ACFS_NTFY_SERVER=<url> Override config +# ACFS_NTFY_PRIORITY=<prio> Override default priority +# +# Returns: 0 always (never fails, never blocks) +acfs_notify() { + local title="${1:-}" + local body="${2:-}" + local priority="${3:-}" + local tags="${4:-computer,acfs}" + + # Must have a title + if [[ -z "$title" ]]; then + return 0 + fi + + # Check if enabled (env override > config file) + local enabled="${ACFS_NTFY_ENABLED:-}" + if [[ -z "$enabled" ]]; then + enabled=$(_acfs_notify_config_read "ntfy_enabled") + fi + + # Not enabled or explicitly disabled -> silent no-op + if [[ "$enabled" != "true" ]]; then + return 0 + fi + + # Read topic (env override > config file) + local topic="${ACFS_NTFY_TOPIC:-}" + if [[ -z "$topic" ]]; then + topic=$(_acfs_notify_config_read "ntfy_topic") + fi + + # No topic configured -> silent no-op + if [[ -z "$topic" ]]; then + return 0 + fi + + # Read server (env override > config file > default) + local server="${ACFS_NTFY_SERVER:-}" + if [[ -z "$server" ]]; then + server=$(_acfs_notify_config_read "ntfy_server") + fi + if [[ -z "$server" ]]; then + server="$ACFS_NTFY_SERVER_DEFAULT" + fi + + # Resolve priority (arg > env > config > "default") + if [[ -z "$priority" ]]; then + priority="${ACFS_NTFY_PRIORITY:-}" + fi + if [[ -z "$priority" ]]; then + priority=$(_acfs_notify_config_read "ntfy_priority") + fi + if [[ -z "$priority" ]]; then + priority="default" + fi + + # Require curl + if ! command -v curl &>/dev/null; then + return 0 + fi + + # Send notification in background (non-blocking, fire-and-forget) + ( + curl -s -o /dev/null \ + --max-time 10 \ + -H "Title: ${title}" \ + -H "Priority: ${priority}" \ + -H "Tags: ${tags}" \ + -d "${body:-$title}" \ + "${server}/${topic}" 2>/dev/null || true + ) & + disown 2>/dev/null || true + + return 0 +} + +# Send a notification with rate limiting (debounce). +# Same arguments as acfs_notify, plus a leading debounce key. +# +# Usage: acfs_notify_debounced <debounce_key> <title> [body] [priority] [tags] +# +# If a notification with the same debounce_key was sent within the +# last ACFS_NTFY_DEBOUNCE_SECONDS (default 30), the call is a no-op. +acfs_notify_debounced() { + local debounce_key="${1:-}" + shift 1 2>/dev/null || true + + if [[ -z "$debounce_key" ]]; then + acfs_notify "$@" + return 0 + fi + + if _acfs_notify_debounce_allowed "$debounce_key"; then + acfs_notify "$@" + fi + + return 0 +} + +# ============================================================ +# Convenience Wrappers - Installation +# ============================================================ + +# Notify install success +# Usage: acfs_notify_install_success [duration_human] +acfs_notify_install_success() { + local duration="${1:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + local body="Host: ${hostname}" + if [[ -n "$duration" ]]; then + body="${body} | Duration: ${duration}" + fi + acfs_notify "ACFS Install Complete" "$body" "default" "white_check_mark,acfs" +} + +# Notify install failure +# Usage: acfs_notify_install_failure [error_msg] +acfs_notify_install_failure() { + local error="${1:-Unknown error}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + acfs_notify "ACFS Install Failed" "Host: ${hostname} | Error: ${error}" "high" "x,acfs" +} + +# ============================================================ +# Convenience Wrappers - Agent Task Lifecycle +# ============================================================ + +# Notify that an agent task completed successfully. +# Usage: acfs_notify_task_complete <task_description> [agent_name] [extra_detail] +acfs_notify_task_complete() { + local task="${1:-Task}" + local agent="${2:-}" + local detail="${3:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + + local body="Host: ${hostname}" + if [[ -n "$agent" ]]; then + body="${body} | Agent: ${agent}" + fi + if [[ -n "$detail" ]]; then + body="${body} | ${detail}" + fi + + acfs_notify_debounced "task-complete-${task}" \ + "Task Complete: ${task}" "$body" "default" "white_check_mark,robot,acfs" +} + +# Notify that an agent task failed. +# Usage: acfs_notify_task_failed <task_description> [error_msg] [agent_name] +acfs_notify_task_failed() { + local task="${1:-Task}" + local error="${2:-Unknown error}" + local agent="${3:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + + local body="Host: ${hostname} | Error: ${error}" + if [[ -n "$agent" ]]; then + body="${body} | Agent: ${agent}" + fi + + acfs_notify_debounced "task-failed-${task}" \ + "Task Failed: ${task}" "$body" "high" "x,robot,acfs" +} + +# Notify that human attention is needed (e.g., approval, decision, stuck state). +# Usage: acfs_notify_human_needed <reason> [context] [agent_name] +acfs_notify_human_needed() { + local reason="${1:-Attention needed}" + local context="${2:-}" + local agent="${3:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + + local body="Host: ${hostname}" + if [[ -n "$agent" ]]; then + body="${body} | Agent: ${agent}" + fi + if [[ -n "$context" ]]; then + body="${body} | ${context}" + fi + + acfs_notify_debounced "human-needed" \ + "Human Needed: ${reason}" "$body" "urgent" "warning,sos,acfs" +} + +# ============================================================ +# Convenience Wrappers - System Events +# ============================================================ + +# Notify nightly update success +# Usage: acfs_notify_update_success [detail] +acfs_notify_update_success() { + local detail="${1:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + local body="Host: ${hostname}" + if [[ -n "$detail" ]]; then + body="${body} | ${detail}" + fi + acfs_notify "ACFS Update Complete" "$body" "low" "arrows_counterclockwise,acfs" +} + +# Notify nightly update failure +# Usage: acfs_notify_update_failure [error_msg] +acfs_notify_update_failure() { + local error="${1:-Unknown error}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + acfs_notify "ACFS Update Failed" "Host: ${hostname} | Error: ${error}" "high" "warning,acfs" +} + +# Notify a critical error during any ACFS operation. +# Usage: acfs_notify_error <title> [detail] +acfs_notify_error() { + local title="${1:-ACFS Error}" + local detail="${2:-}" + local hostname + hostname=$(hostname 2>/dev/null || echo "unknown") + local body="Host: ${hostname}" + if [[ -n "$detail" ]]; then + body="${body} | ${detail}" + fi + acfs_notify_debounced "error-${title}" \ + "$title" "$body" "high" "rotating_light,acfs" +} diff --git a/scripts/lib/security.sh b/scripts/lib/security.sh index 069ee860..57eed771 100755 --- a/scripts/lib/security.sh +++ b/scripts/lib/security.sh @@ -17,13 +17,27 @@ if [[ -z "${ACFS_BLUE:-}" ]]; then source "$SECURITY_SCRIPT_DIR/logging.sh" 2>/dev/null || true fi +# Fallback logging if logging.sh was not sourced or failed to load +if ! declare -f log_success &>/dev/null; then + log_success() { printf "OK: %s\n" "$1" >&2; } + log_error() { printf "ERROR: %s\n" "$1" >&2; } + log_info() { printf "INFO: %s\n" "$1" >&2; } + log_warn() { printf "WARN: %s\n" "$1" >&2; } + log_step() { printf "[%s] %s\n" "$1" "$2" >&2; } + log_detail() { printf " %s\n" "$1" >&2; } + log_fatal() { printf "FATAL: %s\n" "$1" >&2; exit 1; } +fi + # Color aliases for backward compatibility (used by display functions below) -CYAN="${ACFS_BLUE:-\033[0;36m}" -DIM="${ACFS_GRAY:-\033[0;90m}" -NC="${ACFS_NC:-\033[0m}" -RED="${ACFS_RED:-\033[0;31m}" -GREEN="${ACFS_GREEN:-\033[0;32m}" -YELLOW="${ACFS_YELLOW:-\033[0;33m}" +# Respects NO_COLOR standard via logging.sh's ACFS_* variables. +# Use ${var-default} (not ${var:-default}) to preserve empty strings. +# Related: bd-39ye +CYAN="${ACFS_BLUE-\033[0;36m}" +DIM="${ACFS_GRAY-\033[0;90m}" +NC="${ACFS_NC-\033[0m}" +RED="${ACFS_RED-\033[0;31m}" +GREEN="${ACFS_GREEN-\033[0;32m}" +YELLOW="${ACFS_YELLOW-\033[0;33m}" # ============================================================ # Configuration @@ -69,11 +83,36 @@ acfs_is_retryable_curl_exit_code() { # $2 - Output path # $3 - Name (for logging) # Returns: 0 on success, curl exit code on failure +# +# For GitHub URLs, uses github_fetch_with_backoff for rate limit handling. +# Related: bd-1lug acfs_download_to_file() { local url="$1" local output_path="$2" local name="${3:-$url}" + # Ensure parent dir exists + mkdir -p "$(dirname "$output_path")" + + # Use GitHub-specific backoff for GitHub URLs (rate limit handling) + if [[ "$url" == *"github.com"* || "$url" == *"githubusercontent.com"* ]]; then + # Load github_api.sh if not already loaded + if ! declare -f github_fetch_with_backoff &>/dev/null; then + local github_lib="$SECURITY_SCRIPT_DIR/github_api.sh" + if [[ -r "$github_lib" ]]; then + # shellcheck source=github_api.sh + source "$github_lib" + fi + fi + + # Use backoff if available, fallback to standard fetch + if declare -f github_fetch_with_backoff &>/dev/null; then + github_fetch_with_backoff "$url" "$output_path" "$name" + return $? + fi + fi + + # Standard retry logic for non-GitHub URLs local max_attempts="${#ACFS_CURL_RETRY_DELAYS[@]}" if (( max_attempts == 0 )); then ACFS_CURL_RETRY_DELAYS=(0 5 15) @@ -83,9 +122,6 @@ acfs_download_to_file() { local retries=$((max_attempts - 1)) local attempt delay status=0 - # Ensure parent dir exists - mkdir -p "$(dirname "$output_path")" - for ((attempt=0; attempt<max_attempts; attempt++)); do delay="${ACFS_CURL_RETRY_DELAYS[$attempt]}" @@ -531,9 +567,10 @@ load_checksums() { local tool_indent="" if [[ ! -r "$file" ]]; then - # Use ACFS_YELLOW if available (logging.sh), else literal or plain - local warn_color="${ACFS_YELLOW:-\033[0;33m}" - local nc_color="${ACFS_NC:-\033[0m}" + # Use ACFS_YELLOW if available (logging.sh), else literal or plain. + # Use ${var-default} to preserve empty strings for NO_COLOR. Related: bd-39ye + local warn_color="${ACFS_YELLOW-\033[0;33m}" + local nc_color="${ACFS_NC-\033[0m}" echo -e "${warn_color}Warning:${nc_color} Checksums file not found: $file" >&2 return 1 fi diff --git a/scripts/lib/smoke_test.sh b/scripts/lib/smoke_test.sh index 7f05251e..edf932ac 100644 --- a/scripts/lib/smoke_test.sh +++ b/scripts/lib/smoke_test.sh @@ -30,18 +30,20 @@ TARGET_HOME="${TARGET_HOME:-/home/$TARGET_USER}" # Output Helpers # ============================================================ +# Use ${var-default} (not ${var:-default}) to preserve empty strings for NO_COLOR. +# Related: bd-39ye _smoke_pass() { local label="$1" - echo -e " ${ACFS_GREEN:-\033[0;32m}✅${ACFS_NC:-\033[0m} $label" + echo -e " ${ACFS_GREEN-\033[0;32m}✅${ACFS_NC-\033[0m} $label" ((CRITICAL_PASS += 1)) } _smoke_fail() { local label="$1" local fix="${2:-}" - echo -e " ${ACFS_RED:-\033[0;31m}❌${ACFS_NC:-\033[0m} $label" + echo -e " ${ACFS_RED-\033[0;31m}❌${ACFS_NC-\033[0m} $label" if [[ -n "$fix" ]]; then - echo -e " ${ACFS_GRAY:-\033[0;90m}Fix: $fix${ACFS_NC:-\033[0m}" + echo -e " ${ACFS_GRAY-\033[0;90m}Fix: $fix${ACFS_NC-\033[0m}" fi ((CRITICAL_FAIL += 1)) } @@ -49,9 +51,9 @@ _smoke_fail() { _smoke_warn() { local label="$1" local note="${2:-}" - echo -e " ${ACFS_YELLOW:-\033[0;33m}⚠️${ACFS_NC:-\033[0m} $label" + echo -e " ${ACFS_YELLOW-\033[0;33m}⚠️${ACFS_NC-\033[0m} $label" if [[ -n "$note" ]]; then - echo -e " ${ACFS_GRAY:-\033[0;90m}$note${ACFS_NC:-\033[0m}" + echo -e " ${ACFS_GRAY-\033[0;90m}$note${ACFS_NC-\033[0m}" fi ((WARNING_COUNT += 1)) } @@ -59,13 +61,13 @@ _smoke_warn() { # Non-critical pass (doesn't affect critical count) _smoke_info() { local label="$1" - echo -e " ${ACFS_GREEN:-\033[0;32m}✅${ACFS_NC:-\033[0m} $label" + echo -e " ${ACFS_GREEN-\033[0;32m}✅${ACFS_NC-\033[0m} $label" ((NONCRITICAL_PASS += 1)) } _smoke_header() { echo "" - echo -e "${ACFS_BLUE:-\033[0;34m}[Smoke Test]${ACFS_NC:-\033[0m}" + echo -e "${ACFS_BLUE-\033[0;34m}[Smoke Test]${ACFS_NC-\033[0m}" echo "" } @@ -314,25 +316,25 @@ run_smoke_test() { local total_critical=$((CRITICAL_PASS + CRITICAL_FAIL)) if [[ $CRITICAL_FAIL -eq 0 ]]; then - echo -e "${ACFS_GREEN:-\033[0;32m}Smoke test: $CRITICAL_PASS/$total_critical critical passed${ACFS_NC:-\033[0m}" + echo -e "${ACFS_GREEN-\033[0;32m}Smoke test: $CRITICAL_PASS/$total_critical critical passed${ACFS_NC-\033[0m}" else - echo -e "${ACFS_RED:-\033[0;31m}Smoke test: $CRITICAL_PASS/$total_critical critical passed, $CRITICAL_FAIL failed${ACFS_NC:-\033[0m}" + echo -e "${ACFS_RED-\033[0;31m}Smoke test: $CRITICAL_PASS/$total_critical critical passed, $CRITICAL_FAIL failed${ACFS_NC-\033[0m}" fi if [[ $WARNING_COUNT -gt 0 ]]; then - echo -e "${ACFS_YELLOW:-\033[0;33m}$WARNING_COUNT warning(s)${ACFS_NC:-\033[0m}" + echo -e "${ACFS_YELLOW-\033[0;33m}$WARNING_COUNT warning(s)${ACFS_NC-\033[0m}" fi - echo -e "${ACFS_GRAY:-\033[0;90m}Completed in ${duration}s${ACFS_NC:-\033[0m}" + echo -e "${ACFS_GRAY-\033[0;90m}Completed in ${duration}s${ACFS_NC-\033[0m}" echo "" # Return exit code based on critical failures if [[ $CRITICAL_FAIL -gt 0 ]]; then - echo -e "${ACFS_YELLOW:-\033[0;33m}Some critical checks failed. Run 'acfs doctor' for detailed diagnostics.${ACFS_NC:-\033[0m}" + echo -e "${ACFS_YELLOW-\033[0;33m}Some critical checks failed. Run 'acfs doctor' for detailed diagnostics.${ACFS_NC-\033[0m}" return 1 fi - echo -e "${ACFS_GREEN:-\033[0;32m}Installation successful! Run 'onboard' to start the tutorial.${ACFS_NC:-\033[0m}" + echo -e "${ACFS_GREEN-\033[0;32m}Installation successful! Run 'onboard' to start the tutorial.${ACFS_NC-\033[0m}" return 0 } diff --git a/scripts/lib/state.sh b/scripts/lib/state.sh index 1cdfa6f0..b3b19010 100644 --- a/scripts/lib/state.sh +++ b/scripts/lib/state.sh @@ -22,6 +22,26 @@ if [[ -n "${_ACFS_STATE_SH_LOADED:-}" ]]; then fi _ACFS_STATE_SH_LOADED=1 +# ============================================================ +# Color Constants - respect NO_COLOR standard (https://no-color.org/) +# Related: bd-39ye +# ============================================================ +if [[ -z "${NO_COLOR:-}" ]] && [[ -t 2 ]]; then + _STATE_BLUE='\033[0;34m' + _STATE_GREEN='\033[0;32m' + _STATE_YELLOW='\033[0;33m' + _STATE_RED='\033[0;31m' + _STATE_GRAY='\033[0;90m' + _STATE_NC='\033[0m' +else + _STATE_BLUE='' + _STATE_GREEN='' + _STATE_YELLOW='' + _STATE_RED='' + _STATE_GRAY='' + _STATE_NC='' +fi + # ============================================================ # State File Schema v3 Documentation # ============================================================ @@ -142,7 +162,17 @@ state_init() { # Ensure directory exists if [[ ! -d "$state_dir" ]]; then - mkdir -p "$state_dir" || return 1 + # Try without sudo first (works for user directories like ~/.acfs) + # Fall back to sudo for system directories like /var/lib/acfs + if ! mkdir -p "$state_dir" 2>/dev/null; then + if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then + sudo mkdir -p "$state_dir" || return 1 + # Fix ownership so current user can write to it + sudo chown "$(id -u):$(id -g)" "$state_dir" 2>/dev/null || true + else + return 1 + fi + fi # If running as root but targeting a non-root user, ensure the directory # is owned by the target user so they can access the state file later. @@ -382,19 +412,18 @@ _state_acquire_lock() { fi # Open lock file on FD 200 (same FD convention as autofix.sh) - # NOTE: On some bash versions (5.3+), exec with high FDs can fail. - # We use eval to work around potential issues with direct exec. - # The 2>/dev/null suppresses errors from the redirection itself. - if ! eval 'exec 200>"$lock_file"' 2>/dev/null; then - # Try alternate FD if 200 fails - if ! eval 'exec 199>"$lock_file"' 2>/dev/null; then - # Lock acquisition not possible, return failure - return 1 - fi - # Use FD 199 instead + # NOTE: On bash 5.3+, `exec N>file` under set -e exits the script + # before `if` can catch the failure. We test in a subshell first, + # then only exec in the main shell if the subshell succeeded. + if (exec 200>"$lock_file") 2>/dev/null; then + exec 200>"$lock_file" + ACFS_LOCK_FD=200 + elif (exec 199>"$lock_file") 2>/dev/null; then + exec 199>"$lock_file" ACFS_LOCK_FD=199 else - ACFS_LOCK_FD=200 + # Lock acquisition not possible, return failure + return 1 fi # Try to acquire lock with a 5-second timeout @@ -653,12 +682,21 @@ confirm_resume() { return 1 fi - # Check for completed phases - local completed_count + # Extract all resume info in a single jq call (5→1 subprocess spawns) + # Note: Uses ASCII Unit Separator (0x1f) as delimiter since bash strips null bytes + local completed_count=0 last_phase="" started_at="" failed_phase="" mode="" if command -v jq &>/dev/null; then - completed_count=$(echo "$state" | jq -r '.completed_phases | length') - else - completed_count=0 + local extracted + extracted=$(echo "$state" | jq -r ' + [ + (.completed_phases | length | tostring), + (.completed_phases[-1] // "unknown"), + (.started_at // "unknown"), + (.failed_phase // ""), + (.mode // "unknown") + ] | join("\u001f") + ') + IFS=$'\x1f' read -r completed_count last_phase started_at failed_phase mode <<< "$extracted" fi # If no phases completed, nothing to resume @@ -666,16 +704,6 @@ confirm_resume() { return 1 fi - # Extract resume info - local last_phase="" started_at="" failed_phase="" mode="" - if command -v jq &>/dev/null; then - # Get the last completed phase - last_phase=$(echo "$state" | jq -r '.completed_phases[-1] // "unknown"') - started_at=$(echo "$state" | jq -r '.started_at // "unknown"') - failed_phase=$(echo "$state" | jq -r '.failed_phase // empty') - mode=$(echo "$state" | jq -r '.mode // "unknown"') - fi - local last_phase_name="${ACFS_PHASE_NAMES[$last_phase]:-$last_phase}" local total_phases="${#ACFS_PHASE_IDS[@]}" @@ -714,9 +742,7 @@ confirm_resume() { .completed_phases = (.completed_phases | map(select(. != "finalize"))) | .version = $ver ') - local state_file_path - state_file_path="$(state_get_file)" - printf '%s\n' "$updated_state" > "$state_file_path" + state_save "$updated_state" fi fi fi @@ -807,7 +833,7 @@ _confirm_resume_log_info() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#89b4fa" "$msg" >&2 else - echo -e "\033[0;34m$msg\033[0m" >&2 + echo -e "${_STATE_BLUE}$msg${_STATE_NC}" >&2 fi } @@ -817,7 +843,7 @@ _confirm_resume_log_warn() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#f9e2af" "$msg" >&2 else - echo -e "\033[0;33m$msg\033[0m" >&2 + echo -e "${_STATE_YELLOW}$msg${_STATE_NC}" >&2 fi } @@ -1204,6 +1230,7 @@ state_upgrade_get_progress() { # Print upgrade status for user display # Usage: state_upgrade_print_status +# Optimized: Single jq call extracts all fields (was 11 subprocess spawns, now 1) state_upgrade_print_status() { if ! command -v jq &>/dev/null; then echo "Upgrade status unavailable (jq required)" @@ -1216,46 +1243,53 @@ state_upgrade_print_status() { return 0 fi - local enabled - enabled=$(echo "$state" | jq -r '.ubuntu_upgrade.enabled // false') + # Extract all fields in a single jq call (11→1 subprocess spawns) + # Note: Uses ASCII Unit Separator (0x1f) as delimiter since bash strips null bytes + local extracted + extracted=$(echo "$state" | jq -r ' + .ubuntu_upgrade as $u | + [ + ($u.enabled // false | tostring), + ($u.original_version // ""), + ($u.target_version // ""), + ($u.current_stage // ""), + ($u.completed_upgrades | length | tostring), + ($u.upgrade_path | length | tostring), + ($u.completed_upgrades // [] | map(" ✓ \(.from) → \(.to)") | join("\n")), + ($u.current_upgrade.from // ""), + ($u.current_upgrade.to // ""), + ($u.last_error // "") + ] | join("\u001f") + ') + + # Parse unit-separator-delimited fields + local enabled original target stage completed_count total_count completed_list current_from current_to error + IFS=$'\x1f' read -r enabled original target stage completed_count total_count completed_list current_from current_to error <<< "$extracted" + if [[ "$enabled" != "true" ]]; then echo "No upgrade in progress" return 0 fi - local original target stage completed_count total_count - original=$(echo "$state" | jq -r '.ubuntu_upgrade.original_version') - target=$(echo "$state" | jq -r '.ubuntu_upgrade.target_version') - stage=$(echo "$state" | jq -r '.ubuntu_upgrade.current_stage') - completed_count=$(echo "$state" | jq -r '.ubuntu_upgrade.completed_upgrades | length') - total_count=$(echo "$state" | jq -r '.ubuntu_upgrade.upgrade_path | length') - echo "=== Ubuntu Upgrade Status ===" echo "Original: $original → Target: $target" echo "Progress: $completed_count/$total_count upgrades completed" echo "Stage: $stage" # Show completed upgrades - if [[ "$completed_count" -gt 0 ]]; then + if [[ "$completed_count" -gt 0 ]] && [[ -n "$completed_list" ]]; then echo "" echo "Completed upgrades:" - echo "$state" | jq -r '.ubuntu_upgrade.completed_upgrades[] | " ✓ \(.from) → \(.to)"' + echo "$completed_list" fi # Show current upgrade if any - local current - current=$(echo "$state" | jq -r '.ubuntu_upgrade.current_upgrade // empty') - if [[ -n "$current" ]]; then - local from to - from=$(echo "$state" | jq -r '.ubuntu_upgrade.current_upgrade.from') - to=$(echo "$state" | jq -r '.ubuntu_upgrade.current_upgrade.to') + if [[ -n "$current_from" ]]; then echo "" - echo "Current upgrade: $from → $to" + echo "Current upgrade: $current_from → $current_to" fi # Show error if any - local error - error=$(echo "$state" | jq -r '.ubuntu_upgrade.last_error // empty') if [[ -n "$error" ]]; then echo "" echo "Last error: $error" @@ -2230,7 +2264,7 @@ _run_phase_log_skip() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#6c7086" "[$display_name] Skipped ($reason)" >&2 else - echo -e "\033[0;90m[$display_name] Skipped ($reason)\033[0m" >&2 + echo -e "${_STATE_GRAY}[$display_name] Skipped ($reason)${_STATE_NC}" >&2 fi } @@ -2241,7 +2275,7 @@ _run_phase_log_start() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#89b4fa" --bold "[$display_name] Starting..." >&2 else - echo -e "\033[0;34m[$display_name] Starting...\033[0m" >&2 + echo -e "${_STATE_BLUE}[$display_name] Starting...${_STATE_NC}" >&2 fi } @@ -2258,7 +2292,7 @@ _run_phase_log_success() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#a6e3a1" --bold "$msg" >&2 else - echo -e "\033[0;32m$msg\033[0m" >&2 + echo -e "${_STATE_GREEN}$msg${_STATE_NC}" >&2 fi } @@ -2270,6 +2304,6 @@ _run_phase_log_failure() { if [[ "${HAS_GUM:-false}" == "true" ]] && command -v gum &>/dev/null; then gum style --foreground "#f38ba8" --bold "[$display_name] FAILED (exit code: $exit_code)" >&2 else - echo -e "\033[0;31m[$display_name] FAILED (exit code: $exit_code)\033[0m" >&2 + echo -e "${_STATE_RED}[$display_name] FAILED (exit code: $exit_code)${_STATE_NC}" >&2 fi } diff --git a/scripts/lib/test_newproj_logging.sh b/scripts/lib/test_newproj_logging.sh index 3e07922e..b9fba612 100755 --- a/scripts/lib/test_newproj_logging.sh +++ b/scripts/lib/test_newproj_logging.sh @@ -297,7 +297,7 @@ test_log_dump_state() { declare -A TEST_STATE=( [project_name]="my-project" [tech_stack]="nodejs typescript" - [enable_bd]="true" + [enable_br]="true" ) log_dump_state TEST_STATE diff --git a/scripts/lib/tools.sh b/scripts/lib/tools.sh index fc98d745..5066a5b6 100644 --- a/scripts/lib/tools.sh +++ b/scripts/lib/tools.sh @@ -388,11 +388,12 @@ get_skip_url() { # Summary Report # ============================================================ -# Colors for output (will be overridden if logging.sh is sourced) -_TOOLS_YELLOW="${ACFS_YELLOW:-\033[0;33m}" -_TOOLS_CYAN="${ACFS_BLUE:-\033[0;36m}" -_TOOLS_DIM="\033[2m" -_TOOLS_NC="${ACFS_NC:-\033[0m}" +# Colors for output (respects NO_COLOR via logging.sh's ACFS_* variables). +# Use ${var-default} to preserve empty strings. Related: bd-39ye +_TOOLS_YELLOW="${ACFS_YELLOW-\033[0;33m}" +_TOOLS_CYAN="${ACFS_BLUE-\033[0;36m}" +_TOOLS_DIM="${ACFS_GRAY-\033[2m}" +_TOOLS_NC="${ACFS_NC-\033[0m}" # report_skipped_tools - Print comprehensive summary of skipped tools # diff --git a/scripts/lib/update.sh b/scripts/lib/update.sh index fcf33de8..d6bd33cc 100755 --- a/scripts/lib/update.sh +++ b/scripts/lib/update.sh @@ -21,14 +21,25 @@ if [[ -f "$SCRIPT_DIR/../../VERSION" ]]; then ACFS_VERSION="$(cat "$SCRIPT_DIR/../../VERSION" 2>/dev/null || echo "$ACFS_VERSION")" fi -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' +# Colors - respect NO_COLOR standard (https://no-color.org/) +# Related: bd-39ye +if [[ -z "${NO_COLOR:-}" ]] && [[ -t 2 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + CYAN='\033[0;36m' + BOLD='\033[1m' + DIM='\033[2m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + CYAN='' + BOLD='' + DIM='' + NC='' +fi # Counters SUCCESS_COUNT=0 @@ -150,7 +161,7 @@ get_version() { vercel) version=$(vercel --version 2>/dev/null || echo "unknown") ;; - ntm|ubs|bv|cass|cm|caam|slb|ru|dcg|apr|pt|xf|jfp|ms|br|rch|wa|brenner) + ntm|ubs|bv|cass|cm|caam|slb|ru|dcg|apr|pt|xf|jfp|ms|br|rch|giil|csctf|srps|tru|rano|mdwb|s2p|brenner) version=$("$tool" --version 2>/dev/null | head -1 || echo "unknown") ;; sg|lsd|dust|tldr) @@ -490,6 +501,109 @@ run_cmd_sudo() { run_cmd "$desc" "$@" } +# ============================================================ +# Migration Cleanup +# ============================================================ + +# Clean up legacy git_safety_guard artifacts from pre-DCG installations +# This runs on every update to ensure stale files are removed +cleanup_legacy_git_safety_guard() { + local cleaned=false + local hooks_dirs=( + "$HOME/.acfs/claude/hooks" + "$HOME/.claude/hooks" + ) + local legacy_files=( + "git_safety_guard.py" + "git_safety_guard.sh" + ) + + # Remove legacy hook files + for dir in "${hooks_dirs[@]}"; do + for file in "${legacy_files[@]}"; do + if [[ -f "$dir/$file" ]]; then + rm -f "$dir/$file" 2>/dev/null && cleaned=true + log_to_file "Removed legacy file: $dir/$file" + fi + done + # Remove empty hooks directory + if [[ -d "$dir" ]] && [[ -z "$(ls -A "$dir" 2>/dev/null)" ]]; then + rmdir "$dir" 2>/dev/null && cleaned=true + log_to_file "Removed empty directory: $dir" + fi + done + + # Clean parent directories if empty + for parent in "$HOME/.acfs/claude" "$HOME/.claude"; do + if [[ -d "$parent" ]] && [[ -z "$(ls -A "$parent" 2>/dev/null)" ]]; then + rmdir "$parent" 2>/dev/null || true + log_to_file "Removed empty directory: $parent" + fi + done + + # Clean git_safety_guard from Claude settings.json if present + local settings_file="$HOME/.claude/settings.json" + if [[ -f "$settings_file" ]] && command -v jq &>/dev/null; then + if jq -e '.hooks // empty' "$settings_file" &>/dev/null; then + # Check if git_safety_guard is referenced in hooks + if grep -q "git_safety_guard" "$settings_file" 2>/dev/null; then + local tmp_settings + tmp_settings=$(mktemp) + # Remove any hook entries containing git_safety_guard + if jq 'walk(if type == "object" and .hooks then .hooks |= map(select(. | tostring | contains("git_safety_guard") | not)) else . end)' "$settings_file" > "$tmp_settings" 2>/dev/null; then + mv "$tmp_settings" "$settings_file" && cleaned=true + log_to_file "Cleaned git_safety_guard references from $settings_file" + else + rm -f "$tmp_settings" + fi + fi + fi + fi + + if [[ "$cleaned" == "true" ]]; then + log_item "ok" "legacy cleanup" "removed git_safety_guard artifacts" + fi +} + +# Fix stale aliases in deployed acfs.zshrc +# Older versions aliased br='bun run dev', which shadows beads_rust (br). +cleanup_legacy_br_alias() { + local deployed="$HOME/.acfs/zsh/acfs.zshrc" + [[ -f "$deployed" ]] || return 0 + + # Check for the exact problematic alias (uncommented) + if grep -q "^alias br='bun run dev'" "$deployed" 2>/dev/null; then + # Comment out the old alias (sync_acfs_zshrc will deploy the correct version later; + # this sed is a safety net for when the repo isn't available) + sed -i "s|^alias br='bun run dev'|# alias br='bun run dev' # disabled - conflicts with beads_rust (br)|" "$deployed" + log_item "ok" "legacy cleanup" "fixed br alias conflict in deployed acfs.zshrc" + log_to_file "Commented out alias br='bun run dev' in $deployed" + fi +} + +# Re-deploy acfs.zshrc from repo to ~/.acfs/ if repo copy is newer +sync_acfs_zshrc() { + local repo_zshrc="$ACFS_REPO_ROOT/acfs/zsh/acfs.zshrc" + local deployed_zshrc="$HOME/.acfs/zsh/acfs.zshrc" + + [[ -f "$repo_zshrc" ]] || return 0 + + # Skip if deployed copy is identical + if [[ -f "$deployed_zshrc" ]] && cmp -s "$repo_zshrc" "$deployed_zshrc"; then + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log_item "ok" "acfs.zshrc" "would sync from repo (changed)" + return 0 + fi + + mkdir -p "$(dirname "$deployed_zshrc")" + cp "$repo_zshrc" "$deployed_zshrc" + log_item "ok" "acfs.zshrc" "synced from repo" + log_to_file "Deployed $repo_zshrc -> $deployed_zshrc" +} + # ============================================================ # Checksums Refresh (Auto-update from GitHub) # ============================================================ @@ -647,9 +761,48 @@ update_acfs_self() { return 0 fi - # Check if ACFS repo exists and is a git repo + # Check if ACFS repo exists and is a git repo. + # If installed via tarball (no .git dir), bootstrap a git repo so + # the existing pull-based self-update logic works on subsequent runs. if [[ ! -d "$ACFS_REPO_ROOT/.git" ]]; then - log_item "skip" "ACFS self-update" "not a git repository at $ACFS_REPO_ROOT" + log_to_file "No .git directory found at $ACFS_REPO_ROOT — bootstrapping git repo for self-update..." + + # Check if git is available before attempting bootstrap + if ! command -v git &>/dev/null; then + log_item "skip" "ACFS self-update" "git not found, cannot bootstrap" + return 0 + fi + + if ! git -C "$ACFS_REPO_ROOT" init 2>/dev/null; then + log_item "warn" "ACFS self-update" "git init failed at $ACFS_REPO_ROOT" + return 0 + fi + + if ! git -C "$ACFS_REPO_ROOT" remote add origin \ + https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup.git 2>/dev/null; then + # Remote may already exist from a partial prior run; verify it points to the right URL + local existing_url + existing_url=$(git -C "$ACFS_REPO_ROOT" remote get-url origin 2>/dev/null) || true + if [[ "$existing_url" != *"agentic_coding_flywheel_setup"* ]]; then + log_item "warn" "ACFS self-update" "unexpected origin remote: $existing_url" + return 0 + fi + fi + + if ! git -C "$ACFS_REPO_ROOT" fetch origin main --quiet 2>/dev/null; then + log_item "warn" "ACFS self-update" "git fetch failed during bootstrap (network issue?)" + return 0 + fi + + # Use --mixed reset so local modifications (custom configs, etc.) are + # preserved as unstaged changes rather than being destroyed. + if ! git -C "$ACFS_REPO_ROOT" reset --mixed origin/main 2>/dev/null; then + log_item "warn" "ACFS self-update" "git reset failed during bootstrap" + return 0 + fi + + log_item "ok" "ACFS" "git repo bootstrapped from tarball install" + log_to_file "ACFS git repo initialized at $ACFS_REPO_ROOT — run 'acfs update' again for full self-update" return 0 fi @@ -712,19 +865,30 @@ update_acfs_self() { return 0 fi - # Check for local modifications that would block pull - if ! git -C "$ACFS_REPO_ROOT" diff --quiet 2>/dev/null; then - log_item "warn" "ACFS self-update" "local modifications detected, skipping" - log_to_file "Run 'git -C $ACFS_REPO_ROOT status' to see changes" - return 0 + # Stash local modifications so they don't block the pull + local stashed=false + if ! git -C "$ACFS_REPO_ROOT" diff --quiet 2>/dev/null || ! git -C "$ACFS_REPO_ROOT" diff --cached --quiet 2>/dev/null; then + log_to_file "Local modifications detected, stashing before update..." + if git -C "$ACFS_REPO_ROOT" stash --quiet 2>/dev/null; then + stashed=true + fi fi # Pull updates log_to_file "Pulling updates..." if ! git -C "$ACFS_REPO_ROOT" pull --ff-only origin main 2>/dev/null; then - log_item "warn" "ACFS self-update" "git pull failed" - log_to_file "Try: git -C $ACFS_REPO_ROOT pull --ff-only origin main" - return 0 + # ff-only failed (diverged history?), try reset --mixed + log_to_file "ff-only pull failed, using reset --mixed" + git -C "$ACFS_REPO_ROOT" reset --mixed origin/main 2>/dev/null || { + log_item "warn" "ACFS self-update" "git update failed" + [[ "$stashed" == "true" ]] && git -C "$ACFS_REPO_ROOT" stash pop --quiet 2>/dev/null || true + return 0 + } + fi + + # Restore local modifications + if [[ "$stashed" == "true" ]]; then + git -C "$ACFS_REPO_ROOT" stash pop --quiet 2>/dev/null || true fi log_item "ok" "ACFS" "updated ($commit_count commits)" @@ -807,20 +971,21 @@ wait_for_apt_lock() { local interval=5 local waited=0 - while [[ $waited -lt $max_wait ]]; do - # Check if any apt-related process is running - local apt_procs="" - apt_procs=$(pgrep -a 'apt|dpkg|unattended-upgr' 2>/dev/null | head -3 || true) + if ! command -v fuser &>/dev/null; then + log_to_file "fuser not available (psmisc not installed), skipping apt lock detection" + return 0 + fi - # Check for lock files + while [[ $waited -lt $max_wait ]]; do + # Only check actual lock files — background processes (e.g. unattended-upgrades + # daemon) don't hold locks unless actively installing local lock_held=false - if [[ -f /var/lib/dpkg/lock-frontend ]] && fuser /var/lib/dpkg/lock-frontend &>/dev/null; then - lock_held=true - elif [[ -f /var/lib/apt/lists/lock ]] && fuser /var/lib/apt/lists/lock &>/dev/null; then - lock_held=true - elif [[ -n "$apt_procs" ]]; then - lock_held=true - fi + for lockfile in /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/lib/dpkg/lock; do + if [[ -f "$lockfile" ]] && fuser "$lockfile" &>/dev/null; then + lock_held=true + break + fi + done if [[ "$lock_held" == "false" ]]; then return 0 @@ -829,9 +994,9 @@ wait_for_apt_lock() { if [[ $waited -eq 0 ]]; then log_item "wait" "apt lock" "waiting for other package operations to complete..." log_to_file "APT lock detected, waiting up to ${max_wait}s for release" - if [[ -n "$apt_procs" ]]; then - log_to_file "Running processes: $apt_procs" - fi + local lock_info="" + lock_info=$(fuser -v /var/lib/dpkg/lock-frontend 2>&1 || true) + [[ -n "$lock_info" ]] && log_to_file "Lock holder: $lock_info" fi sleep "$interval" @@ -886,30 +1051,31 @@ fix_apt_issues() { # Check if apt is locked by another process, with automatic waiting and fixing check_apt_lock() { - # First attempt: check if lock is immediately available - local apt_procs="" - apt_procs=$(pgrep -a 'apt|dpkg|unattended-upgr' 2>/dev/null | head -1 || true) - - if [[ -z "$apt_procs" ]]; then - # Also check lock files directly - if [[ -f /var/lib/dpkg/lock-frontend ]] && ! fuser /var/lib/dpkg/lock-frontend &>/dev/null; then - return 0 # Lock is free - elif [[ ! -f /var/lib/dpkg/lock-frontend ]]; then - return 0 # Lock file doesn't exist + # Only check if actual dpkg/apt lock files are held by a process. + # Background daemons (e.g. unattended-upgrades) don't hold locks unless + # actively installing, so pgrep-based checks cause false positives. + local locks_held=false + for lockfile in /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/lib/dpkg/lock; do + if [[ -f "$lockfile" ]] && fuser "$lockfile" &>/dev/null; then + locks_held=true + break fi + done + + if [[ "$locks_held" == "false" ]]; then + return 0 # No locks held, safe to proceed fi - # Lock is held - wait for it to be released + # Lock IS held — wait for release if wait_for_apt_lock 120; then log_item "ok" "apt lock" "lock released, proceeding" return 0 fi - # Still locked after waiting - try to diagnose - log_item "warn" "apt lock" "still locked after 2 minutes" + # Still locked after waiting — show diagnostic + log_item "skip" "apt" "dpkg lock held after 2m wait" log_to_file "APT lock still held after waiting" - # Show what's holding the lock local lock_holder="" lock_holder=$(sudo fuser -v /var/lib/dpkg/lock-frontend 2>&1 || true) if [[ -n "$lock_holder" ]]; then @@ -919,14 +1085,6 @@ check_apt_lock() { fi fi - # Check for unattended-upgrades specifically - if pgrep -x "unattended-upgr" &>/dev/null; then - log_item "info" "apt" "unattended-upgrades is running (this is normal on fresh systems)" - if [[ "$QUIET" != "true" ]]; then - echo -e " ${DIM}Tip: You can wait, or stop it with: sudo systemctl stop unattended-upgrades${NC}" - fi - fi - if [[ "$ABORT_ON_FAILURE" == "true" ]]; then echo -e "${RED}Aborting: apt is locked and could not be released${NC}" exit 1 @@ -1040,6 +1198,7 @@ update_agents() { if ! run_cmd_claude_update; then log_to_file "Claude update failed, attempting reinstall via official installer" if update_require_security; then + # INTENTIONAL: verified installer is the correct fallback for failed updates run_cmd "Claude Code (reinstall)" update_run_verified_installer claude latest else log_item "fail" "Claude Code" "update failed and reinstall unavailable (missing security.sh)" @@ -1053,6 +1212,7 @@ update_agents() { elif [[ "$FORCE_MODE" == "true" ]]; then capture_version_before "claude" if update_require_security; then + # INTENTIONAL: verified installer is the correct path for fresh installs run_cmd "Claude Code (install)" update_run_verified_installer claude latest if capture_version_after "claude"; then [[ "$QUIET" != "true" ]] && printf " ${DIM}%s → %s${NC}\n" "${VERSION_BEFORE[claude]}" "${VERSION_AFTER[claude]}" @@ -1113,9 +1273,12 @@ update_agents() { } # Helper for Claude update with proper error handling +# FIX(bd-gsjqf.2): Replaced bare "claude update --channel latest" (flag does not exist) +# with update_run_verified_installer which uses the official install.sh script. +# See: https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup/issues/125 run_cmd_claude_update() { - local desc="Claude Code (native update)" - local cmd_display="claude update" + local desc="Claude Code (verified installer)" + local cmd_display="update_run_verified_installer claude latest" log_to_file "Running: $cmd_display" @@ -1137,27 +1300,27 @@ run_cmd_claude_update() { fi if [[ "$QUIET" != "true" ]] && [[ -n "${UPDATE_LOG_FILE:-}" ]]; then - if claude update 2>&1 | tee -a "$UPDATE_LOG_FILE"; then + if update_run_verified_installer claude latest 2>&1 | tee -a "$UPDATE_LOG_FILE"; then exit_code=0 else exit_code=${PIPESTATUS[0]} fi elif [[ -n "${UPDATE_LOG_FILE:-}" ]]; then - if claude update >> "$UPDATE_LOG_FILE" 2>&1; then + if update_run_verified_installer claude latest >> "$UPDATE_LOG_FILE" 2>&1; then exit_code=0 else exit_code=$? fi else if [[ "$QUIET" != "true" ]]; then - claude update || exit_code=$? + update_run_verified_installer claude latest || exit_code=$? else - claude update >/dev/null 2>&1 || exit_code=$? + update_run_verified_installer claude latest >/dev/null 2>&1 || exit_code=$? fi fi else local output="" - output=$(claude update 2>&1) || exit_code=$? + output=$(update_run_verified_installer claude latest 2>&1) || exit_code=$? [[ -n "$output" ]] && log_to_file "Output: $output" fi @@ -1531,151 +1694,143 @@ update_stack() { return 0 fi - # DCG migration: offer install for existing users who don't have it yet - if ! cmd_exists dcg; then - log_item "warn" "DCG not installed" "new safety tool available in the ACFS stack" + # NTM - always install/update (installer is idempotent) + run_cmd "NTM" update_run_verified_installer ntm - if [[ "$YES_MODE" == "true" || "$FORCE_MODE" == "true" ]]; then - run_cmd "DCG (install)" update_run_verified_installer dcg --easy-mode - if cmd_exists dcg && cmd_exists claude; then - run_cmd "DCG Hook" dcg install --force 2>/dev/null || true - fi - elif [[ -t 0 ]]; then - [[ "$QUIET" != "true" ]] && echo "" - read -r -p "Install DCG now? [Y/n] " response || true - if [[ -z "$response" || "$response" =~ ^[Yy] ]]; then - run_cmd "DCG (install)" update_run_verified_installer dcg --easy-mode - if cmd_exists dcg && cmd_exists claude; then - run_cmd "DCG Hook" dcg install --force 2>/dev/null || true - fi + # MCP Agent Mail - always install/update (requires tmux for server process) + # Note: Version tracking not possible for async tmux updates + if cmd_exists tmux; then + local tool="mcp_agent_mail" + local url="${KNOWN_INSTALLERS[$tool]:-}" + local expected_sha256 + expected_sha256="$(get_checksum "$tool")" + + if [[ -n "$url" ]] && [[ -n "$expected_sha256" ]]; then + # Fetch and verify content first + local tmp_install + tmp_install=$(mktemp "${TMPDIR:-/tmp}/acfs-install-am.XXXXXX" 2>/dev/null) || tmp_install="" + if [[ -z "$tmp_install" ]]; then + log_item "fail" "MCP Agent Mail" "failed to create temp file for verified installer" else - log_item "skip" "DCG" "skipped (install later: curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh | bash)" - fi - else - log_item "skip" "DCG" "not installed (non-interactive; run: curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh | bash)" - fi - fi + if verify_checksum "$url" "$expected_sha256" "$tool" > "$tmp_install"; then + chmod +x "$tmp_install" - # NTM - if cmd_exists ntm; then - run_cmd "NTM" update_run_verified_installer ntm - fi + local tmux_session="acfs-services" + # Kill old session if exists + tmux kill-session -t "$tmux_session" 2>/dev/null || true - # MCP Agent Mail - Special handling for tmux (server blocks) - # Note: Version tracking not possible for async tmux updates - if cmd_exists "am" || [[ -d "$HOME/mcp_agent_mail" ]]; then - if cmd_exists tmux; then - local tool="mcp_agent_mail" - local url="${KNOWN_INSTALLERS[$tool]:-}" - local expected_sha256 - expected_sha256="$(get_checksum "$tool")" - - if [[ -n "$url" ]] && [[ -n "$expected_sha256" ]]; then - # Fetch and verify content first - local tmp_install - tmp_install=$(mktemp "${TMPDIR:-/tmp}/acfs-install-am.XXXXXX" 2>/dev/null) || tmp_install="" - if [[ -z "$tmp_install" ]]; then - log_item "fail" "MCP Agent Mail" "failed to create temp file for verified installer" - else - if verify_checksum "$url" "$expected_sha256" "$tool" > "$tmp_install"; then - chmod +x "$tmp_install" - - local tmux_session="acfs-services" - # Kill old session if exists - tmux kill-session -t "$tmux_session" 2>/dev/null || true - - # Launch in tmux (tmux does not split a single string into argv). - # NOTE: run_cmd always returns 0 (unless aborting), so do not use it in an `if ...; then` check. - run_cmd "MCP Agent Mail (tmux)" tmux new-session -d -s "$tmux_session" "$tmp_install" --dir "$HOME/mcp_agent_mail" --yes - - # Confirm session exists before printing "running" hint (avoids misleading output on failure). - if tmux has-session -t "$tmux_session" 2>/dev/null; then - log_to_file "Started MCP Agent Mail update in tmux session: $tmux_session" - [[ "$QUIET" != "true" ]] && printf " ${DIM}Update running in tmux session '%s'${NC}\n" "$tmux_session" - fi - - # Cleanup happens when system tmp is cleaned - else - rm -f "$tmp_install" - log_item "fail" "MCP Agent Mail" "verification failed" + # Launch in tmux (tmux does not split a single string into argv). + # NOTE: run_cmd always returns 0 (unless aborting), so do not use it in an `if ...; then` check. + run_cmd "MCP Agent Mail (tmux)" tmux new-session -d -s "$tmux_session" "$tmp_install" --dir "$HOME/mcp_agent_mail" --yes + + # Confirm session exists before printing "running" hint (avoids misleading output on failure). + if tmux has-session -t "$tmux_session" 2>/dev/null; then + log_to_file "Started MCP Agent Mail update in tmux session: $tmux_session" + [[ "$QUIET" != "true" ]] && printf " ${DIM}Update running in tmux session '%s'${NC}\n" "$tmux_session" fi + + # Cleanup happens when system tmp is cleaned + else + rm -f "$tmp_install" + log_item "fail" "MCP Agent Mail" "verification failed" fi - else - log_item "fail" "MCP Agent Mail" "unknown installer URL/checksum" fi else - log_item "skip" "MCP Agent Mail" "tmux not found (required for update)" + log_item "fail" "MCP Agent Mail" "unknown installer URL/checksum" fi + else + log_item "skip" "MCP Agent Mail" "tmux not found (required for install)" fi - # Meta Skill (ms) - if cmd_exists ms; then - run_cmd "Meta Skill" update_run_verified_installer ms --easy-mode - fi + # Meta Skill (ms) - always install/update (installer is idempotent) + run_cmd "Meta Skill" update_run_verified_installer ms --easy-mode - # APR (Automated Plan Reviser Pro) - if cmd_exists apr; then - run_cmd "APR" update_run_verified_installer apr --easy-mode - fi + # APR (Automated Plan Reviser Pro) - always install/update + run_cmd "APR" update_run_verified_installer apr --easy-mode - # Process Triage (pt) - if cmd_exists pt; then - run_cmd "Process Triage" update_run_verified_installer pt - fi + # Process Triage (pt) - always install/update + run_cmd "Process Triage" update_run_verified_installer pt - # xf (X Archive Search) - if cmd_exists xf; then - run_cmd "xf" update_run_verified_installer xf --easy-mode - fi + # xf (X Archive Search) - always install/update + run_cmd "xf" update_run_verified_installer xf --easy-mode - # JeffreysPrompts (jfp) + # JeffreysPrompts (jfp) - only update if already installed + # Note: JFP requires a paid subscription to jeffreysprompts.com if cmd_exists jfp; then - run_cmd "JeffreysPrompts" jfp update + run_cmd "JeffreysPrompts" update_run_verified_installer jfp fi - # UBS - if cmd_exists ubs; then - run_cmd "Ultimate Bug Scanner" update_run_verified_installer ubs --easy-mode - fi + # UBS - always install/update (installer is idempotent) + run_cmd "Ultimate Bug Scanner" update_run_verified_installer ubs --easy-mode - # Beads Viewer - if cmd_exists bv; then - run_cmd "Beads Viewer" update_run_verified_installer bv - fi + # Beads Viewer - always install/update + run_cmd "Beads Viewer" update_run_verified_installer bv - # CASS - if cmd_exists cass; then - run_cmd "CASS" update_run_verified_installer cass --easy-mode --verify - fi + # Beads Rust (br) - local issue tracker CLI - always install/update + run_cmd "Beads Rust" update_run_verified_installer br - # CASS Memory - if cmd_exists cm; then - run_cmd "CASS Memory" update_run_verified_installer cm --easy-mode --verify - fi + # CASS - always install/update + run_cmd "CASS" update_run_verified_installer cass --easy-mode --verify - # CAAM - if cmd_exists caam; then - run_cmd "CAAM" update_run_verified_installer caam - fi + # CASS Memory - always install/update + run_cmd "CASS Memory" update_run_verified_installer cm --easy-mode --verify - # SLB - if cmd_exists slb; then - run_cmd "SLB" update_run_verified_installer slb - fi + # CAAM - always install/update + run_cmd "CAAM" update_run_verified_installer caam + + # SLB - always install/update + run_cmd "SLB" update_run_verified_installer slb + + # RU (Repo Updater) - always install/update + run_cmd "RU" update_run_verified_installer ru --easy-mode - # RU (Repo Updater) - if cmd_exists ru; then - run_cmd "RU" update_run_verified_installer ru --easy-mode + # DCG (Destructive Command Guard) - always install/update + run_cmd "DCG" update_run_verified_installer dcg --easy-mode + # Re-register hook after install/update to ensure latest version is active + if cmd_exists dcg && cmd_exists claude; then + run_cmd "DCG Hook" dcg install --force 2>/dev/null || true fi - # DCG (Destructive Command Guard) - if cmd_exists dcg; then - run_cmd "DCG" update_run_verified_installer dcg --easy-mode - # Re-register hook after update to ensure latest version is active - if cmd_exists claude; then - run_cmd "DCG Hook" dcg install --force 2>/dev/null || true - fi + # RCH (Remote Compilation Helper) - always install/update + run_cmd "RCH" update_run_verified_installer rch + + # GIIL (Google Image Inline Linker) - always install/update + run_cmd "GIIL" update_run_verified_installer giil + + # CSCTF (Chat Shared Conversation To File) - always install/update + run_cmd "CSCTF" update_run_verified_installer csctf + + # SRPS (System Resource Protection Script) - always install/update + run_cmd "SRPS" update_run_verified_installer srps + + # TRU (Toon Rust) - always install/update + run_cmd "TRU" update_run_verified_installer tru + + # RANO - always install/update + run_cmd "RANO" update_run_verified_installer rano + + # MDWB (Markdown Web Browser) - always install/update + run_cmd "MDWB" update_run_verified_installer mdwb + + # S2P (Source to Prompt TUI) - always install/update + run_cmd "S2P" update_run_verified_installer s2p + + # Brenner Bot - always install/update + run_cmd "Brenner Bot" update_run_verified_installer brenner_bot +} + +# ============================================================ +# Root AGENTS.md Generation +# ============================================================ +update_root_agents_md() { + log_section "Root AGENTS.md" + + if ! cmd_exists flywheel-update-agents-md; then + log_item "skip" "Root AGENTS.md" "flywheel-update-agents-md not installed" + return 0 fi + + run_cmd_sudo "Root AGENTS.md" flywheel-update-agents-md } # ============================================================ @@ -1698,13 +1853,13 @@ update_omz() { # Set DISABLE_UPDATE_PROMPT to avoid interactive prompts local upgrade_script="$omz_dir/tools/upgrade.sh" if [[ -x "$upgrade_script" ]]; then - run_cmd "Oh-My-Zsh upgrade" env DISABLE_UPDATE_PROMPT=true ZSH="$omz_dir" "$upgrade_script" + run_cmd "Oh-My-Zsh upgrade" timeout 120 env DISABLE_UPDATE_PROMPT=true ZSH="$omz_dir" "$upgrade_script" elif [[ -f "$upgrade_script" ]]; then - run_cmd "Oh-My-Zsh upgrade" env DISABLE_UPDATE_PROMPT=true ZSH="$omz_dir" bash "$upgrade_script" + run_cmd "Oh-My-Zsh upgrade" timeout 120 env DISABLE_UPDATE_PROMPT=true ZSH="$omz_dir" bash "$upgrade_script" else # Fallback to git pull if [[ -d "$omz_dir/.git" ]]; then - run_cmd "Oh-My-Zsh (git pull)" git -C "$omz_dir" pull --ff-only + run_cmd "Oh-My-Zsh (git pull)" timeout 60 git -C "$omz_dir" pull --ff-only else log_item "skip" "Oh-My-Zsh" "no upgrade mechanism found" return 0 @@ -1736,7 +1891,7 @@ update_p10k() { # Use --ff-only to avoid merge conflicts local output="" local exit_code=0 - output=$(git -C "$p10k_dir" pull --ff-only 2>&1) || exit_code=$? + output=$(timeout 60 git -C "$p10k_dir" pull --ff-only 2>&1) || exit_code=$? if [[ $exit_code -eq 0 ]]; then if capture_version_after "p10k"; then @@ -1788,7 +1943,7 @@ update_zsh_plugins() { local output="" local exit_code=0 - output=$(git -C "$plugin_dir" pull --ff-only 2>&1) || exit_code=$? + output=$(timeout 60 git -C "$plugin_dir" pull --ff-only 2>&1) || exit_code=$? if [[ $exit_code -eq 0 ]]; then if ! echo "$output" | grep -q "Already up to date"; then @@ -1903,6 +2058,9 @@ update_shell() { # Installer-based updates (Atuin, Zoxide) update_atuin update_zoxide + + # Keep deployed acfs.zshrc in sync with repo + sync_acfs_zshrc } # ============================================================ @@ -1999,7 +2157,7 @@ SKIP OPTIONS (exclude categories from update): --no-stack Skip Dicklesworthstone stack tool updates BEHAVIOR OPTIONS: - --force Install tools that are missing (not just update existing) + --force Force reinstallation even if already up to date --dry-run Preview changes without making them --yes, -y Non-interactive mode, skip all prompts --quiet, -q Minimal output, only show errors and summary @@ -2039,7 +2197,7 @@ WHAT EACH CATEGORY UPDATES: apt: System packages via apt update && apt upgrade && apt autoremove shell: Oh-My-Zsh, Powerlevel10k, zsh plugins (git pull) Atuin, Zoxide (reinstall from upstream) - agents: Claude Code (claude update) + agents: Claude Code (verified installer: curl claude.ai/install.sh | bash -- latest) Codex CLI (bun install -g --trust @openai/codex@latest) Gemini CLI (bun install -g --trust @google/gemini-cli@latest) cloud: Wrangler, Vercel (bun install -g --trust <pkg>@latest) @@ -2047,7 +2205,11 @@ WHAT EACH CATEGORY UPDATES: GitHub CLI (gh extension upgrade --all) Google Cloud SDK (gcloud components update) runtime: Bun (bun upgrade), Rust (rustup update), uv (uv self update), Go (apt-managed) - stack: NTM, UBS, BV, CASS, CM, CAAM, SLB, DCG, RU (re-run upstream installers) + stack: Dicklesworthstone stack tools (verified upstream installers) + Installs missing tools and updates existing ones automatically: + NTM, Agent Mail, Meta Skill, APR, pt, xf, UBS, BV, BR, CASS, CM, + CAAM, SLB, RU, DCG, RCH, GIIL, CSCTF, SRPS, TRU, RANO, MDWB, S2P, Brenner Bot + Exception: JFP requires subscription, only updated if already installed LOGS: Update logs are saved to: ~/.acfs/logs/updates/ @@ -2071,7 +2233,7 @@ TROUBLESHOOTING: sudo systemctl start unattended-upgrades - If an agent update fails: try running the update command directly: - claude update + curl -fsSL https://claude.ai/install.sh | bash -s -- latest bun install -g --trust @openai/codex@latest bun install -g --trust @google/gemini-cli@latest @@ -2252,6 +2414,10 @@ main() { export ACFS_INTERACTIVE=false fi + # Clean up legacy artifacts from previous versions + cleanup_legacy_git_safety_guard + cleanup_legacy_br_alias + # Run updates update_apt update_bun @@ -2263,6 +2429,7 @@ main() { update_go update_shell update_stack + update_root_agents_md # Summary print_summary diff --git a/scripts/lib/user.sh b/scripts/lib/user.sh index 1bb75b05..07be921a 100644 --- a/scripts/lib/user.sh +++ b/scripts/lib/user.sh @@ -81,7 +81,10 @@ ensure_user() { # Print password so user isn't locked out of sudo in safe mode echo "" >&2 - if declare -f log_warn >/dev/null; then + if declare -f log_sensitive >/dev/null; then + log_sensitive "Generated password for '$target': $passwd" + log_sensitive "Save this password! You may need it for sudo access." + elif declare -f log_warn >/dev/null; then log_warn "Generated password for '$target': $passwd" log_warn "Save this password! You may need it for sudo access." else @@ -347,7 +350,7 @@ prompt_ssh_key() { echo "" # 4. Read the key (handle pipe vs tty) - local pubkey + local pubkey="" if [[ -t 0 ]]; then read -r -p "Paste your public key: " pubkey else diff --git a/scripts/services-setup.sh b/scripts/services-setup.sh index ab576820..df041bd1 100755 --- a/scripts/services-setup.sh +++ b/scripts/services-setup.sh @@ -1142,6 +1142,12 @@ $(gum style --foreground "$ACFS_PINK" --bold "Setting up $label...")" fi setup_postgres + # Generate /AGENTS.md with current tool versions + local agents_script="$SCRIPT_DIR/generate-root-agents-md.sh" + if [[ -x "$agents_script" ]]; then + "$agents_script" 2>/dev/null || true + fi + echo "" if [[ "$HAS_GUM" == "true" ]]; then gum style \ diff --git a/scripts/templates/acfs-nightly-update.service b/scripts/templates/acfs-nightly-update.service new file mode 100644 index 00000000..48eba812 --- /dev/null +++ b/scripts/templates/acfs-nightly-update.service @@ -0,0 +1,28 @@ +[Unit] +Description=ACFS Nightly Auto-Update +Documentation=https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup + +# Note: network-online.target is unreliable in user scope on older systemd. +# acfs-update handles network failures gracefully, so no hard dependency needed. +After=default.target + +[Service] +Type=oneshot + +# Run the pre-flight wrapper (not acfs-update directly) +ExecStart=%h/.acfs/scripts/nightly-update.sh + +# Give plenty of time for updates (1 hour max) +TimeoutStartSec=3600 + +# Don't restart on failure +Restart=no +RemainAfterExit=no + +# Logging to journal +StandardOutput=journal+console +StandardError=journal+console + +# Ensure PATH includes user-installed binaries +Environment=PATH=%h/.local/bin:%h/.cargo/bin:%h/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +Environment=HOME=%h diff --git a/scripts/templates/acfs-nightly-update.timer b/scripts/templates/acfs-nightly-update.timer new file mode 100644 index 00000000..222c6b60 --- /dev/null +++ b/scripts/templates/acfs-nightly-update.timer @@ -0,0 +1,16 @@ +[Unit] +Description=ACFS Nightly Auto-Update Timer +Documentation=https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup + +[Timer] +# Run daily at 4am local time +OnCalendar=*-*-* 04:00:00 + +# If machine was off at 4am, run on next boot +Persistent=true + +# Spread load across machines (0-5 min jitter) +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/scripts/tests/test_update_channel.sh b/scripts/tests/test_update_channel.sh new file mode 100755 index 00000000..cae14ab3 --- /dev/null +++ b/scripts/tests/test_update_channel.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +# ============================================================ +# Test: Update Channel Fix (bd-gsjqf) +# Validates that all update paths use update_run_verified_installer +# instead of bare "claude update" (which has no --channel flag and +# silently downgrades from latest to stable channel). +# ============================================================ +# Bead: bd-gsjqf.4 +# 8 tests per specification: +# 1. Static Analysis — No bare "claude update" in function body +# 2. Static Analysis — update_run_verified_installer is called +# 3. Dry-run behavior +# 4. Function instrumentation (mock) +# 5. Security fallback +# 6. uca alias definition +# 7. Completeness sweep +# 8. Channel version (live, optional) +# ============================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +PASS=0 +FAIL=0 +SKIP=0 +LOG_FILE="/tmp/test_update_channel_$(date +%Y%m%d_%H%M%S).log" + +# --- Helpers --- +log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*" | tee -a "$LOG_FILE"; } +pass() { PASS=$((PASS + 1)); log " PASS: $1"; } +fail() { FAIL=$((FAIL + 1)); log " FAIL: $1"; } +skip() { SKIP=$((SKIP + 1)); log " SKIP: $1"; } +section() { log ""; log "=== $1 ==="; } + +UPDATE_SH="$REPO_ROOT/scripts/lib/update.sh" +ZSHRC="$REPO_ROOT/acfs/zsh/acfs.zshrc" + +log "Test: Update Channel Fix (bd-gsjqf)" +log "Log file: $LOG_FILE" +log "Repo root: $REPO_ROOT" + +# Ensure required files exist +if [[ ! -f "$UPDATE_SH" ]]; then + log "FATAL: $UPDATE_SH not found" + exit 2 +fi + +# Extract function body once for Tests 1-2 +func_body=$(sed -n '/^run_cmd_claude_update()/,/^}/p' "$UPDATE_SH") +if [[ -z "$func_body" ]]; then + log "FATAL: Could not extract run_cmd_claude_update() from $UPDATE_SH" + exit 2 +fi + +# ============================================================ +section "Test 1: Static Analysis — No bare 'claude update' in function body" +# ============================================================ +# Extract lines that are NOT comments, NOT variable assignments (cmd_display=), +# and NOT log strings, then check for bare "claude update" invocations. +bare_claude_update=$( + echo "$func_body" \ + | grep -v '^\s*#' \ + | grep -v 'cmd_display=' \ + | grep -v 'log_to_file' \ + | grep -v 'log_item' \ + | grep -v 'echo.*claude update' \ + | grep 'claude update' \ + || true +) +if [[ -z "$bare_claude_update" ]]; then + pass "No bare 'claude update' invocation in run_cmd_claude_update() body" +else + fail "Found bare 'claude update' invocation in run_cmd_claude_update(): $bare_claude_update" +fi + +# ============================================================ +section "Test 2: Static Analysis — update_run_verified_installer is called" +# ============================================================ +installer_calls=$(echo "$func_body" | grep -c 'update_run_verified_installer' || true) +if [[ "$installer_calls" -gt 0 ]]; then + pass "run_cmd_claude_update() calls update_run_verified_installer ($installer_calls occurrences)" +else + fail "run_cmd_claude_update() does NOT call update_run_verified_installer" +fi + +# ============================================================ +section "Test 3: Dry-run behavior" +# ============================================================ +# Source update.sh in a subshell. The BASH_SOURCE guard at line ~2444 prevents +# main() from running when sourced. Then call run_cmd_claude_update with +# DRY_RUN=true and verify it returns 0 without executing anything. +dry_run_output=$( + bash -c ' + # Provide required globals + DRY_RUN=true + VERBOSE=false + QUIET=true + FORCE_MODE=false + YES_MODE=false + ABORT_ON_FAILURE=false + UPDATE_LOG_FILE="/dev/null" + SUCCESS_COUNT=0 + SKIP_COUNT=0 + FAIL_COUNT=0 + NO_COLOR=1 + RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" NC="" + declare -gA VERSION_BEFORE=() + declare -gA VERSION_AFTER=() + + source "'"$UPDATE_SH"'" + + run_cmd_claude_update + echo "DRY_RUN_EXIT=$?" + ' 2>&1 +) || true + +if echo "$dry_run_output" | grep -q 'DRY_RUN_EXIT=0'; then + pass "Dry-run mode returns 0 without executing installer" +else + fail "Dry-run mode did not return 0. Output: $dry_run_output" +fi + +# ============================================================ +section "Test 4: Function instrumentation (mock)" +# ============================================================ +# Source update.sh, override update_run_verified_installer with a mock, +# call run_cmd_claude_update, verify the mock was called with "claude latest". +# NOTE: The non-verbose code path runs update_run_verified_installer inside +# a $() subshell, so shell variable changes are lost. We use a temp file +# as a signal instead, and run in VERBOSE mode so the mock runs in-process. +MOCK_SIGNAL="/tmp/test_update_channel_mock_$$" +rm -f "$MOCK_SIGNAL" +mock_output=$( + bash -c ' + DRY_RUN=false + VERBOSE=true + QUIET=true + FORCE_MODE=false + YES_MODE=false + ABORT_ON_FAILURE=false + UPDATE_LOG_FILE="/dev/null" + SUCCESS_COUNT=0 + SKIP_COUNT=0 + FAIL_COUNT=0 + NO_COLOR=1 + RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" NC="" + declare -gA VERSION_BEFORE=() + declare -gA VERSION_AFTER=() + + source "'"$UPDATE_SH"'" + + # Override with mock — write args to a temp file signal + update_run_verified_installer() { + echo "$*" > "'"$MOCK_SIGNAL"'" + return 0 + } + + run_cmd_claude_update 2>&1 + ' 2>&1 +) || true + +if [[ -f "$MOCK_SIGNAL" ]]; then + mock_args=$(cat "$MOCK_SIGNAL") + rm -f "$MOCK_SIGNAL" + if [[ "$mock_args" == "claude latest" ]]; then + pass "Mock update_run_verified_installer called with 'claude latest'" + else + fail "Mock called but with wrong args: '$mock_args'" + fi +else + fail "Mock update_run_verified_installer was NOT called. Output: $mock_output" +fi + +# ============================================================ +section "Test 5: Security fallback" +# ============================================================ +# Source update.sh, mock update_require_security to fail, +# verify update_run_verified_installer returns non-zero with a warning. +security_output=$( + bash -c ' + DRY_RUN=false + VERBOSE=false + QUIET=true + FORCE_MODE=false + YES_MODE=false + ABORT_ON_FAILURE=false + UPDATE_LOG_FILE="/dev/null" + SUCCESS_COUNT=0 + SKIP_COUNT=0 + FAIL_COUNT=0 + NO_COLOR=1 + RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" NC="" + declare -gA VERSION_BEFORE=() + declare -gA VERSION_AFTER=() + + source "'"$UPDATE_SH"'" + + # Override security check to always fail + update_require_security() { return 1; } + + # Call the verified installer directly — it should fail gracefully + update_run_verified_installer claude latest 2>&1 || echo "SECURITY_EXIT=$?" + ' 2>&1 +) || true + +if echo "$security_output" | grep -q 'SECURITY_EXIT='; then + if echo "$security_output" | grep -qi 'security\|unavailable\|missing'; then + pass "Security fallback produces warning and non-zero exit when security unavailable" + else + pass "Security fallback returns non-zero when update_require_security fails" + fi +else + fail "update_run_verified_installer did not fail when security was unavailable. Output: $security_output" +fi + +# ============================================================ +section "Test 6: uca alias definition" +# ============================================================ +if [[ -f "$ZSHRC" ]]; then + uca_line=$(grep "alias uca=" "$ZSHRC" || true) + if [[ -z "$uca_line" ]]; then + fail "uca alias not found in acfs.zshrc" + else + # Check 1: no bare "claude update" in the alias + if echo "$uca_line" | grep -q 'claude update'; then + fail "uca alias contains bare 'claude update': $uca_line" + else + # Check 2: uses install.sh with latest (the verified approach) + if echo "$uca_line" | grep -q 'install.sh.*latest'; then + pass "uca alias uses install.sh with latest channel (no bare 'claude update')" + else + pass "uca alias does not contain bare 'claude update'" + fi + fi + + # Check 3: codex and gemini are preserved in the alias chain + has_codex=false + has_gemini=false + echo "$uca_line" | grep -q 'codex' && has_codex=true + echo "$uca_line" | grep -q 'gemini' && has_gemini=true + if $has_codex && $has_gemini; then + pass "uca alias preserves codex and gemini components" + else + fail "uca alias missing components: codex=$has_codex gemini=$has_gemini" + fi + fi +else + skip "acfs.zshrc not found at $ZSHRC" +fi + +# ============================================================ +section "Test 7: Completeness sweep — no bare 'claude update' in repo" +# ============================================================ +# Grep across the whole repo for "claude update", excluding: +# - comments (lines starting with #) +# - this test file itself +# - .beads/ directories +# - node_modules/target/.git +# - known-safe patterns (update_run_verified_installer, install.sh, cmd_display=, FIX(bd-gsjqf) +# - lines with INTENTIONAL marker +sweep_hits=$( + grep -rn "claude update" "$REPO_ROOT" \ + --include='*.sh' --include='*.zsh' --include='*.zshrc' --include='*.bashrc' \ + --exclude-dir='.git' --exclude-dir='node_modules' --exclude-dir='.beads' \ + --exclude-dir='target' \ + 2>/dev/null \ + | grep -v 'update_run_verified_installer' \ + | grep -v 'install\.sh.*latest' \ + | grep -v '^\s*#' \ + | grep -v 'test_update_channel' \ + | grep -v 'cmd_display=' \ + | grep -v 'FIX(bd-gsjqf' \ + | grep -v 'INTENTIONAL' \ + | grep -v 'PLAN_TO_CREATE' \ + || true +) +if [[ -z "$sweep_hits" ]]; then + pass "No unprotected bare 'claude update' found in shell files" +else + log " Bare hits found:" + echo "$sweep_hits" | while IFS= read -r line; do log " $line"; done + fail "Found bare 'claude update' in shell files (see above)" +fi + +# ============================================================ +section "Test 8: Channel version alignment (live, optional)" +# ============================================================ +if command -v npm &>/dev/null && command -v claude &>/dev/null; then + dist_tags=$(npm view @anthropic-ai/claude-code dist-tags 2>/dev/null || true) + installed=$(claude --version 2>/dev/null | grep -oP '[\d]+\.[\d]+\.[\d]+' || true) + latest=$(echo "$dist_tags" | grep -oP "latest: '\K[^']+" || true) + stable=$(echo "$dist_tags" | grep -oP "stable: '\K[^']+" || true) + log " Installed: ${installed:-unknown}" + log " Latest: ${latest:-unknown}" + log " Stable: ${stable:-unknown}" + if [[ -z "$installed" ]] || [[ -z "$latest" ]]; then + skip "Could not determine version info for live channel check" + elif [[ "$installed" == "$latest" ]]; then + pass "Installed claude version matches latest channel ($installed)" + elif [[ "$installed" == "$stable" ]]; then + fail "Installed claude version matches STABLE channel ($installed) — possible downgrade!" + else + skip "Version $installed matches neither latest ($latest) nor stable ($stable)" + fi +else + skip "npm or claude not available — skipping live channel check" +fi + +# ============================================================ +section "Summary" +# ============================================================ +log "" +log "Results: $PASS passed, $FAIL failed, $SKIP skipped" +log "Log: $LOG_FILE" + +if [[ "$FAIL" -gt 0 ]]; then + log "RESULT: FAIL" + exit 1 +else + log "RESULT: PASS" + exit 0 +fi diff --git a/tests/e2e/test_happy_path.bats b/tests/e2e/test_happy_path.bats index 24cf6a51..b9efa97c 100644 --- a/tests/e2e/test_happy_path.bats +++ b/tests/e2e/test_happy_path.bats @@ -40,7 +40,7 @@ teardown() { assert_success [[ -f "$project_dir/AGENTS.md" ]] - # Verify AGENTS.md has some content (bd or newproj creates it) + # Verify AGENTS.md has some content (br or newproj creates it) [[ -s "$project_dir/AGENTS.md" ]] } @@ -51,25 +51,25 @@ teardown() { run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" assert_success - [[ -d "$project_dir/.beads" ]] || skip "bd not installed" + [[ -d "$project_dir/.beads" ]] || skip "br not installed" } -@test "CLI mode with --no-bd skips beads" { - local project_name="cli-no-bd-test" +@test "CLI mode with --no-br skips beads" { + local project_name="cli-no-br-test" local project_dir="$E2E_TEST_DIR/$project_name" - run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-bd + run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-br assert_success [[ ! -d "$project_dir/.beads" ]] } -@test "CLI mode with --no-agents --no-bd skips AGENTS.md completely" { - # Note: bd creates its own AGENTS.md, so we need --no-bd too +@test "CLI mode with --no-agents --no-br skips AGENTS.md completely" { + # Note: br creates its own AGENTS.md, so we need --no-br too local project_name="cli-no-agents-test" local project_dir="$E2E_TEST_DIR/$project_name" - run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-agents --no-bd + run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-agents --no-br assert_success [[ ! -f "$project_dir/AGENTS.md" ]] @@ -236,6 +236,6 @@ EOF [[ -s "$project_dir/AGENTS.md" ]] # AGENTS.md should have some meaningful content - # bd creates "landing the plane" instructions, newproj creates standard template + # br creates "landing the plane" instructions, newproj creates standard template grep -qE "(AGENT|Landing|plane|session|git)" "$project_dir/AGENTS.md" } diff --git a/tests/e2e/test_helper.bash b/tests/e2e/test_helper.bash index acac138f..d403b73c 100644 --- a/tests/e2e/test_helper.bash +++ b/tests/e2e/test_helper.bash @@ -202,7 +202,7 @@ verify_feature_enabled() { agents|AGENTS.md) [[ -f "$project_dir/AGENTS.md" ]] ;; - beads|bd) + beads|br) [[ -d "$project_dir/.beads" ]] && [[ -f "$project_dir/.beads/beads.db" ]] ;; claude) diff --git a/tests/e2e/test_navigation.bats b/tests/e2e/test_navigation.bats index 8103deef..f7a7a703 100644 --- a/tests/e2e/test_navigation.bats +++ b/tests/e2e/test_navigation.bats @@ -179,7 +179,7 @@ teardown() { local project_name="multi-flag-test" local project_dir="$E2E_TEST_DIR/$project_name" - run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-bd --no-agents + run bash "$ACFS_LIB_DIR/newproj.sh" "$project_name" "$project_dir" --no-br --no-agents assert_success [[ -d "$project_dir" ]] diff --git a/tests/e2e/test_new_tools_e2e.sh b/tests/e2e/test_new_tools_e2e.sh index 1cc37fd7..981b2515 100755 --- a/tests/e2e/test_new_tools_e2e.sh +++ b/tests/e2e/test_new_tools_e2e.sh @@ -4,7 +4,7 @@ # Tests: # - 7 First-class flywheel tools: br, ms, rch, wa, brenner, dcg, ru # - 9 Utility tools: tru, rust_proxy, rano, xf, mdwb, pt, aadc, s2p, caut -# - Integration: acfs doctor, flywheel.ts, bd alias +# - Integration: acfs doctor, flywheel.ts, br primary command # # Related: bead bd-1ega.7 @@ -247,24 +247,16 @@ test_integration() { skip "doctor_no_git_safety_guard" "acfs command not found" fi - # Test 2: bd alias maps to br - log "INFO" "bd_alias" "Testing bd alias..." - if command -v bd >/dev/null 2>&1; then - local bd_version br_version - bd_version=$(bd --version 2>&1 | head -1) || true - br_version=$(br --version 2>&1 | head -1) || true - if [[ "$bd_version" == "$br_version" ]]; then - pass "bd_alias" "bd alias correctly maps to br" + # Test 2: br is the primary command (bd alias was removed) + log "INFO" "br_primary" "Testing br is the primary beads command..." + if command -v br >/dev/null 2>&1; then + if br --help >/dev/null 2>&1; then + pass "br_primary" "br is the primary beads_rust command" else - fail "bd_alias" "bd and br version mismatch: bd='$bd_version' br='$br_version'" + fail "br_primary" "br --help failed" fi else - # Check zshrc - if [[ -f ~/.acfs/zsh/acfs.zshrc ]] && command grep -q "alias bd=" ~/.acfs/zsh/acfs.zshrc 2>/dev/null; then - pass "bd_alias" "bd alias defined in acfs.zshrc" - else - fail "bd_alias" "bd alias not found" - fi + fail "br_primary" "br binary not found" fi # Test 3: Flywheel.ts contains all new tools @@ -277,7 +269,7 @@ test_integration() { if [[ -f "$flywheel_file" ]]; then local missing_tools=() for tool in br ms rch wa brenner dcg ru tru rust_proxy rano xf mdwb pt aadc s2p caut; do - if ! command grep -qE "id:\s*[\"']$tool[\"']" "$flywheel_file"; then + if ! command grep -qE "id:\s*[\"']${tool}[\"']" "$flywheel_file"; then missing_tools+=("$tool") fi done diff --git a/tests/unit/lib/test_install_helpers.bats b/tests/unit/lib/test_install_helpers.bats index e09d4a79..0264b641 100644 --- a/tests/unit/lib/test_install_helpers.bats +++ b/tests/unit/lib/test_install_helpers.bats @@ -110,10 +110,6 @@ teardown() { fi } -# ============================================================ -# Skip-if-installed tests (bd-1eop) -# ============================================================ - @test "_acfs_force_reinstall_enabled: returns 0 when true" { export ACFS_FORCE_REINSTALL="true" run _acfs_force_reinstall_enabled @@ -126,31 +122,70 @@ teardown() { assert_failure } -@test "acfs_module_is_installed: returns 1 if no check defined" { +# ------------------------------------------------- +# Skip-if-installed tests (bd-1eop) +# ------------------------------------------------- + +@test "acfs_module_is_installed: returns false when no check defined" { + # Clear installed_check arrays + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS declare -gA ACFS_MODULE_INSTALLED_CHECK=() + declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=() + run acfs_module_is_installed "mod1" assert_failure } -@test "acfs_module_is_installed: runs check command" { +@test "acfs_module_is_installed: returns true when check succeeds" { + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS declare -gA ACFS_MODULE_INSTALLED_CHECK=( ["mod1"]="true" ) declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( ["mod1"]="current" ) + run acfs_module_is_installed "mod1" assert_success } -@test "acfs_should_skip_module: skips when installed" { - export ACFS_FORCE_REINSTALL="false" +@test "acfs_module_is_installed: returns false when check fails" { + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS + declare -gA ACFS_MODULE_INSTALLED_CHECK=( ["mod1"]="false" ) + declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( ["mod1"]="current" ) + + run acfs_module_is_installed "mod1" + assert_failure +} + +@test "acfs_should_skip_module: skips installed modules" { + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS declare -gA ACFS_MODULE_INSTALLED_CHECK=( ["mod1"]="true" ) declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( ["mod1"]="current" ) + export ACFS_FORCE_REINSTALL=false + run acfs_should_skip_module "mod1" - assert_success + assert_success # 0 means should skip } @test "acfs_should_skip_module: does not skip when force reinstall" { - export ACFS_FORCE_REINSTALL="true" + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS declare -gA ACFS_MODULE_INSTALLED_CHECK=( ["mod1"]="true" ) declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( ["mod1"]="current" ) + export ACFS_FORCE_REINSTALL=true + run acfs_should_skip_module "mod1" - assert_failure + assert_failure # 1 means do not skip (install) +} + +@test "acfs_module_is_installed: respects run_as target_user" { + unset ACFS_MODULE_INSTALLED_CHECK ACFS_MODULE_INSTALLED_CHECK_RUN_AS + declare -gA ACFS_MODULE_INSTALLED_CHECK=( ["mod1"]="true" ) + declare -gA ACFS_MODULE_INSTALLED_CHECK_RUN_AS=( ["mod1"]="target_user" ) + + # Mock run_as_target to just pass through to bash + # Note: We need to export the function for subshells + run_as_target() { + "$@" + } + export -f run_as_target + + run acfs_module_is_installed "mod1" + assert_success } diff --git a/tests/unit/lib/test_logging.bats b/tests/unit/lib/test_logging.bats index a02f8760..951af3e3 100644 --- a/tests/unit/lib/test_logging.bats +++ b/tests/unit/lib/test_logging.bats @@ -13,13 +13,17 @@ teardown() { } @test "logging: color variables are exported" { - # Color variables should be exported with ANSI escape sequences + # Color variables should be exported (but may be empty if NO_COLOR or no TTY) + # Test that the ACFS_COLORS_ENABLED flag is set correctly [[ -v ACFS_RED ]] [[ -v ACFS_GREEN ]] [[ -v ACFS_NC ]] - # Verify they contain escape sequences (start with \033) - [[ "$ACFS_RED" == *"033"* ]] - [[ "$ACFS_GREEN" == *"033"* ]] + [[ -v ACFS_COLORS_ENABLED ]] + + # Without a TTY (bats environment), colors should be disabled + if [[ ! -t 2 ]]; then + [[ "$ACFS_COLORS_ENABLED" == "false" ]] + fi } @test "logging: log_success prints green checkmark to stderr" { diff --git a/tests/unit/lib/test_newproj_errors.bats b/tests/unit/lib/test_newproj_errors.bats index 783d454b..419e4793 100644 --- a/tests/unit/lib/test_newproj_errors.bats +++ b/tests/unit/lib/test_newproj_errors.bats @@ -151,35 +151,35 @@ teardown() { } # ============================================================ -# bd Initialization Tests +# br Initialization Tests # ============================================================ -@test "try_bd_init gracefully skips if bd not installed" { - local project_dir="$TEST_DIR/bd-project" +@test "try_br_init gracefully skips if br not installed" { + local project_dir="$TEST_DIR/br-project" mkdir -p "$project_dir" - # Create a mock function that pretends bd is not installed + # Create a mock function that pretends br is not installed # by temporarily overriding command - bd() { + br() { return 127 # Command not found } - export -f bd + export -f br - # Use a subshell to test the case where bd command doesn't exist + # Use a subshell to test the case where br command doesn't exist run bash -c ' source '"$ACFS_LIB_DIR"'/newproj_errors.sh - # Override command -v to report bd as missing + # Override command -v to report br as missing command() { - if [[ "$2" == "bd" ]]; then + if [[ "$2" == "br" ]]; then return 1 fi builtin command "$@" } - try_bd_init "'"$project_dir"'" + try_br_init "'"$project_dir"'" ' assert_success # Should not fail, just skip - [[ "$output" == *"bd not installed"* ]] + [[ "$output" == *"br not installed"* ]] } # ============================================================ diff --git a/tests/unit/lib/test_security.bats b/tests/unit/lib/test_security.bats index 58da6ec3..032a6954 100644 --- a/tests/unit/lib/test_security.bats +++ b/tests/unit/lib/test_security.bats @@ -33,7 +33,7 @@ teardown() { else sha=$(echo -n "$content" | shasum -a 256 | cut -d' ' -f1) fi - + # Stub curl to return content (handles -o flag) stub_curl "$content" 0 diff --git a/tests/unit/newproj/test_agents_md_generator.bats b/tests/unit/newproj/test_agents_md_generator.bats index 42c568f8..31d450ac 100644 --- a/tests/unit/newproj/test_agents_md_generator.bats +++ b/tests/unit/newproj/test_agents_md_generator.bats @@ -145,7 +145,7 @@ teardown() { local content content=$(get_section_content "issue_tracking") - [[ "$content" == *"bd"* ]] + [[ "$content" == *"br"* ]] [[ "$content" == *"beads"* ]] } @@ -285,21 +285,21 @@ teardown() { [[ "$content" == *"Docker Workflow"* ]] } -@test "generate_agents_md includes bd section when enabled" { - export AGENTS_ENABLE_BD=true +@test "generate_agents_md includes br section when enabled" { + export AGENTS_ENABLE_BR=true local content content=$(generate_agents_md "test-project") - [[ "$content" == *"Issue Tracking with bd"* ]] + [[ "$content" == *"Issue Tracking with br"* ]] [[ "$content" == *"beads"* ]] } -@test "generate_agents_md excludes bd section when disabled" { - export AGENTS_ENABLE_BD=false +@test "generate_agents_md excludes br section when disabled" { + export AGENTS_ENABLE_BR=false local content content=$(generate_agents_md "test-project") - [[ "$content" != *"Issue Tracking with bd"* ]] + [[ "$content" != *"Issue Tracking with br"* ]] } @test "generate_agents_md includes console section when enabled" { diff --git a/tests/unit/newproj/test_interactive_flag.bats b/tests/unit/newproj/test_interactive_flag.bats index d677ef9a..27240229 100644 --- a/tests/unit/newproj/test_interactive_flag.bats +++ b/tests/unit/newproj/test_interactive_flag.bats @@ -288,12 +288,12 @@ teardown() { # Combined Flag Tests # ============================================================ -@test "main allows --interactive with --no-bd" { +@test "main allows --interactive with --no-br" { export CI=true run bash -c ' source '"$ACFS_LIB_DIR"'/newproj.sh - main --interactive --no-bd 2>&1 + main --interactive --no-br 2>&1 ' # Should reach interactive mode (and fail on CI check) diff --git a/tests/unit/newproj/test_screens.bats b/tests/unit/newproj/test_screens.bats index 6f429c37..ef03d480 100644 --- a/tests/unit/newproj/test_screens.bats +++ b/tests/unit/newproj/test_screens.bats @@ -332,15 +332,15 @@ teardown() { source_lib "newproj_screens" load_screens - local found_bd=false + local found_br=false local found_agents=false for opt in "${FEATURE_OPTIONS[@]}"; do - [[ "$opt" == "bd:"* ]] && found_bd=true + [[ "$opt" == "br:"* ]] && found_br=true [[ "$opt" == "agents:"* ]] && found_agents=true done - [[ "$found_bd" == "true" ]] + [[ "$found_br" == "true" ]] [[ "$found_agents" == "true" ]] } @@ -349,8 +349,8 @@ teardown() { load_screens local key - key=$(get_feature_key "bd") - [[ "$key" == "enable_bd" ]] + key=$(get_feature_key "br") + [[ "$key" == "enable_br" ]] key=$(get_feature_key "agents") [[ "$key" == "enable_agents" ]] @@ -360,12 +360,12 @@ teardown() { source_lib "newproj_screens" load_screens - state_set "enable_bd" "true" - toggle_feature "bd" - [[ "$(state_get "enable_bd")" == "false" ]] + state_set "enable_br" "true" + toggle_feature "br" + [[ "$(state_get "enable_br")" == "false" ]] - toggle_feature "bd" - [[ "$(state_get "enable_bd")" == "true" ]] + toggle_feature "br" + [[ "$(state_get "enable_br")" == "true" ]] } # ============================================================ @@ -423,7 +423,7 @@ teardown() { state_set "project_dir" "/tmp/test-project" state_set "enable_agents" "true" - state_set "enable_bd" "true" + state_set "enable_br" "true" local files files=$(get_files_to_create) @@ -438,7 +438,7 @@ teardown() { state_set "project_dir" "/tmp/test-project" state_set "enable_agents" "false" - state_set "enable_bd" "false" + state_set "enable_br" "false" local files files=$(get_files_to_create) @@ -463,7 +463,7 @@ teardown() { load_screens state_set "enable_agents" "true" - state_set "enable_bd" "false" + state_set "enable_br" "false" init_creation_steps @@ -471,7 +471,7 @@ teardown() { [[ " ${STEP_ORDER[*]} " =~ " create_dir " ]] [[ " ${STEP_ORDER[*]} " =~ " init_git " ]] [[ " ${STEP_ORDER[*]} " =~ " create_agents " ]] - [[ ! " ${STEP_ORDER[*]} " =~ " init_bd " ]] + [[ ! " ${STEP_ORDER[*]} " =~ " init_br " ]] } @test "get_step_name returns readable names" { diff --git a/tests/unit/test_br_integration.sh b/tests/unit/test_br_integration.sh index cb751424..e8572a7e 100755 --- a/tests/unit/test_br_integration.sh +++ b/tests/unit/test_br_integration.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Unit tests for beads_rust (br) integration -# Tests that br binary works, bd alias is configured, and basic operations succeed +# Tests that br binary works and basic operations succeed set -uo pipefail # Note: Not using -e to allow tests to continue after failures @@ -44,26 +44,14 @@ test_br_version() { fi } -# Test 3: bd alias works (requires sourcing zshrc) -test_bd_alias() { - log "Test 3: bd alias..." - # Check if bd is available (either as alias or binary) - if command -v bd >/dev/null 2>&1; then - local bd_output br_output - bd_output=$(bd --version 2>&1 | head -1) || true - br_output=$(br --version 2>&1 | head -1) || true - if [[ "$bd_output" == "$br_output" ]]; then - pass "bd alias correctly maps to br" - else - fail "bd and br versions differ: bd='$bd_output' br='$br_output'" - fi +# Test 3: br is the primary command (bd alias removed) +test_br_primary() { + log "Test 3: br is the primary beads command..." + # Confirm br works as primary command + if br --help >/dev/null 2>&1; then + pass "br is the primary beads_rust command" else - # Check if alias is defined in zshrc - if grep -q "alias bd=.br" ~/.acfs/zsh/acfs.zshrc 2>/dev/null; then - pass "bd alias defined in acfs.zshrc (need to source it)" - else - fail "bd alias not found" - fi + fail "br --help failed" fi } @@ -142,7 +130,7 @@ main() { test_br_binary test_br_version - test_bd_alias + test_br_primary test_br_list test_br_ready test_bv_binary diff --git a/tests/vm/test_runner.sh b/tests/vm/test_runner.sh index 25bc7ba3..02e63c17 100644 --- a/tests/vm/test_runner.sh +++ b/tests/vm/test_runner.sh @@ -33,7 +33,7 @@ fi # PHASE 1: Fresh Install log "PHASE 1: Fresh Install (mode=${ACFS_TEST_MODE})" -if bash install.sh --yes --mode "${ACFS_TEST_MODE}" ${STRICT_FLAG} > "${ARTIFACTS_DIR}/install.log" 2>&1; then +if bash install.sh --yes --skip-ubuntu-upgrade --mode "${ACFS_TEST_MODE}" ${STRICT_FLAG} > "${ARTIFACTS_DIR}/install.log" 2>&1; then log "Install successful" else log "Install failed! Last 50 lines:" @@ -58,30 +58,30 @@ run_check() { failed_checks=0 -run_check "doctor" "zsh -ic 'acfs doctor'" || ((failed_checks++)) -run_check "state_file" "test -f ~/.acfs/VERSION" || ((failed_checks++)) -run_check "onboard" "zsh -ic 'onboard --help >/dev/null'" || ((failed_checks++)) -run_check "ntm" "zsh -ic 'ntm --help >/dev/null'" || ((failed_checks++)) -run_check "gh" "zsh -ic 'gh --version >/dev/null'" || ((failed_checks++)) -run_check "jq" "zsh -ic 'jq --version >/dev/null'" || ((failed_checks++)) -run_check "sg" "zsh -ic 'sg --version >/dev/null'" || ((failed_checks++)) -run_check "codex" "zsh -ic 'codex --version >/dev/null'" || ((failed_checks++)) -run_check "gemini" "zsh -ic 'gemini --version >/dev/null'" || ((failed_checks++)) -run_check "claude" "zsh -ic 'claude --version >/dev/null'" || ((failed_checks++)) -run_check "ru" "zsh -ic 'ru --version >/dev/null'" || ((failed_checks++)) -run_check "dcg" "zsh -ic 'dcg --version >/dev/null'" || ((failed_checks++)) +run_check "doctor" "zsh -ic 'acfs doctor'" || failed_checks=$((failed_checks + 1)) +run_check "state_file" "test -f ~/.acfs/VERSION" || failed_checks=$((failed_checks + 1)) +run_check "onboard" "zsh -ic 'onboard --help >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "ntm" "zsh -ic 'ntm --help >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "gh" "zsh -ic 'gh --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "jq" "zsh -ic 'jq --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "sg" "zsh -ic 'sg --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "codex" "zsh -ic 'codex --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "gemini" "zsh -ic 'gemini --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "claude" "zsh -ic 'claude --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "ru" "zsh -ic 'ru --version >/dev/null'" || failed_checks=$((failed_checks + 1)) +run_check "dcg" "zsh -ic 'dcg --version >/dev/null'" || failed_checks=$((failed_checks + 1)) # Check DCG hook -run_check "dcg_hook" "zsh -ic 'set -o pipefail; dcg doctor --format json 2>/dev/null | jq -e \".hook_registered == true\" >/dev/null || dcg doctor 2>/dev/null | grep -qi \"hook wiring.*OK\"'" || ((failed_checks++)) -run_check "dcg_block" "zsh -ic 'dcg test \"git reset --hard\" | grep -Eqi \"deny|block\"'" || ((failed_checks++)) -run_check "dcg_allow" "zsh -ic 'dcg test \"git status\" | grep -Eqi \"allow\"'" || ((failed_checks++)) +run_check "dcg_hook" "zsh -ic 'set -o pipefail; dcg doctor --format json 2>/dev/null | jq -e \".hook_registered == true\" >/dev/null || dcg doctor 2>/dev/null | grep -qi \"hook wiring.*OK\"'" || failed_checks=$((failed_checks + 1)) +run_check "dcg_block" "zsh -ic 'dcg test \"git reset --hard\" | grep -Eqi \"deny|block\"'" || failed_checks=$((failed_checks + 1)) +run_check "dcg_allow" "zsh -ic 'dcg test \"git status\" | grep -Eqi \"allow\"'" || failed_checks=$((failed_checks + 1)) # Resume checks if bash /repo/tests/vm/resume_checks.sh >> "$VERIFY_LOG" 2>&1; then echo " [ok] resume_checks" else echo " [fail] resume_checks" - ((failed_checks++)) + failed_checks=$((failed_checks + 1)) fi if [[ $failed_checks -gt 0 ]]; then @@ -121,7 +121,7 @@ fi # PHASE 3: Idempotency log "PHASE 3: Idempotency Check" -if bash install.sh --yes --mode "${ACFS_TEST_MODE}" ${STRICT_FLAG} > "${ARTIFACTS_DIR}/idempotency.log" 2>&1; then +if bash install.sh --yes --skip-ubuntu-upgrade --mode "${ACFS_TEST_MODE}" ${STRICT_FLAG} > "${ARTIFACTS_DIR}/idempotency.log" 2>&1; then log "Idempotency run successful" else log "Idempotency run failed! Last 50 lines:"