huskies: merge 1121 story Remove the marketing website from the huskies OSS repo (now lives in huskies-server)
This commit is contained in:
Generated
-46
@@ -1931,7 +1931,6 @@ dependencies = [
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"matrix-sdk",
|
||||
"mime_guess",
|
||||
"mockito",
|
||||
"notify",
|
||||
"nutype",
|
||||
@@ -1941,7 +1940,6 @@ dependencies = [
|
||||
"rand 0.10.1",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -2978,16 +2976,6 @@ version = "0.1.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -4206,40 +4194,6 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.117",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||
dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
|
||||
@@ -13,12 +13,10 @@ chrono-tz = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
homedir = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
poem = { workspace = true, features = ["websocket"] }
|
||||
portable-pty = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream", "form"] }
|
||||
rust-embed = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
|
||||
@@ -62,13 +62,6 @@ pub fn build_gateway_route(state_arc: Arc<GatewayState>) -> impl poem::Endpoint
|
||||
"/gateway/agents/:id/assign",
|
||||
poem::post(gateway_assign_agent_handler),
|
||||
)
|
||||
// Serve the embedded React frontend so the gateway has a UI.
|
||||
.at(
|
||||
"/assets/*path",
|
||||
poem::get(crate::http::assets::embedded_asset),
|
||||
)
|
||||
.at("/*path", poem::get(crate::http::assets::embedded_file))
|
||||
.at("/", poem::get(crate::http::assets::embedded_index))
|
||||
.data(state_arc)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
//! Static asset serving — serves the embedded React frontend via `rust-embed`.
|
||||
use poem::{
|
||||
Response, handler,
|
||||
http::{StatusCode, header},
|
||||
web::Path,
|
||||
};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../frontend/dist"]
|
||||
struct EmbeddedAssets;
|
||||
|
||||
fn serve_embedded(path: &str) -> Response {
|
||||
let normalized = if path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
path.trim_start_matches('/')
|
||||
};
|
||||
|
||||
let is_asset_request = normalized.starts_with("assets/");
|
||||
let asset = if is_asset_request {
|
||||
EmbeddedAssets::get(normalized)
|
||||
} else {
|
||||
EmbeddedAssets::get(normalized).or_else(|| {
|
||||
if normalized == "index.html" {
|
||||
None
|
||||
} else {
|
||||
EmbeddedAssets::get("index.html")
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
match asset {
|
||||
Some(content) => {
|
||||
let body = content.data.into_owned();
|
||||
let mime = mime_guess::from_path(normalized)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime)
|
||||
.body(body)
|
||||
}
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body("Not Found"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve a single embedded asset from the `assets/` folder.
|
||||
#[handler]
|
||||
pub fn embedded_asset(Path(path): Path<String>) -> Response {
|
||||
let asset_path = format!("assets/{path}");
|
||||
serve_embedded(&asset_path)
|
||||
}
|
||||
|
||||
/// Serve an embedded file by path (falls back to `index.html` for SPA routing).
|
||||
#[handler]
|
||||
pub fn embedded_file(Path(path): Path<String>) -> Response {
|
||||
serve_embedded(&path)
|
||||
}
|
||||
|
||||
/// Serve the embedded SPA entrypoint.
|
||||
#[handler]
|
||||
pub fn embedded_index() -> Response {
|
||||
serve_embedded("index.html")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[test]
|
||||
fn non_asset_path_spa_fallback_or_not_found() {
|
||||
// Non-asset paths fall back to index.html for SPA client-side routing.
|
||||
// In release builds (with embedded dist/) this returns 200.
|
||||
// In debug builds without a built frontend dist/ it returns 404.
|
||||
let response = serve_embedded("__nonexistent_spa_route__.html");
|
||||
let status = response.status();
|
||||
assert!(
|
||||
status == StatusCode::OK || status == StatusCode::NOT_FOUND,
|
||||
"unexpected status: {status}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_asset_path_prefix_returns_not_found() {
|
||||
// assets/ prefix: no SPA fallback – returns 404 if the file does not exist
|
||||
let response = serve_embedded("assets/__nonexistent__.js");
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_embedded_does_not_panic_on_empty_path() {
|
||||
// Empty path normalises to index.html; OK in release, 404 in debug without dist/
|
||||
let response = serve_embedded("");
|
||||
let status = response.status();
|
||||
assert!(
|
||||
status == StatusCode::OK || status == StatusCode::NOT_FOUND,
|
||||
"unexpected status: {status}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_assets_struct_is_iterable() {
|
||||
// Verifies that rust-embed compiled the EmbeddedAssets struct correctly.
|
||||
// In debug builds without a built frontend dist/ directory the iterator is empty; that is
|
||||
// expected. In release builds it will contain all bundled frontend files.
|
||||
let _files: Vec<_> = EmbeddedAssets::iter().collect();
|
||||
// No assertion needed – the test passes as long as it compiles and does not panic.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_index_handler_returns_ok_or_not_found() {
|
||||
// Route the handler through TestClient; index.html is the SPA entry point.
|
||||
let app = poem::Route::new().at("/", poem::get(embedded_index));
|
||||
let cli = poem::test::TestClient::new(app);
|
||||
let resp = cli.get("/").send().await;
|
||||
let status = resp.0.status();
|
||||
assert!(
|
||||
status == StatusCode::OK || status == StatusCode::NOT_FOUND,
|
||||
"unexpected status: {status}",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_file_handler_with_path_returns_ok_or_not_found() {
|
||||
// Non-asset paths fall back to index.html (SPA routing) or 404.
|
||||
let app = poem::Route::new().at("/*path", poem::get(embedded_file));
|
||||
let cli = poem::test::TestClient::new(app);
|
||||
let resp = cli.get("/__spa_route__").send().await;
|
||||
let status = resp.0.status();
|
||||
assert!(
|
||||
status == StatusCode::OK || status == StatusCode::NOT_FOUND,
|
||||
"unexpected status: {status}",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_asset_handler_missing_file_returns_not_found() {
|
||||
// The assets/ prefix disables SPA fallback; missing files must return 404.
|
||||
let app = poem::Route::new().at("/assets/*path", poem::get(embedded_asset));
|
||||
let cli = poem::test::TestClient::new(app);
|
||||
let resp = cli.get("/assets/__nonexistent__.js").send().await;
|
||||
assert_eq!(resp.0.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
//! HTTP server — module declarations for all REST, MCP, WebSocket, and SSE endpoints.
|
||||
/// Server-sent event stream for real-time agent output.
|
||||
pub mod agents_sse;
|
||||
/// Static asset serving (embedded frontend files).
|
||||
pub mod assets;
|
||||
/// Shared application context threaded through handlers.
|
||||
pub mod context;
|
||||
/// Server-sent event stream for pipeline/watcher events.
|
||||
@@ -100,10 +98,7 @@ pub fn build_routes(
|
||||
get(oauth::oauth_callback).data(oauth_state.clone()),
|
||||
)
|
||||
.at("/oauth/status", get(oauth::oauth_status))
|
||||
.at("/debug/crdt", get(debug_crdt_handler))
|
||||
.at("/assets/*path", get(assets::embedded_asset))
|
||||
.at("/", get(assets::embedded_index))
|
||||
.at("/*path", get(assets::embedded_file));
|
||||
.at("/debug/crdt", get(debug_crdt_handler));
|
||||
|
||||
if let Some(buf) = event_buffer {
|
||||
route = route.at("/api/events", get(events::events_handler).data(buf));
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
next-env.d.ts
|
||||
@@ -1,57 +0,0 @@
|
||||
# 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
|
||||
@@ -1,384 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">CLI Reference</h1>
|
||||
<p className="page-subtitle">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>huskies</h2>
|
||||
<p>Start the huskies server.</p>
|
||||
<pre>
|
||||
<code>huskies [OPTIONS]</code>
|
||||
</pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flag</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>--port <PORT></td>
|
||||
<td>3000</td>
|
||||
<td>
|
||||
HTTP port to listen on. Set the <code>HUSKIES_PORT</code> environment variable as an
|
||||
alternative.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--project <PATH></td>
|
||||
<td>current dir</td>
|
||||
<td>
|
||||
Path to the project directory. Huskies looks for <code>.huskies/</code> here.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--help</td>
|
||||
<td>—</td>
|
||||
<td>Print help and exit.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--version</td>
|
||||
<td>—</td>
|
||||
<td>Print version and exit.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Examples</h3>
|
||||
<pre>
|
||||
<code>{`# 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`}</code>
|
||||
</pre>
|
||||
|
||||
<div className="note">
|
||||
<strong>Multiple instances:</strong> Each worktree or project can run its own huskies instance on a
|
||||
different port. Use <code>HUSKIES_PORT</code> to avoid conflicts when running several instances
|
||||
simultaneously.
|
||||
</div>
|
||||
|
||||
<h2>huskies init</h2>
|
||||
<p>
|
||||
Initialise a project directory for use with huskies. Creates the <code>.huskies/</code> directory
|
||||
structure, default configuration files, and <code>.mcp.json</code>.
|
||||
</p>
|
||||
<pre>
|
||||
<code>huskies init [OPTIONS]</code>
|
||||
</pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flag</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>--port <PORT></td>
|
||||
<td>3000</td>
|
||||
<td>
|
||||
Port written into <code>.mcp.json</code> for MCP tool discovery.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--project <PATH></td>
|
||||
<td>current dir</td>
|
||||
<td>Directory to initialise. Must be a git repository.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--help</td>
|
||||
<td>—</td>
|
||||
<td>Print help and exit.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>What it creates</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.huskies/project.toml</code>
|
||||
</td>
|
||||
<td>Project-wide settings (QA mode, agent limits, timezone, etc.).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.huskies/agents.toml</code>
|
||||
</td>
|
||||
<td>Agent definitions for coder, QA, and mergemaster roles.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.huskies/work/1_backlog/</code>
|
||||
</td>
|
||||
<td>Pipeline stage directories (1 through 6).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.huskies/specs/00_CONTEXT.md</code>
|
||||
</td>
|
||||
<td>Placeholder project context file for the setup wizard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.huskies/specs/tech/STACK.md</code>
|
||||
</td>
|
||||
<td>Placeholder tech stack file for the setup wizard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>.mcp.json</code>
|
||||
</td>
|
||||
<td>
|
||||
MCP server config so Claude Code discovers huskies' tools automatically.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="note">
|
||||
<strong>Git required:</strong> The project directory must be a git repository. Run{' '}
|
||||
<code>git init</code> first if needed.
|
||||
</div>
|
||||
|
||||
<h2>huskies agent</h2>
|
||||
<p>
|
||||
Spawn a single agent process directly from the command line. This is the command the server uses
|
||||
internally when you run <code>start <number></code> in chat — you rarely need to invoke
|
||||
it manually.
|
||||
</p>
|
||||
<pre>
|
||||
<code>huskies agent [OPTIONS]</code>
|
||||
</pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flag</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>--story <ID></td>
|
||||
<td>
|
||||
Story ID slug to work on (e.g. <code>42_story_add_login</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--agent <NAME></td>
|
||||
<td>
|
||||
Agent name from <code>agents.toml</code> to use (e.g. <code>coder-1</code>,{' '}
|
||||
<code>qa</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--worktree <PATH></td>
|
||||
<td>Path to the git worktree the agent should work in.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--port <PORT></td>
|
||||
<td>Huskies server port, so the agent can call MCP tools.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>--help</td>
|
||||
<td>Print help and exit.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Environment variables</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>HUSKIES_PORT</code>
|
||||
</td>
|
||||
<td>
|
||||
Server port. Overrides the <code>--port</code> flag.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>ANTHROPIC_API_KEY</code>
|
||||
</td>
|
||||
<td>
|
||||
Anthropic API key for agent sessions. Can also be set via the web UI on first use.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>GITEA_TOKEN</code>
|
||||
</td>
|
||||
<td>
|
||||
Gitea API token used by the <code>script/release</code> script when publishing releases.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Gateway event-push protocol</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Connecting</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Obtain a one-time join token: <code>POST /gateway/tokens</code> →{' '}
|
||||
<code>{'{"token":"…"}'}</code>
|
||||
</li>
|
||||
<li>
|
||||
Open a WebSocket upgrade to{' '}
|
||||
<code>GET /gateway/events/push?token=TOKEN&project=PROJECT_NAME</code>
|
||||
</li>
|
||||
<li>
|
||||
The token is consumed on upgrade. The project name is attached to every event the server
|
||||
broadcasts downstream.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Sending events</h3>
|
||||
<p>
|
||||
Each message must be a JSON-encoded <code>StoredEvent</code> frame:
|
||||
</p>
|
||||
<pre>
|
||||
<code>{`// 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}`}</code>
|
||||
</pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Reconnect with exponential back-off</h3>
|
||||
<p>
|
||||
Project nodes <strong>must</strong> reconnect on any disconnect. Use the following policy to avoid
|
||||
thundering herds after a gateway restart:
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Initial delay</td>
|
||||
<td>1 s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Back-off multiplier</td>
|
||||
<td>2× per attempt</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Maximum delay</td>
|
||||
<td>60 s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jitter</td>
|
||||
<td>±10 % of the computed delay</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Pseudocode:</p>
|
||||
<pre>
|
||||
<code>{`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)`}</code>
|
||||
</pre>
|
||||
|
||||
<div className="note">
|
||||
<strong>New token per connection:</strong> Each WebSocket upgrade consumes the join token. Request a
|
||||
fresh token for every reconnect attempt.
|
||||
</div>
|
||||
|
||||
<h2>Building from source</h2>
|
||||
<h3>Standard release build</h3>
|
||||
<pre>
|
||||
<code>{`cargo build --release\n# Output: target/release/huskies`}</code>
|
||||
</pre>
|
||||
|
||||
<h3>Static Linux binary (musl)</h3>
|
||||
<p>
|
||||
Requires <code>cross</code>: <code>cargo install cross</code>.
|
||||
</p>
|
||||
<pre>
|
||||
<code>cross build --release --target x86_64-unknown-linux-musl</code>
|
||||
</pre>
|
||||
|
||||
<h3>Docker image</h3>
|
||||
<pre>
|
||||
<code>docker compose -f docker/docker-compose.yml build</code>
|
||||
</pre>
|
||||
|
||||
<h3>Release script</h3>
|
||||
<p>
|
||||
Builds macOS arm64 and Linux amd64 binaries, bumps the version, tags the repo, and publishes a
|
||||
Gitea release with changelog and binaries attached.
|
||||
</p>
|
||||
<pre>
|
||||
<code>script/release 0.8.0</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">Bot Commands</h1>
|
||||
<p className="page-subtitle">
|
||||
Commands available in every chat transport (Matrix, Slack, WhatsApp, Discord) and the built-in web
|
||||
UI. Commands are case-insensitive. Run <code>help</code> in any chat to see the list.
|
||||
</p>
|
||||
|
||||
<div className="note">
|
||||
<strong>How to invoke:</strong> In chat rooms, address the bot first (e.g.{' '}
|
||||
<code>@huskies start 42</code>) or enable ambient mode so it responds to all messages. In the web
|
||||
UI, type commands directly.
|
||||
</div>
|
||||
|
||||
<h2>Pipeline management</h2>
|
||||
<div className="cmd-grid">
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">status</div>
|
||||
<div className="cmd-desc">
|
||||
Show pipeline status and agent availability. Use <code>status <number></code> for a
|
||||
detailed triage dump on a specific story.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">start</div>
|
||||
<div className="cmd-desc">
|
||||
Start an agent on a story: <code>start <number></code>. To use the opus model:{' '}
|
||||
<code>start <number> opus</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">move</div>
|
||||
<div className="cmd-desc">
|
||||
Move a work item to a pipeline stage: <code>move <number> <stage></code>. Stages:{' '}
|
||||
<code>backlog</code>, <code>current</code>, <code>qa</code>, <code>merge</code>,{' '}
|
||||
<code>done</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">show</div>
|
||||
<div className="cmd-desc">
|
||||
Display the full text of a work item: <code>show <number></code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">delete</div>
|
||||
<div className="cmd-desc">
|
||||
Remove a work item from the pipeline: <code>delete <number></code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">unblock</div>
|
||||
<div className="cmd-desc">
|
||||
Reset a blocked story: <code>unblock <number></code>. Clears the blocked flag and resets
|
||||
the retry count.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">assign</div>
|
||||
<div className="cmd-desc">
|
||||
Pre-assign a model to a story before starting: <code>assign <number> <model></code>{' '}
|
||||
(e.g. <code>assign 42 opus</code>).
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">backlog</div>
|
||||
<div className="cmd-desc">
|
||||
Show all items in the backlog with dependency satisfaction status — which are ready to
|
||||
start and which are still waiting on other stories.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">depends</div>
|
||||
<div className="cmd-desc">
|
||||
Set story dependencies: <code>depends <number> [dep1 dep2 ...]</code>. Call with no deps
|
||||
to clear all dependencies.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">timer</div>
|
||||
<div className="cmd-desc">
|
||||
Schedule a deferred agent start: <code>timer <number> HH:MM</code>. List all timers:{' '}
|
||||
<code>timer list</code>. Cancel: <code>timer cancel <number></code>. Times are interpreted
|
||||
in the project timezone.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Worktrees</h2>
|
||||
<div className="cmd-grid">
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">rmtree</div>
|
||||
<div className="cmd-desc">
|
||||
Delete the worktree for a story without removing the story from the pipeline:{' '}
|
||||
<code>rmtree <number></code>. Useful for freeing disk space on a story that needs to be
|
||||
restarted.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Observability</h2>
|
||||
<div className="cmd-grid">
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">cost</div>
|
||||
<div className="cmd-desc">
|
||||
Show token spend: 24h total, top stories, breakdown by agent type, and all-time total.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">coverage</div>
|
||||
<div className="cmd-desc">
|
||||
Show test coverage from the cached baseline. Use <code>coverage run</code> to rerun the full
|
||||
test suite and regenerate the report.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">git</div>
|
||||
<div className="cmd-desc">
|
||||
Show git status for the main repository: current branch, uncommitted changes, and ahead/behind
|
||||
remote.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">htop</div>
|
||||
<div className="cmd-desc">
|
||||
Live system and agent process dashboard. Use <code>htop</code> to start,{' '}
|
||||
<code>htop 10m</code> to run for 10 minutes, <code>htop stop</code> to stop.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">run_tests</div>
|
||||
<div className="cmd-desc">
|
||||
Run the project's test suite (<code>script/test</code>) and show a pass/fail summary with
|
||||
output. Use <code>run_tests <number></code> to run tests inside a specific story's
|
||||
worktree instead of the project root.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">loc</div>
|
||||
<div className="cmd-desc">
|
||||
Show top source files by line count: <code>loc</code> (top 10), <code>loc <N></code> for
|
||||
N files, or <code>loc <filepath></code> for a specific file.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">overview</div>
|
||||
<div className="cmd-desc">
|
||||
Show an implementation summary for a merged story: <code>overview <number></code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">unreleased</div>
|
||||
<div className="cmd-desc">
|
||||
Show stories merged to master since the last release tag.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Server management</h2>
|
||||
<div className="cmd-grid">
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">rebuild</div>
|
||||
<div className="cmd-desc">Rebuild the huskies server binary and restart the process.</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">reset</div>
|
||||
<div className="cmd-desc">
|
||||
Clear the current Claude Code session and start a fresh context window.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Setup & configuration</h2>
|
||||
<div className="cmd-grid">
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">setup</div>
|
||||
<div className="cmd-desc">
|
||||
Show setup wizard progress. Drive the wizard from chat: <code>setup generate</code>,{' '}
|
||||
<code>setup confirm</code>, <code>setup skip</code>, <code>setup retry</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">ambient</div>
|
||||
<div className="cmd-desc">
|
||||
Toggle ambient mode for the current room: <code>ambient on</code> or{' '}
|
||||
<code>ambient off</code>. In ambient mode the bot responds to all messages, not just addressed
|
||||
ones.
|
||||
</div>
|
||||
</div>
|
||||
<div className="cmd-row">
|
||||
<div className="cmd-name">help</div>
|
||||
<div className="cmd-desc">Show the list of available commands.</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">Configuration</h1>
|
||||
<p className="page-subtitle">
|
||||
Huskies is configured via three TOML files in your <code>.huskies/</code> directory. All files are
|
||||
created by <code>huskies init</code> with sensible defaults.
|
||||
</p>
|
||||
|
||||
<h2 id="project-toml">project.toml</h2>
|
||||
<p>
|
||||
Project-wide settings. Lives at <code>.huskies/project.toml</code>.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>default_qa</td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<code>"server"</code>
|
||||
</td>
|
||||
<td>
|
||||
Default QA mode. One of <code>"server"</code> (automated gate run),{' '}
|
||||
<code>"agent"</code> (spawn a QA agent), or <code>"human"</code> (manual
|
||||
approval).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>default_coder_model</td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<code>"sonnet"</code>
|
||||
</td>
|
||||
<td>
|
||||
Default model for coder agents. Only agents matching this model are auto-assigned. Use{' '}
|
||||
<code>"opus"</code> on individual stories for complex tasks.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>max_coders</td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
<code>3</code>
|
||||
</td>
|
||||
<td>
|
||||
Maximum concurrent coder agents. Stories wait in <code>2_current/</code> when all slots are
|
||||
full.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>max_retries</td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
<code>3</code>
|
||||
</td>
|
||||
<td>
|
||||
Maximum retries per story per pipeline stage before marking it as blocked. Set to{' '}
|
||||
<code>0</code> to disable.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>base_branch</td>
|
||||
<td>string</td>
|
||||
<td>auto-detected</td>
|
||||
<td>
|
||||
Base branch for merges and agent prompts. When unset, huskies reads the current HEAD branch.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>rate_limit_notifications</td>
|
||||
<td>bool</td>
|
||||
<td>
|
||||
<code>false</code>
|
||||
</td>
|
||||
<td>
|
||||
Send chat notifications when API soft rate limits are hit. Hard blocks and story-blocked
|
||||
notifications are always sent.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>timezone</td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<code>"UTC"</code>
|
||||
</td>
|
||||
<td>
|
||||
IANA timezone for timer scheduling (e.g. <code>"Europe/London"</code>,{' '}
|
||||
<code>"America/New_York"</code>). Timer HH:MM inputs are interpreted in this
|
||||
timezone.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Component setup</h3>
|
||||
<p>
|
||||
The <code>[[component]]</code> 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.
|
||||
</p>
|
||||
<pre>
|
||||
<code>{`[[component]]
|
||||
name = "frontend"
|
||||
path = "frontend"
|
||||
setup = ["npm ci", "npm run build"]
|
||||
teardown = []
|
||||
|
||||
[[component]]
|
||||
name = "server"
|
||||
path = "."
|
||||
setup = ["mkdir -p frontend/dist", "cargo check"]
|
||||
teardown = []`}</code>
|
||||
</pre>
|
||||
|
||||
<h3>Story front matter overrides</h3>
|
||||
<p>Individual stories can override project defaults using YAML front matter:</p>
|
||||
<pre>
|
||||
<code>{`---
|
||||
name: "My Complex Story"
|
||||
qa: agent # override default_qa
|
||||
agent: opus # use the opus coder agent
|
||||
---`}</code>
|
||||
</pre>
|
||||
|
||||
<h2 id="agents-toml">agents.toml</h2>
|
||||
<p>
|
||||
Agent definitions. Lives at <code>.huskies/agents.toml</code>. Each <code>[[agent]]</code> block
|
||||
defines one agent slot.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>name</td>
|
||||
<td>
|
||||
Unique identifier for this agent slot (e.g. <code>"coder-1"</code>,{' '}
|
||||
<code>"qa"</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>stage</td>
|
||||
<td>
|
||||
Pipeline stage this agent handles. One of <code>"coder"</code>,{' '}
|
||||
<code>"qa"</code>, or <code>"mergemaster"</code>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>role</td>
|
||||
<td>
|
||||
Human-readable description of the agent's responsibilities (shown in status output).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>model</td>
|
||||
<td>
|
||||
Claude model to use. One of <code>"sonnet"</code> (claude-sonnet-4-6) or{' '}
|
||||
<code>"opus"</code> (claude-opus-4-6).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>max_turns</td>
|
||||
<td>Maximum conversation turns before the agent is forcefully stopped.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>max_budget_usd</td>
|
||||
<td>Maximum API spend in USD before the agent is stopped.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>prompt</td>
|
||||
<td>
|
||||
The initial user-turn prompt sent to the agent. Supports template variables (see below).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>system_prompt</td>
|
||||
<td>The system prompt sent to the agent session.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Template variables</h3>
|
||||
<p>
|
||||
The following variables are interpolated into <code>prompt</code> and <code>system_prompt</code> at
|
||||
agent start time:
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{'{{story_id}}'}</td>
|
||||
<td>
|
||||
The story's ID slug (e.g. <code>42_story_add_login</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{'{{worktree_path}}'}</td>
|
||||
<td>Absolute path to the agent's git worktree.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{'{{base_branch}}'}</td>
|
||||
<td>
|
||||
The base branch name from <code>project.toml</code>.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Example: adding an opus coder</h3>
|
||||
<pre>
|
||||
<code>{`[[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 ..."`}</code>
|
||||
</pre>
|
||||
<p>
|
||||
To use this agent for a specific story, add <code>agent: opus</code> to the story's front
|
||||
matter, or run <code>start <number> opus</code> in chat.
|
||||
</p>
|
||||
|
||||
<h2 id="agent-md">
|
||||
Project-local agent prompt (<code>.huskies/AGENT.md</code>)
|
||||
</h2>
|
||||
<p>
|
||||
Place a file at <code>.huskies/AGENT.md</code> in your project root to append project-specific
|
||||
guidance to every agent's initial prompt at spawn time.
|
||||
</p>
|
||||
|
||||
<h3>How it works</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Huskies reads <code>.huskies/AGENT.md</code> each time an agent is spawned — no caching, no
|
||||
restart required.
|
||||
</li>
|
||||
<li>
|
||||
The file content is appended <em>after</em> the baked-in agent prompt, so project guidance
|
||||
refines core instructions without overriding them.
|
||||
</li>
|
||||
<li>Applies to all agent roles: coder, QA, mergemaster, and supervisor.</li>
|
||||
<li>
|
||||
If the file is missing or empty, agents spawn normally — no warnings, no errors.
|
||||
</li>
|
||||
<li>
|
||||
When the file exists and is non-empty, a single <code>INFO</code> log line is emitted showing the
|
||||
file path and byte count.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Ordering</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Baked-in agent prompt (from <code>agents.toml</code> or <code>project.toml</code>)
|
||||
</li>
|
||||
<li>
|
||||
Project-local content from <code>.huskies/AGENT.md</code>
|
||||
</li>
|
||||
<li>Resume context (only on agent restart after a gate failure)</li>
|
||||
</ol>
|
||||
|
||||
<h3>Example</h3>
|
||||
<pre>
|
||||
<code>{`# .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.`}</code>
|
||||
</pre>
|
||||
<p>
|
||||
Edit the file at any time — the next agent spawn picks up the latest content automatically.
|
||||
</p>
|
||||
|
||||
<h2 id="bot-toml">bot.toml</h2>
|
||||
<p>
|
||||
Chat transport configuration. Lives at <code>.huskies/bot.toml</code>. This file is gitignored as
|
||||
it contains credentials. Copy the appropriate example file to get started:
|
||||
</p>
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.matrix.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
<p>
|
||||
Only one transport can be active at a time. See the{' '}
|
||||
<a href="/docs/transports">Chat transports</a> guide for setup instructions for each platform.
|
||||
</p>
|
||||
|
||||
<h3>Common fields</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>enabled</td>
|
||||
<td>
|
||||
Set to <code>true</code> to activate the bot. Set to <code>false</code> to disable without
|
||||
removing the file.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>transport</td>
|
||||
<td>
|
||||
Transport type: <code>"matrix"</code>, <code>"whatsapp"</code>,{' '}
|
||||
<code>"slack"</code>, or <code>"discord"</code>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>display_name</td>
|
||||
<td>Optional. Bot display name in chat messages.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>history_size</td>
|
||||
<td>
|
||||
Optional. Maximum conversation turns to remember per room/user (default: 20).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="gateway-aggregated-stream">Gateway: aggregated chat stream</h2>
|
||||
<p>
|
||||
When running <code>huskies --gateway</code>, you can configure a single bot that receives pipeline
|
||||
notifications from <strong>all</strong> registered projects. Events are prefixed with{' '}
|
||||
<code>[project-name]</code> so you can tell them apart in one shared room.
|
||||
</p>
|
||||
<p>
|
||||
The aggregated stream is configured entirely in the <strong>gateway's</strong>{' '}
|
||||
<code>.huskies/bot.toml</code> — no per-project bot config is required and no per-project
|
||||
files need to change when you add a new project to <code>projects.toml</code>.
|
||||
</p>
|
||||
|
||||
<h3>Enabling the aggregated stream</h3>
|
||||
<p>
|
||||
Add or edit <code><gateway-config-dir>/.huskies/bot.toml</code> and set{' '}
|
||||
<code>enabled = true</code>. The gateway bot will automatically poll every project listed in{' '}
|
||||
<code>projects.toml</code> and forward events to the configured rooms.
|
||||
</p>
|
||||
<pre>
|
||||
<code>{`# <gateway-config-dir>/.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`}</code>
|
||||
</pre>
|
||||
|
||||
<h3>Aggregated stream settings</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>aggregated_notifications_enabled</td>
|
||||
<td>bool</td>
|
||||
<td>
|
||||
<code>true</code>
|
||||
</td>
|
||||
<td>
|
||||
Set to <code>false</code> to disable the aggregated stream without disabling the gateway bot
|
||||
entirely. Per-project configs are never consulted.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>aggregated_notifications_poll_interval_secs</td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
<code>5</code>
|
||||
</td>
|
||||
<td>
|
||||
How often (in seconds) the gateway polls each project's <code>/api/events</code>{' '}
|
||||
endpoint. Lower values reduce notification latency.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>No-duplicate guarantee</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Unreachable projects</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3>Supported event types</h3>
|
||||
<p>
|
||||
The aggregated stream delivers the following event types, each prefixed with the project name:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Stage transitions</strong> — story created, agent started, QA requested, QA
|
||||
approved/rejected, merge succeeded (all pipeline stage moves)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Merge failures</strong> — merge failed with a reason
|
||||
</li>
|
||||
<li>
|
||||
<strong>Story blocked</strong> — story blocked after exceeding retry limit
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
/* 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); }
|
||||
@@ -1,48 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<div className="shell">
|
||||
<header className="reveal r1">
|
||||
<a href="/" className="logo">
|
||||
huskies
|
||||
</a>
|
||||
<nav>
|
||||
<a href="/#how">How it works</a>
|
||||
<a href="/#features">Features</a>
|
||||
<a href="/docs" className="active">
|
||||
Docs
|
||||
</a>
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies">Source</a>
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies/releases">Releases</a>
|
||||
<a href="mailto:hello@huskies.dev" className="nav-cta">
|
||||
Get in touch
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="shell">
|
||||
<div className="docs-layout">
|
||||
<DocsSidebar />
|
||||
<main className="docs-main reveal r3">{children}</main>
|
||||
</div>
|
||||
|
||||
<footer className="reveal r3">
|
||||
<span>© 2026 Libby Labs Ltd.</span>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<p
|
||||
className="hero-kicker"
|
||||
style={{
|
||||
fontFamily: 'var(--display)',
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--cyan)',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
Documentation
|
||||
</p>
|
||||
<h1 className="page-title">Huskies Docs</h1>
|
||||
<p className="page-subtitle">
|
||||
Everything you need to set up and run huskies — a story-driven development pipeline that turns
|
||||
coding agents into a disciplined team.
|
||||
</p>
|
||||
|
||||
<div className="doc-cards">
|
||||
<Link className="doc-card" href="/docs/quickstart">
|
||||
<div className="doc-card-title">Quickstart</div>
|
||||
<div className="doc-card-desc">
|
||||
Install huskies, run the server, create your first story, and watch an agent implement it.
|
||||
</div>
|
||||
</Link>
|
||||
<Link className="doc-card" href="/docs/configuration">
|
||||
<div className="doc-card-title">Configuration</div>
|
||||
<div className="doc-card-desc">
|
||||
Reference for <code>project.toml</code>, <code>agents.toml</code>, and <code>bot.toml</code>.
|
||||
</div>
|
||||
</Link>
|
||||
<Link className="doc-card" href="/docs/commands">
|
||||
<div className="doc-card-title">Bot commands</div>
|
||||
<div className="doc-card-desc">
|
||||
Full list of commands available in Matrix, Slack, WhatsApp, and the web UI.
|
||||
</div>
|
||||
</Link>
|
||||
<Link className="doc-card" href="/docs/pipeline">
|
||||
<div className="doc-card-title">Pipeline stages</div>
|
||||
<div className="doc-card-desc">
|
||||
How work items move from backlog through QA and merge to done.
|
||||
</div>
|
||||
</Link>
|
||||
<Link className="doc-card" href="/docs/transports">
|
||||
<div className="doc-card-title">Chat transports</div>
|
||||
<div className="doc-card-desc">
|
||||
Connect huskies to Matrix, WhatsApp, Slack, Discord, or the built-in web UI.
|
||||
</div>
|
||||
</Link>
|
||||
<Link className="doc-card" href="/docs/cli">
|
||||
<div className="doc-card-title">CLI reference</div>
|
||||
<div className="doc-card-desc">
|
||||
Command-line flags for <code>huskies</code>, <code>huskies init</code>, and{' '}
|
||||
<code>huskies agent</code>.
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h2>What is huskies?</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
It ships as a single Rust binary with an embedded React frontend. No separate database or build
|
||||
infrastructure required.
|
||||
</p>
|
||||
|
||||
<h2>How it works</h2>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>
|
||||
<strong>Write a story.</strong> Describe the change with acceptance criteria via the web UI, a chat
|
||||
room (Matrix, WhatsApp, Slack), or by dropping a Markdown file in{' '}
|
||||
<code>.huskies/work/1_backlog/</code>.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Agent picks it up.</strong> Run <code>start <number></code> (or configure
|
||||
auto-start). A coder agent creates a feature branch, implements the code, and writes tests against
|
||||
your criteria.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Quality gates run.</strong> Linters, tests, and compilation checks run automatically when
|
||||
the agent exits. Nothing moves forward until everything passes.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>QA review.</strong> A QA agent verifies each acceptance criterion, runs your test suite,
|
||||
and either approves or rejects with detailed findings.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Merge & land.</strong> A merge agent resolves conflicts and squash-merges to your main
|
||||
branch. The worktree is cleaned up automatically.
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Key concepts</h2>
|
||||
<p>
|
||||
<strong>Stories</strong> are Markdown files with YAML front matter. They live in{' '}
|
||||
<code>.huskies/work/</code> and move through pipeline stages as work progresses.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Agents</strong> 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.
|
||||
</p>
|
||||
<p>
|
||||
<strong>MCP tools</strong> give Claude Code sessions programmatic access to the pipeline: creating
|
||||
stories, starting agents, checking status, recording test results.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">Pipeline Stages</h1>
|
||||
<p className="page-subtitle">
|
||||
Work items move through six stages from idea to archive. Each stage is a directory under{' '}
|
||||
<code>.huskies/work/</code>. Moving a file between directories advances the story.
|
||||
</p>
|
||||
|
||||
<div className="pipeline-stages">
|
||||
<div className="stage-row">
|
||||
<div className="stage-num">1</div>
|
||||
<div className="stage-name">Backlog</div>
|
||||
<div className="stage-desc">
|
||||
<code>1_backlog/</code> — New work items awaiting prioritisation. Stories sit here until
|
||||
you decide to start them.
|
||||
</div>
|
||||
</div>
|
||||
<div className="stage-row active">
|
||||
<div className="stage-num">2</div>
|
||||
<div className="stage-name">Current</div>
|
||||
<div className="stage-desc">
|
||||
<code>2_current/</code> — Work in progress. Run <code>start <number></code> to
|
||||
assign a coder agent. Multiple stories can be in current simultaneously (up to{' '}
|
||||
<code>max_coders</code>).
|
||||
</div>
|
||||
</div>
|
||||
<div className="stage-row">
|
||||
<div className="stage-num">3</div>
|
||||
<div className="stage-name">QA</div>
|
||||
<div className="stage-desc">
|
||||
<code>3_qa/</code> — 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.
|
||||
</div>
|
||||
</div>
|
||||
<div className="stage-row">
|
||||
<div className="stage-num">4</div>
|
||||
<div className="stage-name">Merge</div>
|
||||
<div className="stage-desc">
|
||||
<code>4_merge/</code> — Ready to merge. Stories reach here after QA approval. Run{' '}
|
||||
<code>start <number></code> to trigger the mergemaster agent, which squash-merges to your
|
||||
base branch.
|
||||
</div>
|
||||
</div>
|
||||
<div className="stage-row">
|
||||
<div className="stage-num">5</div>
|
||||
<div className="stage-name">Done</div>
|
||||
<div className="stage-desc">
|
||||
<code>5_done/</code> — Merged and complete. The mergemaster moves stories here after a
|
||||
successful merge. Auto-swept to archive after 4 hours.
|
||||
</div>
|
||||
</div>
|
||||
<div className="stage-row">
|
||||
<div className="stage-num">6</div>
|
||||
<div className="stage-name">Archived</div>
|
||||
<div className="stage-desc">
|
||||
<code>6_archived/</code> — Long-term storage. Stories land here automatically from done.
|
||||
Use <code>overview <number></code> to see the implementation summary for any archived
|
||||
story.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Work item types</h2>
|
||||
<p>
|
||||
All work item types move through the same pipeline. They differ in naming convention and workflow:
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Filename pattern</th>
|
||||
<th>When to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>story</td>
|
||||
<td>
|
||||
<code>42_story_add_login.md</code>
|
||||
</td>
|
||||
<td>New functionality. Requires acceptance criteria and tests.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bug</td>
|
||||
<td>
|
||||
<code>43_bug_login_crashes.md</code>
|
||||
</td>
|
||||
<td>Defect in existing functionality. Write a failing test first.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>spike</td>
|
||||
<td>
|
||||
<code>44_spike_auth_options.md</code>
|
||||
</td>
|
||||
<td>Time-boxed research to reduce uncertainty. No production code.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>refactor</td>
|
||||
<td>
|
||||
<code>45_refactor_extract_auth.md</code>
|
||||
</td>
|
||||
<td>Code quality improvement. Behaviour must not change.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Story file format</h2>
|
||||
<p>Every work item is a Markdown file with YAML front matter:</p>
|
||||
<pre>
|
||||
<code>{`---
|
||||
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`}</code>
|
||||
</pre>
|
||||
|
||||
<h2>Acceptance criteria tracking</h2>
|
||||
<p>
|
||||
Acceptance criteria use Markdown checkboxes (<code>- [ ]</code>). The QA agent reviews each criterion
|
||||
against the code diff and marks passing criteria as <code>- [x]</code> in the story file. Criteria
|
||||
that fail are noted in the QA report.
|
||||
</p>
|
||||
|
||||
<div className="note">
|
||||
<strong>Golden rule:</strong> 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.
|
||||
</div>
|
||||
|
||||
<h2>Filesystem watcher</h2>
|
||||
<p>
|
||||
The server watches <code>.huskies/work/</code> 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:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
You can drag a story between stage folders in your IDE and it advances automatically.
|
||||
</li>
|
||||
<li>MCP tools only need to write or move files — the watcher handles git commits.</li>
|
||||
<li>The pipeline board updates in real time without a manual refresh.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Blocked stories</h2>
|
||||
<p>
|
||||
A story is marked <strong>blocked</strong> when it fails the same pipeline stage more than{' '}
|
||||
<code>max_retries</code> times (default: 3). Blocked stories require manual intervention:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Run <code>status <number></code> to see the failure log.
|
||||
</li>
|
||||
<li>Fix the underlying issue (update the story, fix a build problem, etc.).</li>
|
||||
<li>
|
||||
Run <code>unblock <number></code> to reset the retry counter.
|
||||
</li>
|
||||
<li>
|
||||
Run <code>start <number></code> to try again.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Dependencies</h2>
|
||||
<p>
|
||||
Stories can declare dependencies using the <code>depends</code> command. A story with unresolved
|
||||
dependencies waits in <code>2_current/</code> until all its dependencies have reached{' '}
|
||||
<code>5_done/</code>.
|
||||
</p>
|
||||
<pre>
|
||||
<code>{`depends 45 42 43 # story 45 waits for 42 and 43 to finish\ndepends 45 # clear all dependencies`}</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">Quickstart</h1>
|
||||
<p className="page-subtitle">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Option A: Docker (recommended)</h2>
|
||||
<p>
|
||||
The easiest way to run huskies is with Docker Compose. This requires Docker and a Claude API key.
|
||||
</p>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>
|
||||
<strong>Get the compose file.</strong> Download <code>docker-compose.yml</code> from the{' '}
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies/releases">releases page</a> or copy it from
|
||||
the repository's <code>docker/</code> directory.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Set your API key.</strong> Create a <code>.env</code> file next to the compose file:
|
||||
<pre><code>ANTHROPIC_API_KEY=sk-ant-...</code></pre>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Mount your project.</strong> Edit the compose file to mount your project directory:
|
||||
<pre><code>{`volumes:\n - /path/to/your/project:/workspace`}</code></pre>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<strong>Start the server.</strong>
|
||||
<pre><code>docker compose up</code></pre>
|
||||
Open <a href="http://localhost:3000">http://localhost:3000</a> to see the pipeline board.
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Option B: Binary</h2>
|
||||
<p>
|
||||
Download the pre-built binary for your platform from the{' '}
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies/releases">releases page</a> and place it
|
||||
somewhere on your <code>PATH</code>.
|
||||
</p>
|
||||
|
||||
<h3>macOS (Apple Silicon)</h3>
|
||||
<pre><code>{`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`}</code></pre>
|
||||
|
||||
<h3>Linux (x86-64)</h3>
|
||||
<pre><code>{`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`}</code></pre>
|
||||
|
||||
<h2>Option C: Build from source</h2>
|
||||
<p>Requires Rust (stable), Node.js, and npm.</p>
|
||||
<pre><code>{`git clone https://code.crashlabs.io/crashlabs/huskies
|
||||
cd huskies
|
||||
cargo build --release
|
||||
# Binary is at target/release/huskies`}</code></pre>
|
||||
|
||||
<h2>Initialise your project</h2>
|
||||
<p>
|
||||
From your project directory, run the init command. This creates the <code>.huskies/</code> directory
|
||||
with the pipeline structure and configuration files.
|
||||
</p>
|
||||
<pre><code>{`cd /path/to/your/project\nhuskies init --port 3000`}</code></pre>
|
||||
<p>This creates:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>.huskies/project.toml</code> — project-wide settings
|
||||
</li>
|
||||
<li>
|
||||
<code>.huskies/agents.toml</code> — agent definitions (coder, QA, mergemaster)
|
||||
</li>
|
||||
<li>
|
||||
<code>.huskies/work/</code> — the 6-stage pipeline directory
|
||||
</li>
|
||||
<li>
|
||||
<code>.mcp.json</code> — MCP server config for Claude Code integration
|
||||
</li>
|
||||
<li>
|
||||
<code>.huskies/specs/</code> — placeholder spec files for your project context
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="note">
|
||||
<strong>Claude Code integration:</strong> The <code>.mcp.json</code> file automatically registers
|
||||
huskies' MCP tools with Claude Code. Open a Claude Code session in your project and it will
|
||||
discover tools like <code>create_story</code>, <code>start_agent</code>, and{' '}
|
||||
<code>get_pipeline_status</code> automatically.
|
||||
</div>
|
||||
|
||||
<h2>Start the server</h2>
|
||||
<pre><code>huskies --port 3000</code></pre>
|
||||
<p>
|
||||
Open <a href="http://localhost:3000">http://localhost:3000</a> to see the pipeline board, agent
|
||||
status, and chat interface.
|
||||
</p>
|
||||
|
||||
<h2>Run the setup wizard</h2>
|
||||
<p>
|
||||
Open a Claude Code session in your project directory (or use the web chat UI), and tell Claude:
|
||||
</p>
|
||||
<pre><code>help me set up this project with huskies</code></pre>
|
||||
<p>
|
||||
Claude will walk you through the setup wizard — generating project context (
|
||||
<code>specs/00_CONTEXT.md</code>), tech stack docs (<code>specs/tech/STACK.md</code>), and
|
||||
test/release scripts. Review each step and confirm or ask to retry.
|
||||
</p>
|
||||
|
||||
<h2>Create your first story</h2>
|
||||
<p>In the chat UI or via a chat transport, type:</p>
|
||||
<pre><code>I want to add a health check endpoint to the API</code></pre>
|
||||
<p>
|
||||
Claude will create a story file in <code>.huskies/work/1_backlog/</code> with a user story and
|
||||
acceptance criteria. Review it, then move it to current:
|
||||
</p>
|
||||
<pre><code>move <story-number> current</code></pre>
|
||||
|
||||
<h2>Start an agent</h2>
|
||||
<p>
|
||||
Once a story is in <code>2_current/</code>, start a coding agent:
|
||||
</p>
|
||||
<pre><code>start <story-number></code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Review and merge</h2>
|
||||
<p>Once QA passes, the story moves to <code>4_merge/</code>. To merge:</p>
|
||||
<pre><code>start <story-number></code></pre>
|
||||
<p>
|
||||
The mergemaster agent resolves any conflicts and squash-merges to your main branch. The worktree is
|
||||
cleaned up automatically.
|
||||
</p>
|
||||
|
||||
<div className="note">
|
||||
<strong>Tip:</strong> Use <code>status</code> in the chat at any time to see the current pipeline
|
||||
state, active agents, and their progress.
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
/** 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 (
|
||||
<>
|
||||
<h1 className="page-title">Chat Transports</h1>
|
||||
<p className="page-subtitle">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="note">
|
||||
<strong>Configuration:</strong> Copy the relevant example file to{' '}
|
||||
<code>.huskies/bot.toml</code> and fill in your credentials. The file is gitignored. Restart
|
||||
huskies after changes.
|
||||
</div>
|
||||
|
||||
<h2>Web UI</h2>
|
||||
<p>
|
||||
The built-in web interface is always available at <code>http://localhost:<port></code>. No
|
||||
configuration required. It provides:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Pipeline board showing all work items and their stages</li>
|
||||
<li>Agent status panel with live output streaming</li>
|
||||
<li>Chat interface for running commands and talking to Claude</li>
|
||||
<li>Coverage and cost dashboards</li>
|
||||
</ul>
|
||||
<p>
|
||||
No <code>bot.toml</code> is required for the web UI. If no transport is configured, huskies runs
|
||||
in web-only mode.
|
||||
</p>
|
||||
|
||||
<h2>Matrix</h2>
|
||||
<p>
|
||||
Matrix uses the Matrix Client-Server API with long-polling sync. No public webhook URL is required
|
||||
— the bot connects outbound to your homeserver.
|
||||
</p>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>Register a Matrix account for the bot on your homeserver (e.g. <code>@huskies:example.com</code>).</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Invite the bot account to the rooms you want it to monitor.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Copy the example config and fill in your credentials:
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.matrix.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>bot.toml fields</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>homeserver</td>
|
||||
<td>
|
||||
Your Matrix homeserver URL (e.g. <code>https://matrix.example.com</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>username</td>
|
||||
<td>
|
||||
Bot account Matrix ID (e.g. <code>@huskies:example.com</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>password</td>
|
||||
<td>Bot account password.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>room_ids</td>
|
||||
<td>
|
||||
List of room IDs to listen in (e.g. <code>{`["!roomid:example.com"]`}</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>allowed_users</td>
|
||||
<td>
|
||||
Matrix IDs allowed to interact. Empty list means nobody — always set this.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ambient_rooms</td>
|
||||
<td>
|
||||
Rooms where the bot responds to all messages (not just addressed ones). Updated automatically
|
||||
by <code>ambient on/off</code>.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Slack</h2>
|
||||
<p>
|
||||
Slack uses event subscriptions over a webhook. You'll need a public HTTPS URL pointing to your
|
||||
huskies server.
|
||||
</p>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>
|
||||
Create a Slack App at{' '}
|
||||
<a href="https://api.slack.com/apps">api.slack.com/apps</a>.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Add OAuth scopes: <code>chat:write</code>, <code>chat:update</code>.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Subscribe to bot events: <code>message.channels</code>, <code>message.groups</code>,{' '}
|
||||
<code>message.im</code>.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Install the app to your workspace and copy the bot token.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Set your webhook URL in Event Subscriptions:{' '}
|
||||
<code>https://your-server/webhook/slack</code>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Copy the example config:
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.slack.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>bot.toml fields</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>slack_bot_token</td>
|
||||
<td>
|
||||
OAuth bot token starting with <code>xoxb-</code>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>slack_signing_secret</td>
|
||||
<td>Signing secret from the app's Basic Information page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>slack_channel_ids</td>
|
||||
<td>
|
||||
List of channel IDs to listen in (e.g. <code>{`["C01ABCDEF"]`}</code>).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>WhatsApp (Meta Cloud API)</h2>
|
||||
<p>
|
||||
Connects huskies to WhatsApp Business via the Meta Cloud API. Requires a Meta Business account and
|
||||
a public webhook URL.
|
||||
</p>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>
|
||||
Create a Meta Business App at{' '}
|
||||
<a href="https://developers.facebook.com">developers.facebook.com</a>.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Add the WhatsApp product and get a Phone Number ID.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Generate a permanent access token.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Register your webhook URL in Meta's dashboard:{' '}
|
||||
<code>https://your-server/webhook/whatsapp</code>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Copy the example config:
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.whatsapp-meta.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>bot.toml fields</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>whatsapp_provider</td>
|
||||
<td>
|
||||
Set to <code>"meta"</code> for the Meta Cloud API.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whatsapp_phone_number_id</td>
|
||||
<td>Phone Number ID from the Meta dashboard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whatsapp_access_token</td>
|
||||
<td>Permanent access token.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whatsapp_verify_token</td>
|
||||
<td>
|
||||
Webhook verify token — must match what you set in Meta's dashboard.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whatsapp_allowed_phones</td>
|
||||
<td>
|
||||
Optional. List of phone numbers allowed to interact (e.g.{' '}
|
||||
<code>{`["+15551234567"]`}</code>). When absent, all numbers are allowed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whatsapp_notification_template</td>
|
||||
<td>
|
||||
Optional. Name of the approved Meta message template for out-of-window notifications
|
||||
(default: <code>"pipeline_notification"</code>).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>WhatsApp (Twilio)</h2>
|
||||
<p>
|
||||
An alternative WhatsApp integration using Twilio's WhatsApp API. Requires a Twilio account.
|
||||
</p>
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.whatsapp-twilio.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
<p>
|
||||
Set <code>whatsapp_provider = "twilio"</code> and fill in your Twilio account SID, auth
|
||||
token, and phone numbers. The webhook URL is the same:{' '}
|
||||
<code>https://your-server/webhook/whatsapp</code>.
|
||||
</p>
|
||||
|
||||
<h2>Discord</h2>
|
||||
<p>
|
||||
Connects huskies to Discord using the Discord Gateway WebSocket. No public webhook URL required
|
||||
— the bot connects outbound.
|
||||
</p>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<ol className="step-list">
|
||||
<li>
|
||||
<div>
|
||||
Create a Discord Application at{' '}
|
||||
<a href="https://discord.com/developers/applications">
|
||||
discord.com/developers/applications
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Go to Bot, create a bot, and copy the token.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Enable <strong>Message Content Intent</strong> under Privileged Gateway Intents.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Go to OAuth2 → URL Generator, select the <code>bot</code> scope with permissions: Send
|
||||
Messages, Read Message History, Manage Messages.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>Use the generated URL to invite the bot to your server.</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Right-click target channels → Copy Channel ID (requires Developer Mode enabled in Discord
|
||||
settings).
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Copy the example config:
|
||||
<pre>
|
||||
<code>cp .huskies/bot.toml.discord.example .huskies/bot.toml</code>
|
||||
</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>bot.toml fields</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>discord_bot_token</td>
|
||||
<td>Bot token from the Discord developer portal.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>discord_channel_ids</td>
|
||||
<td>
|
||||
List of channel IDs to listen in (e.g.{' '}
|
||||
<code>{`["123456789012345678"]`}</code>).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>discord_allowed_users</td>
|
||||
<td>
|
||||
Optional. Discord user IDs allowed to interact. When absent, all users in configured
|
||||
channels can interact.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="gateway-aggregated">Gateway: aggregated notifications</h2>
|
||||
<p>
|
||||
When using <code>huskies --gateway</code>, you can configure the gateway bot to receive
|
||||
notifications from <strong>all</strong> registered projects in a single room. Events are prefixed
|
||||
with <code>[project-name]</code>.
|
||||
</p>
|
||||
<p>
|
||||
No additional transport is required — the gateway aggregated stream works with any of the
|
||||
transports above. Configure the gateway's <code>.huskies/bot.toml</code> with your transport
|
||||
credentials and set <code>aggregated_notifications_enabled = true</code> (the default). See{' '}
|
||||
<Link href="/docs/configuration#gateway-aggregated-stream">
|
||||
Configuration → Gateway aggregated stream
|
||||
</Link>{' '}
|
||||
for the full reference.
|
||||
</p>
|
||||
<div className="note">
|
||||
<strong>No per-project changes needed:</strong> Adding a new project to{' '}
|
||||
<code>projects.toml</code> does not require editing per-project bot configs — the gateway
|
||||
picks it up automatically.
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
: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); }
|
||||
@@ -1,39 +0,0 @@
|
||||
/** 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 (
|
||||
<html lang="en">
|
||||
<body className={`${bricolage.variable} ${karla.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/** 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 (
|
||||
<div className="page">
|
||||
<header className="reveal r1">
|
||||
<a href="/" className="logo">huskies</a>
|
||||
<nav>
|
||||
<a href="#how">How it works</a>
|
||||
<a href="#features">Features</a>
|
||||
<div className="nav-dropdown">
|
||||
<a href="#" className="nav-cta nav-dropdown-toggle">Start</a>
|
||||
<div className="nav-dropdown-menu">
|
||||
<a href="/docs/">Docs</a>
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies">Source</a>
|
||||
<a href="https://code.crashlabs.io/crashlabs/huskies/releases">Releases</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero">
|
||||
<div className="hero-graphic reveal r1">
|
||||
<Image src="/husky.png" alt="" className="hero-husky" width={160} height={160} />
|
||||
</div>
|
||||
<p className="hero-kicker reveal r1">Story-driven development</p>
|
||||
<h1 className="reveal r2">
|
||||
Coding agents are huskies,<br />not <span className="glow">labradors.</span>
|
||||
</h1>
|
||||
<p className="hero-sub reveal r3">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="pipeline reveal r4">
|
||||
<div className="pipe-stage">
|
||||
<span className="pipe-dot"></span>
|
||||
<span className="pipe-label">Story</span>
|
||||
</div>
|
||||
<span className="pipe-line"></span>
|
||||
<div className="pipe-stage">
|
||||
<span className="pipe-dot active"></span>
|
||||
<span className="pipe-label">Implement</span>
|
||||
</div>
|
||||
<span className="pipe-line"></span>
|
||||
<div className="pipe-stage">
|
||||
<span className="pipe-dot"></span>
|
||||
<span className="pipe-label">QA</span>
|
||||
</div>
|
||||
<span className="pipe-line"></span>
|
||||
<div className="pipe-stage">
|
||||
<span className="pipe-dot"></span>
|
||||
<span className="pipe-label">Merge</span>
|
||||
</div>
|
||||
<span className="pipe-line"></span>
|
||||
<div className="pipe-stage">
|
||||
<span className="pipe-dot done"></span>
|
||||
<span className="pipe-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="how" className="how-section">
|
||||
<h2 className="section-title reveal r5">How it works</h2>
|
||||
<ol className="steps">
|
||||
<li className="step reveal r5">
|
||||
<span className="step-num">01</span>
|
||||
<div className="step-body">
|
||||
<h3>Write a story</h3>
|
||||
<p>Describe what you want with acceptance criteria. From your IDE, a chat room, or WhatsApp.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="step reveal r5">
|
||||
<span className="step-num">02</span>
|
||||
<div className="step-body">
|
||||
<h3>Agent picks it up</h3>
|
||||
<p>A coder agent creates a feature branch, implements the code, and writes tests against your criteria.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="step reveal r6">
|
||||
<span className="step-num">03</span>
|
||||
<div className="step-body">
|
||||
<h3>Quality gates run</h3>
|
||||
<p>Linters, tests, and compilation checks run automatically. Nothing moves forward until everything passes.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="step reveal r6">
|
||||
<span className="step-num">04</span>
|
||||
<div className="step-body">
|
||||
<h3>Merge & land</h3>
|
||||
<p>A merge agent resolves conflicts and squash-merges to your main branch. You review and accept.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section id="features" className="features-section">
|
||||
<h2 className="section-title reveal r7">Features</h2>
|
||||
<div className="feature-grid">
|
||||
<div className="feature reveal r7">
|
||||
<div className="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>The Harness</h3>
|
||||
<p>
|
||||
Stories define the change. Tests define the truth. Code defines the reality. Every agent runs on
|
||||
rails — nothing ships without acceptance criteria.
|
||||
</p>
|
||||
</div>
|
||||
<div className="feature reveal r7">
|
||||
<div className="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
<circle cx="18" cy="19" r="3" />
|
||||
<path d="M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>The Pack</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="feature reveal r8">
|
||||
<div className="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Chat Anywhere</h3>
|
||||
<p>
|
||||
Control the pipeline from Matrix, WhatsApp, Slack, or the built-in web UI. Create stories, start
|
||||
agents, check status.
|
||||
</p>
|
||||
</div>
|
||||
<div className="feature reveal r8">
|
||||
<div className="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>You're the Musher</h3>
|
||||
<p>
|
||||
Agents implement, test, and merge independently. You set the direction and approve what ships. Every
|
||||
story is traceable from request to release.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="cta-section reveal r8">
|
||||
<h2>Interested?</h2>
|
||||
<p>
|
||||
Huskies is built by <a href="https://crashlabs.io">Crash Labs</a>. Get in touch at{' '}
|
||||
<a href="mailto:hello@huskies.dev">hello@huskies.dev</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer className="reveal r8">
|
||||
<span>© 2026 Libby Labs Ltd.</span>
|
||||
<a href="mailto:hello@huskies.dev">Get in touch</a>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/** 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 (
|
||||
<div className="container">
|
||||
<header>
|
||||
<h1>
|
||||
<a href="/" style={{ color: 'inherit' }}>
|
||||
stor<span>kit</span>
|
||||
</a>
|
||||
</h1>
|
||||
<p className="tagline">Privacy Policy</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
<strong>Last updated:</strong> 25 March 2026
|
||||
</p>
|
||||
|
||||
<h2>Who we are</h2>
|
||||
<p>
|
||||
Huskies is operated by Libby Labs Ltd (“we”, “us”, “our”), trading
|
||||
as Crashlabs. Our contact email is{' '}
|
||||
<a href="mailto:hello@huskies.dev">hello@huskies.dev</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>What we collect</h2>
|
||||
<p>
|
||||
When you interact with Huskies via WhatsApp, Slack, Matrix, or the web interface, we may collect:
|
||||
</p>
|
||||
<p>
|
||||
<strong>Messaging data:</strong> 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.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Usage data:</strong> Basic server logs including timestamps and request metadata. We do not use
|
||||
analytics trackers on this website.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>How we use your data</h2>
|
||||
<p>We use your data only to provide and improve the Huskies service. Specifically:</p>
|
||||
<p>
|
||||
- To process commands and respond to your messages.<br />
|
||||
- To maintain conversation history within active sessions.<br />
|
||||
- To diagnose and fix technical issues.
|
||||
</p>
|
||||
<p>We do not sell, rent, or share your personal data with third parties for marketing purposes.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Third-party services</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Data retention</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Your rights</h2>
|
||||
<p>
|
||||
You may request access to, correction of, or deletion of your personal data at any time by contacting
|
||||
us at <a href="mailto:hello@huskies.dev">hello@huskies.dev</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Changes to this policy</h2>
|
||||
<p>
|
||||
We may update this policy from time to time. Changes will be posted on this page with an updated date.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
© 2026 Libby Labs Ltd. All rights reserved. · <a href="/">Home</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/** 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 (
|
||||
<aside className="sidebar reveal r2">
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-heading">Getting started</div>
|
||||
<nav>
|
||||
<Link href="/docs" className={pathname === '/docs' ? 'active' : ''}>
|
||||
Overview
|
||||
</Link>
|
||||
<Link href="/docs/quickstart" className={pathname === '/docs/quickstart' ? 'active' : ''}>
|
||||
Quickstart
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-heading">Reference</div>
|
||||
<nav>
|
||||
<Link href="/docs/configuration" className={pathname === '/docs/configuration' ? 'active' : ''}>
|
||||
Configuration
|
||||
</Link>
|
||||
<Link href="/docs/commands" className={pathname === '/docs/commands' ? 'active' : ''}>
|
||||
Bot commands
|
||||
</Link>
|
||||
<Link href="/docs/cli" className={pathname === '/docs/cli' ? 'active' : ''}>
|
||||
CLI
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-heading">Guides</div>
|
||||
<nav>
|
||||
<Link href="/docs/pipeline" className={pathname === '/docs/pipeline' ? 'active' : ''}>
|
||||
Pipeline stages
|
||||
</Link>
|
||||
<Link href="/docs/transports" className={pathname === '/docs/transports' ? 'active' : ''}>
|
||||
Chat transports
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/** Next.js configuration for the huskies static marketing site. */
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
Generated
-499
@@ -1,499 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -1,34 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||
<!-- Husky head - geometric/angular style -->
|
||||
<!-- Ears -->
|
||||
<path d="M60 85 L45 35 L85 65 Z" fill="#8892a8" stroke="#e8ecf4" stroke-width="1.5"/>
|
||||
<path d="M140 85 L155 35 L115 65 Z" fill="#8892a8" stroke="#e8ecf4" stroke-width="1.5"/>
|
||||
<!-- Inner ears -->
|
||||
<path d="M62 78 L52 45 L80 65 Z" fill="#4a5568"/>
|
||||
<path d="M138 78 L148 45 L120 65 Z" fill="#4a5568"/>
|
||||
<!-- Head shape -->
|
||||
<path d="M55 80 Q55 60 75 60 L125 60 Q145 60 145 80 L145 120 Q145 155 120 165 L110 170 Q100 175 90 170 L80 165 Q55 155 55 120 Z" fill="#8892a8" stroke="#e8ecf4" stroke-width="1.5"/>
|
||||
<!-- Face mask (white marking) -->
|
||||
<path d="M75 70 L100 65 L125 70 L120 110 L110 135 Q100 142 90 135 L80 110 Z" fill="#e8ecf4"/>
|
||||
<!-- Forehead stripe -->
|
||||
<path d="M92 65 L100 62 L108 65 L104 95 L100 100 L96 95 Z" fill="#8892a8"/>
|
||||
<!-- Eyes -->
|
||||
<ellipse cx="82" cy="95" rx="7" ry="7.5" fill="#080c15"/>
|
||||
<ellipse cx="118" cy="95" rx="7" ry="7.5" fill="#080c15"/>
|
||||
<!-- Eye shine - cyan to match brand -->
|
||||
<circle cx="80" cy="93" r="2.5" fill="#22d3ee"/>
|
||||
<circle cx="116" cy="93" r="2.5" fill="#22d3ee"/>
|
||||
<!-- Iris detail -->
|
||||
<ellipse cx="82" cy="95" rx="4" ry="4.5" fill="none" stroke="#22d3ee" stroke-width="0.5" opacity="0.4"/>
|
||||
<ellipse cx="118" cy="95" rx="4" ry="4.5" fill="none" stroke="#22d3ee" stroke-width="0.5" opacity="0.4"/>
|
||||
<!-- Nose -->
|
||||
<path d="M95 120 Q100 115 105 120 Q105 126 100 128 Q95 126 95 120 Z" fill="#080c15"/>
|
||||
<!-- Nose highlight -->
|
||||
<ellipse cx="100" cy="120" rx="2" ry="1" fill="#4a5568"/>
|
||||
<!-- Mouth -->
|
||||
<path d="M100 128 L100 135" stroke="#4a5568" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M92 138 Q100 143 108 138" stroke="#4a5568" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Cheek fur tufts -->
|
||||
<path d="M55 105 Q48 110 50 120" stroke="#e8ecf4" stroke-width="1" fill="none" opacity="0.5"/>
|
||||
<path d="M145 105 Q152 110 150 120" stroke="#e8ecf4" stroke-width="1" fill="none" opacity="0.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
Reference in New Issue
Block a user