diff --git a/.huskies/AGENT.md b/.huskies/AGENT.md index 0c8a3f86..7100f6a7 100644 --- a/.huskies/AGENT.md +++ b/.huskies/AGENT.md @@ -56,7 +56,7 @@ There are no exceptions. The merge gate runs `source-map-check` and rejects the Before committing, run `cargo run -p source-map-gen --bin source-map-check -- --worktree . --base master` and address every missing-docs direction it prints. If you added a new module file (e.g. `foo.rs` or `foo/mod.rs`), the FIRST line of that file MUST be a `//! What this module is for` doc comment. ## Documentation -Docs live in `website/docs/*.html` (static HTML), **not** Markdown files. When a story asks you to document something, edit the relevant `.html` file in `website/docs/`. +Docs live in `website/app/docs/*.tsx` (Next.js pages), **not** Markdown files. When a story asks you to document something, edit the relevant `.tsx` file under `website/app/docs/`. Run `npm run build` in `website/` to verify your changes render correctly. ## Configuration files - Agent config: `.huskies/agents.toml` (preferred) or `[[agent]]` blocks in `.huskies/project.toml` diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..c03a1711 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +out/ +next-env.d.ts diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..45c65894 --- /dev/null +++ b/website/README.md @@ -0,0 +1,57 @@ +# Huskies Website + +Static marketing and documentation site, built with Next.js (`output: 'export'`). + +## Development + +```bash +npm install +npm run dev # starts dev server at http://localhost:3000 +``` + +## Build + +```bash +npm install +npm run build # produces out/ directory +``` + +The `out/` directory is a fully static export — plain HTML, CSS, JS, and assets. No Node.js required at runtime. + +## Deploy + +Serve the `out/` directory with any static file server. nginx example: + +```nginx +server { + listen 80; + server_name huskies.dev; + root /path/to/out; + index index.html; + + location / { + try_files $uri $uri.html $uri/ =404; + } +} +``` + +## Pages + +| Route | File | +|-------|------| +| `/` | `app/page.tsx` | +| `/privacy` | `app/privacy/page.tsx` | +| `/docs` | `app/docs/page.tsx` | +| `/docs/quickstart` | `app/docs/quickstart/page.tsx` | +| `/docs/pipeline` | `app/docs/pipeline/page.tsx` | +| `/docs/commands` | `app/docs/commands/page.tsx` | +| `/docs/configuration` | `app/docs/configuration/page.tsx` | +| `/docs/cli` | `app/docs/cli/page.tsx` | +| `/docs/transports` | `app/docs/transports/page.tsx` | + +## Tech + +- Next.js 14 App Router with `output: 'export'` +- TypeScript +- `next/font/google` for self-hosted Bricolage Grotesque and Karla fonts +- No server components with dynamic data, no API routes, no server actions diff --git a/website/app/docs/cli/page.tsx b/website/app/docs/cli/page.tsx new file mode 100644 index 00000000..a7066911 --- /dev/null +++ b/website/app/docs/cli/page.tsx @@ -0,0 +1,384 @@ +/** CLI reference — flags and subcommands for the huskies binary. */ +import type { Metadata } from 'next' + +/** Page metadata for the CLI reference. */ +export const metadata: Metadata = { + title: 'CLI Reference — Huskies Docs', + description: 'Command-line reference for huskies, huskies init, and related subcommands.', +} + +/** Renders the CLI reference for huskies command-line flags. */ +export default function CliPage() { + return ( + <> +

CLI Reference

+

+ Huskies ships as a single binary. Most interaction happens through the web UI or chat transports, + but the CLI is used for initial setup and server control. +

+ +

huskies

+

Start the huskies server.

+
+        huskies [OPTIONS]
+      
+ +

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDefaultDescription
--port <PORT>3000 + HTTP port to listen on. Set the HUSKIES_PORT environment variable as an + alternative. +
--project <PATH>current dir + Path to the project directory. Huskies looks for .huskies/ here. +
--helpPrint help and exit.
--versionPrint version and exit.
+ +

Examples

+
+        {`# Start on the default port
+huskies
+
+# Start on a custom port
+huskies --port 3001
+
+# Specify project directory explicitly
+huskies --project /path/to/project --port 3000
+
+# Using environment variable
+HUSKIES_PORT=3002 huskies`}
+      
+ +
+ Multiple instances: Each worktree or project can run its own huskies instance on a + different port. Use HUSKIES_PORT to avoid conflicts when running several instances + simultaneously. +
+ +

huskies init

+

+ Initialise a project directory for use with huskies. Creates the .huskies/ directory + structure, default configuration files, and .mcp.json. +

+
+        huskies init [OPTIONS]
+      
+ +

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDefaultDescription
--port <PORT>3000 + Port written into .mcp.json for MCP tool discovery. +
--project <PATH>current dirDirectory to initialise. Must be a git repository.
--helpPrint help and exit.
+ +

What it creates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathDescription
+ .huskies/project.toml + Project-wide settings (QA mode, agent limits, timezone, etc.).
+ .huskies/agents.toml + Agent definitions for coder, QA, and mergemaster roles.
+ .huskies/work/1_backlog/ + Pipeline stage directories (1 through 6).
+ .huskies/specs/00_CONTEXT.md + Placeholder project context file for the setup wizard.
+ .huskies/specs/tech/STACK.md + Placeholder tech stack file for the setup wizard.
+ .mcp.json + + MCP server config so Claude Code discovers huskies' tools automatically. +
+ +
+ Git required: The project directory must be a git repository. Run{' '} + git init first if needed. +
+ +

huskies agent

+

+ Spawn a single agent process directly from the command line. This is the command the server uses + internally when you run start <number> in chat — you rarely need to invoke + it manually. +

+
+        huskies agent [OPTIONS]
+      
+ +

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDescription
--story <ID> + Story ID slug to work on (e.g. 42_story_add_login). +
--agent <NAME> + Agent name from agents.toml to use (e.g. coder-1,{' '} + qa). +
--worktree <PATH>Path to the git worktree the agent should work in.
--port <PORT>Huskies server port, so the agent can call MCP tools.
--helpPrint help and exit.
+ +

Environment variables

+ + + + + + + + + + + + + + + + + + + + + +
VariableDescription
+ HUSKIES_PORT + + Server port. Overrides the --port flag. +
+ ANTHROPIC_API_KEY + + Anthropic API key for agent sessions. Can also be set via the web UI on first use. +
+ GITEA_TOKEN + + Gitea API token used by the script/release script when publishing releases. +
+ +

Gateway event-push protocol

+

+ Project nodes can push pipeline status events to the gateway in real time over a WebSocket + connection. The gateway fans each event out to all connected local subscribers. +

+ +

Connecting

+
    +
  1. + Obtain a one-time join token: POST /gateway/tokens →{' '} + {'{"token":"…"}'} +
  2. +
  3. + Open a WebSocket upgrade to{' '} + GET /gateway/events/push?token=TOKEN&project=PROJECT_NAME +
  4. +
  5. + The token is consumed on upgrade. The project name is attached to every event the server + broadcasts downstream. +
  6. +
+ +

Sending events

+

+ Each message must be a JSON-encoded StoredEvent frame: +

+
+        {`// Stage transition
+{"type":"stage_transition","story_id":"42_story_login","from_stage":"2_current","to_stage":"3_qa","timestamp_ms":1700000000000}
+
+// Merge failure
+{"type":"merge_failure","story_id":"42_story_login","reason":"conflict in src/main.rs","timestamp_ms":1700000001000}
+
+// Story blocked
+{"type":"story_blocked","story_id":"42_story_login","reason":"retry limit exceeded","timestamp_ms":1700000002000}`}
+      
+

+ The server does not send frames back. Any other frames received by the project node indicate an + error or server restart — treat them as a disconnect signal. +

+ +

Reconnect with exponential back-off

+

+ Project nodes must reconnect on any disconnect. Use the following policy to avoid + thundering herds after a gateway restart: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterValue
Initial delay1 s
Back-off multiplier2× per attempt
Maximum delay60 s
Jitter±10 % of the computed delay
+

Pseudocode:

+
+        {`delay = 1.0          // seconds
+max_delay = 60.0
+
+loop:
+    token = POST /gateway/tokens
+    connect ws:/gateway/events/push?token=TOKEN&project=NAME
+    while connected:
+        send StoredEvent frames
+    // disconnected — wait and retry
+    jitter = delay * (random(0.9, 1.1))
+    sleep(min(jitter, max_delay))
+    delay = min(delay * 2, max_delay)`}
+      
+ +
+ New token per connection: Each WebSocket upgrade consumes the join token. Request a + fresh token for every reconnect attempt. +
+ +

Building from source

+

Standard release build

+
+        {`cargo build --release\n# Output: target/release/huskies`}
+      
+ +

Static Linux binary (musl)

+

+ Requires cross: cargo install cross. +

+
+        cross build --release --target x86_64-unknown-linux-musl
+      
+ +

Docker image

+
+        docker compose -f docker/docker-compose.yml build
+      
+ +

Release script

+

+ Builds macOS arm64 and Linux amd64 binaries, bumps the version, tags the repo, and publishes a + Gitea release with changelog and binaries attached. +

+
+        script/release 0.8.0
+      
+ + ) +} diff --git a/website/app/docs/commands/page.tsx b/website/app/docs/commands/page.tsx new file mode 100644 index 00000000..9defb66b --- /dev/null +++ b/website/app/docs/commands/page.tsx @@ -0,0 +1,209 @@ +/** Bot commands reference — full list of chat transport commands. */ +import type { Metadata } from 'next' + +/** Page metadata for the bot commands reference. */ +export const metadata: Metadata = { + title: 'Bot Commands — Huskies Docs', + description: + 'Full reference of huskies bot commands available in Matrix, Slack, WhatsApp, Discord, and the web UI.', +} + +/** Renders the full bot commands reference page. */ +export default function CommandsPage() { + return ( + <> +

Bot Commands

+

+ Commands available in every chat transport (Matrix, Slack, WhatsApp, Discord) and the built-in web + UI. Commands are case-insensitive. Run help in any chat to see the list. +

+ +
+ How to invoke: In chat rooms, address the bot first (e.g.{' '} + @huskies start 42) or enable ambient mode so it responds to all messages. In the web + UI, type commands directly. +
+ +

Pipeline management

+
+
+
status
+
+ Show pipeline status and agent availability. Use status <number> for a + detailed triage dump on a specific story. +
+
+
+
start
+
+ Start an agent on a story: start <number>. To use the opus model:{' '} + start <number> opus. +
+
+
+
move
+
+ Move a work item to a pipeline stage: move <number> <stage>. Stages:{' '} + backlog, current, qa, merge,{' '} + done. +
+
+
+
show
+
+ Display the full text of a work item: show <number>. +
+
+
+
delete
+
+ Remove a work item from the pipeline: delete <number>. +
+
+
+
unblock
+
+ Reset a blocked story: unblock <number>. Clears the blocked flag and resets + the retry count. +
+
+
+
assign
+
+ Pre-assign a model to a story before starting: assign <number> <model>{' '} + (e.g. assign 42 opus). +
+
+
+
backlog
+
+ Show all items in the backlog with dependency satisfaction status — which are ready to + start and which are still waiting on other stories. +
+
+
+
depends
+
+ Set story dependencies: depends <number> [dep1 dep2 ...]. Call with no deps + to clear all dependencies. +
+
+
+
timer
+
+ Schedule a deferred agent start: timer <number> HH:MM. List all timers:{' '} + timer list. Cancel: timer cancel <number>. Times are interpreted + in the project timezone. +
+
+
+ +

Worktrees

+
+
+
rmtree
+
+ Delete the worktree for a story without removing the story from the pipeline:{' '} + rmtree <number>. Useful for freeing disk space on a story that needs to be + restarted. +
+
+
+ +

Observability

+
+
+
cost
+
+ Show token spend: 24h total, top stories, breakdown by agent type, and all-time total. +
+
+
+
coverage
+
+ Show test coverage from the cached baseline. Use coverage run to rerun the full + test suite and regenerate the report. +
+
+
+
git
+
+ Show git status for the main repository: current branch, uncommitted changes, and ahead/behind + remote. +
+
+
+
htop
+
+ Live system and agent process dashboard. Use htop to start,{' '} + htop 10m to run for 10 minutes, htop stop to stop. +
+
+
+
run_tests
+
+ Run the project's test suite (script/test) and show a pass/fail summary with + output. Use run_tests <number> to run tests inside a specific story's + worktree instead of the project root. +
+
+
+
loc
+
+ Show top source files by line count: loc (top 10), loc <N> for + N files, or loc <filepath> for a specific file. +
+
+
+
overview
+
+ Show an implementation summary for a merged story: overview <number>. +
+
+
+
unreleased
+
+ Show stories merged to master since the last release tag. +
+
+
+ +

Server management

+
+
+
rebuild
+
Rebuild the huskies server binary and restart the process.
+
+
+
reset
+
+ Clear the current Claude Code session and start a fresh context window. +
+
+
+ +

Setup & configuration

+
+
+
setup
+
+ Show setup wizard progress. Drive the wizard from chat: setup generate,{' '} + setup confirm, setup skip, setup retry. +
+
+
+
ambient
+
+ Toggle ambient mode for the current room: ambient on or{' '} + ambient off. In ambient mode the bot responds to all messages, not just addressed + ones. +
+
+
+
help
+
Show the list of available commands.
+
+
+ + ) +} diff --git a/website/app/docs/configuration/page.tsx b/website/app/docs/configuration/page.tsx new file mode 100644 index 00000000..0dac07d7 --- /dev/null +++ b/website/app/docs/configuration/page.tsx @@ -0,0 +1,459 @@ +/** Configuration reference — project.toml, agents.toml, and bot.toml docs. */ +import type { Metadata } from 'next' + +/** Page metadata for the configuration reference. */ +export const metadata: Metadata = { + title: 'Configuration — Huskies Docs', + description: 'Reference for project.toml, agents.toml, and bot.toml configuration files.', +} + +/** Renders the configuration reference for all huskies TOML files. */ +export default function ConfigurationPage() { + return ( + <> +

Configuration

+

+ Huskies is configured via three TOML files in your .huskies/ directory. All files are + created by huskies init with sensible defaults. +

+ +

project.toml

+

+ Project-wide settings. Lives at .huskies/project.toml. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDefaultDescription
default_qastring + "server" + + Default QA mode. One of "server" (automated gate run),{' '} + "agent" (spawn a QA agent), or "human" (manual + approval). +
default_coder_modelstring + "sonnet" + + Default model for coder agents. Only agents matching this model are auto-assigned. Use{' '} + "opus" on individual stories for complex tasks. +
max_codersinteger + 3 + + Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are + full. +
max_retriesinteger + 3 + + Maximum retries per story per pipeline stage before marking it as blocked. Set to{' '} + 0 to disable. +
base_branchstringauto-detected + Base branch for merges and agent prompts. When unset, huskies reads the current HEAD branch. +
rate_limit_notificationsbool + false + + Send chat notifications when API soft rate limits are hit. Hard blocks and story-blocked + notifications are always sent. +
timezonestring + "UTC" + + IANA timezone for timer scheduling (e.g. "Europe/London",{' '} + "America/New_York"). Timer HH:MM inputs are interpreted in this + timezone. +
+ +

Component setup

+

+ The [[component]] sections define how to build and verify each part of your project. + The server runs setup commands before accepting a story's QA and teardown commands after + merging. +

+
+        {`[[component]]
+name = "frontend"
+path = "frontend"
+setup = ["npm ci", "npm run build"]
+teardown = []
+
+[[component]]
+name = "server"
+path = "."
+setup = ["mkdir -p frontend/dist", "cargo check"]
+teardown = []`}
+      
+ +

Story front matter overrides

+

Individual stories can override project defaults using YAML front matter:

+
+        {`---
+name: "My Complex Story"
+qa: agent        # override default_qa
+agent: opus      # use the opus coder agent
+---`}
+      
+ +

agents.toml

+

+ Agent definitions. Lives at .huskies/agents.toml. Each [[agent]] block + defines one agent slot. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescription
name + Unique identifier for this agent slot (e.g. "coder-1",{' '} + "qa"). +
stage + Pipeline stage this agent handles. One of "coder",{' '} + "qa", or "mergemaster". +
role + Human-readable description of the agent's responsibilities (shown in status output). +
model + Claude model to use. One of "sonnet" (claude-sonnet-4-6) or{' '} + "opus" (claude-opus-4-6). +
max_turnsMaximum conversation turns before the agent is forcefully stopped.
max_budget_usdMaximum API spend in USD before the agent is stopped.
prompt + The initial user-turn prompt sent to the agent. Supports template variables (see below). +
system_promptThe system prompt sent to the agent session.
+ +

Template variables

+

+ The following variables are interpolated into prompt and system_prompt at + agent start time: +

+ + + + + + + + + + + + + + + + + + + + + +
VariableDescription
{'{{story_id}}'} + The story's ID slug (e.g. 42_story_add_login). +
{'{{worktree_path}}'}Absolute path to the agent's git worktree.
{'{{base_branch}}'} + The base branch name from project.toml. +
+ +

Example: adding an opus coder

+
+        {`[[agent]]
+name = "coder-opus"
+stage = "coder"
+role = "Senior engineer for complex tasks."
+model = "opus"
+max_turns = 80
+max_budget_usd = 20.00
+prompt = "You are working on story {{story_id}} ..."
+system_prompt = "You are a senior full-stack engineer ..."`}
+      
+

+ To use this agent for a specific story, add agent: opus to the story's front + matter, or run start <number> opus in chat. +

+ +

+ Project-local agent prompt (.huskies/AGENT.md) +

+

+ Place a file at .huskies/AGENT.md in your project root to append project-specific + guidance to every agent's initial prompt at spawn time. +

+ +

How it works

+ + +

Ordering

+
    +
  1. + Baked-in agent prompt (from agents.toml or project.toml) +
  2. +
  3. + Project-local content from .huskies/AGENT.md +
  4. +
  5. Resume context (only on agent restart after a gate failure)
  6. +
+ +

Example

+
+        {`# .huskies/AGENT.md
+
+## Documentation
+Docs live in \`website/docs/*.html\`, not Markdown files.
+Edit the relevant .html file when a story asks for documentation.
+
+## Quality gates
+Run \`cargo clippy -- -D warnings\` before committing. Zero warnings allowed.`}
+      
+

+ Edit the file at any time — the next agent spawn picks up the latest content automatically. +

+ +

bot.toml

+

+ Chat transport configuration. Lives at .huskies/bot.toml. This file is gitignored as + it contains credentials. Copy the appropriate example file to get started: +

+
+        cp .huskies/bot.toml.matrix.example .huskies/bot.toml
+      
+

+ Only one transport can be active at a time. See the{' '} + Chat transports guide for setup instructions for each platform. +

+ +

Common fields

+ + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescription
enabled + Set to true to activate the bot. Set to false to disable without + removing the file. +
transport + Transport type: "matrix", "whatsapp",{' '} + "slack", or "discord". +
display_nameOptional. Bot display name in chat messages.
history_size + Optional. Maximum conversation turns to remember per room/user (default: 20). +
+ +

Gateway: aggregated chat stream

+

+ When running huskies --gateway, you can configure a single bot that receives pipeline + notifications from all registered projects. Events are prefixed with{' '} + [project-name] so you can tell them apart in one shared room. +

+

+ The aggregated stream is configured entirely in the gateway's{' '} + .huskies/bot.toml — no per-project bot config is required and no per-project + files need to change when you add a new project to projects.toml. +

+ +

Enabling the aggregated stream

+

+ Add or edit <gateway-config-dir>/.huskies/bot.toml and set{' '} + enabled = true. The gateway bot will automatically poll every project listed in{' '} + projects.toml and forward events to the configured rooms. +

+
+        {`# /.huskies/bot.toml
+enabled = true
+transport = "matrix"
+homeserver = "https://matrix.example.com"
+username = "@gateway-bot:example.com"
+password = "secret"
+room_ids = ["!gateway-room:example.com"]
+allowed_users = ["@you:example.com"]
+
+# Gateway-specific: poll interval and on/off switch
+aggregated_notifications_poll_interval_secs = 5   # default
+aggregated_notifications_enabled = true            # default`}
+      
+ +

Aggregated stream settings

+ + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDefaultDescription
aggregated_notifications_enabledbool + true + + Set to false to disable the aggregated stream without disabling the gateway bot + entirely. Per-project configs are never consulted. +
aggregated_notifications_poll_interval_secsinteger + 5 + + How often (in seconds) the gateway polls each project's /api/events{' '} + endpoint. Lower values reduce notification latency. +
+ +

No-duplicate guarantee

+

+ Per-project bots and the gateway aggregated stream send to different rooms — they are + independent. Events from a per-project bot go to that project's rooms; events from the gateway + stream go to the gateway rooms. The same event will never appear twice in either room. +

+ +

Unreachable projects

+

+ If a per-project server is temporarily unreachable, the gateway logs a warning and skips that + project for the current poll cycle. All other projects continue to deliver notifications normally. + No configuration change is required — the poller retries on the next interval. +

+ +

Supported event types

+

+ The aggregated stream delivers the following event types, each prefixed with the project name: +

+ + + ) +} diff --git a/website/app/docs/docs.css b/website/app/docs/docs.css new file mode 100644 index 00000000..53b71834 --- /dev/null +++ b/website/app/docs/docs.css @@ -0,0 +1,431 @@ +/* Outer shell */ +.shell { + max-width: 1100px; + margin: 0 auto; + padding: 0 2rem; +} + +@media (max-width: 640px) { + .shell { padding: 0 1.25rem; } +} + +/* Header (docs) */ +.shell header { + padding: 2rem 0; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); + margin-bottom: 0; +} + +.shell header nav a.active { color: var(--cyan); } + +/* Docs layout */ +.docs-layout { + display: grid; + grid-template-columns: 220px 1fr; + gap: 0; + min-height: calc(100vh - 80px); +} + +/* Sidebar */ +.sidebar { + border-right: 1px solid var(--border); + padding: 2.5rem 0 2.5rem 0; + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} + +.sidebar-section { + margin-bottom: 2rem; +} + +.sidebar-heading { + font-family: var(--display); + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-dim); + padding: 0 1.5rem; + margin-bottom: 0.5rem; +} + +.sidebar nav a { + display: block; + padding: 0.4rem 1.5rem; + font-size: 0.83rem; + color: var(--text-secondary); + transition: color 0.15s, background 0.15s; + border-left: 2px solid transparent; +} + +.sidebar nav a:hover { + color: var(--text); + opacity: 1; +} + +.sidebar nav a.active { + color: var(--cyan); + border-left-color: var(--cyan); + background: var(--cyan-dim); +} + +/* Main content */ +.docs-main { + padding: 2.5rem 3rem 4rem; + max-width: 780px; +} + +@media (max-width: 768px) { + .docs-layout { grid-template-columns: 1fr; } + .sidebar { + position: static; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border); + padding: 1.5rem 0; + } + .docs-main { padding: 2rem 0 3rem; } +} + +/* Typography */ +.page-title { + font-family: var(--display); + font-size: 2rem; + font-weight: 800; + letter-spacing: -0.03em; + margin-bottom: 0.5rem; + line-height: 1.2; +} + +.page-subtitle { + font-size: 1rem; + color: var(--text-secondary); + font-weight: 300; + margin-bottom: 3rem; + line-height: 1.7; + max-width: 560px; +} + +h2 { + font-family: var(--display); + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.02em; + margin: 3rem 0 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border); + scroll-margin-top: 2rem; +} + +h2:first-of-type { + margin-top: 0; + padding-top: 0; + border-top: none; +} + +h3 { + font-family: var(--display); + font-size: 0.95rem; + font-weight: 600; + margin: 1.8rem 0 0.5rem; +} + +p { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.8; + margin-bottom: 1rem; +} + +p strong { + color: var(--text); + font-weight: 600; +} + +ul, ol { + padding-left: 1.4rem; + margin-bottom: 1rem; +} + +li { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.8; + margin-bottom: 0.2rem; +} + +li strong { + color: var(--text); + font-weight: 600; +} + +/* Code */ +code { + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 0.8rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.1em 0.4em; + color: var(--cyan); +} + +pre { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.2rem 1.4rem; + overflow-x: auto; + margin: 1rem 0 1.5rem; +} + +pre code { + background: none; + border: none; + padding: 0; + font-size: 0.82rem; + color: var(--text); + line-height: 1.7; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0 1.5rem; + font-size: 0.85rem; +} + +th { + text-align: left; + font-family: var(--display); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-dim); + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--border); +} + +td { + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + vertical-align: top; + line-height: 1.6; +} + +td:first-child { + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 0.8rem; + color: var(--cyan); + white-space: nowrap; +} + +tr:last-child td { border-bottom: none; } + +/* Callout / note box */ +.note { + background: var(--cyan-dim); + border: 1px solid rgba(34, 211, 238, 0.2); + border-radius: 6px; + padding: 1rem 1.2rem; + margin: 1.2rem 0; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.7; +} + +.note strong { + color: var(--cyan); + font-weight: 600; +} + +/* Step list */ +.step-list { + list-style: none; + padding: 0; + counter-reset: steps; +} + +.step-list li { + counter-increment: steps; + display: grid; + grid-template-columns: 40px 1fr; + gap: 1rem; + padding: 1.2rem 0; + border-bottom: 1px solid var(--border); + align-items: start; +} + +.step-list li:first-child { border-top: 1px solid var(--border); } + +.step-list li::before { + content: counter(steps, decimal-leading-zero); + font-family: var(--display); + font-size: 0.72rem; + font-weight: 700; + color: var(--text-dim); + padding-top: 0.15rem; +} + +/* Command cards */ +.cmd-grid { + display: flex; + flex-direction: column; + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + margin: 1rem 0 1.5rem; +} + +.cmd-row { + display: grid; + grid-template-columns: 160px 1fr; + background: var(--surface); + transition: background 0.2s; +} + +.cmd-row:hover { background: var(--surface-hover); } + +.cmd-name { + padding: 0.9rem 1.1rem; + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 0.8rem; + color: var(--cyan); + border-right: 1px solid var(--border); + display: flex; + align-items: center; +} + +.cmd-desc { + padding: 0.9rem 1.1rem; + font-size: 0.84rem; + color: var(--text-secondary); + line-height: 1.6; + display: flex; + align-items: center; +} + +/* Docs index cards */ +.doc-cards { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + margin-top: 2rem; +} + +.doc-card { + background: var(--surface); + padding: 1.6rem; + transition: background 0.2s; + text-decoration: none; + display: block; + color: inherit; +} + +.doc-card:hover { background: var(--surface-hover); opacity: 1; } + +.doc-card-title { + font-family: var(--display); + font-size: 0.95rem; + font-weight: 600; + color: var(--text); + margin-bottom: 0.4rem; +} + +.doc-card-desc { + font-size: 0.82rem; + color: var(--text-secondary); + line-height: 1.6; +} + +@media (max-width: 600px) { + .doc-cards { grid-template-columns: 1fr; } + .cmd-row { grid-template-columns: 130px 1fr; } +} + +/* Footer (docs) */ +.shell footer { + padding: 2rem 0; + border-top: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + color: var(--text-dim); + margin-top: 0; +} + +.shell footer a { + color: var(--text-dim); + font-size: 0.75rem; +} + +.shell footer a:hover { color: var(--text-secondary); } + +/* Pipeline stages (used in pipeline page) */ +.pipeline-stages { + display: flex; + flex-direction: column; + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + margin: 1.5rem 0; +} + +.stage-row { + display: grid; + grid-template-columns: 48px 140px 1fr; + gap: 0; + background: var(--surface); + transition: background 0.2s; + align-items: stretch; +} + +.stage-row:hover { background: var(--surface-hover); } + +.stage-num { + display: flex; + align-items: center; + justify-content: center; + font-family: var(--display); + font-size: 0.7rem; + font-weight: 700; + color: var(--text-dim); + border-right: 1px solid var(--border); +} + +.stage-name { + display: flex; + align-items: center; + padding: 1rem 1.1rem; + font-family: var(--display); + font-size: 0.88rem; + font-weight: 600; + color: var(--text); + border-right: 1px solid var(--border); +} + +.stage-desc { + padding: 1rem 1.2rem; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.stage-row.active .stage-name { color: var(--cyan); } diff --git a/website/app/docs/layout.tsx b/website/app/docs/layout.tsx new file mode 100644 index 00000000..b11bd682 --- /dev/null +++ b/website/app/docs/layout.tsx @@ -0,0 +1,48 @@ +/** Shared layout for all docs pages: header, sidebar, and footer. */ +import type { Metadata } from 'next' +import DocsSidebar from '@/components/DocsSidebar' +import './docs.css' + +/** Default metadata for docs pages. */ +export const metadata: Metadata = { + title: 'Documentation — Huskies', +} + +/** Wraps all doc pages with the shared docs shell (header, sidebar, footer). */ +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( + <> +
+
+ + huskies + + +
+
+ +
+
+ +
{children}
+
+ + +
+ + ) +} diff --git a/website/app/docs/page.tsx b/website/app/docs/page.tsx new file mode 100644 index 00000000..068a0cc3 --- /dev/null +++ b/website/app/docs/page.tsx @@ -0,0 +1,138 @@ +/** Docs index — overview and card links to all documentation sections. */ +import type { Metadata } from 'next' +import Link from 'next/link' + +/** Page metadata for the docs index. */ +export const metadata: Metadata = { + title: 'Documentation — Huskies', + description: + 'Huskies documentation: quickstart, configuration, bot commands, pipeline, and more.', +} + +/** Renders the documentation overview page with section cards. */ +export default function DocsIndexPage() { + return ( + <> +

+ Documentation +

+

Huskies Docs

+

+ Everything you need to set up and run huskies — a story-driven development pipeline that turns + coding agents into a disciplined team. +

+ +
+ +
Quickstart
+
+ Install huskies, run the server, create your first story, and watch an agent implement it. +
+ + +
Configuration
+
+ Reference for project.toml, agents.toml, and bot.toml. +
+ + +
Bot commands
+
+ Full list of commands available in Matrix, Slack, WhatsApp, and the web UI. +
+ + +
Pipeline stages
+
+ How work items move from backlog through QA and merge to done. +
+ + +
Chat transports
+
+ Connect huskies to Matrix, WhatsApp, Slack, Discord, or the built-in web UI. +
+ + +
CLI reference
+
+ Command-line flags for huskies, huskies init, and{' '} + huskies agent. +
+ +
+ +

What is huskies?

+

+ Huskies is a story-driven development server. You write stories (feature requests) with acceptance + criteria; huskies spawns coding agents in isolated git worktrees, runs them through quality gates, and + squash-merges the result to your main branch — all without you writing a line of code. +

+

+ It ships as a single Rust binary with an embedded React frontend. No separate database or build + infrastructure required. +

+ +

How it works

+
    +
  1. +
    + Write a story. Describe the change with acceptance criteria via the web UI, a chat + room (Matrix, WhatsApp, Slack), or by dropping a Markdown file in{' '} + .huskies/work/1_backlog/. +
    +
  2. +
  3. +
    + Agent picks it up. Run start <number> (or configure + auto-start). A coder agent creates a feature branch, implements the code, and writes tests against + your criteria. +
    +
  4. +
  5. +
    + Quality gates run. Linters, tests, and compilation checks run automatically when + the agent exits. Nothing moves forward until everything passes. +
    +
  6. +
  7. +
    + QA review. A QA agent verifies each acceptance criterion, runs your test suite, + and either approves or rejects with detailed findings. +
    +
  8. +
  9. +
    + Merge & land. A merge agent resolves conflicts and squash-merges to your main + branch. The worktree is cleaned up automatically. +
    +
  10. +
+ +

Key concepts

+

+ Stories are Markdown files with YAML front matter. They live in{' '} + .huskies/work/ and move through pipeline stages as work progresses. +

+

+ Agents are Claude Code sessions that run autonomously in git worktrees. Each story gets + its own isolated worktree so multiple stories can be in flight simultaneously. +

+

+ MCP tools give Claude Code sessions programmatic access to the pipeline: creating + stories, starting agents, checking status, recording test results. +

+ + ) +} diff --git a/website/app/docs/pipeline/page.tsx b/website/app/docs/pipeline/page.tsx new file mode 100644 index 00000000..89cd83b0 --- /dev/null +++ b/website/app/docs/pipeline/page.tsx @@ -0,0 +1,200 @@ +/** Pipeline stages reference — describes the six-stage story workflow. */ +import type { Metadata } from 'next' + +/** Page metadata for the pipeline stages guide. */ +export const metadata: Metadata = { + title: 'Pipeline Stages — Huskies Docs', + description: + 'How work items move through the huskies pipeline: backlog, current, QA, merge, done.', +} + +/** Renders the pipeline stages reference page. */ +export default function PipelinePage() { + return ( + <> +

Pipeline Stages

+

+ Work items move through six stages from idea to archive. Each stage is a directory under{' '} + .huskies/work/. Moving a file between directories advances the story. +

+ +
+
+
1
+
Backlog
+
+ 1_backlog/ — New work items awaiting prioritisation. Stories sit here until + you decide to start them. +
+
+
+
2
+
Current
+
+ 2_current/ — Work in progress. Run start <number> to + assign a coder agent. Multiple stories can be in current simultaneously (up to{' '} + max_coders). +
+
+
+
3
+
QA
+
+ 3_qa/ — Quality review. The server automatically moves stories here when the + coder agent passes all quality gates. A QA agent (or a human) verifies each acceptance criterion. +
+
+
+
4
+
Merge
+
+ 4_merge/ — Ready to merge. Stories reach here after QA approval. Run{' '} + start <number> to trigger the mergemaster agent, which squash-merges to your + base branch. +
+
+
+
5
+
Done
+
+ 5_done/ — Merged and complete. The mergemaster moves stories here after a + successful merge. Auto-swept to archive after 4 hours. +
+
+
+
6
+
Archived
+
+ 6_archived/ — Long-term storage. Stories land here automatically from done. + Use overview <number> to see the implementation summary for any archived + story. +
+
+
+ +

Work item types

+

+ All work item types move through the same pipeline. They differ in naming convention and workflow: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeFilename patternWhen to use
story + 42_story_add_login.md + New functionality. Requires acceptance criteria and tests.
bug + 43_bug_login_crashes.md + Defect in existing functionality. Write a failing test first.
spike + 44_spike_auth_options.md + Time-boxed research to reduce uncertainty. No production code.
refactor + 45_refactor_extract_auth.md + Code quality improvement. Behaviour must not change.
+ +

Story file format

+

Every work item is a Markdown file with YAML front matter:

+
+        {`---
+name: "Short human-readable name"
+qa: agent          # optional: override default_qa
+agent: opus        # optional: request specific agent model
+---
+
+# Story 42: Add login endpoint
+
+## User Story
+As a user, I want to log in with email and password so that I can access my account.
+
+## Acceptance Criteria
+- [ ] POST /auth/login accepts email and password
+- [ ] Returns a JWT token on success
+- [ ] Returns 401 on invalid credentials
+- [ ] Rate-limited to 5 attempts per minute per IP
+
+## Out of Scope
+- OAuth / social login
+- Password reset flow`}
+      
+ +

Acceptance criteria tracking

+

+ Acceptance criteria use Markdown checkboxes (- [ ]). The QA agent reviews each criterion + against the code diff and marks passing criteria as - [x] in the story file. Criteria + that fail are noted in the QA report. +

+ +
+ Golden rule: No code is written until acceptance criteria are captured in the story. + The agent reads the story file to understand what to build and what to test. +
+ +

Filesystem watcher

+

+ The server watches .huskies/work/ for changes. When a file is created, moved, or + modified, the watcher auto-commits with a deterministic message and broadcasts a WebSocket update to + the frontend. This means: +

+ + +

Blocked stories

+

+ A story is marked blocked when it fails the same pipeline stage more than{' '} + max_retries times (default: 3). Blocked stories require manual intervention: +

+
    +
  1. + Run status <number> to see the failure log. +
  2. +
  3. Fix the underlying issue (update the story, fix a build problem, etc.).
  4. +
  5. + Run unblock <number> to reset the retry counter. +
  6. +
  7. + Run start <number> to try again. +
  8. +
+ +

Dependencies

+

+ Stories can declare dependencies using the depends command. A story with unresolved + dependencies waits in 2_current/ until all its dependencies have reached{' '} + 5_done/. +

+
+        {`depends 45 42 43   # story 45 waits for 42 and 43 to finish\ndepends 45          # clear all dependencies`}
+      
+ + ) +} diff --git a/website/app/docs/quickstart/page.tsx b/website/app/docs/quickstart/page.tsx new file mode 100644 index 00000000..934e1cc4 --- /dev/null +++ b/website/app/docs/quickstart/page.tsx @@ -0,0 +1,161 @@ +/** Quickstart guide — Docker, binary, and source install instructions. */ +import type { Metadata } from 'next' + +/** Page metadata for the quickstart guide. */ +export const metadata: Metadata = { + title: 'Quickstart — Huskies Docs', + description: 'Get huskies running in minutes: Docker setup, first story, first agent run.', +} + +/** Renders the quickstart installation and setup guide. */ +export default function QuickstartPage() { + return ( + <> +

Quickstart

+

+ Get huskies running in your project in a few minutes. This guide covers Docker setup, running from a + binary, your first story, and your first agent run. +

+ +

Option A: Docker (recommended)

+

+ The easiest way to run huskies is with Docker Compose. This requires Docker and a Claude API key. +

+
    +
  1. +
    + Get the compose file. Download docker-compose.yml from the{' '} + releases page or copy it from + the repository's docker/ directory. +
    +
  2. +
  3. +
    + Set your API key. Create a .env file next to the compose file: +
    ANTHROPIC_API_KEY=sk-ant-...
    +
    +
  4. +
  5. +
    + Mount your project. Edit the compose file to mount your project directory: +
    {`volumes:\n  - /path/to/your/project:/workspace`}
    +
    +
  6. +
  7. +
    + Start the server. +
    docker compose up
    + Open http://localhost:3000 to see the pipeline board. +
    +
  8. +
+ +

Option B: Binary

+

+ Download the pre-built binary for your platform from the{' '} + releases page and place it + somewhere on your PATH. +

+ +

macOS (Apple Silicon)

+
{`curl -L https://code.crashlabs.io/crashlabs/huskies/releases/download/latest/huskies-aarch64-apple-darwin \\
+  -o /usr/local/bin/huskies
+chmod +x /usr/local/bin/huskies`}
+ +

Linux (x86-64)

+
{`curl -L https://code.crashlabs.io/crashlabs/huskies/releases/download/latest/huskies-x86_64-unknown-linux-musl \\
+  -o /usr/local/bin/huskies
+chmod +x /usr/local/bin/huskies`}
+ +

Option C: Build from source

+

Requires Rust (stable), Node.js, and npm.

+
{`git clone https://code.crashlabs.io/crashlabs/huskies
+cd huskies
+cargo build --release
+# Binary is at target/release/huskies`}
+ +

Initialise your project

+

+ From your project directory, run the init command. This creates the .huskies/ directory + with the pipeline structure and configuration files. +

+
{`cd /path/to/your/project\nhuskies init --port 3000`}
+

This creates:

+ + +
+ Claude Code integration: The .mcp.json file automatically registers + huskies' MCP tools with Claude Code. Open a Claude Code session in your project and it will + discover tools like create_story, start_agent, and{' '} + get_pipeline_status automatically. +
+ +

Start the server

+
huskies --port 3000
+

+ Open http://localhost:3000 to see the pipeline board, agent + status, and chat interface. +

+ +

Run the setup wizard

+

+ Open a Claude Code session in your project directory (or use the web chat UI), and tell Claude: +

+
help me set up this project with huskies
+

+ Claude will walk you through the setup wizard — generating project context ( + specs/00_CONTEXT.md), tech stack docs (specs/tech/STACK.md), and + test/release scripts. Review each step and confirm or ask to retry. +

+ +

Create your first story

+

In the chat UI or via a chat transport, type:

+
I want to add a health check endpoint to the API
+

+ Claude will create a story file in .huskies/work/1_backlog/ with a user story and + acceptance criteria. Review it, then move it to current: +

+
move <story-number> current
+ +

Start an agent

+

+ Once a story is in 2_current/, start a coding agent: +

+
start <story-number>
+

+ The agent creates an isolated git worktree, implements the feature against the acceptance criteria, + runs quality gates (clippy, tests, biome), and exits. The server automatically advances the story to + QA if all gates pass. +

+ +

Review and merge

+

Once QA passes, the story moves to 4_merge/. To merge:

+
start <story-number>
+

+ The mergemaster agent resolves any conflicts and squash-merges to your main branch. The worktree is + cleaned up automatically. +

+ +
+ Tip: Use status in the chat at any time to see the current pipeline + state, active agents, and their progress. +
+ + ) +} diff --git a/website/app/docs/transports/page.tsx b/website/app/docs/transports/page.tsx new file mode 100644 index 00000000..5ed6dfc6 --- /dev/null +++ b/website/app/docs/transports/page.tsx @@ -0,0 +1,384 @@ +/** Chat transports guide — Matrix, Slack, WhatsApp, Discord, and web UI setup. */ +import type { Metadata } from 'next' +import Link from 'next/link' + +/** Page metadata for the chat transports guide. */ +export const metadata: Metadata = { + title: 'Chat Transports — Huskies Docs', + description: + 'Connect huskies to Matrix, WhatsApp, Slack, Discord, or the built-in web UI.', +} + +/** Renders the chat transports setup guide for all supported platforms. */ +export default function TransportsPage() { + return ( + <> +

Chat Transports

+

+ Huskies can be controlled via bot commands in any of five transports. Only one external transport + can be active at a time. The web UI is always available regardless. +

+ +
+ Configuration: Copy the relevant example file to{' '} + .huskies/bot.toml and fill in your credentials. The file is gitignored. Restart + huskies after changes. +
+ +

Web UI

+

+ The built-in web interface is always available at http://localhost:<port>. No + configuration required. It provides: +

+ +

+ No bot.toml is required for the web UI. If no transport is configured, huskies runs + in web-only mode. +

+ +

Matrix

+

+ Matrix uses the Matrix Client-Server API with long-polling sync. No public webhook URL is required + — the bot connects outbound to your homeserver. +

+ +

Setup

+
    +
  1. +
    Register a Matrix account for the bot on your homeserver (e.g. @huskies:example.com).
    +
  2. +
  3. +
    Invite the bot account to the rooms you want it to monitor.
    +
  4. +
  5. +
    + Copy the example config and fill in your credentials: +
    +              cp .huskies/bot.toml.matrix.example .huskies/bot.toml
    +            
    +
    +
  6. +
+ +

bot.toml fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescription
homeserver + Your Matrix homeserver URL (e.g. https://matrix.example.com). +
username + Bot account Matrix ID (e.g. @huskies:example.com). +
passwordBot account password.
room_ids + List of room IDs to listen in (e.g. {`["!roomid:example.com"]`}). +
allowed_users + Matrix IDs allowed to interact. Empty list means nobody — always set this. +
ambient_rooms + Rooms where the bot responds to all messages (not just addressed ones). Updated automatically + by ambient on/off. +
+ +

Slack

+

+ Slack uses event subscriptions over a webhook. You'll need a public HTTPS URL pointing to your + huskies server. +

+ +

Setup

+
    +
  1. +
    + Create a Slack App at{' '} + api.slack.com/apps. +
    +
  2. +
  3. +
    + Add OAuth scopes: chat:write, chat:update. +
    +
  4. +
  5. +
    + Subscribe to bot events: message.channels, message.groups,{' '} + message.im. +
    +
  6. +
  7. +
    Install the app to your workspace and copy the bot token.
    +
  8. +
  9. +
    + Set your webhook URL in Event Subscriptions:{' '} + https://your-server/webhook/slack +
    +
  10. +
  11. +
    + Copy the example config: +
    +              cp .huskies/bot.toml.slack.example .huskies/bot.toml
    +            
    +
    +
  12. +
+ +

bot.toml fields

+ + + + + + + + + + + + + + + + + + + + + +
KeyDescription
slack_bot_token + OAuth bot token starting with xoxb-. +
slack_signing_secretSigning secret from the app's Basic Information page.
slack_channel_ids + List of channel IDs to listen in (e.g. {`["C01ABCDEF"]`}). +
+ +

WhatsApp (Meta Cloud API)

+

+ Connects huskies to WhatsApp Business via the Meta Cloud API. Requires a Meta Business account and + a public webhook URL. +

+ +

Setup

+
    +
  1. +
    + Create a Meta Business App at{' '} + developers.facebook.com. +
    +
  2. +
  3. +
    Add the WhatsApp product and get a Phone Number ID.
    +
  4. +
  5. +
    Generate a permanent access token.
    +
  6. +
  7. +
    + Register your webhook URL in Meta's dashboard:{' '} + https://your-server/webhook/whatsapp +
    +
  8. +
  9. +
    + Copy the example config: +
    +              cp .huskies/bot.toml.whatsapp-meta.example .huskies/bot.toml
    +            
    +
    +
  10. +
+ +

bot.toml fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescription
whatsapp_provider + Set to "meta" for the Meta Cloud API. +
whatsapp_phone_number_idPhone Number ID from the Meta dashboard.
whatsapp_access_tokenPermanent access token.
whatsapp_verify_token + Webhook verify token — must match what you set in Meta's dashboard. +
whatsapp_allowed_phones + Optional. List of phone numbers allowed to interact (e.g.{' '} + {`["+15551234567"]`}). When absent, all numbers are allowed. +
whatsapp_notification_template + Optional. Name of the approved Meta message template for out-of-window notifications + (default: "pipeline_notification"). +
+ +

WhatsApp (Twilio)

+

+ An alternative WhatsApp integration using Twilio's WhatsApp API. Requires a Twilio account. +

+
+        cp .huskies/bot.toml.whatsapp-twilio.example .huskies/bot.toml
+      
+

+ Set whatsapp_provider = "twilio" and fill in your Twilio account SID, auth + token, and phone numbers. The webhook URL is the same:{' '} + https://your-server/webhook/whatsapp. +

+ +

Discord

+

+ Connects huskies to Discord using the Discord Gateway WebSocket. No public webhook URL required + — the bot connects outbound. +

+ +

Setup

+
    +
  1. +
    + Create a Discord Application at{' '} + + discord.com/developers/applications + + . +
    +
  2. +
  3. +
    Go to Bot, create a bot, and copy the token.
    +
  4. +
  5. +
    + Enable Message Content Intent under Privileged Gateway Intents. +
    +
  6. +
  7. +
    + Go to OAuth2 → URL Generator, select the bot scope with permissions: Send + Messages, Read Message History, Manage Messages. +
    +
  8. +
  9. +
    Use the generated URL to invite the bot to your server.
    +
  10. +
  11. +
    + Right-click target channels → Copy Channel ID (requires Developer Mode enabled in Discord + settings). +
    +
  12. +
  13. +
    + Copy the example config: +
    +              cp .huskies/bot.toml.discord.example .huskies/bot.toml
    +            
    +
    +
  14. +
+ +

bot.toml fields

+ + + + + + + + + + + + + + + + + + + + + +
KeyDescription
discord_bot_tokenBot token from the Discord developer portal.
discord_channel_ids + List of channel IDs to listen in (e.g.{' '} + {`["123456789012345678"]`}). +
discord_allowed_users + Optional. Discord user IDs allowed to interact. When absent, all users in configured + channels can interact. +
+ +

Gateway: aggregated notifications

+

+ When using huskies --gateway, you can configure the gateway bot to receive + notifications from all registered projects in a single room. Events are prefixed + with [project-name]. +

+

+ No additional transport is required — the gateway aggregated stream works with any of the + transports above. Configure the gateway's .huskies/bot.toml with your transport + credentials and set aggregated_notifications_enabled = true (the default). See{' '} + + Configuration → Gateway aggregated stream + {' '} + for the full reference. +

+
+ No per-project changes needed: Adding a new project to{' '} + projects.toml does not require editing per-project bot configs — the gateway + picks it up automatically. +
+ + ) +} diff --git a/website/app/globals.css b/website/app/globals.css new file mode 100644 index 00000000..7619da59 --- /dev/null +++ b/website/app/globals.css @@ -0,0 +1,416 @@ +:root { + --bg: #080c15; + --surface: #0e1420; + --surface-hover: #131a28; + --border: #1a2235; + --text: #e8ecf4; + --text-secondary: #8892a8; + --text-dim: #4a5568; + --cyan: #22d3ee; + --cyan-dim: rgba(34, 211, 238, 0.07); + --cyan-glow: rgba(34, 211, 238, 0.15); + --display: var(--font-display), sans-serif; + --body: var(--font-body), sans-serif; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { scroll-behavior: smooth; } + +body { + font-family: var(--body); + background: var(--bg); + color: var(--text); + line-height: 1.6; + min-height: 100vh; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--cyan); text-decoration: none; transition: opacity 0.2s; } +a:hover { opacity: 0.7; } + +/* Animations */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 0 0 var(--cyan-glow); } + 50% { box-shadow: 0 0 12px 4px var(--cyan-glow); } +} + +.reveal { + opacity: 0; + animation: fadeUp 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +.r1 { animation-delay: 0.05s; } +.r2 { animation-delay: 0.15s; } +.r3 { animation-delay: 0.3s; } +.r4 { animation-delay: 0.5s; } +.r5 { animation-delay: 0.65s; } +.r6 { animation-delay: 0.8s; } +.r7 { animation-delay: 0.95s; } +.r8 { animation-delay: 1.1s; } + +/* Layout */ +.page { + max-width: 960px; + margin: 0 auto; + padding: 0 3rem; +} + +@media (max-width: 640px) { + .page { padding: 0 1.5rem; } + header { + flex-direction: column; + gap: 0.75rem; + } + header nav { + gap: 1rem; + flex-wrap: wrap; + justify-content: center; + } +} + +/* Header */ +header { + padding: 2rem 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-family: var(--display); + font-size: 1.1rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text) !important; +} + +header nav { + display: flex; + align-items: center; + gap: 2rem; +} + +header nav a { + font-size: 0.82rem; + color: var(--text-secondary); +} + +header nav a:hover { color: var(--text); opacity: 1; } + +.nav-cta { + color: var(--cyan) !important; + font-weight: 500; +} + +.nav-dropdown { + position: relative; +} + +.nav-dropdown-toggle { + cursor: pointer; +} + +/* Invisible bridge between toggle and menu so hover doesn't break */ +.nav-dropdown::after { + content: ''; + position: absolute; + top: 100%; + right: 0; + width: 100%; + height: 0.5rem; +} + +.nav-dropdown-menu { + display: none; + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.5rem 0; + min-width: 140px; + z-index: 200; +} + +.nav-dropdown-menu a { + display: block; + padding: 0.4rem 1rem; + font-size: 0.82rem; + color: var(--text-secondary); +} + +.nav-dropdown-menu a:hover { + background: var(--surface-hover); + color: var(--text); + opacity: 1; +} + +.nav-dropdown:hover .nav-dropdown-menu { + display: block; +} + +/* Hero */ +.hero { + padding: 10vh 0 6vh; + text-align: center; +} + +.hero-graphic { + margin-bottom: 2.5rem; + position: relative; + z-index: 1; +} + +.hero-husky { + width: 160px; + height: auto; + filter: drop-shadow(0 0 40px rgba(34, 211, 238, 0.2)); +} + +.hero-kicker { + font-family: var(--display); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cyan); + margin-bottom: 2rem; +} + +.hero h1 { + font-family: var(--display); + font-size: clamp(2.5rem, 6vw, 4.2rem); + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 1.8rem; + max-width: 700px; + margin-left: auto; + margin-right: auto; +} + +.glow { + color: var(--cyan); + text-shadow: 0 0 30px var(--cyan-glow); +} + +.hero-sub { + font-size: 1.05rem; + font-weight: 300; + color: var(--text-secondary); + line-height: 1.8; + max-width: 520px; + margin: 0 auto; +} + +/* Pipeline visualisation */ +.pipeline { + display: flex; + align-items: center; + justify-content: center; + gap: 0; + margin-top: 4rem; + padding: 2rem 0; +} + +.pipe-stage { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; +} + +.pipe-dot { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--border); + background: var(--surface); + transition: all 0.3s; +} + +.pipe-dot.active { + border-color: var(--cyan); + background: var(--cyan); + animation: pulse 2s ease-in-out infinite; +} + +.pipe-dot.done { + border-color: var(--text-dim); + background: var(--text-dim); +} + +.pipe-label { + font-family: var(--display); + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dim); +} + +.pipe-stage:has(.active) .pipe-label { + color: var(--cyan); +} + +.pipe-line { + width: 60px; + height: 1px; + background: var(--border); + margin: 0 0.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 500px) { + .pipe-line { width: 30px; } + .pipe-label { font-size: 0.55rem; } +} + +/* Sections */ +.section-title { + font-family: var(--display); + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 2.5rem; +} + +/* How it works */ +.how-section { + padding: 5rem 0; + border-top: 1px solid var(--border); +} + +.steps { + list-style: none; + display: flex; + flex-direction: column; + gap: 0; +} + +.step { + display: grid; + grid-template-columns: 56px 1fr; + gap: 1.5rem; + padding: 1.8rem 0; + border-bottom: 1px solid var(--border); + align-items: start; +} + +.step:first-child { + border-top: 1px solid var(--border); +} + +.step-num { + font-family: var(--display); + font-size: 0.75rem; + font-weight: 700; + color: var(--text-dim); + padding-top: 0.15rem; +} + +.step-body h3 { + font-family: var(--display); + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.4rem; +} + +.step-body p { + font-size: 0.88rem; + font-weight: 300; + color: var(--text-secondary); + line-height: 1.7; +} + +/* Features */ +.features-section { + padding: 5rem 0; + border-top: 1px solid var(--border); +} + +.feature-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: var(--border); + border: 1px solid var(--border); +} + +.feature { + background: var(--surface); + padding: 2rem; + transition: background 0.3s; +} + +.feature:hover { + background: var(--surface-hover); +} + +.feature-icon { + color: var(--cyan); + margin-bottom: 1.2rem; + opacity: 0.8; +} + +.feature h3 { + font-family: var(--display); + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.feature p { + font-size: 0.82rem; + font-weight: 300; + color: var(--text-secondary); + line-height: 1.7; +} + +@media (max-width: 600px) { + .feature-grid { grid-template-columns: 1fr; } +} + +/* CTA */ +.cta-section { + padding: 5rem 0; + border-top: 1px solid var(--border); + text-align: center; +} + +.cta-section h2 { + font-family: var(--display); + font-size: 1.8rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 1rem; +} + +.cta-section p { + font-size: 0.95rem; + color: var(--text-secondary); + font-weight: 300; +} + +/* Footer */ +footer { + padding: 2rem 0; + border-top: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + color: var(--text-dim); +} + +footer a { + color: var(--text-dim); + font-size: 0.75rem; +} + +footer a:hover { color: var(--text-secondary); } diff --git a/website/app/layout.tsx b/website/app/layout.tsx new file mode 100644 index 00000000..e33ce308 --- /dev/null +++ b/website/app/layout.tsx @@ -0,0 +1,39 @@ +/** Root layout: loads fonts and global CSS for every page. */ +import type { Metadata } from 'next' +import { Bricolage_Grotesque, Karla } from 'next/font/google' +import './globals.css' + +const bricolage = Bricolage_Grotesque({ + weight: ['400', '500', '600', '700', '800'], + subsets: ['latin'], + variable: '--font-display', +}) + +const karla = Karla({ + weight: ['300', '400', '500'], + style: ['normal', 'italic'], + subsets: ['latin'], + variable: '--font-body', +}) + +/** Default page metadata for the site. */ +export const metadata: Metadata = { + title: 'Huskies — Story-Driven Development for AI Agents', + description: + 'Huskies is an autonomous development pipeline that turns user stories into tested, shipped code using AI agents.', +} + +/** Wraps every page with html/body, font CSS variables, and global styles. */ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/website/app/page.tsx b/website/app/page.tsx new file mode 100644 index 00000000..9ed86328 --- /dev/null +++ b/website/app/page.tsx @@ -0,0 +1,180 @@ +/** Homepage — marketing landing page with hero, features, and CTA. */ +import type { Metadata } from 'next' +import Image from 'next/image' + +/** Page metadata for the homepage. */ +export const metadata: Metadata = { + title: 'Huskies — Story-Driven Development for AI Agents', + description: + 'Huskies is an autonomous development pipeline that turns user stories into tested, shipped code using AI agents.', +} + +/** Renders the huskies marketing homepage. */ +export default function HomePage() { + return ( +
+
+ huskies + +
+ +
+
+ +
+

Story-driven development

+

+ Coding agents are huskies,
not labradors. +

+

+ They're enthusiastic, sometimes wild, and they'll happily wander off on their own. But put + them in a harness and they'll take you anywhere. Huskies is the harness — a story-driven + pipeline that turns coding agents into a disciplined team. +

+ +
+
+ + Story +
+ +
+ + Implement +
+ +
+ + QA +
+ +
+ + Merge +
+ +
+ + Done +
+
+
+ +
+

How it works

+
    +
  1. + 01 +
    +

    Write a story

    +

    Describe what you want with acceptance criteria. From your IDE, a chat room, or WhatsApp.

    +
    +
  2. +
  3. + 02 +
    +

    Agent picks it up

    +

    A coder agent creates a feature branch, implements the code, and writes tests against your criteria.

    +
    +
  4. +
  5. + 03 +
    +

    Quality gates run

    +

    Linters, tests, and compilation checks run automatically. Nothing moves forward until everything passes.

    +
    +
  6. +
  7. + 04 +
    +

    Merge & land

    +

    A merge agent resolves conflicts and squash-merges to your main branch. You review and accept.

    +
    +
  8. +
+
+ +
+

Features

+
+
+
+ + + +
+

The Harness

+

+ Stories define the change. Tests define the truth. Code defines the reality. Every agent runs on + rails — nothing ships without acceptance criteria. +

+
+
+
+ + + + + + +
+

The Pack

+

+ Coder, QA, and merge agents work in parallel across isolated git worktrees. A coordinated pack, not + a lone wolf. Configure agent count, models, and budgets. +

+
+
+
+ + + +
+

Chat Anywhere

+

+ Control the pipeline from Matrix, WhatsApp, Slack, or the built-in web UI. Create stories, start + agents, check status. +

+
+
+
+ + + +
+

You're the Musher

+

+ Agents implement, test, and merge independently. You set the direction and approve what ships. Every + story is traceable from request to release. +

+
+
+
+ +
+

Interested?

+

+ Huskies is built by Crash Labs. Get in touch at{' '} + hello@huskies.dev. +

+
+ + +
+ ) +} diff --git a/website/app/privacy/page.tsx b/website/app/privacy/page.tsx new file mode 100644 index 00000000..6dc372bf --- /dev/null +++ b/website/app/privacy/page.tsx @@ -0,0 +1,100 @@ +/** Privacy policy page. */ +import type { Metadata } from 'next' + +/** Page metadata for the privacy policy. */ +export const metadata: Metadata = { + title: 'Privacy Policy — Huskies', +} + +/** Renders the privacy policy page. */ +export default function PrivacyPage() { + return ( +
+
+

+ + storkit + +

+

Privacy Policy

+
+ +
+

+ Last updated: 25 March 2026 +

+ +

Who we are

+

+ Huskies is operated by Libby Labs Ltd (“we”, “us”, “our”), trading + as Crashlabs. Our contact email is{' '} + hello@huskies.dev. +

+
+ +
+

What we collect

+

+ When you interact with Huskies via WhatsApp, Slack, Matrix, or the web interface, we may collect: +

+

+ Messaging data: Your phone number or chat identifier and the content of messages you + send to the bot. This is used solely to process your requests and maintain conversation context. +

+

+ Usage data: Basic server logs including timestamps and request metadata. We do not use + analytics trackers on this website. +

+
+ +
+

How we use your data

+

We use your data only to provide and improve the Huskies service. Specifically:

+

+ - To process commands and respond to your messages.
+ - To maintain conversation history within active sessions.
+ - To diagnose and fix technical issues. +

+

We do not sell, rent, or share your personal data with third parties for marketing purposes.

+
+ +
+

Third-party services

+

+ Messages sent via WhatsApp are processed through Meta's WhatsApp Business API or Twilio's + messaging platform, subject to their respective privacy policies. Messages sent via Slack or Matrix pass + through those platforms' infrastructure. +

+
+ +
+

Data retention

+

+ Conversation history is stored locally on our servers and retained only for the duration needed to + maintain session context. We do not retain message data indefinitely. +

+
+ +
+

Your rights

+

+ You may request access to, correction of, or deletion of your personal data at any time by contacting + us at hello@huskies.dev. +

+
+ +
+

Changes to this policy

+

+ We may update this policy from time to time. Changes will be posted on this page with an updated date. +

+
+ + +
+ ) +} diff --git a/website/components/DocsSidebar.tsx b/website/components/DocsSidebar.tsx new file mode 100644 index 00000000..9f15a73e --- /dev/null +++ b/website/components/DocsSidebar.tsx @@ -0,0 +1,51 @@ +/** Docs sidebar navigation — highlights the active page via usePathname. */ +'use client' + +import { usePathname } from 'next/navigation' +import Link from 'next/link' + +/** Renders the docs sidebar with active-link highlighting for the current route. */ +export default function DocsSidebar() { + const pathname = usePathname() + + return ( + + ) +} diff --git a/website/next.config.mjs b/website/next.config.mjs new file mode 100644 index 00000000..1cabe002 --- /dev/null +++ b/website/next.config.mjs @@ -0,0 +1,11 @@ +/** Next.js configuration for the huskies static marketing site. */ + +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, +} + +export default nextConfig diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 00000000..130ce8ff --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,499 @@ +{ + "name": "huskies-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "huskies-website", + "version": "1.0.0", + "dependencies": { + "next": "14.2.29", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } + }, + "node_modules/@next/env": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.29.tgz", + "integrity": "sha512-UzgLR2eBfhKIQt0aJ7PWH7XRPYw7SXz0Fpzdl5THjUnvxy4kfBk9OU4RNPNiETewEEtaBcExNFNn1QWH8wQTjg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.29.tgz", + "integrity": "sha512-wWtrAaxCVMejxPHFb1SK/PVV1WDIrXGs9ki0C/kUM8ubKHQm+3hU9MouUywCw8Wbhj3pewfHT2wjunLEr/TaLA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.29.tgz", + "integrity": "sha512-7Z/jk+6EVBj4pNLw/JQrvZVrAh9Bv8q81zCFSfvTMZ51WySyEHWVpwCEaJY910LyBftv2F37kuDPQm0w9CEXyg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.29.tgz", + "integrity": "sha512-o6hrz5xRBwi+G7JFTHc+RUsXo2lVXEfwh4/qsuWBMQq6aut+0w98WEnoNwAwt7hkEqegzvazf81dNiwo7KjITw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.29.tgz", + "integrity": "sha512-9i+JEHBOVgqxQ92HHRFlSW1EQXqa/89IVjtHgOqsShCcB/ZBjTtkWGi+SGCJaYyWkr/lzu51NTMCfKuBf7ULNw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.29.tgz", + "integrity": "sha512-B7JtMbkUwHijrGBOhgSQu2ncbCYq9E7PZ7MX58kxheiEOwdkM+jGx0cBb+rN5AeqF96JypEppK6i/bEL9T13lA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.29.tgz", + "integrity": "sha512-yCcZo1OrO3aQ38B5zctqKU1Z3klOohIxug6qdiKO3Q3qNye/1n6XIs01YJ+Uf+TdpZQ0fNrOQI2HrTLF3Zprnw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.29.tgz", + "integrity": "sha512-WnrfeOEtTVidI9Z6jDLy+gxrpDcEJtZva54LYC0bSKQqmyuHzl0ego+v0F/v2aXq0am67BRqo/ybmmt45Tzo4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.29.tgz", + "integrity": "sha512-vkcriFROT4wsTdSeIzbxaZjTNTFKjSYmLd8q/GVH3Dn8JmYjUKOuKXHK8n+lovW/kdcpIvydO5GtN+It2CvKWA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.29.tgz", + "integrity": "sha512-iPPwUEKnVs7pwR0EBLJlwxLD7TTHWS/AoVZx1l9ZQzfQciqaFEr5AlYzA2uB6Fyby1IF18t4PL0nTpB+k4Tzlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.29", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.29.tgz", + "integrity": "sha512-s98mCOMOWLGGpGOfgKSnleXLuegvvH415qtRZXpSp00HeEgdmrxmwL9cgKU+h4XrhB16zEI5d/7BnkS3ATInsA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.29", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.29", + "@next/swc-darwin-x64": "14.2.29", + "@next/swc-linux-arm64-gnu": "14.2.29", + "@next/swc-linux-arm64-musl": "14.2.29", + "@next/swc-linux-x64-gnu": "14.2.29", + "@next/swc-linux-x64-musl": "14.2.29", + "@next/swc-win32-arm64-msvc": "14.2.29", + "@next/swc-win32-ia32-msvc": "14.2.29", + "@next/swc-win32-x64-msvc": "14.2.29" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..84302164 --- /dev/null +++ b/website/package.json @@ -0,0 +1,21 @@ +{ + "name": "huskies-website", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "14.2.29", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} diff --git a/website/public/husky.png b/website/public/husky.png new file mode 100644 index 00000000..25037f5e Binary files /dev/null and b/website/public/husky.png differ diff --git a/website/public/husky.svg b/website/public/husky.svg new file mode 100644 index 00000000..295bba47 --- /dev/null +++ b/website/public/husky.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 00000000..522b6a0f --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}