refactor(HAI-116): rename kb to hai across all packages, CLI, and docs

- Rename npm packages from @kb/* to @hai/* and update all workspace references
- Rename CLI binary from kb to hai and config directory from .kb to .hai
- Update dashboard UI branding, titles, and references from kb to hai
- Update all test files, CI workflows, and documentation to reflect new naming
- Run comprehensive grep verification to ensure no stale kb references remain
This commit is contained in:
Dustin Byrne
2026-03-26 22:25:30 -04:00
parent 1c5eb494db
commit c802108a02
84 changed files with 792 additions and 1363 deletions

View File

@@ -1,14 +1,14 @@
--- ---
name: hai-board name: kb-board
description: Start and manage the hai dashboard web UI and AI engine. Use when asked to start the board, run the dashboard, enable the AI engine, or configure hai settings. description: Start and manage the kb dashboard web UI and AI engine. Use when asked to start the board, run the dashboard, enable the AI engine, or configure kb settings.
--- ---
# hai board # kb board
## Start the dashboard ## Start the dashboard
```bash ```bash
hai dashboard kb dashboard
``` ```
Opens the kanban board at http://localhost:4040 and starts the AI engine. Opens the kanban board at http://localhost:4040 and starts the AI engine.
@@ -30,7 +30,7 @@ Run in two terminals:
```bash ```bash
# Terminal 1: start the server + engine # Terminal 1: start the server + engine
hai dashboard kb dashboard
# Terminal 2: watch-rebuild the React dashboard UI # Terminal 2: watch-rebuild the React dashboard UI
pnpm dev:ui pnpm dev:ui
@@ -38,7 +38,7 @@ pnpm dev:ui
## Configuration ## Configuration
Settings are in `.hai/config.json`: Settings are in `.kb/config.json`:
```json ```json
{ {
@@ -61,13 +61,13 @@ Settings are in `.hai/config.json`:
## Task storage ## Task storage
Tasks live in `.hai/tasks/`: Tasks live in `.kb/tasks/`:
``` ```
.hai/ .kb/
├── config.json ├── config.json
└── tasks/ └── tasks/
└── HAI-001/ └── KB-001/
├── task.json # Metadata, steps, log ├── task.json # Metadata, steps, log
└── PROMPT.md # Task specification └── PROMPT.md # Task specification
``` ```

View File

@@ -1,11 +1,11 @@
--- ---
name: hai-task name: kb-task
description: Create, manage, and track tasks on the hai board. Use when asked to create a task, file a bug, report an issue, check task status, update progress, or interact with the hai task board in any way. description: Create, manage, and track tasks on the kb board. Use when asked to create a task, file a bug, report an issue, check task status, update progress, or interact with the kb task board in any way.
--- ---
# hai task # kb task
hai is an AI-orchestrated task board. Tasks flow through columns: kb is an AI-orchestrated task board. Tasks flow through columns:
**triage → todo → in-progress → in-review → done** **triage → todo → in-progress → in-review → done**
## Commands ## Commands
@@ -13,9 +13,9 @@ hai is an AI-orchestrated task board. Tasks flow through columns:
### Create a task ### Create a task
```bash ```bash
hai task create "description of what needs to be done" kb task create "description of what needs to be done"
hai task create "button is misaligned" --attach screenshot.png kb task create "button is misaligned" --attach screenshot.png
hai task create "server crash" --attach error.log --attach trace.txt kb task create "server crash" --attach error.log --attach trace.txt
``` ```
Creates a task in **triage**. The AI triage agent will specify it into a full Creates a task in **triage**. The AI triage agent will specify it into a full
@@ -25,7 +25,7 @@ move it to **todo**.
Options: Options:
- `--attach <file>` — attach files (images, logs, configs). Repeatable. - `--attach <file>` — attach files (images, logs, configs). Repeatable.
Images are sent to the triage agent for visual context. Images are sent to the triage agent for visual context.
Files are stored in `.hai/tasks/HAI-XXX/attachments/`. Files are stored in `.kb/tasks/KB-XXX/attachments/`.
Tips: Tips:
- Be descriptive — the triage agent uses this to write the spec - Be descriptive — the triage agent uses this to write the spec
@@ -37,7 +37,7 @@ Tips:
### List tasks ### List tasks
```bash ```bash
hai task list kb task list
``` ```
Shows all tasks grouped by column with IDs and descriptions. Shows all tasks grouped by column with IDs and descriptions.
@@ -45,7 +45,7 @@ Shows all tasks grouped by column with IDs and descriptions.
### Show task details ### Show task details
```bash ```bash
hai task show HAI-001 kb task show KB-001
``` ```
Shows full task info: steps, progress, log entries, dependencies. Shows full task info: steps, progress, log entries, dependencies.
@@ -53,7 +53,7 @@ Shows full task info: steps, progress, log entries, dependencies.
### Move a task ### Move a task
```bash ```bash
hai task move HAI-001 <column> kb task move KB-001 <column>
``` ```
Columns: `triage`, `todo`, `in-progress`, `in-review`, `done` Columns: `triage`, `todo`, `in-progress`, `in-review`, `done`
@@ -68,7 +68,7 @@ Transitions are validated:
### Update step status ### Update step status
```bash ```bash
hai task update HAI-001 <step-number> <status> kb task update KB-001 <step-number> <status>
``` ```
Status: `pending`, `in-progress`, `done`, `skipped` Status: `pending`, `in-progress`, `done`, `skipped`
@@ -78,7 +78,7 @@ Steps are 0-indexed and auto-parsed from the PROMPT.md headings.
### Log an entry ### Log an entry
```bash ```bash
hai task log HAI-001 "what happened" kb task log KB-001 "what happened"
``` ```
Adds a timestamped log entry visible on the task card. Adds a timestamped log entry visible on the task card.
@@ -86,7 +86,7 @@ Adds a timestamped log entry visible on the task card.
### Merge a completed task ### Merge a completed task
```bash ```bash
hai task merge HAI-001 kb task merge KB-001
``` ```
Squash-merges the task's branch into main with an AI-written commit message. Squash-merges the task's branch into main with an AI-written commit message.
@@ -95,12 +95,12 @@ Cleans up the worktree and branch after merge.
## Workflow ## Workflow
1. **Create**`hai task create "description"` → goes to triage 1. **Create**`kb task create "description"` → goes to triage
2. **Triage** — AI agent reads the codebase, writes a PROMPT.md spec, moves to todo 2. **Triage** — AI agent reads the codebase, writes a PROMPT.md spec, moves to todo
3. **Schedule** — Scheduler moves to in-progress when deps are met and concurrency allows 3. **Schedule** — Scheduler moves to in-progress when deps are met and concurrency allows
4. **Execute** — AI agent works the task in a git worktree, reports progress via tools 4. **Execute** — AI agent works the task in a git worktree, reports progress via tools
5. **Review** — Cross-model reviewer checks plan/code at step boundaries 5. **Review** — Cross-model reviewer checks plan/code at step boundaries
6. **Merge**`hai task merge HAI-001` squash-merges to main 6. **Merge**`kb task merge KB-001` squash-merges to main
## Filing good tasks ## Filing good tasks
@@ -108,14 +108,14 @@ A task can be anything from a rough idea to a detailed spec:
```bash ```bash
# Rough — triage agent will flesh it out # Rough — triage agent will flesh it out
hai task create "the login page is slow" kb task create "the login page is slow"
# Specific — triage agent will structure it # Specific — triage agent will structure it
hai task create "Add rate limiting to POST /api/tasks. Use a token bucket algorithm with 100 req/min per IP. Return 429 with Retry-After header when exceeded." kb task create "Add rate limiting to POST /api/tasks. Use a token bucket algorithm with 100 req/min per IP. Return 429 with Retry-After header when exceeded."
# Bug report with screenshot # Bug report with screenshot
hai task create "button is misaligned on mobile" --attach screenshot.png kb task create "button is misaligned on mobile" --attach screenshot.png
# Bug report with logs # Bug report with logs
hai task create "server crashes on startup" --attach crash.log kb task create "server crashes on startup" --attach crash.log
``` ```

View File

@@ -1,5 +1,5 @@
--- ---
"hai": minor "@dustinbyrne/kb": minor
--- ---
Add automated versioning pipeline using changesets. Developers now add changeset files to describe changes, and a CI workflow automatically opens version PRs that bump versions and generate changelogs. Add automated versioning pipeline using changesets. Developers now add changeset files to describe changes, and a CI workflow automatically opens version PRs that bump versions and generate changelogs.

View File

@@ -37,7 +37,7 @@ jobs:
run: pnpm test run: pnpm test
- name: Build standalone binary - name: Build standalone binary
run: pnpm --filter hai build:exe run: pnpm --filter @dustinbyrne/kb build:exe
- name: Verify binary exists - name: Verify binary exists
run: test -f packages/cli/dist/hai run: test -f packages/cli/dist/kb

View File

@@ -29,16 +29,16 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
target: bun-linux-x64 target: bun-linux-x64
binary: hai-linux-x64 binary: kb-linux-x64
- os: macos-latest - os: macos-latest
target: bun-darwin-arm64 target: bun-darwin-arm64
binary: hai-darwin-arm64 binary: kb-darwin-arm64
- os: macos-13 - os: macos-13
target: bun-darwin-x64 target: bun-darwin-x64
binary: hai-darwin-x64 binary: kb-darwin-x64
- os: windows-latest - os: windows-latest
target: bun-windows-x64 target: bun-windows-x64
binary: hai-windows-x64.exe binary: kb-windows-x64.exe
steps: steps:
- name: Checkout - name: Checkout
@@ -63,7 +63,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Build binary - name: Build binary
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }} run: pnpm --filter @dustinbyrne/kb build:exe -- --target ${{ matrix.target }}
- name: Verify binary exists - name: Verify binary exists
shell: bash shell: bash
@@ -132,7 +132,7 @@ jobs:
- name: Collect release files - name: Collect release files
run: | run: |
mkdir release-files mkdir release-files
find artifacts -type f \( -name "hai-*" -o -name "*.sha256" \) -exec cp {} release-files/ \; find artifacts -type f \( -name "kb-*" -o -name "*.sha256" \) -exec cp {} release-files/ \;
ls -la release-files/ ls -la release-files/
- name: Create GitHub Release - name: Create GitHub Release

View File

@@ -19,16 +19,16 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
target: bun-linux-x64 target: bun-linux-x64
binary: hai-linux-x64 binary: kb-linux-x64
- os: macos-latest - os: macos-latest
target: bun-darwin-arm64 target: bun-darwin-arm64
binary: hai-darwin-arm64 binary: kb-darwin-arm64
- os: macos-13 - os: macos-13
target: bun-darwin-x64 target: bun-darwin-x64
binary: hai-darwin-x64 binary: kb-darwin-x64
- os: windows-latest - os: windows-latest
target: bun-windows-x64 target: bun-windows-x64
binary: hai-windows-x64.exe binary: kb-windows-x64.exe
steps: steps:
- name: Checkout - name: Checkout
@@ -53,7 +53,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Build binary - name: Build binary
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }} run: pnpm --filter @dustinbyrne/kb build:exe -- --target ${{ matrix.target }}
- name: Verify binary exists - name: Verify binary exists
shell: bash shell: bash
@@ -132,7 +132,7 @@ jobs:
- name: Combine artifacts - name: Combine artifacts
run: | run: |
mkdir combined mkdir combined
find artifacts -type f \( -name "hai-*" -o -name "*.sha256" \) -exec cp {} combined/ \; find artifacts -type f \( -name "kb-*" -o -name "*.sha256" \) -exec cp {} combined/ \;
ls -la combined/ ls -la combined/
- name: Upload combined archive - name: Upload combined archive

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ dist/
*.tsbuildinfo *.tsbuildinfo
# hai runtime (local board state — don't commit) # hai runtime (local board state — don't commit)
.kb/
.hai/ .hai/
# Taskplane runtime artifacts # Taskplane runtime artifacts

View File

@@ -1,4 +1,4 @@
# hai # kb
AI-orchestrated task board. Like Trello, but your tasks get specified, executed, and delivered by AI — powered by [pi](https://github.com/badlogic/pi-mono). AI-orchestrated task board. Like Trello, but your tasks get specified, executed, and delivered by AI — powered by [pi](https://github.com/badlogic/pi-mono).
@@ -39,16 +39,16 @@ pnpm dev task create "Fix the login redirect bug"
pnpm dev task list pnpm dev task list
# Move a task # Move a task
pnpm dev task move HAI-001 todo pnpm dev task move KB-001 todo
# Pause a task (stops all automation) # Pause a task (stops all automation)
pnpm dev task pause HAI-001 pnpm dev task pause KB-001
# Unpause a task (resumes automation) # Unpause a task (resumes automation)
pnpm dev task unpause HAI-001 pnpm dev task unpause KB-001
# Attach a file to a task (images, logs, configs) # Attach a file to a task (images, logs, configs)
pnpm dev task attach HAI-001 ./screenshot.png pnpm dev task attach KB-001 ./screenshot.png
# Create a task with attachments # Create a task with attachments
pnpm dev task create "Fix the login bug" -- --attach screenshot.png --attach error.log pnpm dev task create "Fix the login bug" -- --attach screenshot.png --attach error.log
@@ -63,28 +63,28 @@ The AI engine uses [pi](https://github.com/badlogic/pi-mono) agent sessions unde
1. **pi installed:** `npm install -g @mariozechner/pi-coding-agent` 1. **pi installed:** `npm install -g @mariozechner/pi-coding-agent`
2. **API key configured:** Run `pi` and use `/login` or set `ANTHROPIC_API_KEY` 2. **API key configured:** Run `pi` and use `/login` or set `ANTHROPIC_API_KEY`
hai reuses your existing pi authentication — no separate setup needed. kb reuses your existing pi authentication — no separate setup needed.
## Packages ## Packages
| Package | Description | | Package | Description |
|---------|-------------| |---------|-------------|
| `@hai/core` | Domain model — tasks, board columns, file-based store | | `@kb/core` | Domain model — tasks, board columns, file-based store |
| `@hai/dashboard` | Web UI — Express server + kanban board with SSE | | `@kb/dashboard` | Web UI — Express server + kanban board with SSE |
| `@hai/engine` | AI engine — triage (pi), execution (pi + worktrees), scheduling | | `@kb/engine` | AI engine — triage (pi), execution (pi + worktrees), scheduling |
| `hai` (cli) | CLI — `hai dashboard`, `hai task create/list/move/attach` | | `kb` (cli) | CLI — `kb dashboard`, `kb task create/list/move/attach` |
## Architecture ## Architecture
### Task Storage ### Task Storage
Tasks live on disk in `.hai/tasks/` in the project root: Tasks live on disk in `.kb/tasks/` in the project root:
``` ```
.hai/ .kb/
├── config.json # Board config + ID counter ├── config.json # Board config + ID counter
└── tasks/ └── tasks/
└── HAI-001/ └── KB-001/
├── task.json # Metadata (column, deps, timestamps) ├── task.json # Metadata (column, deps, timestamps)
├── PROMPT.md # Task specification ├── PROMPT.md # Task specification
└── attachments/ # File attachments — images & text files (optional) └── attachments/ # File attachments — images & text files (optional)
@@ -144,7 +144,7 @@ pnpm dev task list # CLI commands
## Building a standalone executable ## Building a standalone executable
You can build a single self-contained `hai` binary using [Bun](https://bun.sh/): You can build a single self-contained `kb` binary using [Bun](https://bun.sh/):
```bash ```bash
pnpm build:exe pnpm build:exe
@@ -152,18 +152,18 @@ pnpm build:exe
This compiles all TypeScript, builds the dashboard client, and produces: This compiles all TypeScript, builds the dashboard client, and produces:
- `packages/cli/dist/hai` — the standalone binary - `packages/cli/dist/kb` — the standalone binary
- `packages/cli/dist/client/` — co-located dashboard assets - `packages/cli/dist/client/` — co-located dashboard assets
Run the binary directly — no Node.js, pnpm, or workspace setup needed: Run the binary directly — no Node.js, pnpm, or workspace setup needed:
```bash ```bash
./packages/cli/dist/hai --help ./packages/cli/dist/kb --help
./packages/cli/dist/hai task list ./packages/cli/dist/kb task list
./packages/cli/dist/hai dashboard ./packages/cli/dist/kb dashboard
``` ```
To distribute, copy both the `hai` binary and the `client/` directory together. To distribute, copy both the `kb` binary and the `client/` directory together.
### Cross-compilation ### Cross-compilation
@@ -177,24 +177,24 @@ This produces binaries for all supported targets in `packages/cli/dist/`:
| Target | Output | | Target | Output |
|--------|--------| |--------|--------|
| `bun-linux-x64` | `hai-linux-x64` | | `bun-linux-x64` | `kb-linux-x64` |
| `bun-linux-arm64` | `hai-linux-arm64` | | `bun-linux-arm64` | `kb-linux-arm64` |
| `bun-darwin-x64` | `hai-darwin-x64` | | `bun-darwin-x64` | `kb-darwin-x64` |
| `bun-darwin-arm64` | `hai-darwin-arm64` | | `bun-darwin-arm64` | `kb-darwin-arm64` |
| `bun-windows-x64` | `hai-windows-x64.exe` | | `bun-windows-x64` | `kb-windows-x64.exe` |
To build for a specific platform: To build for a specific platform:
```bash ```bash
pnpm --filter hai build:exe -- --target bun-linux-x64 pnpm --filter kb build:exe -- --target bun-linux-x64
``` ```
The `client/` directory is shared across all binaries (platform-independent assets). The `client/` directory is shared across all binaries (platform-independent assets).
You can override the dashboard asset path via the `HAI_CLIENT_DIR` environment variable: You can override the dashboard asset path via the `KB_CLIENT_DIR` environment variable:
```bash ```bash
HAI_CLIENT_DIR=/path/to/client ./hai dashboard KB_CLIENT_DIR=/path/to/client ./kb dashboard
``` ```
**Prerequisites:** Bun ≥ 1.0 (`bun --version`) **Prerequisites:** Bun ≥ 1.0 (`bun --version`)
@@ -206,7 +206,7 @@ Packages are published to npm automatically via GitHub Actions and [changesets](
### Installing from npm ### Installing from npm
```bash ```bash
npm install -g hai npm install -g kb
``` ```
### Triggering a release ### Triggering a release

View File

@@ -36,7 +36,7 @@ When you merge the Version Packages PR:
- The `version.yml` workflow detects that all changesets have been consumed - The `version.yml` workflow detects that all changesets have been consumed
- It builds all packages and publishes them to **npm** with provenance attestation - It builds all packages and publishes them to **npm** with provenance attestation
- It creates a git tag `v{version}` based on the `hai` CLI package version - It creates a git tag `v{version}` based on the `kb` CLI package version
- The tag push triggers `release.yml`, which: - The tag push triggers `release.yml`, which:
- Builds platform-specific binaries for Linux x64, macOS x64, macOS arm64, and Windows x64 - Builds platform-specific binaries for Linux x64, macOS x64, macOS arm64, and Windows x64
- Signs macOS binaries (codesign + notarization) and Windows binaries (Authenticode) - Signs macOS binaries (codesign + notarization) and Windows binaries (Authenticode)
@@ -54,10 +54,10 @@ When you merge the Version Packages PR:
| Platform | Binary name | Signed | | Platform | Binary name | Signed |
|----------|------------|--------| |----------|------------|--------|
| Linux x64 | `hai-linux-x64` | — | | Linux x64 | `kb-linux-x64` | — |
| macOS arm64 | `hai-darwin-arm64` | ✓ (codesign + notarization) | | macOS arm64 | `kb-darwin-arm64` | ✓ (codesign + notarization) |
| macOS x64 | `hai-darwin-x64` | ✓ (codesign + notarization) | | macOS x64 | `kb-darwin-x64` | ✓ (codesign + notarization) |
| Windows x64 | `hai-windows-x64.exe` | ✓ (Authenticode) | | Windows x64 | `kb-windows-x64.exe` | ✓ (Authenticode) |
## Testing binary builds ## Testing binary builds
@@ -85,9 +85,9 @@ This will trigger `release.yml` to build binaries and create a GitHub Release. N
| `pnpm changeset` | Add a new changeset | | `pnpm changeset` | Add a new changeset |
| `pnpm changeset status` | Check pending changesets | | `pnpm changeset status` | Check pending changesets |
| `pnpm release:version` | Apply changesets and bump versions (used by CI) | | `pnpm release:version` | Apply changesets and bump versions (used by CI) |
| `pnpm --filter hai build:exe` | Build binary for current platform | | `pnpm --filter @dustinbyrne/kb build:exe` | Build binary for current platform |
| `pnpm --filter hai build:exe -- --target <target>` | Cross-compile for a specific platform | | `pnpm --filter @dustinbyrne/kb build:exe -- --target <target>` | Cross-compile for a specific platform |
| `pnpm --filter hai build:exe:all` | Build binaries for all platforms | | `pnpm --filter @dustinbyrne/kb build:exe:all` | Build binaries for all platforms |
## Tips ## Tips

View File

@@ -1,6 +1,6 @@
# Code Signing Setup Guide # Code Signing Setup Guide
This document explains how to configure code signing for `hai` release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen). This document explains how to configure code signing for `kb` release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen).
## Overview ## Overview
@@ -56,7 +56,7 @@ Your Team ID is visible at [developer.apple.com/account](https://developer.apple
1. Go to [appleid.apple.com](https://appleid.apple.com/) 1. Go to [appleid.apple.com](https://appleid.apple.com/)
2. Sign in and navigate to **Sign-In and Security****App-Specific Passwords** 2. Sign in and navigate to **Sign-In and Security****App-Specific Passwords**
3. Generate a new password and label it (e.g., "hai notarization") 3. Generate a new password and label it (e.g., "kb notarization")
4. Use this as the `APPLE_APP_PASSWORD` secret 4. Use this as the `APPLE_APP_PASSWORD` secret
### 5. Determine Your Signing Identity ### 5. Determine Your Signing Identity

572
package-lock.json generated
View File

@@ -1,572 +0,0 @@
{
"name": "hai-workspace",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hai-workspace",
"version": "0.0.0",
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.4",
"@esbuild/android-arm": "0.27.4",
"@esbuild/android-arm64": "0.27.4",
"@esbuild/android-x64": "0.27.4",
"@esbuild/darwin-arm64": "0.27.4",
"@esbuild/darwin-x64": "0.27.4",
"@esbuild/freebsd-arm64": "0.27.4",
"@esbuild/freebsd-x64": "0.27.4",
"@esbuild/linux-arm": "0.27.4",
"@esbuild/linux-arm64": "0.27.4",
"@esbuild/linux-ia32": "0.27.4",
"@esbuild/linux-loong64": "0.27.4",
"@esbuild/linux-mips64el": "0.27.4",
"@esbuild/linux-ppc64": "0.27.4",
"@esbuild/linux-riscv64": "0.27.4",
"@esbuild/linux-s390x": "0.27.4",
"@esbuild/linux-x64": "0.27.4",
"@esbuild/netbsd-arm64": "0.27.4",
"@esbuild/netbsd-x64": "0.27.4",
"@esbuild/openbsd-arm64": "0.27.4",
"@esbuild/openbsd-x64": "0.27.4",
"@esbuild/openharmony-arm64": "0.27.4",
"@esbuild/sunos-x64": "0.27.4",
"@esbuild/win32-arm64": "0.27.4",
"@esbuild/win32-ia32": "0.27.4",
"@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.7",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"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"
}
}
}
}

View File

@@ -1,15 +1,15 @@
{ {
"name": "hai-workspace", "name": "kb-workspace",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@10.33.0", "packageManager": "pnpm@10.33.0",
"scripts": { "scripts": {
"dev": "tsx packages/cli/src/bin.ts", "dev": "tsx packages/cli/src/bin.ts",
"dev:ui": "pnpm --filter @hai/dashboard dev", "dev:ui": "pnpm --filter @kb/dashboard dev",
"build": "pnpm -r build", "build": "pnpm -r build",
"build:exe": "pnpm build && pnpm --filter hai build:exe", "build:exe": "pnpm build && pnpm --filter @dustinbyrne/kb build:exe",
"build:exe:all": "pnpm build && pnpm --filter hai build:exe:all", "build:exe:all": "pnpm build && pnpm --filter @dustinbyrne/kb build:exe:all",
"test": "pnpm -r test", "test": "pnpm -r test",
"typecheck": "pnpm -r typecheck", "typecheck": "pnpm -r typecheck",
"changeset": "changeset", "changeset": "changeset",

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bun #!/usr/bin/env bun
/** /**
* Bun compile build script for the `hai` CLI. * Bun compile build script for the `kb` CLI.
* *
* Produces a single self-contained executable at packages/cli/dist/hai * Produces a single self-contained executable at packages/cli/dist/kb
* with the dashboard client assets co-located at packages/cli/dist/client/. * with the dashboard client assets co-located at packages/cli/dist/client/.
* *
* Usage: * Usage:
@@ -38,20 +38,20 @@ type BunTarget = (typeof SUPPORTED_TARGETS)[number];
/** /**
* Map a Bun target identifier to the output binary name. * Map a Bun target identifier to the output binary name.
* e.g. "bun-linux-x64" → "hai-linux-x64", "bun-windows-x64" → "hai-windows-x64.exe" * e.g. "bun-linux-x64" → "kb-linux-x64", "bun-windows-x64" → "kb-windows-x64.exe"
*/ */
function binaryNameForTarget(target: BunTarget): string { function binaryNameForTarget(target: BunTarget): string {
// "bun-linux-x64" → "linux-x64" // "bun-linux-x64" → "linux-x64"
const suffix = target.replace(/^bun-/, ""); const suffix = target.replace(/^bun-/, "");
const isWindows = target.includes("windows"); const isWindows = target.includes("windows");
return `hai-${suffix}${isWindows ? ".exe" : ""}`; return `kb-${suffix}${isWindows ? ".exe" : ""}`;
} }
/** /**
* Determine the default binary name for the current platform (no cross-compile). * Determine the default binary name for the current platform (no cross-compile).
*/ */
function defaultBinaryName(): string { function defaultBinaryName(): string {
return process.platform === "win32" ? "hai.exe" : "hai"; return process.platform === "win32" ? "kb.exe" : "kb";
} }
// ── Parse CLI arguments ─────────────────────────────────────────────── // ── Parse CLI arguments ───────────────────────────────────────────────
@@ -146,7 +146,7 @@ const { targets } = parseArgs();
copyClientAssets(); copyClientAssets();
if (targets === null) { if (targets === null) {
// Default: build for current platform → dist/hai // Default: build for current platform → dist/kb
const outBinary = join(outDir, defaultBinaryName()); const outBinary = join(outDir, defaultBinaryName());
const ok = compileBinary(outBinary, "bun"); const ok = compileBinary(outBinary, "bun");
if (!ok) process.exit(1); if (!ok) process.exit(1);

View File

@@ -1,9 +1,9 @@
{ {
"name": "@dustinbyrne/hai", "name": "@dustinbyrne/kb",
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"bin": { "bin": {
"hai": "./dist/bin.js" "kb": "./dist/bin.js"
}, },
"files": [ "files": [
"dist", "dist",
@@ -18,9 +18,9 @@
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@hai/core": "workspace:*", "@kb/core": "workspace:*",
"@hai/dashboard": "workspace:*", "@kb/dashboard": "workspace:*",
"@hai/engine": "workspace:*", "@kb/engine": "workspace:*",
"@mariozechner/pi-coding-agent": "^0.62.0" "@mariozechner/pi-coding-agent": "^0.62.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -21,7 +21,7 @@ const SUPPORTED_TARGETS = [
function expectedBinaryName(target: string): string { function expectedBinaryName(target: string): string {
const suffix = target.replace(/^bun-/, ""); const suffix = target.replace(/^bun-/, "");
const isWindows = target.includes("windows"); const isWindows = target.includes("windows");
return `hai-${suffix}${isWindows ? ".exe" : ""}`; return `kb-${suffix}${isWindows ? ".exe" : ""}`;
} }
/** /**
@@ -44,8 +44,8 @@ describe("build-exe-cross: single target", () => {
}); });
}, 180_000); }, 180_000);
it("produces dist/hai-linux-x64", () => { it("produces dist/kb-linux-x64", () => {
const bin = join(distDir, "hai-linux-x64"); const bin = join(distDir, "kb-linux-x64");
expect(existsSync(bin)).toBe(true); expect(existsSync(bin)).toBe(true);
expect(statSync(bin).size).toBeGreaterThan(0); expect(statSync(bin).size).toBeGreaterThan(0);
}); });
@@ -64,8 +64,8 @@ describe("build-exe-cross: windows target has .exe extension", () => {
}); });
}, 180_000); }, 180_000);
it("produces dist/hai-windows-x64.exe", () => { it("produces dist/kb-windows-x64.exe", () => {
const bin = join(distDir, "hai-windows-x64.exe"); const bin = join(distDir, "kb-windows-x64.exe");
expect(existsSync(bin)).toBe(true); expect(existsSync(bin)).toBe(true);
expect(statSync(bin).size).toBeGreaterThan(0); expect(statSync(bin).size).toBeGreaterThan(0);
}); });
@@ -108,7 +108,7 @@ describe("build-exe-cross: --all builds all platforms", () => {
timeout: 15_000, timeout: 15_000,
}); });
expect(result.status).toBe(0); expect(result.status).toBe(0);
expect(result.stdout).toContain("hai"); expect(result.stdout).toContain("kb");
}); });
}); });
@@ -121,8 +121,8 @@ describe("build-exe-cross: default (no args) backward compatibility", () => {
}); });
}, 180_000); }, 180_000);
it("produces dist/hai (no platform suffix)", () => { it("produces dist/kb (no platform suffix)", () => {
const defaultName = process.platform === "win32" ? "hai.exe" : "hai"; const defaultName = process.platform === "win32" ? "kb.exe" : "kb";
const bin = join(distDir, defaultName); const bin = join(distDir, defaultName);
expect(existsSync(bin)).toBe(true); expect(existsSync(bin)).toBe(true);
expect(statSync(bin).size).toBeGreaterThan(0); expect(statSync(bin).size).toBeGreaterThan(0);

View File

@@ -6,8 +6,8 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
const cliRoot = join(import.meta.dirname!, "..", ".."); const cliRoot = join(import.meta.dirname!, "..", "..");
const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "hai.exe" : "hai"); const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "kb.exe" : "kb");
const binaryName = process.platform === "win32" ? "hai.exe" : "hai"; const binaryName = process.platform === "win32" ? "kb.exe" : "kb";
const clientDir = join(cliRoot, "dist", "client"); const clientDir = join(cliRoot, "dist", "client");
/** /**
@@ -15,7 +15,7 @@ const clientDir = join(cliRoot, "dist", "client");
* assets — no package.json. Returns the dir path and a cleanup function. * assets — no package.json. Returns the dir path and a cleanup function.
*/ */
function createIsolatedDir(): { dir: string; binary: string; cleanup: () => void } { function createIsolatedDir(): { dir: string; binary: string; cleanup: () => void } {
const dir = mkdtempSync(join(tmpdir(), "hai-iso-")); const dir = mkdtempSync(join(tmpdir(), "kb-iso-"));
cpSync(outBinary, join(dir, binaryName), { recursive: true }); cpSync(outBinary, join(dir, binaryName), { recursive: true });
cpSync(clientDir, join(dir, "client"), { recursive: true }); cpSync(clientDir, join(dir, "client"), { recursive: true });
return { return {
@@ -60,7 +60,7 @@ describe("build-exe", () => {
timeout: 15_000, timeout: 15_000,
}); });
expect(result.status).toBe(0); expect(result.status).toBe(0);
expect(result.stdout).toContain("hai — AI-orchestrated task board"); expect(result.stdout).toContain("kb — AI-orchestrated task board");
expect(result.stdout).toContain("dashboard"); expect(result.stdout).toContain("dashboard");
expect(result.stdout).toContain("task create"); expect(result.stdout).toContain("task create");
expect(result.stdout).toContain("task list"); expect(result.stdout).toContain("task list");
@@ -110,7 +110,7 @@ describe("build-exe", () => {
resolve(out); resolve(out);
}); });
}); });
expect(output).toContain("hai board"); expect(output).toContain("kb board");
} finally { } finally {
cleanup(); cleanup();
} }

View File

@@ -51,7 +51,7 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
}); });
it("verifies binary exists after build", () => { it("verifies binary exists after build", () => {
expect(content).toContain("test -f packages/cli/dist/hai"); expect(content).toContain("test -f packages/cli/dist/kb");
}); });
it("includes pnpm test step", () => { it("includes pnpm test step", () => {

View File

@@ -19,9 +19,9 @@ function loadWorkflowYaml(name: string): any {
describe("CLI package.json publishing config", () => { describe("CLI package.json publishing config", () => {
const pkg = loadPackageJson("cli"); const pkg = loadPackageJson("cli");
it('has "bin" field with hai pointing to ./dist/bin.js', () => { it('has "bin" field with kb pointing to ./dist/bin.js', () => {
expect(pkg.bin).toBeDefined(); expect(pkg.bin).toBeDefined();
expect(pkg.bin.hai).toBe("./dist/bin.js"); expect(pkg.bin.kb).toBe("./dist/bin.js");
}); });
it('has "files" array that includes "dist"', () => { it('has "files" array that includes "dist"', () => {
@@ -35,11 +35,11 @@ describe("CLI package.json publishing config", () => {
}); });
}); });
describe("Scoped @hai/* packages publishing config", () => { describe("Scoped @kb/* packages publishing config", () => {
const scopedPackages = ["core", "engine", "dashboard"]; const scopedPackages = ["core", "engine", "dashboard"];
for (const name of scopedPackages) { for (const name of scopedPackages) {
describe(`@hai/${name}`, () => { describe(`@kb/${name}`, () => {
const pkg = loadPackageJson(name); const pkg = loadPackageJson(name);
it('has publishConfig with access "public"', () => { it('has publishConfig with access "public"', () => {

View File

@@ -24,11 +24,11 @@ if (isBunBinary) {
if (!existsSync(localPkg)) { if (!existsSync(localPkg)) {
// Write a minimal package.json to a temp dir and redirect PI_PACKAGE_DIR // Write a minimal package.json to a temp dir and redirect PI_PACKAGE_DIR
const tmp = mkdtempSync(join(tmpdir(), "hai-pkg-")); const tmp = mkdtempSync(join(tmpdir(), "kb-pkg-"));
writeFileSync( writeFileSync(
join(tmp, "package.json"), join(tmp, "package.json"),
JSON.stringify( JSON.stringify(
{ name: "hai", version: "0.1.0", type: "module", piConfig: { name: "hai", configDir: ".hai" } }, { name: "kb", version: "0.1.0", type: "module", piConfig: { name: "kb", configDir: ".kb" } },
null, null,
2, 2,
) + "\n", ) + "\n",
@@ -42,20 +42,20 @@ const { runDashboard } = await import("./commands/dashboard.js");
const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskShow, runTaskAttach, runTaskPause, runTaskUnpause } = await import("./commands/task.js"); const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskShow, runTaskAttach, runTaskPause, runTaskUnpause } = await import("./commands/task.js");
const HELP = ` const HELP = `
hai — AI-orchestrated task board kb — AI-orchestrated task board
Usage: Usage:
hai dashboard Start the board web UI kb dashboard Start the board web UI
hai task create [desc] [--attach f] Create a new task (goes to triage) kb task create [desc] [--attach f] Create a new task (goes to triage)
hai task list List all tasks kb task list List all tasks
hai task show <id> Show task details, steps, log kb task show <id> Show task details, steps, log
hai task move <id> <col> Move a task to a column kb task move <id> <col> Move a task to a column
hai task update <id> <step> <status> Update step status (pending|in-progress|done|skipped) kb task update <id> <step> <status> Update step status (pending|in-progress|done|skipped)
hai task log <id> <message> Add a log entry kb task log <id> <message> Add a log entry
hai task merge <id> Merge an in-review task and close it kb task merge <id> Merge an in-review task and close it
hai task attach <id> <file> Attach a file to a task kb task attach <id> <file> Attach a file to a task
hai task pause <id> Pause a task (stops all automation) kb task pause <id> Pause a task (stops all automation)
hai task unpause <id> Unpause a task (resumes automation) kb task unpause <id> Unpause a task (resumes automation)
Options: Options:
--port, -p <port> Dashboard port (default: 4040) --port, -p <port> Dashboard port (default: 4040)
@@ -118,7 +118,7 @@ async function main() {
const id = args[2]; const id = args[2];
const column = args[3]; const column = args[3];
if (!id || !column) { if (!id || !column) {
console.error("Usage: hai task move <id> <column>"); console.error("Usage: kb task move <id> <column>");
process.exit(1); process.exit(1);
} }
await runTaskMove(id, column); await runTaskMove(id, column);
@@ -126,14 +126,14 @@ async function main() {
} }
case "show": { case "show": {
const id = args[2]; const id = args[2];
if (!id) { console.error("Usage: hai task show <id>"); process.exit(1); } if (!id) { console.error("Usage: kb task show <id>"); process.exit(1); }
await runTaskShow(id); await runTaskShow(id);
break; break;
} }
case "update": { case "update": {
const id = args[2], step = args[3], status = args[4]; const id = args[2], step = args[3], status = args[4];
if (!id || !step || !status) { if (!id || !step || !status) {
console.error("Usage: hai task update <id> <step> <status>"); console.error("Usage: kb task update <id> <step> <status>");
console.error("Status: pending | in-progress | done | skipped"); console.error("Status: pending | in-progress | done | skipped");
process.exit(1); process.exit(1);
} }
@@ -142,20 +142,20 @@ async function main() {
} }
case "log": { case "log": {
const id = args[2], message = args.slice(3).join(" "); const id = args[2], message = args.slice(3).join(" ");
if (!id || !message) { console.error("Usage: hai task log <id> <message>"); process.exit(1); } if (!id || !message) { console.error("Usage: kb task log <id> <message>"); process.exit(1); }
await runTaskLog(id, message); await runTaskLog(id, message);
break; break;
} }
case "merge": { case "merge": {
const id = args[2]; const id = args[2];
if (!id) { console.error("Usage: hai task merge <id>"); process.exit(1); } if (!id) { console.error("Usage: kb task merge <id>"); process.exit(1); }
await runTaskMerge(id); await runTaskMerge(id);
break; break;
} }
case "attach": { case "attach": {
const id = args[2], file = args[3]; const id = args[2], file = args[3];
if (!id || !file) { if (!id || !file) {
console.error("Usage: hai task attach <id> <file>"); console.error("Usage: kb task attach <id> <file>");
process.exit(1); process.exit(1);
} }
await runTaskAttach(id, file); await runTaskAttach(id, file);
@@ -163,19 +163,19 @@ async function main() {
} }
case "pause": { case "pause": {
const id = args[2]; const id = args[2];
if (!id) { console.error("Usage: hai task pause <id>"); process.exit(1); } if (!id) { console.error("Usage: kb task pause <id>"); process.exit(1); }
await runTaskPause(id); await runTaskPause(id);
break; break;
} }
case "unpause": { case "unpause": {
const id = args[2]; const id = args[2];
if (!id) { console.error("Usage: hai task unpause <id>"); process.exit(1); } if (!id) { console.error("Usage: kb task unpause <id>"); process.exit(1); }
await runTaskUnpause(id); await runTaskUnpause(id);
break; break;
} }
default: default:
console.error(`Unknown subcommand: task ${subcommand || ""}`); console.error(`Unknown subcommand: task ${subcommand || ""}`);
console.log("Try: hai task create | list | move"); console.log("Try: kb task create | list | move");
process.exit(1); process.exit(1);
} }
break; break;

View File

@@ -24,23 +24,23 @@ function makeMockStore() {
}; };
} }
// ── Mock @hai/core ────────────────────────────────────────────────── // ── Mock @kb/core ──────────────────────────────────────────────────
vi.mock("@hai/core", () => ({ vi.mock("@kb/core", () => ({
TaskStore: vi.fn().mockImplementation(() => makeMockStore()), TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
})); }));
// ── Mock @hai/dashboard ───────────────────────────────────────────── // ── Mock @kb/dashboard ─────────────────────────────────────────────
const mockListen = vi.fn(); const mockListen = vi.fn();
vi.mock("@hai/dashboard", () => ({ vi.mock("@kb/dashboard", () => ({
createServer: vi.fn(() => ({ listen: mockListen })), createServer: vi.fn(() => ({ listen: mockListen })),
})); }));
// ── Mock @hai/engine ──────────────────────────────────────────────── // ── Mock @kb/engine ────────────────────────────────────────────────
vi.mock("@hai/engine", async (importOriginal) => { vi.mock("@kb/engine", async (importOriginal) => {
const original = await importOriginal<typeof import("@hai/engine")>(); const original = await importOriginal<typeof import("@kb/engine")>();
return { return {
...original, ...original,
WorktreePool: original.WorktreePool, WorktreePool: original.WorktreePool,
@@ -81,12 +81,12 @@ const { runDashboard } = await import("../dashboard.js");
describe("runDashboard — AuthStorage & ModelRegistry wiring", () => { describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
beforeEach(async () => { beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
const { TaskStore } = await import("@hai/core"); const { TaskStore } = await import("@kb/core");
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore()); (TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
}); });
it("passes authStorage and modelRegistry to createServer", async () => { it("passes authStorage and modelRegistry to createServer", async () => {
const { createServer } = await import("@hai/dashboard"); const { createServer } = await import("@kb/dashboard");
await runDashboard(0, { open: false }); await runDashboard(0, { open: false });

View File

@@ -26,26 +26,26 @@ function makeMockStore() {
}; };
} }
// ── Mock @hai/core ────────────────────────────────────────────────── // ── Mock @kb/core ──────────────────────────────────────────────────
vi.mock("@hai/core", () => ({ vi.mock("@kb/core", () => ({
TaskStore: vi.fn().mockImplementation(() => makeMockStore()), TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
})); }));
// ── Mock @hai/dashboard ───────────────────────────────────────────── // ── Mock @kb/dashboard ─────────────────────────────────────────────
const mockListen = vi.fn(); const mockListen = vi.fn();
vi.mock("@hai/dashboard", () => ({ vi.mock("@kb/dashboard", () => ({
createServer: vi.fn(() => ({ listen: mockListen })), createServer: vi.fn(() => ({ listen: mockListen })),
})); }));
// ── Mock @hai/engine ──────────────────────────────────────────────── // ── Mock @kb/engine ────────────────────────────────────────────────
// We need the real WorktreePool class so we can assert `instanceof`. // We need the real WorktreePool class so we can assert `instanceof`.
const { WorktreePool } = await import("@hai/engine"); const { WorktreePool } = await import("@kb/engine");
vi.mock("@hai/engine", async (importOriginal) => { vi.mock("@kb/engine", async (importOriginal) => {
const original = await importOriginal<typeof import("@hai/engine")>(); const original = await importOriginal<typeof import("@kb/engine")>();
return { return {
...original, ...original,
// Keep real WorktreePool & AgentSemaphore // Keep real WorktreePool & AgentSemaphore
@@ -81,10 +81,10 @@ describe("runDashboard — WorktreePool wiring", () => {
capturedExecutorOpts = undefined; capturedExecutorOpts = undefined;
vi.clearAllMocks(); vi.clearAllMocks();
// Re-set TaskStore mock (clearAllMocks wipes implementations) // Re-set TaskStore mock (clearAllMocks wipes implementations)
const { TaskStore } = await import("@hai/core"); const { TaskStore } = await import("@kb/core");
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore()); (TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
// Re-set engine mocks // Re-set engine mocks
const engine = await import("@hai/engine"); const engine = await import("@kb/engine");
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() => (engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
Promise.resolve({ merged: true }), Promise.resolve({ merged: true }),
); );
@@ -104,8 +104,8 @@ describe("runDashboard — WorktreePool wiring", () => {
}); });
it("passes a WorktreePool instance to aiMergeTask via rawMerge", async () => { it("passes a WorktreePool instance to aiMergeTask via rawMerge", async () => {
const { aiMergeTask } = await import("@hai/engine"); const { aiMergeTask } = await import("@kb/engine");
const { createServer } = await import("@hai/dashboard"); const { createServer } = await import("@kb/dashboard");
await runDashboard(0, { open: false }); await runDashboard(0, { open: false });
@@ -114,7 +114,7 @@ describe("runDashboard — WorktreePool wiring", () => {
const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> }; const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> };
// Invoke the merge handler // Invoke the merge handler
await serverOpts.onMerge("HAI-TEST"); await serverOpts.onMerge("KB-TEST");
expect(aiMergeTask).toHaveBeenCalled(); expect(aiMergeTask).toHaveBeenCalled();
const mergeCallOpts = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls[0][3]; const mergeCallOpts = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls[0][3];
@@ -122,15 +122,15 @@ describe("runDashboard — WorktreePool wiring", () => {
}); });
it("shares the same WorktreePool instance between executor and merger", async () => { it("shares the same WorktreePool instance between executor and merger", async () => {
const { aiMergeTask } = await import("@hai/engine"); const { aiMergeTask } = await import("@kb/engine");
const { createServer } = await import("@hai/dashboard"); const { createServer } = await import("@kb/dashboard");
await runDashboard(0, { open: false }); await runDashboard(0, { open: false });
// Trigger merger via onMerge // Trigger merger via onMerge
const createServerCall = (createServer as ReturnType<typeof vi.fn>).mock.calls[0]; const createServerCall = (createServer as ReturnType<typeof vi.fn>).mock.calls[0];
const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> }; const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> };
await serverOpts.onMerge("HAI-TEST"); await serverOpts.onMerge("KB-TEST");
const executorPool = capturedExecutorOpts!.pool; const executorPool = capturedExecutorOpts!.pool;
const mergerPool = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls[0][3].pool; const mergerPool = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls[0][3].pool;
@@ -148,9 +148,9 @@ describe("runDashboard — auto-merge pause exclusion", () => {
capturedExecutorOpts = undefined; capturedExecutorOpts = undefined;
vi.clearAllMocks(); vi.clearAllMocks();
mockStore = makeMockStore(); mockStore = makeMockStore();
const { TaskStore } = await import("@hai/core"); const { TaskStore } = await import("@kb/core");
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore); (TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
const engine = await import("@hai/engine"); const engine = await import("@kb/engine");
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() => (engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
Promise.resolve({ merged: true }), Promise.resolve({ merged: true }),
); );
@@ -172,11 +172,11 @@ describe("runDashboard — auto-merge pause exclusion", () => {
await runDashboard(0, { open: false }); await runDashboard(0, { open: false });
const { aiMergeTask } = await import("@hai/engine"); const { aiMergeTask } = await import("@kb/engine");
// Emit task:moved with a paused task // Emit task:moved with a paused task
mockStore.emit("task:moved", { mockStore.emit("task:moved", {
task: { id: "HAI-PAUSED", column: "in-review", paused: true }, task: { id: "KB-PAUSED", column: "in-review", paused: true },
from: "in-progress", from: "in-progress",
to: "in-review", to: "in-review",
}); });
@@ -195,11 +195,11 @@ describe("runDashboard — auto-merge pause exclusion", () => {
pollIntervalMs: 60_000, pollIntervalMs: 60_000,
}); });
mockStore.listTasks.mockResolvedValue([ mockStore.listTasks.mockResolvedValue([
{ id: "HAI-PAUSED", column: "in-review", paused: true }, { id: "KB-PAUSED", column: "in-review", paused: true },
{ id: "HAI-ACTIVE", column: "in-review", paused: false }, { id: "KB-ACTIVE", column: "in-review", paused: false },
]); ]);
const { aiMergeTask } = await import("@hai/engine"); const { aiMergeTask } = await import("@kb/engine");
// Reset after import // Reset after import
(aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() => (aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
Promise.resolve({ merged: true }), Promise.resolve({ merged: true }),
@@ -214,6 +214,6 @@ describe("runDashboard — auto-merge pause exclusion", () => {
const mergedIds = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls.map( const mergedIds = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls.map(
(call: any[]) => call[2], (call: any[]) => call[2],
); );
expect(mergedIds).not.toContain("HAI-PAUSED"); expect(mergedIds).not.toContain("KB-PAUSED");
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import { TaskStore } from "@hai/core"; import { TaskStore } from "@kb/core";
import { createServer } from "@hai/dashboard"; import { createServer } from "@kb/dashboard";
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask } from "@hai/engine"; import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask } from "@kb/engine";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
function openBrowser(url: string): void { function openBrowser(url: string): void {
@@ -228,11 +228,11 @@ export async function runDashboard(port: number, opts: { open?: boolean } = {})
app.listen(port, () => { app.listen(port, () => {
console.log(); console.log();
console.log(` hai board`); console.log(` kb board`);
console.log(` ────────────────────────`); console.log(` ────────────────────────`);
console.log(` → http://localhost:${port}`); console.log(` → http://localhost:${port}`);
console.log(); console.log();
console.log(` Tasks stored in .hai/tasks/`); console.log(` Tasks stored in .kb/tasks/`);
console.log(` Merge: AI-assisted (conflict resolution + commit messages)`); console.log(` Merge: AI-assisted (conflict resolution + commit messages)`);
console.log(` AI engine: ✓ active`); console.log(` AI engine: ✓ active`);
console.log(` • triage: auto-specifying tasks`); console.log(` • triage: auto-specifying tasks`);

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock @hai/core before importing the module under test // Mock @kb/core before importing the module under test
vi.mock("@hai/core", () => { vi.mock("@kb/core", () => {
const COLUMNS = ["triage", "specified", "in-progress", "review", "done"]; const COLUMNS = ["triage", "specified", "in-progress", "review", "done"];
const COLUMN_LABELS: Record<string, string> = { const COLUMN_LABELS: Record<string, string> = {
triage: "Triage", triage: "Triage",
@@ -18,15 +18,15 @@ vi.mock("@hai/core", () => {
}; };
}); });
// Mock @hai/engine // Mock @kb/engine
vi.mock("@hai/engine", () => ({ aiMergeTask: vi.fn() })); vi.mock("@kb/engine", () => ({ aiMergeTask: vi.fn() }));
import { TaskStore } from "@hai/core"; import { TaskStore } from "@kb/core";
import { runTaskShow, runTaskCreate } from "./task.js"; import { runTaskShow, runTaskCreate } from "./task.js";
function makeTask(overrides: Record<string, unknown> = {}) { function makeTask(overrides: Record<string, unknown> = {}) {
return { return {
id: "HAI-001", id: "KB-001",
description: "A short description", description: "A short description",
column: "triage", column: "triage",
dependencies: [], dependencies: [],
@@ -59,16 +59,16 @@ describe("runTaskShow", () => {
getTask: vi.fn().mockResolvedValue(task), getTask: vi.fn().mockResolvedValue(task),
})); }));
await runTaskShow("HAI-001"); await runTaskShow("KB-001");
const headerLine = logSpy.mock.calls.find( const headerLine = logSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("HAI-001:") (call) => typeof call[0] === "string" && call[0].includes("KB-001:")
); );
expect(headerLine).toBeDefined(); expect(headerLine).toBeDefined();
expect(headerLine![0]).toContain(longDesc); expect(headerLine![0]).toContain(longDesc);
// Ensure no truncation happened // Ensure no truncation happened
expect(headerLine![0]).not.toContain(longDesc.slice(0, 60) + "…"); expect(headerLine![0]).not.toContain(longDesc.slice(0, 60) + "…");
expect(headerLine![0].length).toBeGreaterThan(60 + " HAI-001: ".length); expect(headerLine![0].length).toBeGreaterThan(60 + " KB-001: ".length);
}); });
it("displays the title when present instead of description", async () => { it("displays the title when present instead of description", async () => {
@@ -82,10 +82,10 @@ describe("runTaskShow", () => {
getTask: vi.fn().mockResolvedValue(task), getTask: vi.fn().mockResolvedValue(task),
})); }));
await runTaskShow("HAI-001"); await runTaskShow("KB-001");
const headerLine = logSpy.mock.calls.find( const headerLine = logSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("HAI-001:") (call) => typeof call[0] === "string" && call[0].includes("KB-001:")
); );
expect(headerLine).toBeDefined(); expect(headerLine).toBeDefined();
expect(headerLine![0]).toContain("My Task Title"); expect(headerLine![0]).toContain("My Task Title");
@@ -119,7 +119,7 @@ describe("runTaskCreate with --attach", () => {
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({ (TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
init: vi.fn(), init: vi.fn(),
createTask: vi.fn().mockResolvedValue({ createTask: vi.fn().mockResolvedValue({
id: "HAI-002", id: "KB-002",
description: "test task", description: "test task",
column: "triage", column: "triage",
dependencies: [], dependencies: [],
@@ -146,7 +146,7 @@ describe("runTaskCreate with --attach", () => {
expect(mockAddAttachment).toHaveBeenCalledOnce(); expect(mockAddAttachment).toHaveBeenCalledOnce();
expect(mockAddAttachment).toHaveBeenCalledWith( expect(mockAddAttachment).toHaveBeenCalledWith(
"HAI-002", "KB-002",
"test.png", "test.png",
expect.any(Buffer), expect.any(Buffer),
"image/png", "image/png",

View File

@@ -1,5 +1,5 @@
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus } from "@hai/core"; import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus } from "@kb/core";
import { aiMergeTask } from "@hai/engine"; import { aiMergeTask } from "@kb/engine";
import { createInterface } from "node:readline/promises"; import { createInterface } from "node:readline/promises";
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"]; const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
@@ -34,7 +34,7 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin
console.log(); console.log();
console.log(` ✓ Created ${task.id}: ${label}`); console.log(` ✓ Created ${task.id}: ${label}`);
console.log(` Column: triage`); console.log(` Column: triage`);
console.log(` Path: .hai/tasks/${task.id}/`); console.log(` Path: .kb/tasks/${task.id}/`);
if (attachFiles && attachFiles.length > 0) { if (attachFiles && attachFiles.length > 0) {
const { readFile } = await import("node:fs/promises"); const { readFile } = await import("node:fs/promises");
@@ -73,7 +73,7 @@ export async function runTaskList() {
const tasks = await store.listTasks(); const tasks = await store.listTasks();
if (tasks.length === 0) { if (tasks.length === 0) {
console.log("\n No tasks yet. Create one with: hai task create\n"); console.log("\n No tasks yet. Create one with: kb task create\n");
return; return;
} }
@@ -246,7 +246,7 @@ export async function runTaskAttach(id: string, filePath: string) {
console.log(); console.log();
console.log(` ✓ Attached to ${id}: ${attachment.originalName}`); console.log(` ✓ Attached to ${id}: ${attachment.originalName}`);
console.log(` File: ${attachment.filename} (${sizeKB} KB)`); console.log(` File: ${attachment.filename} (${sizeKB} KB)`);
console.log(` Path: .hai/tasks/${id}/attachments/${attachment.filename}`); console.log(` Path: .kb/tasks/${id}/attachments/${attachment.filename}`);
console.log(); console.log();
} }

View File

@@ -1,5 +1,5 @@
{ {
"name": "@hai/core", "name": "@kb/core",
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {

View File

@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
import type { Task } from "./types.js"; import type { Task } from "./types.js";
function makeTmpDir(): string { function makeTmpDir(): string {
return mkdtempSync(join(tmpdir(), "hai-store-test-")); return mkdtempSync(join(tmpdir(), "kb-store-test-"));
} }
describe("TaskStore", () => { describe("TaskStore", () => {
@@ -32,7 +32,7 @@ describe("TaskStore", () => {
async function createTaskWithSteps(): Promise<Task> { async function createTaskWithSteps(): Promise<Task> {
const task = await store.createTask({ description: "Task with steps" }); const task = await store.createTask({ description: "Task with steps" });
// Write a PROMPT.md with steps so updateStep works // Write a PROMPT.md with steps so updateStep works
const dir = join(rootDir, ".hai", "tasks", task.id); const dir = join(rootDir, ".kb", "tasks", task.id);
await writeFile( await writeFile(
join(dir, "PROMPT.md"), join(dir, "PROMPT.md"),
`# ${task.id}: Task with steps `# ${task.id}: Task with steps
@@ -63,7 +63,7 @@ describe("TaskStore", () => {
const detail = await store.getTask(task.id); const detail = await store.getTask(task.id);
// Heading should be just the ID, not the description // Heading should be just the ID, not the description
expect(detail.prompt).toMatch(/^# HAI-001\n/); expect(detail.prompt).toMatch(/^# KB-001\n/);
// Description appears exactly once // Description appears exactly once
const count = detail.prompt.split("Fix the login bug").length - 1; const count = detail.prompt.split("Fix the login bug").length - 1;
expect(count).toBe(1); expect(count).toBe(1);
@@ -76,7 +76,7 @@ describe("TaskStore", () => {
}); });
const detail = await store.getTask(task.id); const detail = await store.getTask(task.id);
expect(detail.prompt).toMatch(/^# HAI-001: Login bug\n/); expect(detail.prompt).toMatch(/^# KB-001: Login bug\n/);
expect(detail.prompt).toContain("Fix the login bug on the settings page"); expect(detail.prompt).toContain("Fix the login bug on the settings page");
}); });
@@ -88,7 +88,7 @@ describe("TaskStore", () => {
const detail = await store.getTask(task.id); const detail = await store.getTask(task.id);
// Heading should be just the ID // Heading should be just the ID
expect(detail.prompt).toMatch(/^# HAI-001\n/); expect(detail.prompt).toMatch(/^# KB-001\n/);
// Description appears exactly once (in Mission section) // Description appears exactly once (in Mission section)
const count = detail.prompt.split("Implement caching layer").length - 1; const count = detail.prompt.split("Implement caching layer").length - 1;
expect(count).toBe(1); expect(count).toBe(1);
@@ -102,7 +102,7 @@ describe("TaskStore", () => {
}); });
const detail = await store.getTask(task.id); const detail = await store.getTask(task.id);
expect(detail.prompt).toMatch(/^# HAI-001: Add caching\n/); expect(detail.prompt).toMatch(/^# KB-001: Add caching\n/);
expect(detail.prompt).toContain("Implement caching layer for API responses"); expect(detail.prompt).toContain("Implement caching layer for API responses");
}); });
}); });
@@ -129,7 +129,7 @@ describe("TaskStore", () => {
await Promise.all(promises); await Promise.all(promises);
// Read back and verify valid JSON // Read back and verify valid JSON
const taskJsonPath = join(rootDir, ".hai", "tasks", id, "task.json"); const taskJsonPath = join(rootDir, ".kb", "tasks", id, "task.json");
const raw = await readFile(taskJsonPath, "utf-8"); const raw = await readFile(taskJsonPath, "utf-8");
const result = JSON.parse(raw) as Task; const result = JSON.parse(raw) as Task;
@@ -144,7 +144,7 @@ describe("TaskStore", () => {
describe("defensive JSON parsing", () => { describe("defensive JSON parsing", () => {
it("throws on corrupted task.json with trailing duplicate content (atomic writes prevent this)", async () => { it("throws on corrupted task.json with trailing duplicate content (atomic writes prevent this)", async () => {
const task = await createTestTask(); const task = await createTestTask();
const taskJsonPath = join(rootDir, ".hai", "tasks", task.id, "task.json"); const taskJsonPath = join(rootDir, ".kb", "tasks", task.id, "task.json");
// Corrupt the file: append duplicate trailing content // Corrupt the file: append duplicate trailing content
const validJson = await readFile(taskJsonPath, "utf-8"); const validJson = await readFile(taskJsonPath, "utf-8");
@@ -157,7 +157,7 @@ describe("TaskStore", () => {
it("throws a clear error when JSON is completely unrecoverable", async () => { it("throws a clear error when JSON is completely unrecoverable", async () => {
const task = await createTestTask(); const task = await createTestTask();
const taskJsonPath = join(rootDir, ".hai", "tasks", task.id, "task.json"); const taskJsonPath = join(rootDir, ".kb", "tasks", task.id, "task.json");
// Write completely invalid content // Write completely invalid content
await writeFile(taskJsonPath, "not json at all {{{"); await writeFile(taskJsonPath, "not json at all {{{");
@@ -171,7 +171,7 @@ describe("TaskStore", () => {
describe("atomic writes", () => { describe("atomic writes", () => {
it("produces valid JSON after write with no .tmp files left behind", async () => { it("produces valid JSON after write with no .tmp files left behind", async () => {
const task = await createTestTask(); const task = await createTestTask();
const dir = join(rootDir, ".hai", "tasks", task.id); const dir = join(rootDir, ".kb", "tasks", task.id);
// Perform a write // Perform a write
await store.logEntry(task.id, "atomic test"); await store.logEntry(task.id, "atomic test");
@@ -200,18 +200,18 @@ describe("TaskStore", () => {
const ids = tasks.map((t) => t.id); const ids = tasks.map((t) => t.id);
expect(new Set(ids).size).toBe(5); expect(new Set(ids).size).toBe(5);
// IDs should be sequential (HAI-001 through HAI-005) // IDs should be sequential (KB-001 through KB-005)
const sortedIds = [...ids].sort(); const sortedIds = [...ids].sort();
expect(sortedIds).toEqual(["HAI-001", "HAI-002", "HAI-003", "HAI-004", "HAI-005"]); expect(sortedIds).toEqual(["KB-001", "KB-002", "KB-003", "KB-004", "KB-005"]);
// config.json should be valid JSON with nextId = 6 // config.json should be valid JSON with nextId = 6
const configPath = join(rootDir, ".hai", "config.json"); const configPath = join(rootDir, ".kb", "config.json");
const raw = await readFile(configPath, "utf-8"); const raw = await readFile(configPath, "utf-8");
const config = JSON.parse(raw); const config = JSON.parse(raw);
expect(config.nextId).toBe(6); expect(config.nextId).toBe(6);
// No .tmp files left behind // No .tmp files left behind
const haiDir = join(rootDir, ".hai"); const haiDir = join(rootDir, ".kb");
const files = await readdir(haiDir); const files = await readdir(haiDir);
expect(files.filter((f) => f.endsWith(".tmp"))).toHaveLength(0); expect(files.filter((f) => f.endsWith(".tmp"))).toHaveLength(0);
}); });
@@ -240,7 +240,7 @@ describe("TaskStore", () => {
expect(updated.attachments![0].filename).toBe(attachment.filename); expect(updated.attachments![0].filename).toBe(attachment.filename);
// Verify file on disk // Verify file on disk
const filePath = join(rootDir, ".hai", "tasks", task.id, "attachments", attachment.filename); const filePath = join(rootDir, ".kb", "tasks", task.id, "attachments", attachment.filename);
const content = await readFile(filePath); const content = await readFile(filePath);
expect(content).toEqual(TINY_PNG); expect(content).toEqual(TINY_PNG);
}); });
@@ -296,7 +296,7 @@ describe("TaskStore", () => {
expect(updated.attachments).toBeUndefined(); expect(updated.attachments).toBeUndefined();
// Verify file removed from disk // Verify file removed from disk
const filePath = join(rootDir, ".hai", "tasks", task.id, "attachments", attachment.filename); const filePath = join(rootDir, ".kb", "tasks", task.id, "attachments", attachment.filename);
expect(existsSync(filePath)).toBe(false); expect(existsSync(filePath)).toBe(false);
}); });
@@ -369,48 +369,48 @@ describe("TaskStore", () => {
const task = await createTestTask(); const task = await createTestTask();
expect(task.dependencies).toEqual([]); expect(task.dependencies).toEqual([]);
const updated = await store.updateTask(task.id, { dependencies: ["HAI-001", "HAI-002"] }); const updated = await store.updateTask(task.id, { dependencies: ["KB-001", "KB-002"] });
expect(updated.dependencies).toEqual(["HAI-001", "HAI-002"]); expect(updated.dependencies).toEqual(["KB-001", "KB-002"]);
// Verify persistence // Verify persistence
const fetched = await store.getTask(task.id); const fetched = await store.getTask(task.id);
expect(fetched.dependencies).toEqual(["HAI-001", "HAI-002"]); expect(fetched.dependencies).toEqual(["KB-001", "KB-002"]);
}); });
it("replaces existing dependencies", async () => { it("replaces existing dependencies", async () => {
const task = await store.createTask({ description: "Dep task", dependencies: ["HAI-001"] }); const task = await store.createTask({ description: "Dep task", dependencies: ["KB-001"] });
expect(task.dependencies).toEqual(["HAI-001"]); expect(task.dependencies).toEqual(["KB-001"]);
const updated = await store.updateTask(task.id, { dependencies: ["HAI-002", "HAI-003"] }); const updated = await store.updateTask(task.id, { dependencies: ["KB-002", "KB-003"] });
expect(updated.dependencies).toEqual(["HAI-002", "HAI-003"]); expect(updated.dependencies).toEqual(["KB-002", "KB-003"]);
}); });
it("clears dependencies with empty array", async () => { it("clears dependencies with empty array", async () => {
const task = await store.createTask({ description: "Dep task", dependencies: ["HAI-001"] }); const task = await store.createTask({ description: "Dep task", dependencies: ["KB-001"] });
expect(task.dependencies).toEqual(["HAI-001"]); expect(task.dependencies).toEqual(["KB-001"]);
const updated = await store.updateTask(task.id, { dependencies: [] }); const updated = await store.updateTask(task.id, { dependencies: [] });
expect(updated.dependencies).toEqual([]); expect(updated.dependencies).toEqual([]);
}); });
it("leaves dependencies unchanged when not provided", async () => { it("leaves dependencies unchanged when not provided", async () => {
const task = await store.createTask({ description: "Dep task", dependencies: ["HAI-001"] }); const task = await store.createTask({ description: "Dep task", dependencies: ["KB-001"] });
const updated = await store.updateTask(task.id, { title: "New title" }); const updated = await store.updateTask(task.id, { title: "New title" });
expect(updated.dependencies).toEqual(["HAI-001"]); expect(updated.dependencies).toEqual(["KB-001"]);
}); });
}); });
describe("updateTask — blockedBy", () => { describe("updateTask — blockedBy", () => {
it("sets blockedBy to a string value", async () => { it("sets blockedBy to a string value", async () => {
const task = await store.createTask({ title: "Blocked task", description: "A task" }); const task = await store.createTask({ title: "Blocked task", description: "A task" });
const updated = await store.updateTask(task.id, { blockedBy: "HAI-999" }); const updated = await store.updateTask(task.id, { blockedBy: "KB-999" });
expect(updated.blockedBy).toBe("HAI-999"); expect(updated.blockedBy).toBe("KB-999");
}); });
it("clears blockedBy when set to null", async () => { it("clears blockedBy when set to null", async () => {
const task = await store.createTask({ title: "Blocked task", description: "A task" }); const task = await store.createTask({ title: "Blocked task", description: "A task" });
await store.updateTask(task.id, { blockedBy: "HAI-999" }); await store.updateTask(task.id, { blockedBy: "KB-999" });
const updated = await store.updateTask(task.id, { blockedBy: null }); const updated = await store.updateTask(task.id, { blockedBy: null });
expect(updated.blockedBy).toBeUndefined(); expect(updated.blockedBy).toBeUndefined();
}); });
@@ -419,9 +419,9 @@ describe("TaskStore", () => {
// ── Task prefix tests ────────────────────────────────────────── // ── Task prefix tests ──────────────────────────────────────────
describe("taskPrefix setting", () => { describe("taskPrefix setting", () => {
it("default prefix produces HAI-001 IDs", async () => { it("default prefix produces KB-001 IDs", async () => {
const task = await store.createTask({ description: "Default prefix" }); const task = await store.createTask({ description: "Default prefix" });
expect(task.id).toBe("HAI-001"); expect(task.id).toBe("KB-001");
}); });
it("custom prefix produces PROJ-001 IDs", async () => { it("custom prefix produces PROJ-001 IDs", async () => {
@@ -433,8 +433,8 @@ describe("TaskStore", () => {
it("prefix change mid-stream continues sequence", async () => { it("prefix change mid-stream continues sequence", async () => {
const t1 = await store.createTask({ description: "First" }); const t1 = await store.createTask({ description: "First" });
const t2 = await store.createTask({ description: "Second" }); const t2 = await store.createTask({ description: "Second" });
expect(t1.id).toBe("HAI-001"); expect(t1.id).toBe("KB-001");
expect(t2.id).toBe("HAI-002"); expect(t2.id).toBe("KB-002");
await store.updateSettings({ taskPrefix: "PROJ" }); await store.updateSettings({ taskPrefix: "PROJ" });
const t3 = await store.createTask({ description: "Third" }); const t3 = await store.createTask({ description: "Third" });
@@ -448,7 +448,7 @@ describe("TaskStore", () => {
const tasks = await store.listTasks(); const tasks = await store.listTasks();
expect(tasks).toHaveLength(2); expect(tasks).toHaveLength(2);
expect(tasks.map((t) => t.id).sort()).toEqual(["HAI-001", "PROJ-002"]); expect(tasks.map((t) => t.id).sort()).toEqual(["KB-001", "PROJ-002"]);
}); });
}); });

View File

@@ -16,7 +16,7 @@ export interface TaskStoreEvents {
} }
export class TaskStore extends EventEmitter<TaskStoreEvents> { export class TaskStore extends EventEmitter<TaskStoreEvents> {
private haiDir: string; private kbDir: string;
private tasksDir: string; private tasksDir: string;
private configPath: string; private configPath: string;
@@ -37,9 +37,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
constructor(private rootDir: string) { constructor(private rootDir: string) {
super(); super();
this.haiDir = join(rootDir, ".hai"); this.kbDir = join(rootDir, ".kb");
this.tasksDir = join(this.haiDir, "tasks"); this.tasksDir = join(this.kbDir, "tasks");
this.configPath = join(this.haiDir, "config.json"); this.configPath = join(this.kbDir, "config.json");
} }
async init(): Promise<void> { async init(): Promise<void> {
@@ -160,7 +160,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private async allocateId(): Promise<string> { private async allocateId(): Promise<string> {
return this.withConfigLock(async () => { return this.withConfigLock(async () => {
const config = await this.readConfig(); const config = await this.readConfig();
const prefix = config.settings?.taskPrefix || "HAI"; const prefix = config.settings?.taskPrefix || "KB";
const id = `${prefix}-${String(config.nextId).padStart(3, "0")}`; const id = `${prefix}-${String(config.nextId).padStart(3, "0")}`;
config.nextId++; config.nextId++;
await this.writeConfig(config); await this.writeConfig(config);
@@ -509,7 +509,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
); );
} }
const branch = `hai/${id.toLowerCase()}`; const branch = `kb/${id.toLowerCase()}`;
const worktreePath = task.worktree; const worktreePath = task.worktree;
const result: MergeResult = { const result: MergeResult = {
task, task,
@@ -556,7 +556,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
`Merge conflict merging '${branch}'. Resolve manually:\n` + `Merge conflict merging '${branch}'. Resolve manually:\n` +
` cd ${this.rootDir}\n` + ` cd ${this.rootDir}\n` +
` git merge --squash ${branch}\n` + ` git merge --squash ${branch}\n` +
` # resolve conflicts, then: hai task move ${id} done`, ` # resolve conflicts, then: kb task move ${id} done`,
); );
} }
@@ -877,10 +877,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/** /**
* Append an agent log entry to the task's agent log file (JSONL format). * Append an agent log entry to the task's agent log file (JSONL format).
* Each entry is a single JSON line appended to `.hai/tasks/{ID}/agent.log`. * Each entry is a single JSON line appended to `.kb/tasks/{ID}/agent.log`.
* Also emits an `agent:log` event for live streaming. * Also emits an `agent:log` event for live streaming.
* *
* @param taskId - The task ID (e.g. "HAI-001") * @param taskId - The task ID (e.g. "KB-001")
* @param text - The text content (delta for "text", tool name for "tool") * @param text - The text content (delta for "text", tool name for "tool")
* @param type - Whether this is a "text" delta or a "tool" invocation marker * @param type - Whether this is a "text" delta or a "tool" invocation marker
* @param detail - Optional human-readable summary of tool args (e.g. file path, command) * @param detail - Optional human-readable summary of tool args (e.g. file path, command)
@@ -903,7 +903,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Read all historical agent log entries for a task from its agent log file. * Read all historical agent log entries for a task from its agent log file.
* Returns entries in chronological order (oldest first). * Returns entries in chronological order (oldest first).
* *
* @param taskId - The task ID (e.g. "HAI-001") * @param taskId - The task ID (e.g. "KB-001")
* @returns Array of agent log entries, empty if no log file exists * @returns Array of agent log entries, empty if no log file exists
*/ */
async getAgentLogs(taskId: string): Promise<AgentLogEntry[]> { async getAgentLogs(taskId: string): Promise<AgentLogEntry[]> {

View File

@@ -94,12 +94,12 @@ export interface Settings {
* of being deleted. New tasks acquire a warm worktree from the pool, * of being deleted. New tasks acquire a warm worktree from the pool,
* preserving build caches (node_modules, target/, dist/). Default: false. */ * preserving build caches (node_modules, target/, dist/). Default: false. */
recycleWorktrees?: boolean; recycleWorktrees?: boolean;
/** Prefix for generated task IDs (e.g. `"HAI"` produces `HAI-001`). /** Prefix for generated task IDs (e.g. `"KB"` produces `KB-001`).
* Defaults to `"HAI"`. Only affects new tasks — existing tasks retain * Defaults to `"KB"`. Only affects new tasks — existing tasks retain
* their original IDs. */ * their original IDs. */
taskPrefix?: string; taskPrefix?: string;
/** When true, merge commit messages include the task ID as the conventional /** When true, merge commit messages include the task ID as the conventional
* commit scope (e.g. `feat(HAI-001): ...`). When false, the scope is * commit scope (e.g. `feat(KB-001): ...`). When false, the scope is
* omitted (e.g. `feat: ...`). Default: true. */ * omitted (e.g. `feat: ...`). Default: true. */
includeTaskIdInCommit?: boolean; includeTaskIdInCommit?: boolean;
/** Default AI model provider name (e.g. `"anthropic"`, `"openai"`). /** Default AI model provider name (e.g. `"anthropic"`, `"openai"`).

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import type { TaskDetail, TaskCreateInput, Task } from "@hai/core"; import type { TaskDetail, TaskCreateInput, Task } from "@kb/core";
import { fetchConfig, fetchSettings, updateSettings } from "./api"; import { fetchConfig, fetchSettings, updateSettings } from "./api";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { Board } from "./components/Board"; import { Board } from "./components/Board";

View File

@@ -1,9 +1,9 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fetchTaskDetail, updateTask, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "./api"; import { fetchTaskDetail, updateTask, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "./api";
import type { Task, TaskDetail } from "@hai/core"; import type { Task, TaskDetail } from "@kb/core";
const FAKE_DETAIL: TaskDetail = { const FAKE_DETAIL: TaskDetail = {
id: "HAI-001", id: "KB-001",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
dependencies: [], dependencies: [],
@@ -12,7 +12,7 @@ const FAKE_DETAIL: TaskDetail = {
log: [], log: [],
createdAt: "2026-01-01T00:00:00.000Z", createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z",
prompt: "# HAI-001", prompt: "# KB-001",
}; };
function mockFetchResponse(ok: boolean, body: unknown, status = ok ? 200 : 500) { function mockFetchResponse(ok: boolean, body: unknown, status = ok ? 200 : 500) {
@@ -38,9 +38,9 @@ describe("fetchTaskDetail", () => {
it("returns data on first success", async () => { it("returns data on first success", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_DETAIL)); globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_DETAIL));
const result = await fetchTaskDetail("HAI-001"); const result = await fetchTaskDetail("KB-001");
expect(result.id).toBe("HAI-001"); expect(result.id).toBe("KB-001");
expect(globalThis.fetch).toHaveBeenCalledTimes(1); expect(globalThis.fetch).toHaveBeenCalledTimes(1);
}); });
@@ -49,9 +49,9 @@ describe("fetchTaskDetail", () => {
.mockReturnValueOnce(mockFetchResponse(false, { error: "Transient error" })) .mockReturnValueOnce(mockFetchResponse(false, { error: "Transient error" }))
.mockReturnValueOnce(mockFetchResponse(true, FAKE_DETAIL)); .mockReturnValueOnce(mockFetchResponse(true, FAKE_DETAIL));
const result = await fetchTaskDetail("HAI-001"); const result = await fetchTaskDetail("KB-001");
expect(result.id).toBe("HAI-001"); expect(result.id).toBe("KB-001");
expect(globalThis.fetch).toHaveBeenCalledTimes(2); expect(globalThis.fetch).toHaveBeenCalledTimes(2);
}); });
@@ -59,7 +59,7 @@ describe("fetchTaskDetail", () => {
globalThis.fetch = vi.fn() globalThis.fetch = vi.fn()
.mockReturnValue(mockFetchResponse(false, { error: "Server error" })); .mockReturnValue(mockFetchResponse(false, { error: "Server error" }));
await expect(fetchTaskDetail("HAI-001")).rejects.toThrow("Server error"); await expect(fetchTaskDetail("KB-001")).rejects.toThrow("Server error");
expect(globalThis.fetch).toHaveBeenCalledTimes(2); // initial + 1 retry expect(globalThis.fetch).toHaveBeenCalledTimes(2); // initial + 1 retry
}); });
}); });
@@ -72,10 +72,10 @@ describe("updateTask", () => {
}); });
const FAKE_TASK: Task = { const FAKE_TASK: Task = {
id: "HAI-001", id: "KB-001",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
dependencies: ["HAI-002"], dependencies: ["KB-002"],
steps: [], steps: [],
currentStep: 0, currentStep: 0,
log: [], log: [],
@@ -86,20 +86,20 @@ describe("updateTask", () => {
it("sends PATCH with dependencies and returns updated task", async () => { it("sends PATCH with dependencies and returns updated task", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_TASK)); globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_TASK));
const result = await updateTask("HAI-001", { dependencies: ["HAI-002"] }); const result = await updateTask("KB-001", { dependencies: ["KB-002"] });
expect(result.dependencies).toEqual(["HAI-002"]); expect(result.dependencies).toEqual(["KB-002"]);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/HAI-001", { expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001", {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
method: "PATCH", method: "PATCH",
body: JSON.stringify({ dependencies: ["HAI-002"] }), body: JSON.stringify({ dependencies: ["KB-002"] }),
}); });
}); });
it("throws on error response", async () => { it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(false, { error: "Not found" })); globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(false, { error: "Not found" }));
await expect(updateTask("HAI-001", { dependencies: [] })).rejects.toThrow("Not found"); await expect(updateTask("KB-001", { dependencies: [] })).rejects.toThrow("Not found");
}); });
}); });

View File

@@ -1,4 +1,4 @@
import type { Task, TaskDetail, TaskAttachment, TaskCreateInput, AgentLogEntry, Column, MergeResult, Settings } from "@hai/core"; import type { Task, TaskDetail, TaskAttachment, TaskCreateInput, AgentLogEntry, Column, MergeResult, Settings } from "@kb/core";
async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> { async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> {
const res = await fetch(`/api${path}`, { const res = await fetch(`/api${path}`, {

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { AgentLogEntry } from "@hai/core"; import type { AgentLogEntry } from "@kb/core";
interface AgentLogViewerProps { interface AgentLogViewerProps {
entries: AgentLogEntry[]; entries: AgentLogEntry[];

View File

@@ -1,5 +1,5 @@
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core"; import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
import { COLUMNS } from "@hai/core"; import { COLUMNS } from "@kb/core";
import { Column } from "./Column"; import { Column } from "./Column";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";

View File

@@ -1,7 +1,7 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease"; import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core"; import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@hai/core"; import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@kb/core";
import { TaskCard } from "./TaskCard"; import { TaskCard } from "./TaskCard";
import { WorktreeGroup } from "./WorktreeGroup"; import { WorktreeGroup } from "./WorktreeGroup";
import { InlineCreateCard } from "./InlineCreateCard"; import { InlineCreateCard } from "./InlineCreateCard";

View File

@@ -8,8 +8,8 @@ export function Header({ onOpenSettings }: HeaderProps) {
return ( return (
<header className="header"> <header className="header">
<div className="header-left"> <div className="header-left">
<img src="/logo.svg" alt="hai logo" className="header-logo" width={24} height={24} /> <img src="/logo.svg" alt="kb logo" className="header-logo" width={24} height={24} />
<h1 className="logo">hai</h1> <h1 className="logo">kb</h1>
<span className="logo-sub">board</span> <span className="logo-sub">board</span>
</div> </div>
<div className="header-actions"> <div className="header-actions">

View File

@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { Link } from "lucide-react"; import { Link } from "lucide-react";
import type { Task, TaskCreateInput } from "@hai/core"; import type { Task, TaskCreateInput } from "@kb/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { uploadAttachment } from "../api"; import { uploadAttachment } from "../api";

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import type { Settings } from "@hai/core"; import type { Settings } from "@kb/core";
import { fetchSettings, updateSettings, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "../api"; import { fetchSettings, updateSettings, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "../api";
import type { AuthProvider, ModelInfo } from "../api"; import type { AuthProvider, ModelInfo } from "../api";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
@@ -185,7 +185,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
<input <input
id="taskPrefix" id="taskPrefix"
type="text" type="text"
placeholder="HAI" placeholder="KB"
value={form.taskPrefix || ""} value={form.taskPrefix || ""}
onChange={(e) => { onChange={(e) => {
const val = e.target.value; const val = e.target.value;
@@ -198,7 +198,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
}} }}
/> />
{prefixError && <small className="field-error">{prefixError}</small>} {prefixError && <small className="field-error">{prefixError}</small>}
{!prefixError && <small>Prefix for new task IDs (e.g. HAI, PROJ)</small>} {!prefixError && <small>Prefix for new task IDs (e.g. KB, PROJ)</small>}
</div> </div>
</> </>
); );
@@ -412,7 +412,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
/> />
Include task ID in commit scope Include task ID in commit scope
</label> </label>
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(HAI-001): ...</code>)</small> <small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(KB-001): ...</code>)</small>
</div> </div>
</> </>
); );

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { TaskCard } from "./TaskCard"; import { TaskCard } from "./TaskCard";
import type { Task } from "@hai/core"; import type { Task } from "@kb/core";
// Mock lucide-react to avoid SVG rendering issues in test env // Mock lucide-react to avoid SVG rendering issues in test env
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
@@ -19,7 +19,7 @@ import { uploadAttachment } from "../api";
function makeTask(overrides: Partial<Task> = {}): Task { function makeTask(overrides: Partial<Task> = {}): Task {
return { return {
id: "HAI-001", id: "KB-001",
title: "Test task", title: "Test task",
column: "in-progress", column: "in-progress",
status: undefined as any, status: undefined as any,
@@ -35,7 +35,7 @@ const noop = () => {};
describe("TaskCard", () => { describe("TaskCard", () => {
it("renders the card ID text", () => { it("renders the card ID text", () => {
render(<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} />); render(<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} />);
expect(screen.getByText("HAI-001")).toBeDefined(); expect(screen.getByText("KB-001")).toBeDefined();
}); });
it("renders the status badge when task.status is set", () => { it("renders the status badge when task.status is set", () => {
@@ -126,7 +126,7 @@ describe("TaskCard", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(mockUpload).toHaveBeenCalledWith("HAI-001", file); expect(mockUpload).toHaveBeenCalledWith("KB-001", file);
expect(addToast).toHaveBeenCalledWith( expect(addToast).toHaveBeenCalledWith(
expect.stringContaining("Attached test.png"), expect.stringContaining("Attached test.png"),
"success", "success",

View File

@@ -1,6 +1,6 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Link, Clock, Layers } from "lucide-react"; import { Link, Clock, Layers } from "lucide-react";
import type { Task, TaskDetail, Column } from "@hai/core"; import type { Task, TaskDetail, Column } from "@kb/core";
import { fetchTaskDetail, uploadAttachment } from "../api"; import { fetchTaskDetail, uploadAttachment } from "../api";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@hai/core"; import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@kb/core";
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@hai/core"; import { COLUMN_LABELS, VALID_TRANSITIONS } from "@kb/core";
import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask } from "../api"; import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask } from "../api";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { useAgentLogs } from "../hooks/useAgentLogs"; import { useAgentLogs } from "../hooks/useAgentLogs";

View File

@@ -1,4 +1,4 @@
import type { Task, TaskDetail } from "@hai/core"; import type { Task, TaskDetail } from "@kb/core";
import { ClipboardList, GitBranch } from "lucide-react"; import { ClipboardList, GitBranch } from "lucide-react";
import { TaskCard } from "./TaskCard"; import { TaskCard } from "./TaskCard";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";

View File

@@ -1,12 +1,12 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { AgentLogViewer } from "../AgentLogViewer"; import { AgentLogViewer } from "../AgentLogViewer";
import type { AgentLogEntry } from "@hai/core"; import type { AgentLogEntry } from "@kb/core";
function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry { function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry {
return { return {
timestamp: "2026-01-01T00:00:00Z", timestamp: "2026-01-01T00:00:00Z",
taskId: "HAI-001", taskId: "KB-001",
text: "Hello world", text: "Hello world",
type: "text", type: "text",
...overrides, ...overrides,

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { Board } from "../Board"; import { Board } from "../Board";
import { COLUMNS } from "@hai/core"; import { COLUMNS } from "@kb/core";
// Mock child components so we only test Board's own rendering // Mock child components so we only test Board's own rendering
vi.mock("../Column", () => ({ vi.mock("../Column", () => ({

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { Column } from "../Column"; import { Column } from "../Column";
import type { Task, Column as ColumnType } from "@hai/core"; import type { Task, Column as ColumnType } from "@kb/core";
// Mock child components to keep tests focused on the Column badge behavior // Mock child components to keep tests focused on the Column badge behavior
vi.mock("../TaskCard", () => ({ vi.mock("../TaskCard", () => ({
@@ -43,7 +43,7 @@ const defaultProps = {
describe("Column count-flash", () => { describe("Column count-flash", () => {
it("does not apply count-flash class on initial render", () => { it("does not apply count-flash class on initial render", () => {
const tasks = [makeTask("HAI-001")]; const tasks = [makeTask("KB-001")];
render(<Column {...defaultProps} tasks={tasks} />); render(<Column {...defaultProps} tasks={tasks} />);
const badge = screen.getByText("1"); const badge = screen.getByText("1");
@@ -52,10 +52,10 @@ describe("Column count-flash", () => {
}); });
it("applies count-flash class when task count increases", () => { it("applies count-flash class when task count increases", () => {
const tasks = [makeTask("HAI-001")]; const tasks = [makeTask("KB-001")];
const { rerender } = render(<Column {...defaultProps} tasks={tasks} />); const { rerender } = render(<Column {...defaultProps} tasks={tasks} />);
const moreTasks = [makeTask("HAI-001"), makeTask("HAI-002")]; const moreTasks = [makeTask("KB-001"), makeTask("KB-002")];
rerender(<Column {...defaultProps} tasks={moreTasks} />); rerender(<Column {...defaultProps} tasks={moreTasks} />);
const badge = screen.getByText("2"); const badge = screen.getByText("2");
@@ -63,10 +63,10 @@ describe("Column count-flash", () => {
}); });
it("does not apply count-flash class when task count decreases", () => { it("does not apply count-flash class when task count decreases", () => {
const tasks = [makeTask("HAI-001"), makeTask("HAI-002")]; const tasks = [makeTask("KB-001"), makeTask("KB-002")];
const { rerender } = render(<Column {...defaultProps} tasks={tasks} />); const { rerender } = render(<Column {...defaultProps} tasks={tasks} />);
const fewerTasks = [makeTask("HAI-001")]; const fewerTasks = [makeTask("KB-001")];
rerender(<Column {...defaultProps} tasks={fewerTasks} />); rerender(<Column {...defaultProps} tasks={fewerTasks} />);
const badge = screen.getByText("1"); const badge = screen.getByText("1");

View File

@@ -5,7 +5,7 @@ import { Header } from "../Header";
describe("Header", () => { describe("Header", () => {
it("renders a logo image with correct src and alt", () => { it("renders a logo image with correct src and alt", () => {
render(<Header />); render(<Header />);
const logo = screen.getByAltText("hai logo"); const logo = screen.getByAltText("kb logo");
expect(logo).toBeDefined(); expect(logo).toBeDefined();
expect(logo.tagName).toBe("IMG"); expect(logo.tagName).toBe("IMG");
expect((logo as HTMLImageElement).src).toContain("/logo.svg"); expect((logo as HTMLImageElement).src).toContain("/logo.svg");
@@ -13,7 +13,7 @@ describe("Header", () => {
it("renders the logo before the h1 element", () => { it("renders the logo before the h1 element", () => {
render(<Header />); render(<Header />);
const logo = screen.getByAltText("hai logo"); const logo = screen.getByAltText("kb logo");
const h1 = screen.getByRole("heading", { level: 1 }); const h1 = screen.getByRole("heading", { level: 1 });
// Logo should be a preceding sibling of the h1 // Logo should be a preceding sibling of the h1
expect(logo.compareDocumentPosition(h1) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(logo.compareDocumentPosition(h1) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();

View File

@@ -15,7 +15,7 @@ vi.mock("../../api", () => ({
function renderCard() { function renderCard() {
const props = { const props = {
tasks: [], tasks: [],
onSubmit: vi.fn().mockResolvedValue({ id: "HAI-001" }), onSubmit: vi.fn().mockResolvedValue({ id: "KB-001" }),
onCancel: vi.fn(), onCancel: vi.fn(),
addToast: vi.fn(), addToast: vi.fn(),
}; };

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { SettingsModal } from "../SettingsModal"; import { SettingsModal } from "../SettingsModal";
import type { Settings } from "@hai/core"; import type { Settings } from "@kb/core";
const defaultSettings: Settings = { const defaultSettings: Settings = {
maxConcurrent: 2, maxConcurrent: 2,

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import type { Column } from "@hai/core"; import type { Column } from "@kb/core";
/** /**
* Tests for the agent-active class logic in TaskCard. * Tests for the agent-active class logic in TaskCard.
@@ -139,11 +139,11 @@ describe("TaskCard dependency tooltip", () => {
} }
it("returns comma-separated dependency IDs when dependencies are present", () => { it("returns comma-separated dependency IDs when dependencies are present", () => {
expect(computeDepTooltip(["HAI-001", "HAI-042"])).toBe("HAI-001, HAI-042"); expect(computeDepTooltip(["KB-001", "KB-042"])).toBe("KB-001, KB-042");
}); });
it("returns single dependency ID when only one dependency", () => { it("returns single dependency ID when only one dependency", () => {
expect(computeDepTooltip(["HAI-010"])).toBe("HAI-010"); expect(computeDepTooltip(["KB-010"])).toBe("KB-010");
}); });
it("returns undefined when dependencies array is empty", () => { it("returns undefined when dependencies array is empty", () => {
@@ -151,12 +151,12 @@ describe("TaskCard dependency tooltip", () => {
}); });
it("handles many dependencies", () => { it("handles many dependencies", () => {
const deps = ["HAI-001", "HAI-002", "HAI-003", "HAI-004"]; const deps = ["KB-001", "KB-002", "KB-003", "KB-004"];
expect(computeDepTooltip(deps)).toBe("HAI-001, HAI-002, HAI-003, HAI-004"); expect(computeDepTooltip(deps)).toBe("KB-001, KB-002, KB-003, KB-004");
}); });
it("data-tooltip attribute contains dependency IDs as a readable string", () => { it("data-tooltip attribute contains dependency IDs as a readable string", () => {
const deps = ["HAI-005", "HAI-012"]; const deps = ["KB-005", "KB-012"];
const tooltip = computeDepTooltip(deps); const tooltip = computeDepTooltip(deps);
expect(tooltip).toBeDefined(); expect(tooltip).toBeDefined();
// Each dependency ID should appear in the tooltip // Each dependency ID should appear in the tooltip
@@ -179,7 +179,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
} }
it("shows scope badge when blockedBy is set", () => { it("shows scope badge when blockedBy is set", () => {
expect(shouldShowScopeBadge("HAI-003")).toBe(true); expect(shouldShowScopeBadge("KB-003")).toBe(true);
}); });
it("does NOT show scope badge when blockedBy is undefined", () => { it("does NOT show scope badge when blockedBy is undefined", () => {
@@ -187,7 +187,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
}); });
it("shows card-meta when blockedBy is set even with no deps or queued status", () => { it("shows card-meta when blockedBy is set even with no deps or queued status", () => {
expect(shouldShowCardMeta({ blockedBy: "HAI-003" })).toBe(true); expect(shouldShowCardMeta({ blockedBy: "KB-003" })).toBe(true);
}); });
it("does NOT show card-meta when no deps, not queued, and no blockedBy", () => { it("does NOT show card-meta when no deps, not queued, and no blockedBy", () => {
@@ -200,7 +200,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
} }
it("generates correct tooltip text", () => { it("generates correct tooltip text", () => {
expect(computeScopeTooltip("HAI-005")).toBe("Blocked by HAI-005 (file overlap)"); expect(computeScopeTooltip("KB-005")).toBe("Blocked by KB-005 (file overlap)");
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import { TaskDetailModal } from "../TaskDetailModal"; import { TaskDetailModal } from "../TaskDetailModal";
import type { TaskDetail, Column, MergeResult, Task } from "@hai/core"; import type { TaskDetail, Column, MergeResult, Task } from "@kb/core";
vi.mock("../../api", () => ({ vi.mock("../../api", () => ({
uploadAttachment: vi.fn(), uploadAttachment: vi.fn(),
@@ -16,7 +16,7 @@ vi.mock("../../hooks/useAgentLogs", () => ({
function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail { function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
return { return {
id: "HAI-099", id: "KB-099",
description: "Test task", description: "Test task",
column: "in-progress" as Column, column: "in-progress" as Column,
dependencies: [], dependencies: [],
@@ -157,7 +157,7 @@ describe("TaskDetailModal", () => {
task={makeTask({ task={makeTask({
title: undefined, title: undefined,
description: "Fix the login bug", description: "Fix the login bug",
prompt: "# HAI-099\n\nFix the login bug\n", prompt: "# KB-099\n\nFix the login bug\n",
})} })}
onClose={noop} onClose={noop}
onMoveTask={noopMove} onMoveTask={noopMove}
@@ -167,13 +167,13 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// The heading "HAI-099" should be stripped from the markdown // The heading "KB-099" should be stripped from the markdown
const markdownBody = container.querySelector(".markdown-body"); const markdownBody = container.querySelector(".markdown-body");
expect(markdownBody?.innerHTML).not.toContain("HAI-099"); expect(markdownBody?.innerHTML).not.toContain("KB-099");
// Description appears in the markdown body // Description appears in the markdown body
expect(markdownBody?.textContent).toContain("Fix the login bug"); expect(markdownBody?.textContent).toContain("Fix the login bug");
// The detail header shows the ID (not duplicated as markdown heading) // The detail header shows the ID (not duplicated as markdown heading)
expect(container.querySelector(".detail-id")?.textContent).toBe("HAI-099"); expect(container.querySelector(".detail-id")?.textContent).toBe("KB-099");
// The h2 title shows description, not the task ID // The h2 title shows description, not the task ID
const h2 = container.querySelector("h2.detail-title"); const h2 = container.querySelector("h2.detail-title");
expect(h2?.textContent).toBe("Fix the login bug"); expect(h2?.textContent).toBe("Fix the login bug");
@@ -210,7 +210,7 @@ describe("TaskDetailModal", () => {
addToast={noop} addToast={noop}
/>, />,
); );
expect(withTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099"); expect(withTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
// Without title // Without title
const { container: withoutTitle } = render( const { container: withoutTitle } = render(
@@ -223,7 +223,7 @@ describe("TaskDetailModal", () => {
addToast={noop} addToast={noop}
/>, />,
); );
expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099"); expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
}); });
describe("paste image upload", () => { describe("paste image upload", () => {
@@ -267,7 +267,7 @@ describe("TaskDetailModal", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile); expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success"); expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
}); });
}); });
@@ -394,7 +394,7 @@ describe("TaskDetailModal", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile); expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success"); expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
}); });
}); });
@@ -418,7 +418,7 @@ describe("TaskDetailModal", () => {
it("renders dependency list when dependencies exist", () => { it("renders dependency list when dependencies exist", () => {
render( render(
<TaskDetailModal <TaskDetailModal
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })} task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
onClose={noop} onClose={noop}
onMoveTask={noopMove} onMoveTask={noopMove}
onDeleteTask={noopDelete} onDeleteTask={noopDelete}
@@ -427,16 +427,16 @@ describe("TaskDetailModal", () => {
/>, />,
); );
expect(screen.getByText("HAI-001")).toBeTruthy(); expect(screen.getByText("KB-001")).toBeTruthy();
expect(screen.getByText("HAI-002")).toBeTruthy(); expect(screen.getByText("KB-002")).toBeTruthy();
expect(screen.queryByText("(no dependencies)")).toBeNull(); expect(screen.queryByText("(no dependencies)")).toBeNull();
}); });
it("can add a dependency via the dropdown", async () => { it("can add a dependency via the dropdown", async () => {
const { updateTask } = await import("../../api"); const { updateTask } = await import("../../api");
const allTasks: Task[] = [ const allTasks: Task[] = [
{ id: "HAI-001", description: "Dep 1", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" }, { id: "KB-001", description: "Dep 1", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
{ id: "HAI-099", description: "Self", column: "in-progress" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" }, { id: "KB-099", description: "Self", column: "in-progress" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
]; ];
render( render(
@@ -452,16 +452,16 @@ describe("TaskDetailModal", () => {
); );
fireEvent.click(screen.getByText("Add Dependency")); fireEvent.click(screen.getByText("Add Dependency"));
// Should show HAI-001 in the dropdown but not HAI-099 (self is excluded) // Should show KB-001 in the dropdown but not KB-099 (self is excluded)
const dropdown = document.querySelector(".dep-dropdown")!; const dropdown = document.querySelector(".dep-dropdown")!;
expect(dropdown).toBeTruthy(); expect(dropdown).toBeTruthy();
expect(dropdown.textContent).toContain("HAI-001"); expect(dropdown.textContent).toContain("KB-001");
expect(dropdown.querySelectorAll(".dep-dropdown-item")).toHaveLength(1); expect(dropdown.querySelectorAll(".dep-dropdown-item")).toHaveLength(1);
fireEvent.click(screen.getByText("HAI-001")); fireEvent.click(screen.getByText("KB-001"));
await waitFor(() => { await waitFor(() => {
expect(updateTask).toHaveBeenCalledWith("HAI-099", { dependencies: ["HAI-001"] }); expect(updateTask).toHaveBeenCalledWith("KB-099", { dependencies: ["KB-001"] });
}); });
}); });
@@ -470,7 +470,7 @@ describe("TaskDetailModal", () => {
render( render(
<TaskDetailModal <TaskDetailModal
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })} task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
onClose={noop} onClose={noop}
onMoveTask={noopMove} onMoveTask={noopMove}
onDeleteTask={noopDelete} onDeleteTask={noopDelete}
@@ -480,10 +480,10 @@ describe("TaskDetailModal", () => {
); );
const removeButtons = screen.getAllByTitle(/Remove dependency/); const removeButtons = screen.getAllByTitle(/Remove dependency/);
fireEvent.click(removeButtons[0]); // Remove HAI-001 fireEvent.click(removeButtons[0]); // Remove KB-001
await waitFor(() => { await waitFor(() => {
expect(updateTask).toHaveBeenCalledWith("HAI-099", { dependencies: ["HAI-002"] }); expect(updateTask).toHaveBeenCalledWith("KB-099", { dependencies: ["KB-002"] });
}); });
}); });

View File

@@ -51,7 +51,7 @@ afterEach(() => {
describe("useAgentLogs", () => { describe("useAgentLogs", () => {
it("does not fetch or connect when enabled=false", () => { it("does not fetch or connect when enabled=false", () => {
const { result } = renderHook(() => useAgentLogs("HAI-001", false)); const { result } = renderHook(() => useAgentLogs("KB-001", false));
expect(mockFetchAgentLogs).not.toHaveBeenCalled(); expect(mockFetchAgentLogs).not.toHaveBeenCalled();
expect(MockEventSource.instances).toHaveLength(0); expect(MockEventSource.instances).toHaveLength(0);
@@ -60,27 +60,27 @@ describe("useAgentLogs", () => {
it("fetches historical logs and opens SSE when enabled=true", async () => { it("fetches historical logs and opens SSE when enabled=true", async () => {
const historicalLogs = [ const historicalLogs = [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "old", type: "text" as const }, { timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "old", type: "text" as const },
]; ];
mockFetchAgentLogs.mockResolvedValueOnce(historicalLogs); mockFetchAgentLogs.mockResolvedValueOnce(historicalLogs);
const { result } = renderHook(() => useAgentLogs("HAI-001", true)); const { result } = renderHook(() => useAgentLogs("KB-001", true));
await waitFor(() => { await waitFor(() => {
expect(result.current.entries).toEqual(historicalLogs); expect(result.current.entries).toEqual(historicalLogs);
}); });
expect(mockFetchAgentLogs).toHaveBeenCalledWith("HAI-001"); expect(mockFetchAgentLogs).toHaveBeenCalledWith("KB-001");
expect(MockEventSource.instances).toHaveLength(1); expect(MockEventSource.instances).toHaveLength(1);
expect(MockEventSource.instances[0].url).toBe("/api/tasks/HAI-001/logs/stream"); expect(MockEventSource.instances[0].url).toBe("/api/tasks/KB-001/logs/stream");
}); });
it("appends live SSE entries to historical entries", async () => { it("appends live SSE entries to historical entries", async () => {
mockFetchAgentLogs.mockResolvedValueOnce([ mockFetchAgentLogs.mockResolvedValueOnce([
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "old", type: "text" as const }, { timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "old", type: "text" as const },
]); ]);
const { result } = renderHook(() => useAgentLogs("HAI-001", true)); const { result } = renderHook(() => useAgentLogs("KB-001", true));
await waitFor(() => { await waitFor(() => {
expect(result.current.entries).toHaveLength(1); expect(result.current.entries).toHaveLength(1);
@@ -90,7 +90,7 @@ describe("useAgentLogs", () => {
act(() => { act(() => {
es._emit("agent:log", { es._emit("agent:log", {
timestamp: "2026-01-01T00:01:00Z", timestamp: "2026-01-01T00:01:00Z",
taskId: "HAI-001", taskId: "KB-001",
text: "new", text: "new",
type: "text", type: "text",
}); });
@@ -104,7 +104,7 @@ describe("useAgentLogs", () => {
mockFetchAgentLogs.mockResolvedValueOnce([]); mockFetchAgentLogs.mockResolvedValueOnce([]);
const { rerender } = renderHook( const { rerender } = renderHook(
({ enabled }) => useAgentLogs("HAI-001", enabled), ({ enabled }) => useAgentLogs("KB-001", enabled),
{ initialProps: { enabled: true } }, { initialProps: { enabled: true } },
); );
@@ -122,7 +122,7 @@ describe("useAgentLogs", () => {
it("closes SSE on unmount", async () => { it("closes SSE on unmount", async () => {
mockFetchAgentLogs.mockResolvedValueOnce([]); mockFetchAgentLogs.mockResolvedValueOnce([]);
const { unmount } = renderHook(() => useAgentLogs("HAI-001", true)); const { unmount } = renderHook(() => useAgentLogs("KB-001", true));
await waitFor(() => { await waitFor(() => {
expect(MockEventSource.instances).toHaveLength(1); expect(MockEventSource.instances).toHaveLength(1);

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import type { AgentLogEntry } from "@hai/core"; import type { AgentLogEntry } from "@kb/core";
import { fetchAgentLogs } from "../api"; import { fetchAgentLogs } from "../api";
/** /**

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import type { Task, Column, TaskCreateInput, MergeResult } from "@hai/core"; import type { Task, Column, TaskCreateInput, MergeResult } from "@kb/core";
import * as api from "../api"; import * as api from "../api";
export function useTasks() { export function useTasks() {

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>hai | board</title> <title>kb | board</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg" /> <link rel="icon" type="image/svg+xml" href="/logo.svg" />
</head> </head>
<body> <body>

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { groupByWorktree, getWorktreeLabel } from "./worktreeGrouping"; import { groupByWorktree, getWorktreeLabel } from "./worktreeGrouping";
import type { Task } from "@hai/core"; import type { Task } from "@kb/core";
function makeTask(overrides: Partial<Task> & { id: string }): Task { function makeTask(overrides: Partial<Task> & { id: string }): Task {
return { return {
@@ -18,8 +18,8 @@ function makeTask(overrides: Partial<Task> & { id: string }): Task {
describe("getWorktreeLabel", () => { describe("getWorktreeLabel", () => {
it("extracts last path segment", () => { it("extracts last path segment", () => {
expect(getWorktreeLabel(".worktrees/HAI-001")).toBe("HAI-001"); expect(getWorktreeLabel(".worktrees/KB-001")).toBe("KB-001");
expect(getWorktreeLabel("/path/to/hai/hai-001")).toBe("hai-001"); expect(getWorktreeLabel("/path/to/kb/kb-001")).toBe("kb-001");
}); });
it("extracts humanized worktree names", () => { it("extracts humanized worktree names", () => {
@@ -31,8 +31,8 @@ describe("getWorktreeLabel", () => {
describe("groupByWorktree", () => { describe("groupByWorktree", () => {
it("groups active in-progress tasks by worktree", () => { it("groups active in-progress tasks by worktree", () => {
const t1 = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const t1 = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const t2 = makeTask({ id: "HAI-002", worktree: ".worktrees/quiet-robin" }); const t2 = makeTask({ id: "KB-002", worktree: ".worktrees/quiet-robin" });
const groups = groupByWorktree([t1, t2], [t1, t2], 2); const groups = groupByWorktree([t1, t2], [t1, t2], 2);
@@ -44,9 +44,9 @@ describe("groupByWorktree", () => {
}); });
it("places queued tasks only in the Up Next group, never in worktree groups", () => { it("places queued tasks only in the Up Next group, never in worktree groups", () => {
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const queued = makeTask({ const queued = makeTask({
id: "HAI-002", id: "KB-002",
column: "todo", column: "todo",
dependencies: [], dependencies: [],
}); });
@@ -66,7 +66,7 @@ describe("groupByWorktree", () => {
}); });
it("does not create Up Next group when there are no eligible queued tasks", () => { it("does not create Up Next group when there are no eligible queued tasks", () => {
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const groups = groupByWorktree([active], [active], 2); const groups = groupByWorktree([active], [active], 2);
@@ -74,11 +74,11 @@ describe("groupByWorktree", () => {
}); });
it("does not create Up Next when queued tasks have unsatisfied dependencies", () => { it("does not create Up Next when queued tasks have unsatisfied dependencies", () => {
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const blocked = makeTask({ const blocked = makeTask({
id: "HAI-002", id: "KB-002",
column: "todo", column: "todo",
dependencies: ["HAI-003"], // HAI-003 doesn't exist or isn't done dependencies: ["KB-003"], // KB-003 doesn't exist or isn't done
}); });
const groups = groupByWorktree([active], [active, blocked], 2); const groups = groupByWorktree([active], [active, blocked], 2);
@@ -87,10 +87,10 @@ describe("groupByWorktree", () => {
}); });
it("respects maxConcurrent limit on queued tasks shown", () => { it("respects maxConcurrent limit on queued tasks shown", () => {
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const q1 = makeTask({ id: "HAI-010", column: "todo" }); const q1 = makeTask({ id: "KB-010", column: "todo" });
const q2 = makeTask({ id: "HAI-011", column: "todo" }); const q2 = makeTask({ id: "KB-011", column: "todo" });
const q3 = makeTask({ id: "HAI-012", column: "todo" }); const q3 = makeTask({ id: "KB-012", column: "todo" });
const groups = groupByWorktree([active], [active, q1, q2, q3], 2); const groups = groupByWorktree([active], [active, q1, q2, q3], 2);
@@ -100,7 +100,7 @@ describe("groupByWorktree", () => {
}); });
it("places unassigned in-progress tasks in Unassigned group", () => { it("places unassigned in-progress tasks in Unassigned group", () => {
const unassigned = makeTask({ id: "HAI-001" }); // no worktree const unassigned = makeTask({ id: "KB-001" }); // no worktree
const groups = groupByWorktree([unassigned], [unassigned], 2); const groups = groupByWorktree([unassigned], [unassigned], 2);
@@ -110,15 +110,15 @@ describe("groupByWorktree", () => {
}); });
it("excludes paused todo tasks from Up Next", () => { it("excludes paused todo tasks from Up Next", () => {
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" }); const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
const paused = makeTask({ const paused = makeTask({
id: "HAI-002", id: "KB-002",
column: "todo", column: "todo",
dependencies: [], dependencies: [],
paused: true, paused: true,
}); });
const normal = makeTask({ const normal = makeTask({
id: "HAI-003", id: "KB-003",
column: "todo", column: "todo",
dependencies: [], dependencies: [],
}); });
@@ -127,16 +127,16 @@ describe("groupByWorktree", () => {
const upNext = groups.find((g) => g.label === "Up Next"); const upNext = groups.find((g) => g.label === "Up Next");
expect(upNext).toBeDefined(); expect(upNext).toBeDefined();
expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["HAI-003"]); expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["KB-003"]);
expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("HAI-002"); expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("KB-002");
}); });
it("queued tasks with satisfied deps appear in Up Next", () => { it("queued tasks with satisfied deps appear in Up Next", () => {
const done = makeTask({ id: "HAI-001", column: "done" }); const done = makeTask({ id: "KB-001", column: "done" });
const queued = makeTask({ const queued = makeTask({
id: "HAI-002", id: "KB-002",
column: "todo", column: "todo",
dependencies: ["HAI-001"], dependencies: ["KB-001"],
}); });
const groups = groupByWorktree([], [done, queued], 2); const groups = groupByWorktree([], [done, queued], 2);

View File

@@ -1,4 +1,4 @@
import type { Task } from "@hai/core"; import type { Task } from "@kb/core";
export interface WorktreeGroupData { export interface WorktreeGroupData {
label: string; label: string;
@@ -8,7 +8,7 @@ export interface WorktreeGroupData {
/** /**
* Extract a clean display name from a worktree path. * Extract a clean display name from a worktree path.
* e.g. ".worktrees/HAI-001" → "HAI-001", "/path/to/hai/hai-001" → "hai-001" * e.g. ".worktrees/KB-001" → "KB-001", "/path/to/kb/kb-001" → "kb-001"
*/ */
export function getWorktreeLabel(worktreePath: string): string { export function getWorktreeLabel(worktreePath: string): string {
// Take the last segment of the path // Take the last segment of the path
@@ -18,8 +18,8 @@ export function getWorktreeLabel(worktreePath: string): string {
/** /**
* Topological sort of tasks by dependency order. * Topological sort of tasks by dependency order.
* Mirrors resolveDependencyOrder from @hai/core but inlined to avoid * Mirrors resolveDependencyOrder from @kb/core but inlined to avoid
* build alias issues (Vite aliases @hai/core to types.ts only). * build alias issues (Vite aliases @kb/core to types.ts only).
*/ */
function resolveDependencyOrder(tasks: Task[]): string[] { function resolveDependencyOrder(tasks: Task[]): string[] {
const taskMap = new Map(tasks.map((t) => [t.id, t])); const taskMap = new Map(tasks.map((t) => [t.id, t]));

View File

@@ -1,5 +1,5 @@
{ {
"name": "@hai/dashboard", "name": "@kb/dashboard",
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -22,7 +22,7 @@
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json" "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
}, },
"dependencies": { "dependencies": {
"@hai/core": "workspace:*", "@kb/core": "workspace:*",
"@types/multer": "^2.1.0", "@types/multer": "^2.1.0",
"express": "^5.1.0", "express": "^5.1.0",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",

View File

@@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>hai | board</title> <title>kb | board</title>
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
</head> </head>
<body> <body>
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
<h1 class="logo">hai</h1> <h1 class="logo">kb</h1>
<span class="logo-sub">board</span> <span class="logo-sub">board</span>
</div> </div>
<button class="btn btn-primary" id="add-task-btn">+ New Task</button> <button class="btn btn-primary" id="add-task-btn">+ New Task</button>
@@ -96,7 +96,7 @@
>Dependencies >Dependencies
<span class="optional">(comma-separated IDs)</span></label <span class="optional">(comma-separated IDs)</span></label
> >
<input type="text" id="task-deps" placeholder="HAI-001, HAI-002" /> <input type="text" id="task-deps" placeholder="KB-001, KB-002" />
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="btn" data-close="create-modal"> <button type="button" class="btn" data-close="create-modal">

View File

@@ -2,8 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import express from "express"; import express from "express";
import http from "node:http"; import http from "node:http";
import { createApiRoutes } from "./routes.js"; import { createApiRoutes } from "./routes.js";
import type { TaskStore, TaskAttachment } from "@hai/core"; import type { TaskStore, TaskAttachment } from "@kb/core";
import type { TaskDetail } from "@hai/core"; import type { TaskDetail } from "@kb/core";
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js"; import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore { function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
@@ -24,7 +24,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
} }
const FAKE_TASK_DETAIL: TaskDetail = { const FAKE_TASK_DETAIL: TaskDetail = {
id: "HAI-001", id: "KB-001",
description: "Test task", description: "Test task",
column: "in-progress", column: "in-progress",
dependencies: [], dependencies: [],
@@ -33,7 +33,7 @@ const FAKE_TASK_DETAIL: TaskDetail = {
log: [], log: [],
createdAt: "2026-01-01T00:00:00.000Z", createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z",
prompt: "# HAI-001\n\nTest task", prompt: "# KB-001\n\nTest task",
}; };
/** Helper: send GET and return { status, body } */ /** Helper: send GET and return { status, body } */
@@ -117,11 +117,11 @@ describe("GET /tasks/:id", () => {
it("returns task detail on success", async () => { it("returns task detail on success", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_TASK_DETAIL); (store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_TASK_DETAIL);
const res = await GET(buildApp(), "/api/tasks/HAI-001"); const res = await GET(buildApp(), "/api/tasks/KB-001");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.id).toBe("HAI-001"); expect(res.body.id).toBe("KB-001");
expect(res.body.prompt).toBe("# HAI-001\n\nTest task"); expect(res.body.prompt).toBe("# KB-001\n\nTest task");
}); });
it("returns 404 when task genuinely does not exist (ENOENT)", async () => { it("returns 404 when task genuinely does not exist (ENOENT)", async () => {
@@ -129,7 +129,7 @@ describe("GET /tasks/:id", () => {
err.code = "ENOENT"; err.code = "ENOENT";
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err); (store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err);
const res = await GET(buildApp(), "/api/tasks/HAI-999"); const res = await GET(buildApp(), "/api/tasks/KB-999");
expect(res.status).toBe(404); expect(res.status).toBe(404);
expect(res.body.error).toContain("not found"); expect(res.body.error).toContain("not found");
@@ -139,7 +139,7 @@ describe("GET /tasks/:id", () => {
const err = new Error("Unexpected end of JSON input"); const err = new Error("Unexpected end of JSON input");
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err); (store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err);
const res = await GET(buildApp(), "/api/tasks/HAI-001"); const res = await GET(buildApp(), "/api/tasks/KB-001");
expect(res.status).toBe(500); expect(res.status).toBe(500);
expect(res.body.error).toContain("Unexpected end of JSON input"); expect(res.body.error).toContain("Unexpected end of JSON input");
@@ -167,20 +167,20 @@ describe("POST /tasks/:id/retry", () => {
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTask); (store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTask);
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask); (store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json", "Content-Type": "application/json",
}); });
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: undefined }); expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined });
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
}); });
it("returns 400 when task is not in failed state", async () => { it("returns 400 when task is not in failed state", async () => {
const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" }; const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(activeTask); (store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(activeTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json", "Content-Type": "application/json",
}); });
@@ -192,7 +192,7 @@ describe("POST /tasks/:id/retry", () => {
const doneTask = { ...FAKE_TASK_DETAIL, column: "done", status: "failed" }; const doneTask = { ...FAKE_TASK_DETAIL, column: "done", status: "failed" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask); (store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json", "Content-Type": "application/json",
}); });
@@ -216,32 +216,32 @@ describe("PATCH /tasks/:id", () => {
} }
it("forwards dependencies to store.updateTask", async () => { it("forwards dependencies to store.updateTask", async () => {
const updatedTask = { ...FAKE_TASK_DETAIL, dependencies: ["HAI-002"] }; const updatedTask = { ...FAKE_TASK_DETAIL, dependencies: ["KB-002"] };
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask); (store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/HAI-001", JSON.stringify({ dependencies: ["HAI-002"] }), { const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ dependencies: ["KB-002"] }), {
"Content-Type": "application/json", "Content-Type": "application/json",
}); });
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
title: undefined, title: undefined,
description: undefined, description: undefined,
prompt: undefined, prompt: undefined,
dependencies: ["HAI-002"], dependencies: ["KB-002"],
}); });
expect(res.body.dependencies).toEqual(["HAI-002"]); expect(res.body.dependencies).toEqual(["KB-002"]);
}); });
it("forwards title and description without dependencies", async () => { it("forwards title and description without dependencies", async () => {
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({ ...FAKE_TASK_DETAIL, title: "New" }); (store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({ ...FAKE_TASK_DETAIL, title: "New" });
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/HAI-001", JSON.stringify({ title: "New" }), { const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ title: "New" }), {
"Content-Type": "application/json", "Content-Type": "application/json",
}); });
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
title: "New", title: "New",
description: undefined, description: undefined,
prompt: undefined, prompt: undefined,
@@ -280,14 +280,14 @@ describe("Attachment routes", () => {
const content = Buffer.from("fake png content"); const content = Buffer.from("fake png content");
const { body, boundary } = buildMultipart("file", "screenshot.png", "image/png", content); const { body, boundary } = buildMultipart("file", "screenshot.png", "image/png", content);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
"Content-Type": `multipart/form-data; boundary=${boundary}`, "Content-Type": `multipart/form-data; boundary=${boundary}`,
}); });
expect(res.status).toBe(201); expect(res.status).toBe(201);
expect(res.body.filename).toBe("1234-screenshot.png"); expect(res.body.filename).toBe("1234-screenshot.png");
expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith( expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"HAI-001", "KB-001",
"screenshot.png", "screenshot.png",
expect.any(Buffer), expect.any(Buffer),
"image/png", "image/png",
@@ -302,7 +302,7 @@ describe("Attachment routes", () => {
const content = Buffer.from("not an image"); const content = Buffer.from("not an image");
const { body, boundary } = buildMultipart("file", "file.txt", "text/plain", content); const { body, boundary } = buildMultipart("file", "file.txt", "text/plain", content);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
"Content-Type": `multipart/form-data; boundary=${boundary}`, "Content-Type": `multipart/form-data; boundary=${boundary}`,
}); });
@@ -318,7 +318,7 @@ describe("Attachment routes", () => {
const content = Buffer.from("small but store rejects"); const content = Buffer.from("small but store rejects");
const { body, boundary } = buildMultipart("file", "big.png", "image/png", content); const { body, boundary } = buildMultipart("file", "big.png", "image/png", content);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, { const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
"Content-Type": `multipart/form-data; boundary=${boundary}`, "Content-Type": `multipart/form-data; boundary=${boundary}`,
}); });
@@ -327,10 +327,10 @@ describe("Attachment routes", () => {
}); });
it("DELETE /tasks/:id/attachments/:filename — deletes attachment", async () => { it("DELETE /tasks/:id/attachments/:filename — deletes attachment", async () => {
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/HAI-001/attachments/1234-screenshot.png"); const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001/attachments/1234-screenshot.png");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("HAI-001", "1234-screenshot.png"); expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("KB-001", "1234-screenshot.png");
}); });
it("DELETE /tasks/:id/attachments/:filename — returns 404 for missing", async () => { it("DELETE /tasks/:id/attachments/:filename — returns 404 for missing", async () => {
@@ -338,29 +338,29 @@ describe("Attachment routes", () => {
err.code = "ENOENT"; err.code = "ENOENT";
(store.deleteAttachment as ReturnType<typeof vi.fn>).mockRejectedValue(err); (store.deleteAttachment as ReturnType<typeof vi.fn>).mockRejectedValue(err);
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/HAI-001/attachments/nope.png"); const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001/attachments/nope.png");
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });
it("GET /tasks/:id/logs — returns agent logs", async () => { it("GET /tasks/:id/logs — returns agent logs", async () => {
const fakeLogs = [ const fakeLogs = [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "Hello", type: "text" }, { timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "Hello", type: "text" },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "HAI-001", text: "Read", type: "tool" }, { timestamp: "2026-01-01T00:00:01Z", taskId: "KB-001", text: "Read", type: "tool" },
]; ];
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue(fakeLogs); (store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue(fakeLogs);
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs"); const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body).toEqual(fakeLogs); expect(res.body).toEqual(fakeLogs);
expect(store.getAgentLogs).toHaveBeenCalledWith("HAI-001"); expect(store.getAgentLogs).toHaveBeenCalledWith("KB-001");
}); });
it("GET /tasks/:id/logs — returns empty array when no logs", async () => { it("GET /tasks/:id/logs — returns empty array when no logs", async () => {
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue([]); (store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs"); const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body).toEqual([]); expect(res.body).toEqual([]);
@@ -369,7 +369,7 @@ describe("Attachment routes", () => {
it("GET /tasks/:id/logs — returns 500 on store error", async () => { it("GET /tasks/:id/logs — returns 500 on store error", async () => {
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("disk error")); (store.getAgentLogs as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("disk error"));
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs"); const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
expect(res.status).toBe(500); expect(res.status).toBe(500);
expect(res.body.error).toBe("disk error"); expect(res.body.error).toBe("disk error");
@@ -631,27 +631,27 @@ describe("Pause/Unpause endpoints", () => {
beforeEach(() => { beforeEach(() => {
store = createMockStore({ store = createMockStore({
pauseTask: vi.fn().mockResolvedValue({ id: "HAI-001", paused: true }), pauseTask: vi.fn().mockResolvedValue({ id: "KB-001", paused: true }),
}); });
}); });
it("POST /tasks/:id/pause — pauses a task", async () => { it("POST /tasks/:id/pause — pauses a task", async () => {
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/pause"); const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body).toEqual({ id: "HAI-001", paused: true }); expect(res.body).toEqual({ id: "KB-001", paused: true });
expect(store.pauseTask).toHaveBeenCalledWith("HAI-001", true); expect(store.pauseTask).toHaveBeenCalledWith("KB-001", true);
}); });
it("POST /tasks/:id/unpause — unpauses a task", async () => { it("POST /tasks/:id/unpause — unpauses a task", async () => {
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "HAI-001" }); (store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "KB-001" });
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/unpause"); const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/unpause");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.pauseTask).toHaveBeenCalledWith("HAI-001", false); expect(store.pauseTask).toHaveBeenCalledWith("KB-001", false);
}); });
it("POST /tasks/:id/pause — returns 500 on error", async () => { it("POST /tasks/:id/pause — returns 500 on error", async () => {
(store.pauseTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("not found")); (store.pauseTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("not found"));
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/pause"); const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
expect(res.status).toBe(500); expect(res.status).toBe(500);
expect(res.body.error).toBe("not found"); expect(res.body.error).toBe("not found");
}); });

View File

@@ -1,8 +1,8 @@
import { Router } from "express"; import { Router } from "express";
import multer from "multer"; import multer from "multer";
import { createReadStream } from "node:fs"; import { createReadStream } from "node:fs";
import type { TaskStore, Column, MergeResult } from "@hai/core"; import type { TaskStore, Column, MergeResult } from "@kb/core";
import { COLUMNS } from "@hai/core"; import { COLUMNS } from "@kb/core";
import type { ServerOptions } from "./server.js"; import type { ServerOptions } from "./server.js";
/** /**

View File

@@ -2,7 +2,7 @@ import express from "express";
import { join, dirname } from "node:path"; import { join, dirname } from "node:path";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import type { TaskStore, MergeResult } from "@hai/core"; import type { TaskStore, MergeResult } from "@kb/core";
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js"; import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
import { createApiRoutes } from "./routes.js"; import { createApiRoutes } from "./routes.js";
import { createSSE } from "./sse.js"; import { createSSE } from "./sse.js";
@@ -27,14 +27,14 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
// Serve built React app // Serve built React app
// Resolution order: // Resolution order:
// 1. HAI_CLIENT_DIR env override (explicit) // 1. KB_CLIENT_DIR env override (explicit)
// 2. Next to process.execPath (bun-compiled binary: dist/hai + dist/client/) // 2. Next to process.execPath (bun-compiled binary: dist/kb + dist/client/)
// 3. __dirname/../dist/client (running from src/ via tsx/ts-node) // 3. __dirname/../dist/client (running from src/ via tsx/ts-node)
// 4. __dirname/../client (running from dist/ after tsc) // 4. __dirname/../client (running from dist/ after tsc)
// 5. __dirname/../public (fallback for dev) // 5. __dirname/../public (fallback for dev)
const execDir = dirname(process.execPath); const execDir = dirname(process.execPath);
const clientDir = process.env.HAI_CLIENT_DIR const clientDir = process.env.KB_CLIENT_DIR
? process.env.HAI_CLIENT_DIR ? process.env.KB_CLIENT_DIR
: existsSync(join(execDir, "client", "index.html")) : existsSync(join(execDir, "client", "index.html"))
? join(execDir, "client") ? join(execDir, "client")
: existsSync(join(__dirname, "..", "dist", "client")) : existsSync(join(__dirname, "..", "dist", "client"))

View File

@@ -1,5 +1,5 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import type { TaskStore } from "@hai/core"; import type { TaskStore } from "@kb/core";
export function createSSE(store: TaskStore) { export function createSSE(store: TaskStore) {
return (_req: Request, res: Response) => { return (_req: Request, res: Response) => {

View File

@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
"@hai/core": resolve(__dirname, "../core/src/types.ts"), "@kb/core": resolve(__dirname, "../core/src/types.ts"),
}, },
}, },
build: { build: {

View File

@@ -6,7 +6,7 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
"@hai/core": resolve(__dirname, "../core/src/types.ts"), "@kb/core": resolve(__dirname, "../core/src/types.ts"),
}, },
}, },
test: { test: {

View File

@@ -1,5 +1,5 @@
{ {
"name": "@hai/engine", "name": "@kb/engine",
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -21,7 +21,7 @@
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@hai/core": "workspace:*", "@kb/core": "workspace:*",
"@mariozechner/pi-ai": "^0.62.0", "@mariozechner/pi-ai": "^0.62.0",
"@mariozechner/pi-coding-agent": "^0.62.0" "@mariozechner/pi-coding-agent": "^0.62.0"
}, },

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { AgentLogger, summarizeToolArgs } from "./agent-logger.js"; import { AgentLogger, summarizeToolArgs } from "./agent-logger.js";
import type { TaskStore } from "@hai/core"; import type { TaskStore } from "@kb/core";
// ── summarizeToolArgs tests ────────────────────────────────────────── // ── summarizeToolArgs tests ──────────────────────────────────────────
@@ -62,7 +62,7 @@ describe("AgentLogger", () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-001", taskId: "KB-001",
flushSizeBytes: 10, flushSizeBytes: 10,
flushIntervalMs: 500, flushIntervalMs: 500,
}); });
@@ -75,14 +75,14 @@ describe("AgentLogger", () => {
logger.onText("worldextra"); logger.onText("worldextra");
// Allow async flush // Allow async flush
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-001", "helloworldextra", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-001", "helloworldextra", "text");
}); });
it("flushes on timer when under size threshold", async () => { it("flushes on timer when under size threshold", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-002", taskId: "KB-002",
flushSizeBytes: 1024, flushSizeBytes: 1024,
flushIntervalMs: 500, flushIntervalMs: 500,
}); });
@@ -91,14 +91,14 @@ describe("AgentLogger", () => {
expect(store.appendAgentLog).not.toHaveBeenCalled(); expect(store.appendAgentLog).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(500); await vi.advanceTimersByTimeAsync(500);
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-002", "small", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-002", "small", "text");
}); });
it("flushes text before logging tool start", async () => { it("flushes text before logging tool start", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-003", taskId: "KB-003",
flushSizeBytes: 1024, flushSizeBytes: 1024,
}); });
@@ -110,36 +110,36 @@ describe("AgentLogger", () => {
const calls = (store.appendAgentLog as ReturnType<typeof vi.fn>).mock.calls; const calls = (store.appendAgentLog as ReturnType<typeof vi.fn>).mock.calls;
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
// Text flushed first // Text flushed first
expect(calls[0]).toEqual(["HAI-003", "pending text", "text"]); expect(calls[0]).toEqual(["KB-003", "pending text", "text"]);
// Tool logged second with detail // Tool logged second with detail
expect(calls[1]).toEqual(["HAI-003", "Bash", "tool", "ls"]); expect(calls[1]).toEqual(["KB-003", "Bash", "tool", "ls"]);
}); });
it("logs tool detail using summarizeToolArgs", async () => { it("logs tool detail using summarizeToolArgs", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ store, taskId: "HAI-004" }); const logger = new AgentLogger({ store, taskId: "KB-004" });
logger.onToolStart("Read", { path: "src/index.ts" }); logger.onToolStart("Read", { path: "src/index.ts" });
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-004", "Read", "tool", "src/index.ts"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-004", "Read", "tool", "src/index.ts");
}); });
it("logs tool with undefined detail for unknown args", async () => { it("logs tool with undefined detail for unknown args", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ store, taskId: "HAI-005" }); const logger = new AgentLogger({ store, taskId: "KB-005" });
logger.onToolStart("task_done", { count: 42 }); logger.onToolStart("task_done", { count: 42 });
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-005", "task_done", "tool", undefined); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-005", "task_done", "tool", undefined);
}); });
it("flush() clears timer and writes remaining text", async () => { it("flush() clears timer and writes remaining text", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-006", taskId: "KB-006",
flushSizeBytes: 1024, flushSizeBytes: 1024,
flushIntervalMs: 500, flushIntervalMs: 500,
}); });
@@ -147,12 +147,12 @@ describe("AgentLogger", () => {
logger.onText("remaining"); logger.onText("remaining");
await logger.flush(); await logger.flush();
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-006", "remaining", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-006", "remaining", "text");
}); });
it("flush() is safe to call when buffer is empty", async () => { it("flush() is safe to call when buffer is empty", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ store, taskId: "HAI-007" }); const logger = new AgentLogger({ store, taskId: "KB-007" });
await logger.flush(); await logger.flush();
expect(store.appendAgentLog).not.toHaveBeenCalled(); expect(store.appendAgentLog).not.toHaveBeenCalled();
@@ -164,23 +164,23 @@ describe("AgentLogger", () => {
const onAgentTool = vi.fn(); const onAgentTool = vi.fn();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-008", taskId: "KB-008",
onAgentText, onAgentText,
onAgentTool, onAgentTool,
}); });
logger.onText("delta"); logger.onText("delta");
expect(onAgentText).toHaveBeenCalledWith("HAI-008", "delta"); expect(onAgentText).toHaveBeenCalledWith("KB-008", "delta");
logger.onToolStart("Bash", { command: "echo hi" }); logger.onToolStart("Bash", { command: "echo hi" });
expect(onAgentTool).toHaveBeenCalledWith("HAI-008", "Bash"); expect(onAgentTool).toHaveBeenCalledWith("KB-008", "Bash");
}); });
it("does not schedule multiple timers for consecutive small writes", async () => { it("does not schedule multiple timers for consecutive small writes", async () => {
const store = createMockStore(); const store = createMockStore();
const logger = new AgentLogger({ const logger = new AgentLogger({
store, store,
taskId: "HAI-009", taskId: "KB-009",
flushSizeBytes: 1024, flushSizeBytes: 1024,
flushIntervalMs: 500, flushIntervalMs: 500,
}); });
@@ -193,6 +193,6 @@ describe("AgentLogger", () => {
// All text should be flushed in a single call // All text should be flushed in a single call
expect(store.appendAgentLog).toHaveBeenCalledTimes(1); expect(store.appendAgentLog).toHaveBeenCalledTimes(1);
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-009", "abc", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-009", "abc", "text");
}); });
}); });

View File

@@ -1,4 +1,4 @@
import type { TaskStore } from "@hai/core"; import type { TaskStore } from "@kb/core";
/** Default byte threshold before an automatic flush. */ /** Default byte threshold before an automatic flush. */
const FLUSH_SIZE_BYTES = 1024; const FLUSH_SIZE_BYTES = 1024;
@@ -56,12 +56,12 @@ export interface AgentLoggerOptions {
* detailed argument summaries via {@link summarizeToolArgs}. * detailed argument summaries via {@link summarizeToolArgs}.
* *
* Produces `onText` and `onToolStart` callbacks compatible with * Produces `onText` and `onToolStart` callbacks compatible with
* `createHaiAgent`'s `AgentOptions` interface. * `createKbAgent`'s `AgentOptions` interface.
* *
* @example * @example
* ```ts * ```ts
* const logger = new AgentLogger({ store, taskId, onAgentText, onAgentTool }); * const logger = new AgentLogger({ store, taskId, onAgentText, onAgentTool });
* const { session } = await createHaiAgent({ * const { session } = await createKbAgent({
* cwd: worktreePath, * cwd: worktreePath,
* onText: logger.onText, * onText: logger.onText,
* onToolStart: logger.onToolStart, * onToolStart: logger.onToolStart,

View File

@@ -3,7 +3,7 @@ import { AgentSemaphore } from "./concurrency.js";
// Mock external dependencies // Mock external dependencies
vi.mock("./pi.js", () => ({ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(), createKbAgent: vi.fn(),
})); }));
vi.mock("./reviewer.js", () => ({ vi.mock("./reviewer.js", () => ({
reviewStep: vi.fn(), reviewStep: vi.fn(),
@@ -28,14 +28,14 @@ vi.mock("node:fs", () => ({
})); }));
import { TaskExecutor, buildExecutionPrompt } from "./executor.js"; import { TaskExecutor, buildExecutionPrompt } from "./executor.js";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { findWorktreeUser, aiMergeTask } from "./merger.js"; import { findWorktreeUser, aiMergeTask } from "./merger.js";
import { WorktreePool } from "./worktree-pool.js"; import { WorktreePool } from "./worktree-pool.js";
import { generateWorktreeName } from "./worktree-names.js"; import { generateWorktreeName } from "./worktree-names.js";
import type { Column, Task, TaskDetail } from "@hai/core"; import type { Column, Task, TaskDetail } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent); const mockedCreateHaiAgent = vi.mocked(createKbAgent);
function createMockStore() { function createMockStore() {
const listeners = new Map<string, Function[]>(); const listeners = new Map<string, Function[]>();
@@ -52,7 +52,7 @@ function createMockStore() {
emit: vi.fn(), emit: vi.fn(),
listTasks: vi.fn().mockResolvedValue([]), listTasks: vi.fn().mockResolvedValue([]),
getTask: vi.fn().mockResolvedValue({ getTask: vi.fn().mockResolvedValue({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test task", description: "Test task",
column: "in-progress", column: "in-progress",
@@ -102,7 +102,7 @@ describe("TaskExecutor with semaphore", () => {
const executor = new TaskExecutor(store, "/tmp/test", { semaphore: sem }); const executor = new TaskExecutor(store, "/tmp/test", { semaphore: sem });
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -132,7 +132,7 @@ describe("TaskExecutor with semaphore", () => {
}); });
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -157,7 +157,7 @@ describe("TaskExecutor with semaphore", () => {
const executor = new TaskExecutor(store, "/tmp/test", { onError }); const executor = new TaskExecutor(store, "/tmp/test", { onError });
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -169,7 +169,7 @@ describe("TaskExecutor with semaphore", () => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: "failed" }); expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: "failed" });
expect(onError).toHaveBeenCalled(); expect(onError).toHaveBeenCalled();
}); });
@@ -209,9 +209,9 @@ describe("TaskExecutor with semaphore", () => {
}); });
await Promise.all([ await Promise.all([
executor.execute(task("HAI-001")), executor.execute(task("KB-001")),
executor.execute(task("HAI-002")), executor.execute(task("KB-002")),
executor.execute(task("HAI-003")), executor.execute(task("KB-003")),
]); ]);
expect(maxConcurrent).toBe(1); expect(maxConcurrent).toBe(1);
@@ -224,7 +224,7 @@ const { existsSync: mockedExistsSyncRaw } = await import("node:fs");
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw); const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
describe("TaskExecutor worktreeInitCommand", () => { describe("TaskExecutor worktreeInitCommand", () => {
const makeTask = (id = "HAI-010") => ({ const makeTask = (id = "KB-010") => ({
id, id,
title: "Test", title: "Test",
description: "Test", description: "Test",
@@ -275,7 +275,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
// Should log success // Should log success
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"HAI-010", "KB-010",
"Worktree init command completed", "Worktree init command completed",
"pnpm install", "pnpm install",
); );
@@ -322,7 +322,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
// Should log the failure // Should log the failure
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"HAI-010", "KB-010",
expect.stringContaining("Worktree init command failed"), expect.stringContaining("Worktree init command failed"),
); );
@@ -356,7 +356,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
}); });
describe("TaskExecutor worktree naming", () => { describe("TaskExecutor worktree naming", () => {
const makeTask = (id = "HAI-030", worktree?: string) => ({ const makeTask = (id = "KB-030", worktree?: string) => ({
id, id,
title: "Test", title: "Test",
description: "Test", description: "Test",
@@ -391,7 +391,7 @@ describe("TaskExecutor worktree naming", () => {
await executor.execute(makeTask()); await executor.execute(makeTask());
// The worktree path stored should use the generated name, not the task ID // The worktree path stored should use the generated name, not the task ID
expect(store.updateTask).toHaveBeenCalledWith("HAI-030", { expect(store.updateTask).toHaveBeenCalledWith("KB-030", {
worktree: "/tmp/test/.worktrees/swift-falcon", worktree: "/tmp/test/.worktrees/swift-falcon",
}); });
expect(mockedGenerateWorktreeName).toHaveBeenCalledWith("/tmp/test"); expect(mockedGenerateWorktreeName).toHaveBeenCalledWith("/tmp/test");
@@ -401,7 +401,7 @@ describe("TaskExecutor worktree naming", () => {
const store = createMockStore(); const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask("HAI-099")); await executor.execute(makeTask("KB-099"));
// Verify the worktree path does NOT contain the task ID // Verify the worktree path does NOT contain the task ID
const updateCalls = store.updateTask.mock.calls; const updateCalls = store.updateTask.mock.calls;
@@ -409,7 +409,7 @@ describe("TaskExecutor worktree naming", () => {
(call: any[]) => call[1]?.worktree !== undefined, (call: any[]) => call[1]?.worktree !== undefined,
); );
expect(worktreeUpdate).toBeDefined(); expect(worktreeUpdate).toBeDefined();
expect(worktreeUpdate![1].worktree).not.toContain("HAI-099"); expect(worktreeUpdate![1].worktree).not.toContain("KB-099");
expect(worktreeUpdate![1].worktree).toContain("swift-falcon"); expect(worktreeUpdate![1].worktree).toContain("swift-falcon");
}); });
@@ -420,7 +420,7 @@ describe("TaskExecutor worktree naming", () => {
const store = createMockStore(); const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask("HAI-031", existingPath)); await executor.execute(makeTask("KB-031", existingPath));
// Should NOT generate a new name — reuse the stored path // Should NOT generate a new name — reuse the stored path
expect(mockedGenerateWorktreeName).not.toHaveBeenCalled(); expect(mockedGenerateWorktreeName).not.toHaveBeenCalled();
@@ -428,7 +428,7 @@ describe("TaskExecutor worktree naming", () => {
}); });
describe("TaskExecutor worktree pool integration", () => { describe("TaskExecutor worktree pool integration", () => {
const makeTask = (id = "HAI-020") => ({ const makeTask = (id = "KB-020") => ({
id, id,
title: "Test", title: "Test",
description: "Test", description: "Test",
@@ -482,7 +482,7 @@ describe("TaskExecutor worktree pool integration", () => {
// Should log pool acquisition // Should log pool acquisition
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"HAI-020", "KB-020",
expect.stringContaining("Acquired worktree from pool"), expect.stringContaining("Acquired worktree from pool"),
); );
@@ -515,7 +515,7 @@ describe("TaskExecutor worktree pool integration", () => {
// Should log worktree creation, NOT pool acquisition // Should log worktree creation, NOT pool acquisition
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"HAI-020", "KB-020",
expect.stringContaining("Worktree created at"), expect.stringContaining("Worktree created at"),
); );
}); });
@@ -597,12 +597,12 @@ describe("Merger worktree pool integration", () => {
}), }),
emit: vi.fn(), emit: vi.fn(),
getTask: vi.fn().mockResolvedValue({ getTask: vi.fn().mockResolvedValue({
id: "HAI-050", id: "KB-050",
title: "Test merge", title: "Test merge",
description: "Test", description: "Test",
column: "in-review", column: "in-review",
dependencies: [], dependencies: [],
worktree: "/tmp/test/.worktrees/HAI-050", worktree: "/tmp/test/.worktrees/KB-050",
steps: [], steps: [],
currentStep: 0, currentStep: 0,
log: [], log: [],
@@ -612,7 +612,7 @@ describe("Merger worktree pool integration", () => {
}), }),
updateTask: vi.fn().mockResolvedValue({}), updateTask: vi.fn().mockResolvedValue({}),
moveTask: vi.fn().mockResolvedValue({ moveTask: vi.fn().mockResolvedValue({
id: "HAI-050", id: "KB-050",
column: "done", column: "done",
dependencies: [], dependencies: [],
steps: [], steps: [],
@@ -659,10 +659,10 @@ describe("Merger worktree pool integration", () => {
}, },
} as any); } as any);
const result = await aiMergeTask(store, "/tmp/test", "HAI-050", { pool }); const result = await aiMergeTask(store, "/tmp/test", "KB-050", { pool });
// Worktree should be in the pool, NOT removed // Worktree should be in the pool, NOT removed
expect(pool.has("/tmp/test/.worktrees/HAI-050")).toBe(true); expect(pool.has("/tmp/test/.worktrees/KB-050")).toBe(true);
expect(result.worktreeRemoved).toBe(false); expect(result.worktreeRemoved).toBe(false);
// git worktree remove should NOT have been called // git worktree remove should NOT have been called
@@ -686,7 +686,7 @@ describe("Merger worktree pool integration", () => {
}, },
} as any); } as any);
const result = await aiMergeTask(store, "/tmp/test", "HAI-050", { pool }); const result = await aiMergeTask(store, "/tmp/test", "KB-050", { pool });
// Worktree should NOT be in the pool // Worktree should NOT be in the pool
expect(pool.size).toBe(0); expect(pool.size).toBe(0);
@@ -702,7 +702,7 @@ describe("Merger worktree pool integration", () => {
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail { function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
return { return {
id: "HAI-001", id: "KB-001",
title: "Test Task", title: "Test Task",
description: "A test task", description: "A test task",
column: "in-progress", column: "in-progress",
@@ -728,7 +728,7 @@ describe("buildExecutionPrompt", () => {
expect(result).toContain("## Attachments"); expect(result).toContain("## Attachments");
expect(result).toContain("**screenshot.png** (screenshot)"); expect(result).toContain("**screenshot.png** (screenshot)");
expect(result).toContain("/home/user/project/.hai/tasks/HAI-001/attachments/abc123-screenshot.png"); expect(result).toContain("/home/user/project/.kb/tasks/KB-001/attachments/abc123-screenshot.png");
}); });
it("includes attachment section with absolute paths for text attachments", () => { it("includes attachment section with absolute paths for text attachments", () => {
@@ -742,7 +742,7 @@ describe("buildExecutionPrompt", () => {
expect(result).toContain("## Attachments"); expect(result).toContain("## Attachments");
expect(result).toContain("**error.log** (text/plain)"); expect(result).toContain("**error.log** (text/plain)");
expect(result).toContain("read for context"); expect(result).toContain("read for context");
expect(result).toContain("/home/user/project/.hai/tasks/HAI-001/attachments/def456-error.log"); expect(result).toContain("/home/user/project/.kb/tasks/KB-001/attachments/def456-error.log");
}); });
it("includes both image and text attachments", () => { it("includes both image and text attachments", () => {
@@ -853,7 +853,7 @@ describe("buildExecutionPrompt", () => {
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -929,7 +929,7 @@ describe("TaskExecutor pause behavior", () => {
session: { session: {
prompt: vi.fn().mockImplementation(async () => { prompt: vi.fn().mockImplementation(async () => {
// Simulate pause happening during agent execution // Simulate pause happening during agent execution
store._trigger("task:updated", { id: "HAI-001", paused: true, column: "in-progress" }); store._trigger("task:updated", { id: "KB-001", paused: true, column: "in-progress" });
// Simulate the dispose causing an error (session terminated) // Simulate the dispose causing an error (session terminated)
throw new Error("Session terminated"); throw new Error("Session terminated");
}), }),
@@ -940,7 +940,7 @@ describe("TaskExecutor pause behavior", () => {
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -953,8 +953,8 @@ describe("TaskExecutor pause behavior", () => {
}); });
// Should move to todo, NOT mark as failed // Should move to todo, NOT mark as failed
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
expect(store.updateTask).not.toHaveBeenCalledWith("HAI-001", { status: "failed" }); expect(store.updateTask).not.toHaveBeenCalledWith("KB-001", { status: "failed" });
}); });
it("does not move to in-review when paused during execution (graceful session end)", async () => { it("does not move to in-review when paused during execution (graceful session end)", async () => {
@@ -965,7 +965,7 @@ describe("TaskExecutor pause behavior", () => {
session: { session: {
prompt: vi.fn().mockImplementation(async () => { prompt: vi.fn().mockImplementation(async () => {
// Simulate pause — session ends gracefully (no throw) // Simulate pause — session ends gracefully (no throw)
store._trigger("task:updated", { id: "HAI-001", paused: true, column: "in-progress" }); store._trigger("task:updated", { id: "KB-001", paused: true, column: "in-progress" });
}), }),
dispose: vi.fn(), dispose: vi.fn(),
}, },
@@ -974,7 +974,7 @@ describe("TaskExecutor pause behavior", () => {
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute({ await executor.execute({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "in-progress", column: "in-progress",
@@ -987,14 +987,14 @@ describe("TaskExecutor pause behavior", () => {
}); });
// Should NOT move to in-review (paused tasks skip that logic) // Should NOT move to in-review (paused tasks skip that logic)
expect(store.moveTask).not.toHaveBeenCalledWith("HAI-001", "in-review"); expect(store.moveTask).not.toHaveBeenCalledWith("KB-001", "in-review");
}); });
it("skips paused tasks during resumeOrphaned", async () => { it("skips paused tasks during resumeOrphaned", async () => {
const store = createMockStore(); const store = createMockStore();
store.listTasks.mockResolvedValue([ store.listTasks.mockResolvedValue([
{ id: "HAI-001", column: "in-progress", paused: true, title: "Paused task" }, { id: "KB-001", column: "in-progress", paused: true, title: "Paused task" },
{ id: "HAI-002", column: "in-progress", paused: false, title: "Active task" }, { id: "KB-002", column: "in-progress", paused: false, title: "Active task" },
]); ]);
mockedCreateHaiAgent.mockResolvedValue({ mockedCreateHaiAgent.mockResolvedValue({
@@ -1007,8 +1007,8 @@ describe("TaskExecutor pause behavior", () => {
const executor = new TaskExecutor(store, "/tmp/test"); const executor = new TaskExecutor(store, "/tmp/test");
await executor.resumeOrphaned(); await executor.resumeOrphaned();
// Only HAI-002 should be resumed (HAI-001 is paused) // Only KB-002 should be resumed (KB-001 is paused)
expect(store.logEntry).toHaveBeenCalledWith("HAI-002", "Resumed after engine restart"); expect(store.logEntry).toHaveBeenCalledWith("KB-002", "Resumed after engine restart");
expect(store.logEntry).not.toHaveBeenCalledWith("HAI-001", expect.anything()); expect(store.logEntry).not.toHaveBeenCalledWith("KB-001", expect.anything());
}); });
}); });

View File

@@ -1,11 +1,11 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { join } from "node:path"; import { join } from "node:path";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import type { TaskStore, Task, TaskDetail, StepStatus, Settings } from "@hai/core"; import type { TaskStore, Task, TaskDetail, StepStatus, Settings } from "@kb/core";
import { findWorktreeUser } from "./merger.js"; import { findWorktreeUser } from "./merger.js";
import { generateWorktreeName } from "./worktree-names.js"; import { generateWorktreeName } from "./worktree-names.js";
import { Type, type Static } from "@mariozechner/pi-ai"; import { Type, type Static } from "@mariozechner/pi-ai";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import { reviewStep } from "./reviewer.js"; import { reviewStep } from "./reviewer.js";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import type { AgentSemaphore } from "./concurrency.js"; import type { AgentSemaphore } from "./concurrency.js";
@@ -36,7 +36,7 @@ const taskLogParams = Type.Object({
const taskCreateParams = Type.Object({ const taskCreateParams = Type.Object({
description: Type.String({ description: "What needs to be done" }), description: Type.String({ description: "What needs to be done" }),
dependencies: Type.Optional( dependencies: Type.Optional(
Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"HAI-001\"])" }), Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"KB-001\"])" }),
), ),
}); });
@@ -56,7 +56,7 @@ const reviewStepParams = Type.Object({
), ),
}); });
const EXECUTOR_SYSTEM_PROMPT = `You are a task execution agent for "hai", an AI-orchestrated task board. const EXECUTOR_SYSTEM_PROMPT = `You are a task execution agent for "kb", an AI-orchestrated task board.
You are working in a git worktree isolated from the main branch. Your job is to implement the task described in the PROMPT.md specification you're given. You are working in a git worktree isolated from the main branch. Your job is to implement the task described in the PROMPT.md specification you're given.
@@ -239,7 +239,7 @@ export class TaskExecutor {
* than being named after the task ID. This decouples directory names from * than being named after the task ID. This decouples directory names from
* tasks, enabling worktree reuse across dependency chains. When resuming * tasks, enabling worktree reuse across dependency chains. When resuming
* a task that already has `task.worktree` set, the existing path is used * a task that already has `task.worktree` set, the existing path is used
* as-is. Branches remain task-scoped (`hai/{task-id}`). * as-is. Branches remain task-scoped (`kb/{task-id}`).
*/ */
async execute(task: Task): Promise<void> { async execute(task: Task): Promise<void> {
if (this.executing.has(task.id)) return; if (this.executing.has(task.id)) return;
@@ -261,7 +261,7 @@ export class TaskExecutor {
} }
// Create or reuse worktree — try pool first when recycling is enabled // Create or reuse worktree — try pool first when recycling is enabled
const branchName = `hai/${task.id.toLowerCase()}`; const branchName = `kb/${task.id.toLowerCase()}`;
// Use generateWorktreeName for human-friendly directory names (adjective-noun pattern) // Use generateWorktreeName for human-friendly directory names (adjective-noun pattern)
// instead of task.id, so worktrees are named like ".worktrees/swift-falcon" // instead of task.id, so worktrees are named like ".worktrees/swift-falcon"
let worktreePath = task.worktree || join(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir)); let worktreePath = task.worktree || join(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
@@ -343,7 +343,7 @@ export class TaskExecutor {
}); });
const agentWork = async () => { const agentWork = async () => {
const { session } = await createHaiAgent({ const { session } = await createKbAgent({
cwd: worktreePath, cwd: worktreePath,
systemPrompt: EXECUTOR_SYSTEM_PROMPT, systemPrompt: EXECUTOR_SYSTEM_PROMPT,
tools: "coding", tools: "coding",
@@ -664,7 +664,7 @@ git log --oneline
const IMAGE_MIMES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]); const IMAGE_MIMES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
const lines = ["## Attachments", ""]; const lines = ["## Attachments", ""];
for (const att of task.attachments) { for (const att of task.attachments) {
const absPath = `${rootDir}/.hai/tasks/${task.id}/attachments/${att.filename}`; const absPath = `${rootDir}/.kb/tasks/${task.id}/attachments/${att.filename}`;
if (IMAGE_MIMES.has(att.mimeType)) { if (IMAGE_MIMES.has(att.mimeType)) {
lines.push(`- **${att.originalName}** (screenshot): \`${absPath}\``); lines.push(`- **${att.originalName}** (screenshot): \`${absPath}\``);
} else { } else {

View File

@@ -5,6 +5,6 @@ export { TaskExecutor, type TaskExecutorOptions } from "./executor.js";
export { Scheduler, type SchedulerOptions } from "./scheduler.js"; export { Scheduler, type SchedulerOptions } from "./scheduler.js";
export { aiMergeTask, type MergerOptions } from "./merger.js"; export { aiMergeTask, type MergerOptions } from "./merger.js";
export { reviewStep, type ReviewType, type ReviewVerdict, type ReviewResult, type ReviewOptions } from "./reviewer.js"; export { reviewStep, type ReviewType, type ReviewVerdict, type ReviewResult, type ReviewOptions } from "./reviewer.js";
export { createHaiAgent, type AgentOptions, type AgentResult } from "./pi.js"; export { createKbAgent, type AgentOptions, type AgentResult } from "./pi.js";
export { WorktreePool } from "./worktree-pool.js"; export { WorktreePool } from "./worktree-pool.js";
export { createLogger, type Logger } from "./logger.js"; export { createLogger, type Logger } from "./logger.js";

View File

@@ -1,5 +1,5 @@
/** /**
* Lightweight structured logger for the `@hai/engine` package. * Lightweight structured logger for the `@kb/engine` package.
* *
* Usage: * Usage:
* ```ts * ```ts

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock external dependencies // Mock external dependencies
vi.mock("./pi.js", () => ({ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(), createKbAgent: vi.fn(),
})); }));
vi.mock("node:child_process", () => ({ vi.mock("node:child_process", () => ({
@@ -14,23 +14,23 @@ vi.mock("node:fs", () => ({
})); }));
import { aiMergeTask, findWorktreeUser } from "./merger.js"; import { aiMergeTask, findWorktreeUser } from "./merger.js";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@hai/core"; import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent); const mockedCreateHaiAgent = vi.mocked(createKbAgent);
const mockedExecSync = vi.mocked(execSync); const mockedExecSync = vi.mocked(execSync);
const { existsSync: mockedExistsSyncRaw } = await import("node:fs"); const { existsSync: mockedExistsSyncRaw } = await import("node:fs");
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw); const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
function createMockStore(taskOverrides: Partial<Task> = {}, allTasks: Task[] = []) { function createMockStore(taskOverrides: Partial<Task> = {}, allTasks: Task[] = []) {
const baseTask: Task = { const baseTask: Task = {
id: "HAI-050", id: "KB-050",
title: "Test task", title: "Test task",
description: "Test", description: "Test",
column: "in-review", column: "in-review",
dependencies: [], dependencies: [],
worktree: "/tmp/root/.worktrees/HAI-050", worktree: "/tmp/root/.worktrees/KB-050",
steps: [], steps: [],
currentStep: 0, currentStep: 0,
log: [], log: [],
@@ -73,27 +73,27 @@ function setupHappyPathExecSync() {
describe("findWorktreeUser", () => { describe("findWorktreeUser", () => {
it("returns null when no other task uses the worktree", async () => { it("returns null when no other task uses the worktree", async () => {
const store = createMockStore({}, [ const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task, { id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
]); ]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050"); const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it("returns task ID when another non-done task uses the worktree", async () => { it("returns task ID when another non-done task uses the worktree", async () => {
const store = createMockStore({}, [ const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task, { id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "HAI-051", worktree: "/tmp/wt", column: "in-progress" } as Task, { id: "KB-051", worktree: "/tmp/wt", column: "in-progress" } as Task,
]); ]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050"); const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBe("HAI-051"); expect(result).toBe("KB-051");
}); });
it("ignores done tasks", async () => { it("ignores done tasks", async () => {
const store = createMockStore({}, [ const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task, { id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "HAI-051", worktree: "/tmp/wt", column: "done" } as Task, { id: "KB-051", worktree: "/tmp/wt", column: "done" } as Task,
]); ]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050"); const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
@@ -112,16 +112,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
}); });
it("does NOT remove worktree when another task references the same path", async () => { it("does NOT remove worktree when another task references the same path", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[ [
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task, { id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task, { id: "KB-051", worktree: worktreePath, column: "in-progress" } as Task,
], ],
); );
const result = await aiMergeTask(store, "/tmp/root", "HAI-050"); const result = await aiMergeTask(store, "/tmp/root", "KB-050");
// Worktree should NOT be removed // Worktree should NOT be removed
const removeCall = mockedExecSync.mock.calls.find( const removeCall = mockedExecSync.mock.calls.find(
@@ -132,15 +132,15 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
}); });
it("removes worktree when no other task references it", async () => { it("removes worktree when no other task references it", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[ [
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task, { id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
], ],
); );
const result = await aiMergeTask(store, "/tmp/root", "HAI-050"); const result = await aiMergeTask(store, "/tmp/root", "KB-050");
const removeCall = mockedExecSync.mock.calls.find( const removeCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("worktree remove"), (call) => String(call[0]).includes("worktree remove"),
@@ -150,16 +150,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
}); });
it("always deletes the branch regardless of worktree sharing", async () => { it("always deletes the branch regardless of worktree sharing", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[ [
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task, { id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task, { id: "KB-051", worktree: worktreePath, column: "in-progress" } as Task,
], ],
); );
const result = await aiMergeTask(store, "/tmp/root", "HAI-050"); const result = await aiMergeTask(store, "/tmp/root", "KB-050");
// Branch should be deleted even though worktree is shared // Branch should be deleted even though worktree is shared
const branchDeleteCall = mockedExecSync.mock.calls.find( const branchDeleteCall = mockedExecSync.mock.calls.find(
@@ -170,16 +170,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
}); });
it("result.worktreeRemoved is false when worktree is retained", async () => { it("result.worktreeRemoved is false when worktree is retained", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[ [
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task, { id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "todo" } as Task, { id: "KB-051", worktree: worktreePath, column: "todo" } as Task,
], ],
); );
const result = await aiMergeTask(store, "/tmp/root", "HAI-050"); const result = await aiMergeTask(store, "/tmp/root", "KB-050");
expect(result.worktreeRemoved).toBe(false); expect(result.worktreeRemoved).toBe(false);
expect(result.merged).toBe(true); expect(result.merged).toBe(true);
}); });
@@ -200,11 +200,11 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
it("includes task ID in system prompt by default (includeTaskIdInCommit: true)", async () => { it("includes task ID in system prompt by default (includeTaskIdInCommit: true)", async () => {
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any; const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(agentCall.systemPrompt).toContain("<type>(<scope>): <summary>"); expect(agentCall.systemPrompt).toContain("<type>(<scope>): <summary>");
@@ -213,15 +213,15 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
it("omits task ID scope in system prompt when includeTaskIdInCommit is false", async () => { it("omits task ID scope in system prompt when includeTaskIdInCommit is false", async () => {
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ (store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
includeTaskIdInCommit: false, includeTaskIdInCommit: false,
}); });
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any; const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(agentCall.systemPrompt).toContain("<type>: <summary>"); expect(agentCall.systemPrompt).toContain("<type>: <summary>");
@@ -245,17 +245,17 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
}); });
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
const commitCall = mockedExecSync.mock.calls.find( const commitCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("git commit"), (call) => String(call[0]).includes("git commit"),
); );
expect(commitCall).toBeDefined(); expect(commitCall).toBeDefined();
expect(String(commitCall![0])).toContain("feat(HAI-050):"); expect(String(commitCall![0])).toContain("feat(KB-050):");
}); });
it("fallback commit omits task ID when includeTaskIdInCommit is false", async () => { it("fallback commit omits task ID when includeTaskIdInCommit is false", async () => {
@@ -273,22 +273,22 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
}); });
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ (store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
includeTaskIdInCommit: false, includeTaskIdInCommit: false,
}); });
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
const commitCall = mockedExecSync.mock.calls.find( const commitCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("git commit"), (call) => String(call[0]).includes("git commit"),
); );
expect(commitCall).toBeDefined(); expect(commitCall).toBeDefined();
expect(String(commitCall![0])).toContain("feat: merge"); expect(String(commitCall![0])).toContain("feat: merge");
expect(String(commitCall![0])).not.toContain("feat(HAI-050)"); expect(String(commitCall![0])).not.toContain("feat(KB-050)");
}); });
}); });
@@ -305,10 +305,10 @@ describe("aiMergeTask — model settings threading", () => {
} as any); } as any);
}); });
it("passes defaultProvider and defaultModelId from settings to createHaiAgent", async () => { it("passes defaultProvider and defaultModelId from settings to createKbAgent", async () => {
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ (store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
@@ -316,7 +316,7 @@ describe("aiMergeTask — model settings threading", () => {
defaultModelId: "gpt-4o", defaultModelId: "gpt-4o",
}); });
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1); expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any; const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
@@ -326,11 +326,11 @@ describe("aiMergeTask — model settings threading", () => {
it("does not set model fields when settings omit them", async () => { it("does not set model fields when settings omit them", async () => {
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" }, { id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task], [{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any; const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(opts.defaultProvider).toBeUndefined(); expect(opts.defaultProvider).toBeUndefined();
@@ -361,15 +361,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any; } as any;
}); });
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task], [{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "Hello merge", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "Hello merge", "text");
}); });
it("logs tool invocations to store.appendAgentLog", async () => { it("logs tool invocations to store.appendAgentLog", async () => {
@@ -387,15 +387,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any; } as any;
}); });
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task], [{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050"); await aiMergeTask(store, "/tmp/root", "KB-050");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "Bash", "tool", "git status"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "Bash", "tool", "git status");
}); });
it("still fires onAgentText callback alongside logging", async () => { it("still fires onAgentText callback alongside logging", async () => {
@@ -414,15 +414,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any; } as any;
}); });
const worktreePath = "/tmp/root/.worktrees/HAI-050"; const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore( const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath }, { id: "KB-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task], [{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
); );
await aiMergeTask(store, "/tmp/root", "HAI-050", { onAgentText }); await aiMergeTask(store, "/tmp/root", "KB-050", { onAgentText });
expect(onAgentText).toHaveBeenCalledWith("hi"); expect(onAgentText).toHaveBeenCalledWith("hi");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "hi", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "hi", "text");
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import type { TaskStore, Task, MergeResult } from "@hai/core"; import type { TaskStore, Task, MergeResult } from "@kb/core";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import type { WorktreePool } from "./worktree-pool.js"; import type { WorktreePool } from "./worktree-pool.js";
import { AgentLogger } from "./agent-logger.js"; import { AgentLogger } from "./agent-logger.js";
import { mergerLog } from "./logger.js"; import { mergerLog } from "./logger.js";
@@ -19,13 +19,13 @@ git commit -m "<type>(<scope>): <summary>" -m "<body>"
Message format: Message format:
- **Type:** feat, fix, refactor, docs, test, chore - **Type:** feat, fix, refactor, docs, test, chore
- **Scope:** the task ID (e.g., HAI-001) - **Scope:** the task ID (e.g., KB-001)
- **Summary:** one line describing what the squash brings in (imperative mood) - **Summary:** one line describing what the squash brings in (imperative mood)
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- " - **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
Example: Example:
\`\`\` \`\`\`
git commit -m "feat(HAI-003): add user profile page" -m "- Add /profile route with avatar upload git commit -m "feat(KB-003): add user profile page" -m "- Add /profile route with avatar upload
- Create ProfileCard and EditProfileForm components - Create ProfileCard and EditProfileForm components
- Add profile image resizing via sharp - Add profile image resizing via sharp
- Update nav bar with profile link - Update nav bar with profile link
@@ -51,7 +51,7 @@ git commit -m "feat: add user profile page" -m "- Add /profile route with avatar
- Add profile e2e tests" - Add profile e2e tests"
\`\`\``; \`\`\``;
return `You are a merge agent for "hai", an AI-orchestrated task board. return `You are a merge agent for "kb", an AI-orchestrated task board.
Your job is to finalize a squash merge: resolve any conflicts and write a good commit message. Your job is to finalize a squash merge: resolve any conflicts and write a good commit message.
All changes from the branch are squashed into a single commit. All changes from the branch are squashed into a single commit.
@@ -130,7 +130,7 @@ export async function aiMergeTask(
); );
} }
const branch = `hai/${taskId.toLowerCase()}`; const branch = `kb/${taskId.toLowerCase()}`;
const worktreePath = task.worktree; const worktreePath = task.worktree;
const result: MergeResult = { const result: MergeResult = {
task, task,
@@ -231,7 +231,7 @@ export async function aiMergeTask(
}); });
// Forward model settings from store so the merger honours the user's model choice // Forward model settings from store so the merger honours the user's model choice
const { session } = await createHaiAgent({ const { session } = await createKbAgent({
cwd: rootDir, cwd: rootDir,
systemPrompt: buildMergeSystemPrompt(includeTaskId), systemPrompt: buildMergeSystemPrompt(includeTaskId),
tools: "coding", tools: "coding",

View File

@@ -1,5 +1,5 @@
/** /**
* Shared pi SDK setup for hai engine agents. * Shared pi SDK setup for kb engine agents.
* *
* Uses the user's existing pi auth (API keys / OAuth from ~/.pi/agent/auth.json). * Uses the user's existing pi auth (API keys / OAuth from ~/.pi/agent/auth.json).
* Provides factory functions for creating triage and executor agent sessions. * Provides factory functions for creating triage and executor agent sessions.
@@ -37,10 +37,10 @@ export interface AgentOptions {
} }
/** /**
* Create a pi agent session configured for hai. * Create a pi agent session configured for kb.
* Reuses the user's existing pi auth and model configuration. * Reuses the user's existing pi auth and model configuration.
*/ */
export async function createHaiAgent(options: AgentOptions): Promise<AgentResult> { export async function createKbAgent(options: AgentOptions): Promise<AgentResult> {
const authStorage = AuthStorage.create(); const authStorage = AuthStorage.create();
const modelRegistry = new ModelRegistry(authStorage); const modelRegistry = new ModelRegistry(authStorage);

View File

@@ -15,7 +15,7 @@ import { AgentSemaphore } from "./concurrency.js";
// ── Module-level mocks (matching existing test patterns) ────────────────── // ── Module-level mocks (matching existing test patterns) ──────────────────
vi.mock("./pi.js", () => ({ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(), createKbAgent: vi.fn(),
})); }));
vi.mock("./reviewer.js", () => ({ vi.mock("./reviewer.js", () => ({
reviewStep: vi.fn(), reviewStep: vi.fn(),
@@ -31,12 +31,12 @@ import { TaskExecutor } from "./executor.js";
import { TriageProcessor } from "./triage.js"; import { TriageProcessor } from "./triage.js";
import { Scheduler } from "./scheduler.js"; import { Scheduler } from "./scheduler.js";
import { aiMergeTask } from "./merger.js"; import { aiMergeTask } from "./merger.js";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import type { Task, TaskDetail, TaskStep, Column, Settings, StepStatus } from "@hai/core"; import type { Task, TaskDetail, TaskStep, Column, Settings, StepStatus } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent); const mockedCreateHaiAgent = vi.mocked(createKbAgent);
const mockedExecSync = vi.mocked(execSync); const mockedExecSync = vi.mocked(execSync);
const mockedExistsSync = vi.mocked(existsSync); const mockedExistsSync = vi.mocked(existsSync);
@@ -61,7 +61,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
}), }),
emit: vi.fn(), emit: vi.fn(),
listTasks: vi.fn().mockResolvedValue([]), listTasks: vi.fn().mockResolvedValue([]),
getTask: vi.fn().mockResolvedValue(makeTaskDetail("HAI-001", "in-progress")), getTask: vi.fn().mockResolvedValue(makeTaskDetail("KB-001", "in-progress")),
updateTask: vi.fn().mockResolvedValue({}), updateTask: vi.fn().mockResolvedValue({}),
moveTask: vi.fn().mockImplementation(async (id: string, col: Column) => { moveTask: vi.fn().mockImplementation(async (id: string, col: Column) => {
return makeTask(id, col); return makeTask(id, col);
@@ -74,7 +74,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
return makeTaskDetail(id, "in-progress"); return makeTaskDetail(id, "in-progress");
}), }),
createTask: vi.fn().mockImplementation(async (input: any) => { createTask: vi.fn().mockImplementation(async (input: any) => {
return makeTask("HAI-NEW", "triage"); return makeTask("KB-NEW", "triage");
}), }),
deleteTask: vi.fn().mockResolvedValue(undefined), deleteTask: vi.fn().mockResolvedValue(undefined),
_listeners: listeners, _listeners: listeners,
@@ -139,9 +139,9 @@ beforeEach(() => {
describe("In-progress task resume after restart", () => { describe("In-progress task resume after restart", () => {
it("resumeOrphaned() calls execute() for each in-progress task not already executing", async () => { it("resumeOrphaned() calls execute() for each in-progress task not already executing", async () => {
const store = createMockStore(); const store = createMockStore();
const task1 = makeTask("HAI-001", "in-progress"); const task1 = makeTask("KB-001", "in-progress");
const task2 = makeTask("HAI-002", "in-progress"); const task2 = makeTask("KB-002", "in-progress");
const taskDone = makeTask("HAI-003", "done"); const taskDone = makeTask("KB-003", "done");
store.listTasks.mockResolvedValue([task1, task2, taskDone]); store.listTasks.mockResolvedValue([task1, task2, taskDone]);
mockAgentSuccess(); mockAgentSuccess();
@@ -152,18 +152,18 @@ describe("In-progress task resume after restart", () => {
// Wait for async execute calls to complete // Wait for async execute calls to complete
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
// createHaiAgent should have been called once per in-progress task // createKbAgent should have been called once per in-progress task
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2); expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
}); });
it("resumed task reuses existing worktree — no git worktree add called", async () => { it("resumed task reuses existing worktree — no git worktree add called", async () => {
const store = createMockStore(); const store = createMockStore();
const task = makeTask("HAI-010", "in-progress", { const task = makeTask("KB-010", "in-progress", {
worktree: "/tmp/wt/HAI-010", worktree: "/tmp/wt/KB-010",
}); });
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-010", "in-progress", { store.getTask.mockResolvedValue(makeTaskDetail("KB-010", "in-progress", {
worktree: "/tmp/wt/HAI-010", worktree: "/tmp/wt/KB-010",
})); }));
// Worktree exists on disk // Worktree exists on disk
@@ -184,9 +184,9 @@ describe("In-progress task resume after restart", () => {
it("resumed task with step progress includes RESUMING section in agent prompt", async () => { it("resumed task with step progress includes RESUMING section in agent prompt", async () => {
const store = createMockStore(); const store = createMockStore();
const steps = makeSteps("done", "done", "done", "in-progress", "pending"); const steps = makeSteps("done", "done", "done", "in-progress", "pending");
const task = makeTask("HAI-020", "in-progress", { steps, currentStep: 3 }); const task = makeTask("KB-020", "in-progress", { steps, currentStep: 3 });
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-020", "in-progress", { store.getTask.mockResolvedValue(makeTaskDetail("KB-020", "in-progress", {
steps, steps,
currentStep: 3, currentStep: 3,
})); }));
@@ -217,9 +217,9 @@ describe("In-progress task resume after restart", () => {
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
worktreeInitCommand: "pnpm install", worktreeInitCommand: "pnpm install",
}); });
const task = makeTask("HAI-030", "in-progress"); const task = makeTask("KB-030", "in-progress");
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-030", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-030", "in-progress"));
mockedExistsSync.mockReturnValue(true); // worktree exists mockedExistsSync.mockReturnValue(true); // worktree exists
mockAgentSuccess(); mockAgentSuccess();
@@ -240,8 +240,8 @@ describe("In-progress task resume after restart", () => {
it("resumeOrphaned() logs 'Resumed after engine restart' for each orphaned task", async () => { it("resumeOrphaned() logs 'Resumed after engine restart' for each orphaned task", async () => {
const store = createMockStore(); const store = createMockStore();
const task1 = makeTask("HAI-040", "in-progress"); const task1 = makeTask("KB-040", "in-progress");
const task2 = makeTask("HAI-041", "in-progress"); const task2 = makeTask("KB-041", "in-progress");
store.listTasks.mockResolvedValue([task1, task2]); store.listTasks.mockResolvedValue([task1, task2]);
store.getTask.mockImplementation(async (id: string) => store.getTask.mockImplementation(async (id: string) =>
makeTaskDetail(id, "in-progress"), makeTaskDetail(id, "in-progress"),
@@ -253,25 +253,25 @@ describe("In-progress task resume after restart", () => {
await executor.resumeOrphaned(); await executor.resumeOrphaned();
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
expect(store.logEntry).toHaveBeenCalledWith("HAI-040", "Resumed after engine restart"); expect(store.logEntry).toHaveBeenCalledWith("KB-040", "Resumed after engine restart");
expect(store.logEntry).toHaveBeenCalledWith("HAI-041", "Resumed after engine restart"); expect(store.logEntry).toHaveBeenCalledWith("KB-041", "Resumed after engine restart");
}); });
}); });
// ── Step 3: In-review merge re-queue tests ──────────────────────────────── // ── Step 3: In-review merge re-queue tests ────────────────────────────────
// //
// The merge queue/enqueueMerge logic lives in dashboard.ts (CLI layer). // The merge queue/enqueueMerge logic lives in dashboard.ts (CLI layer).
// These tests focus on what @hai/engine owns: aiMergeTask() behaviour // These tests focus on what @kb/engine owns: aiMergeTask() behaviour
// relevant to restart resilience — state validation, status lifecycle, // relevant to restart resilience — state validation, status lifecycle,
// and error handling with git reset --merge cleanup. // and error handling with git reset --merge cleanup.
describe("In-review merge handling after restart", () => { describe("In-review merge handling after restart", () => {
it("aiMergeTask validates task is in 'in-review' before merging", async () => { it("aiMergeTask validates task is in 'in-review' before merging", async () => {
const store = createMockStore(); const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("HAI-050", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-050", "in-progress"));
await expect(aiMergeTask(store, "/tmp/root", "HAI-050")).rejects.toThrow( await expect(aiMergeTask(store, "/tmp/root", "KB-050")).rejects.toThrow(
"Cannot merge HAI-050: task is in 'in-progress', must be in 'in-review'", "Cannot merge KB-050: task is in 'in-progress', must be in 'in-review'",
); );
// No git commands should have been executed // No git commands should have been executed
@@ -280,8 +280,8 @@ describe("In-review merge handling after restart", () => {
it("aiMergeTask sets status to 'merging' during execution and clears on success", async () => { it("aiMergeTask sets status to 'merging' during execution and clears on success", async () => {
const store = createMockStore(); const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("HAI-051", "in-review")); store.getTask.mockResolvedValue(makeTaskDetail("KB-051", "in-review"));
store.moveTask.mockResolvedValue(makeTask("HAI-051", "done")); store.moveTask.mockResolvedValue(makeTask("KB-051", "done"));
// Branch exists, merge succeeds, no conflicts // Branch exists, merge succeeds, no conflicts
mockedExecSync.mockImplementation((cmd: any) => { mockedExecSync.mockImplementation((cmd: any) => {
@@ -294,16 +294,16 @@ describe("In-review merge handling after restart", () => {
mockAgentSuccess(); mockAgentSuccess();
await aiMergeTask(store, "/tmp/root", "HAI-051"); await aiMergeTask(store, "/tmp/root", "KB-051");
// Should have set status to "merging" // Should have set status to "merging"
expect(store.updateTask).toHaveBeenCalledWith("HAI-051", { status: "merging" }); expect(store.updateTask).toHaveBeenCalledWith("KB-051", { status: "merging" });
// Should have cleared status via completeTask (status: null before moveTask) // Should have cleared status via completeTask (status: null before moveTask)
expect(store.updateTask).toHaveBeenCalledWith("HAI-051", { status: null }); expect(store.updateTask).toHaveBeenCalledWith("KB-051", { status: null });
}); });
it("sequential aiMergeTask calls for multiple in-review tasks all succeed", async () => { it("sequential aiMergeTask calls for multiple in-review tasks all succeed", async () => {
const taskIds = ["HAI-052", "HAI-053", "HAI-054"]; const taskIds = ["KB-052", "KB-053", "KB-054"];
for (const taskId of taskIds) { for (const taskId of taskIds) {
const store = createMockStore(); const store = createMockStore();
@@ -326,7 +326,7 @@ describe("In-review merge handling after restart", () => {
it("aiMergeTask throws on agent failure during session.prompt and calls git reset --merge", async () => { it("aiMergeTask throws on agent failure during session.prompt and calls git reset --merge", async () => {
const store = createMockStore(); const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("HAI-055", "in-review")); store.getTask.mockResolvedValue(makeTaskDetail("KB-055", "in-review"));
// Branch exists, merge starts, agent creates but prompt fails // Branch exists, merge starts, agent creates but prompt fails
mockedExecSync.mockImplementation((cmd: any) => { mockedExecSync.mockImplementation((cmd: any) => {
@@ -343,8 +343,8 @@ describe("In-review merge handling after restart", () => {
}, },
} as any); } as any);
await expect(aiMergeTask(store, "/tmp/root", "HAI-055")).rejects.toThrow( await expect(aiMergeTask(store, "/tmp/root", "KB-055")).rejects.toThrow(
"AI merge failed for HAI-055: merge agent crashed", "AI merge failed for KB-055: merge agent crashed",
); );
// Should have attempted git reset --merge cleanup // Should have attempted git reset --merge cleanup
@@ -354,13 +354,13 @@ describe("In-review merge handling after restart", () => {
expect(resetCalls.length).toBeGreaterThan(0); expect(resetCalls.length).toBeGreaterThan(0);
// Status was set to "merging" but NOT cleared by aiMergeTask (that's the dashboard's job) // Status was set to "merging" but NOT cleared by aiMergeTask (that's the dashboard's job)
expect(store.updateTask).toHaveBeenCalledWith("HAI-055", { status: "merging" }); expect(store.updateTask).toHaveBeenCalledWith("KB-055", { status: "merging" });
}); });
it("aiMergeTask moves task to done when branch does not exist", async () => { it("aiMergeTask moves task to done when branch does not exist", async () => {
const store = createMockStore(); const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("HAI-056", "in-review")); store.getTask.mockResolvedValue(makeTaskDetail("KB-056", "in-review"));
store.moveTask.mockResolvedValue(makeTask("HAI-056", "done")); store.moveTask.mockResolvedValue(makeTask("KB-056", "done"));
// git rev-parse --verify throws (branch not found) // git rev-parse --verify throws (branch not found)
mockedExecSync.mockImplementation((cmd: any) => { mockedExecSync.mockImplementation((cmd: any) => {
@@ -370,11 +370,11 @@ describe("In-review merge handling after restart", () => {
return Buffer.from(""); return Buffer.from("");
}); });
const result = await aiMergeTask(store, "/tmp/root", "HAI-056"); const result = await aiMergeTask(store, "/tmp/root", "KB-056");
expect(result.merged).toBe(false); expect(result.merged).toBe(false);
expect(result.error).toContain("Branch"); expect(result.error).toContain("Branch");
expect(store.moveTask).toHaveBeenCalledWith("HAI-056", "done"); expect(store.moveTask).toHaveBeenCalledWith("KB-056", "done");
}); });
}); });
@@ -383,8 +383,8 @@ describe("In-review merge handling after restart", () => {
describe("Triage re-pick after restart", () => { describe("Triage re-pick after restart", () => {
it("TriageProcessor.start() after restart picks up triage tasks (processing set is fresh)", async () => { it("TriageProcessor.start() after restart picks up triage tasks (processing set is fresh)", async () => {
const store = createMockStore(); const store = createMockStore();
const triageTask1 = makeTask("HAI-060", "triage"); const triageTask1 = makeTask("KB-060", "triage");
const triageTask2 = makeTask("HAI-061", "triage"); const triageTask2 = makeTask("KB-061", "triage");
store.listTasks.mockResolvedValue([triageTask1, triageTask2]); store.listTasks.mockResolvedValue([triageTask1, triageTask2]);
store.getTask.mockImplementation(async (id: string) => store.getTask.mockImplementation(async (id: string) =>
makeTaskDetail(id, "triage"), makeTaskDetail(id, "triage"),
@@ -402,15 +402,15 @@ describe("Triage re-pick after restart", () => {
triage.stop(); triage.stop();
// Both triage tasks should have been picked up for specification // Both triage tasks should have been picked up for specification
expect(store.updateTask).toHaveBeenCalledWith("HAI-060", { status: "specifying" }); expect(store.updateTask).toHaveBeenCalledWith("KB-060", { status: "specifying" });
expect(store.updateTask).toHaveBeenCalledWith("HAI-061", { status: "specifying" }); expect(store.updateTask).toHaveBeenCalledWith("KB-061", { status: "specifying" });
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2); expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
}); });
it("specifyTask() skips task already in processing set (no double-specification)", async () => { it("specifyTask() skips task already in processing set (no double-specification)", async () => {
const store = createMockStore(); const store = createMockStore();
const task = makeTask("HAI-062", "triage"); const task = makeTask("KB-062", "triage");
store.getTask.mockResolvedValue(makeTaskDetail("HAI-062", "triage")); store.getTask.mockResolvedValue(makeTaskDetail("KB-062", "triage"));
// Slow agent to keep task in processing // Slow agent to keep task in processing
let resolvePrompt: Function; let resolvePrompt: Function;
@@ -444,7 +444,7 @@ describe("Triage re-pick after restart", () => {
describe("Scheduler after restart", () => { describe("Scheduler after restart", () => {
it("schedule() moves todo tasks to in-progress when deps are satisfied", async () => { it("schedule() moves todo tasks to in-progress when deps are satisfied", async () => {
const store = createMockStore(); const store = createMockStore();
const todoTask = makeTask("HAI-070", "todo"); const todoTask = makeTask("KB-070", "todo");
store.listTasks.mockResolvedValue([todoTask]); store.listTasks.mockResolvedValue([todoTask]);
store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS }); store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
@@ -463,16 +463,16 @@ describe("Scheduler after restart", () => {
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
scheduler.stop(); scheduler.stop();
expect(store.moveTask).toHaveBeenCalledWith("HAI-070", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-070", "in-progress");
expect(store.updateTask).toHaveBeenCalledWith("HAI-070", { status: null, blockedBy: null }); expect(store.updateTask).toHaveBeenCalledWith("KB-070", { status: null, blockedBy: null });
expect(onSchedule).toHaveBeenCalledWith(todoTask); expect(onSchedule).toHaveBeenCalledWith(todoTask);
}); });
it("schedule() respects dependency ordering — blocked tasks stay in todo", async () => { it("schedule() respects dependency ordering — blocked tasks stay in todo", async () => {
const store = createMockStore(); const store = createMockStore();
const depTask = makeTask("HAI-071", "in-progress"); const depTask = makeTask("KB-071", "in-progress");
const blockedTask = makeTask("HAI-072", "todo", { const blockedTask = makeTask("KB-072", "todo", {
dependencies: ["HAI-071"], dependencies: ["KB-071"],
}); });
store.listTasks.mockResolvedValue([depTask, blockedTask]); store.listTasks.mockResolvedValue([depTask, blockedTask]);
store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS }); store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
@@ -489,19 +489,19 @@ describe("Scheduler after restart", () => {
scheduler.stop(); scheduler.stop();
// Task should NOT have been moved // Task should NOT have been moved
expect(store.moveTask).not.toHaveBeenCalledWith("HAI-072", "in-progress"); expect(store.moveTask).not.toHaveBeenCalledWith("KB-072", "in-progress");
expect(onBlocked).toHaveBeenCalledWith(blockedTask, ["HAI-071"]); expect(onBlocked).toHaveBeenCalledWith(blockedTask, ["KB-071"]);
}); });
it("full column coverage: restart with tasks in every column", async () => { it("full column coverage: restart with tasks in every column", async () => {
const store = createMockStore(); const store = createMockStore();
// Tasks across all columns // Tasks across all columns
const triageTask = makeTask("HAI-080", "triage"); const triageTask = makeTask("KB-080", "triage");
const todoTask = makeTask("HAI-081", "todo"); const todoTask = makeTask("KB-081", "todo");
const inProgressTask = makeTask("HAI-082", "in-progress"); const inProgressTask = makeTask("KB-082", "in-progress");
const inReviewTask = makeTask("HAI-083", "in-review"); const inReviewTask = makeTask("KB-083", "in-review");
const doneTask = makeTask("HAI-084", "done"); const doneTask = makeTask("KB-084", "done");
const allTasks = [triageTask, todoTask, inProgressTask, inReviewTask, doneTask]; const allTasks = [triageTask, todoTask, inProgressTask, inReviewTask, doneTask];
store.listTasks.mockResolvedValue(allTasks); store.listTasks.mockResolvedValue(allTasks);
@@ -521,7 +521,7 @@ describe("Scheduler after restart", () => {
await new Promise((r) => setTimeout(r, 100)); await new Promise((r) => setTimeout(r, 100));
triage.stop(); triage.stop();
expect(store.updateTask).toHaveBeenCalledWith("HAI-080", { status: "specifying" }); expect(store.updateTask).toHaveBeenCalledWith("KB-080", { status: "specifying" });
// 2. Scheduler moves todo → in-progress // 2. Scheduler moves todo → in-progress
vi.clearAllMocks(); vi.clearAllMocks();
@@ -533,7 +533,7 @@ describe("Scheduler after restart", () => {
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
scheduler.stop(); scheduler.stop();
expect(store.moveTask).toHaveBeenCalledWith("HAI-081", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-081", "in-progress");
// 3. Executor resumes in-progress tasks // 3. Executor resumes in-progress tasks
vi.clearAllMocks(); vi.clearAllMocks();
@@ -548,13 +548,13 @@ describe("Scheduler after restart", () => {
await executor.resumeOrphaned(); await executor.resumeOrphaned();
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
expect(store.logEntry).toHaveBeenCalledWith("HAI-082", "Resumed after engine restart"); expect(store.logEntry).toHaveBeenCalledWith("KB-082", "Resumed after engine restart");
// 4. Done tasks are untouched (no operations on HAI-084) // 4. Done tasks are untouched (no operations on KB-084)
const doneCalls = [ const doneCalls = [
...store.updateTask.mock.calls, ...store.updateTask.mock.calls,
...store.moveTask.mock.calls, ...store.moveTask.mock.calls,
].filter((call) => call[0] === "HAI-084"); ].filter((call) => call[0] === "KB-084");
expect(doneCalls).toHaveLength(0); expect(doneCalls).toHaveLength(0);
}); });
}); });
@@ -565,9 +565,9 @@ describe("Crash scenario edge cases", () => {
it("agent dies mid-step — onError is called, semaphore slot released, task eligible for resume", async () => { it("agent dies mid-step — onError is called, semaphore slot released, task eligible for resume", async () => {
const sem = new AgentSemaphore(2); const sem = new AgentSemaphore(2);
const store = createMockStore(); const store = createMockStore();
const task = makeTask("HAI-090", "in-progress"); const task = makeTask("KB-090", "in-progress");
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-090", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-090", "in-progress"));
// Agent session.prompt rejects (simulating crash mid-step) // Agent session.prompt rejects (simulating crash mid-step)
mockedCreateHaiAgent.mockResolvedValue({ mockedCreateHaiAgent.mockResolvedValue({
@@ -596,7 +596,7 @@ describe("Crash scenario edge cases", () => {
// Verify by calling resumeOrphaned again — it should try to execute again // Verify by calling resumeOrphaned again — it should try to execute again
vi.clearAllMocks(); vi.clearAllMocks();
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-090", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-090", "in-progress"));
mockAgentSuccess(); mockAgentSuccess();
await executor.resumeOrphaned(); await executor.resumeOrphaned();
@@ -608,7 +608,7 @@ describe("Crash scenario edge cases", () => {
it("engine killed during merge — git reset --merge cleanup, task stays in-review", async () => { it("engine killed during merge — git reset --merge cleanup, task stays in-review", async () => {
const store = createMockStore(); const store = createMockStore();
store.getTask.mockResolvedValue(makeTaskDetail("HAI-091", "in-review")); store.getTask.mockResolvedValue(makeTaskDetail("KB-091", "in-review"));
mockedExecSync.mockReturnValue(Buffer.from("")); mockedExecSync.mockReturnValue(Buffer.from(""));
@@ -620,7 +620,7 @@ describe("Crash scenario edge cases", () => {
}, },
} as any); } as any);
await expect(aiMergeTask(store, "/tmp/root", "HAI-091")).rejects.toThrow(); await expect(aiMergeTask(store, "/tmp/root", "KB-091")).rejects.toThrow();
// git reset --merge should have been called // git reset --merge should have been called
const resetCalls = mockedExecSync.mock.calls.filter( const resetCalls = mockedExecSync.mock.calls.filter(
@@ -629,17 +629,17 @@ describe("Crash scenario edge cases", () => {
expect(resetCalls.length).toBeGreaterThan(0); expect(resetCalls.length).toBeGreaterThan(0);
// Task should NOT have been moved to done // Task should NOT have been moved to done
expect(store.moveTask).not.toHaveBeenCalledWith("HAI-091", "done"); expect(store.moveTask).not.toHaveBeenCalledWith("KB-091", "done");
// Status was set to "merging" during execution // Status was set to "merging" during execution
expect(store.updateTask).toHaveBeenCalledWith("HAI-091", { status: "merging" }); expect(store.updateTask).toHaveBeenCalledWith("KB-091", { status: "merging" });
}); });
it("concurrent resumeOrphaned() calls don't double-execute the same task", async () => { it("concurrent resumeOrphaned() calls don't double-execute the same task", async () => {
const store = createMockStore(); const store = createMockStore();
const task = makeTask("HAI-092", "in-progress"); const task = makeTask("KB-092", "in-progress");
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-092", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-092", "in-progress"));
let resolvePrompt: Function; let resolvePrompt: Function;
mockedCreateHaiAgent.mockResolvedValue({ mockedCreateHaiAgent.mockResolvedValue({
@@ -676,9 +676,9 @@ describe("Crash scenario edge cases", () => {
await sem.acquire(); await sem.acquire();
expect(sem.activeCount).toBe(1); expect(sem.activeCount).toBe(1);
const task = makeTask("HAI-093", "in-progress"); const task = makeTask("KB-093", "in-progress");
store.listTasks.mockResolvedValue([task]); store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("HAI-093", "in-progress")); store.getTask.mockResolvedValue(makeTaskDetail("KB-093", "in-progress"));
// Agent creation itself fails // Agent creation itself fails
mockedCreateHaiAgent.mockRejectedValue(new Error("cannot create agent")); mockedCreateHaiAgent.mockRejectedValue(new Error("cannot create agent"));

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("./pi.js", () => ({ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(), createKbAgent: vi.fn(),
})); }));
import { reviewStep } from "./reviewer.js"; import { reviewStep } from "./reviewer.js";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent); const mockedCreateHaiAgent = vi.mocked(createKbAgent);
function createMockSession(reviewText: string) { function createMockSession(reviewText: string) {
return { return {
@@ -30,13 +30,13 @@ describe("reviewStep — model settings threading", () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it("passes defaultProvider and defaultModelId to createHaiAgent when provided", async () => { it("passes defaultProvider and defaultModelId to createKbAgent when provided", async () => {
mockedCreateHaiAgent.mockResolvedValue( mockedCreateHaiAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."), createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
); );
await reviewStep( await reviewStep(
"/tmp/worktree", "HAI-100", 1, "Test Step", "plan", "# prompt", "/tmp/worktree", "KB-100", 1, "Test Step", "plan", "# prompt",
undefined, undefined,
{ {
defaultProvider: "anthropic", defaultProvider: "anthropic",
@@ -56,7 +56,7 @@ describe("reviewStep — model settings threading", () => {
); );
await reviewStep( await reviewStep(
"/tmp/worktree", "HAI-100", 1, "Test Step", "plan", "# prompt", "/tmp/worktree", "KB-100", 1, "Test Step", "plan", "# prompt",
undefined, undefined,
{}, {},
); );
@@ -73,7 +73,7 @@ describe("reviewStep — model settings threading", () => {
); );
const result = await reviewStep( const result = await reviewStep(
"/tmp/worktree", "HAI-100", 1, "Test Step", "plan", "# prompt", "/tmp/worktree", "KB-100", 1, "Test Step", "plan", "# prompt",
); );
expect(result.verdict).toBe("APPROVE"); expect(result.verdict).toBe("APPROVE");

View File

@@ -8,7 +8,7 @@
* - Verdict + feedback is returned to the worker * - Verdict + feedback is returned to the worker
*/ */
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
const REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer. const REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
@@ -136,7 +136,7 @@ export async function reviewStep(
); );
// Spawn a reviewer agent with read-only tools // Spawn a reviewer agent with read-only tools
const { session } = await createHaiAgent({ const { session } = await createKbAgent({
cwd, cwd,
systemPrompt: REVIEWER_SYSTEM_PROMPT, systemPrompt: REVIEWER_SYSTEM_PROMPT,
tools: "readonly", tools: "readonly",

View File

@@ -3,7 +3,7 @@ import { Scheduler } from "./scheduler.js";
function makeTask(overrides: Record<string, unknown> = {}) { function makeTask(overrides: Record<string, unknown> = {}) {
return { return {
id: "HAI-001", id: "KB-001",
title: "Test Task", title: "Test Task",
column: "todo", column: "todo",
dependencies: [], dependencies: [],
@@ -49,40 +49,40 @@ describe("Scheduler concurrency", () => {
it("respects maxConcurrent with only in-progress tasks", async () => { it("respects maxConcurrent with only in-progress tasks", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-progress" }), makeTask({ id: "KB-002", column: "in-progress" }),
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
await runSchedule(scheduler); await runSchedule(scheduler);
// HAI-003 should NOT be moved — 2 in-progress already fills maxConcurrent // KB-003 should NOT be moved — 2 in-progress already fills maxConcurrent
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
}); });
it("counts specifying tasks toward concurrency", async () => { it("counts specifying tasks toward concurrency", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }), makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
await runSchedule(scheduler); await runSchedule(scheduler);
// 1 in-progress + 1 specifying = 2 agent slots, no room for HAI-003 // 1 in-progress + 1 specifying = 2 agent slots, no room for KB-003
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
}); });
it("blocks all todo tasks when specifying fills all slots", async () => { it("blocks all todo tasks when specifying fills all slots", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "triage", status: "specifying" }), makeTask({ id: "KB-001", column: "triage", status: "specifying" }),
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }), makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
makeTask({ id: "HAI-004", column: "todo" }), makeTask({ id: "KB-004", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
@@ -94,9 +94,9 @@ describe("Scheduler concurrency", () => {
it("allows scheduling when mixed slots leave room", async () => { it("allows scheduling when mixed slots leave room", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }), makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -111,14 +111,14 @@ describe("Scheduler concurrency", () => {
await runSchedule(scheduler); await runSchedule(scheduler);
// 1 in-progress + 1 specifying = 2 slots used, 1 available // 1 in-progress + 1 specifying = 2 slots used, 1 available
expect(store.moveTask).toHaveBeenCalledWith("HAI-003", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-003", "in-progress");
}); });
it("behaves normally when no tasks are specifying", async () => { it("behaves normally when no tasks are specifying", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "triage" }), // no status: "specifying" makeTask({ id: "KB-002", column: "triage" }), // no status: "specifying"
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
@@ -126,7 +126,7 @@ describe("Scheduler concurrency", () => {
await runSchedule(scheduler); await runSchedule(scheduler);
// Only 1 in-progress, triage task without "specifying" doesn't count // Only 1 in-progress, triage task without "specifying" doesn't count
expect(store.moveTask).toHaveBeenCalledWith("HAI-003", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-003", "in-progress");
}); });
}); });
@@ -142,8 +142,8 @@ describe("Scheduler dynamic settings reload", () => {
it("reads maxConcurrent from store settings on each schedule() call", async () => { it("reads maxConcurrent from store settings on each schedule() call", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
// Start with maxConcurrent: 1 — no room // Start with maxConcurrent: 1 — no room
@@ -159,7 +159,7 @@ describe("Scheduler dynamic settings reload", () => {
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
// Now bump maxConcurrent to 2 — room for HAI-002 // Now bump maxConcurrent to 2 — room for KB-002
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
@@ -169,14 +169,14 @@ describe("Scheduler dynamic settings reload", () => {
}); });
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-002", "in-progress");
}); });
it("reads maxWorktrees from store settings on each schedule() call", async () => { it("reads maxWorktrees from store settings on each schedule() call", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-review", worktree: "/tmp/wt" }), makeTask({ id: "KB-002", column: "in-review", worktree: "/tmp/wt" }),
makeTask({ id: "HAI-003", column: "todo" }), makeTask({ id: "KB-003", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
// Start with maxWorktrees: 2 — no room (2 active worktrees) // Start with maxWorktrees: 2 — no room (2 active worktrees)
@@ -192,7 +192,7 @@ describe("Scheduler dynamic settings reload", () => {
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
// Bump maxWorktrees to 3 — room for HAI-003 // Bump maxWorktrees to 3 — room for KB-003
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
maxConcurrent: 4, maxConcurrent: 4,
maxWorktrees: 3, maxWorktrees: 3,
@@ -202,7 +202,7 @@ describe("Scheduler dynamic settings reload", () => {
}); });
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.moveTask).toHaveBeenCalledWith("HAI-003", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-003", "in-progress");
}); });
it("refreshes poll interval when settings.pollIntervalMs changes", async () => { it("refreshes poll interval when settings.pollIntervalMs changes", async () => {
@@ -254,8 +254,8 @@ describe("Scheduler file-scope overlap", () => {
it("sets status 'queued' for a todo task deferred due to file scope overlap", async () => { it("sets status 'queued' for a todo task deferred due to file scope overlap", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
// Enable file scope grouping // Enable file scope grouping
@@ -268,24 +268,24 @@ describe("Scheduler file-scope overlap", () => {
}); });
// Both tasks share overlapping file scopes // Both tasks share overlapping file scopes
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => { store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
if (id === "HAI-001") return ["packages/shared/utils.ts"]; if (id === "KB-001") return ["packages/shared/utils.ts"];
if (id === "HAI-002") return ["packages/shared/utils.ts"]; if (id === "KB-002") return ["packages/shared/utils.ts"];
return []; return [];
}); });
const scheduler = new Scheduler(store, { maxConcurrent: 3 }); const scheduler = new Scheduler(store, { maxConcurrent: 3 });
await runSchedule(scheduler); await runSchedule(scheduler);
// HAI-002 should NOT be moved to in-progress (deferred) // KB-002 should NOT be moved to in-progress (deferred)
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
// HAI-002 should have status set to "queued" with blockedBy // KB-002 should have status set to "queued" with blockedBy
expect(store.updateTask).toHaveBeenCalledWith("HAI-002", { status: "queued", blockedBy: "HAI-001" }); expect(store.updateTask).toHaveBeenCalledWith("KB-002", { status: "queued", blockedBy: "KB-001" });
}); });
it("sets blockedBy to the overlapping task ID when deferred due to file scope overlap", async () => { it("sets blockedBy to the overlapping task ID when deferred due to file scope overlap", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -296,20 +296,20 @@ describe("Scheduler file-scope overlap", () => {
autoMerge: false, autoMerge: false,
}); });
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => { store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
if (id === "HAI-001") return ["packages/shared/utils.ts"]; if (id === "KB-001") return ["packages/shared/utils.ts"];
if (id === "HAI-002") return ["packages/shared/utils.ts"]; if (id === "KB-002") return ["packages/shared/utils.ts"];
return []; return [];
}); });
const scheduler = new Scheduler(store, { maxConcurrent: 3 }); const scheduler = new Scheduler(store, { maxConcurrent: 3 });
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.updateTask).toHaveBeenCalledWith("HAI-002", { status: "queued", blockedBy: "HAI-001" }); expect(store.updateTask).toHaveBeenCalledWith("KB-002", { status: "queued", blockedBy: "KB-001" });
}); });
it("clears blockedBy when a task is started", async () => { it("clears blockedBy when a task is started", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "todo" }), makeTask({ id: "KB-001", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -323,15 +323,15 @@ describe("Scheduler file-scope overlap", () => {
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: null, blockedBy: null }); expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: null, blockedBy: null });
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "in-progress");
}); });
it("does not emit console.log when deferring due to file overlap", async () => { it("does not emit console.log when deferring due to file overlap", async () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -342,8 +342,8 @@ describe("Scheduler file-scope overlap", () => {
autoMerge: false, autoMerge: false,
}); });
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => { store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
if (id === "HAI-001") return ["packages/shared/utils.ts"]; if (id === "KB-001") return ["packages/shared/utils.ts"];
if (id === "HAI-002") return ["packages/shared/utils.ts"]; if (id === "KB-002") return ["packages/shared/utils.ts"];
return []; return [];
}); });
@@ -358,8 +358,8 @@ describe("Scheduler file-scope overlap", () => {
it("does not set status 'queued' when file scopes do not overlap", async () => { it("does not set status 'queued' when file scopes do not overlap", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -370,16 +370,16 @@ describe("Scheduler file-scope overlap", () => {
autoMerge: false, autoMerge: false,
}); });
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => { store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
if (id === "HAI-001") return ["packages/a/file.ts"]; if (id === "KB-001") return ["packages/a/file.ts"];
if (id === "HAI-002") return ["packages/b/file.ts"]; if (id === "KB-002") return ["packages/b/file.ts"];
return []; return [];
}); });
const scheduler = new Scheduler(store, { maxConcurrent: 3 }); const scheduler = new Scheduler(store, { maxConcurrent: 3 });
await runSchedule(scheduler); await runSchedule(scheduler);
// HAI-002 should be moved (no overlap) // KB-002 should be moved (no overlap)
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-002", "in-progress");
}); });
}); });
@@ -395,7 +395,7 @@ describe("Scheduler paused tasks", () => {
it("does not schedule paused todo tasks", async () => { it("does not schedule paused todo tasks", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "todo", paused: true }), makeTask({ id: "KB-001", column: "todo", paused: true }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
@@ -407,20 +407,20 @@ describe("Scheduler paused tasks", () => {
it("schedules non-paused todo tasks normally", async () => { it("schedules non-paused todo tasks normally", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "todo", paused: false }), makeTask({ id: "KB-001", column: "todo", paused: false }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
const scheduler = new Scheduler(store, { maxConcurrent: 2 }); const scheduler = new Scheduler(store, { maxConcurrent: 2 });
await runSchedule(scheduler); await runSchedule(scheduler);
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "in-progress");
}); });
it("does not count paused specifying tasks toward agent slots", async () => { it("does not count paused specifying tasks toward agent slots", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "triage", status: "specifying", paused: true }), makeTask({ id: "KB-001", column: "triage", status: "specifying", paused: true }),
makeTask({ id: "HAI-002", column: "todo" }), makeTask({ id: "KB-002", column: "todo" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -434,8 +434,8 @@ describe("Scheduler paused tasks", () => {
await runSchedule(scheduler); await runSchedule(scheduler);
// The paused specifying task doesn't consume a slot, so HAI-002 should be scheduled // The paused specifying task doesn't consume a slot, so KB-002 should be scheduled
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress"); expect(store.moveTask).toHaveBeenCalledWith("KB-002", "in-progress");
}); });
}); });
@@ -451,8 +451,8 @@ describe("Scheduler worktree limit logging", () => {
it("logs worktree limit on the first pass when maxed out", async () => { it("logs worktree limit on the first pass when maxed out", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-progress" }), makeTask({ id: "KB-002", column: "in-progress" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -475,8 +475,8 @@ describe("Scheduler worktree limit logging", () => {
it("does not log worktree limit on subsequent passes while still maxed", async () => { it("does not log worktree limit on subsequent passes while still maxed", async () => {
const tasks = [ const tasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-progress" }), makeTask({ id: "KB-002", column: "in-progress" }),
]; ];
const store = createMockStore(tasks); const store = createMockStore(tasks);
store.getSettings.mockResolvedValue({ store.getSettings.mockResolvedValue({
@@ -505,11 +505,11 @@ describe("Scheduler worktree limit logging", () => {
it("logs worktree limit again after worktrees free up and become maxed again", async () => { it("logs worktree limit again after worktrees free up and become maxed again", async () => {
const maxedTasks = [ const maxedTasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
makeTask({ id: "HAI-002", column: "in-progress" }), makeTask({ id: "KB-002", column: "in-progress" }),
]; ];
const freeTasks = [ const freeTasks = [
makeTask({ id: "HAI-001", column: "in-progress" }), makeTask({ id: "KB-001", column: "in-progress" }),
]; ];
const store = createMockStore(maxedTasks); const store = createMockStore(maxedTasks);

View File

@@ -1,4 +1,4 @@
import { resolveDependencyOrder, type TaskStore, type Task } from "@hai/core"; import { resolveDependencyOrder, type TaskStore, type Task } from "@kb/core";
import type { AgentSemaphore } from "./concurrency.js"; import type { AgentSemaphore } from "./concurrency.js";
import { schedulerLog } from "./logger.js"; import { schedulerLog } from "./logger.js";

View File

@@ -1,22 +1,22 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { AgentSemaphore } from "./concurrency.js"; import { AgentSemaphore } from "./concurrency.js";
// Mock createHaiAgent before importing TriageProcessor // Mock createKbAgent before importing TriageProcessor
vi.mock("./pi.js", () => ({ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(), createKbAgent: vi.fn(),
})); }));
import { TriageProcessor, buildSpecificationPrompt, type AttachmentContent } from "./triage.js"; import { TriageProcessor, buildSpecificationPrompt, type AttachmentContent } from "./triage.js";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import type { TaskDetail } from "@hai/core"; import type { TaskDetail } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent); const mockedCreateHaiAgent = vi.mocked(createKbAgent);
function createMockStore(tasks: any[] = []) { function createMockStore(tasks: any[] = []) {
return { return {
listTasks: vi.fn().mockResolvedValue(tasks), listTasks: vi.fn().mockResolvedValue(tasks),
getTask: vi.fn().mockResolvedValue({ getTask: vi.fn().mockResolvedValue({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test task", description: "Test task",
column: "triage", column: "triage",
@@ -43,7 +43,7 @@ function createMockStore(tasks: any[] = []) {
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail { function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
return { return {
id: "HAI-001", id: "KB-001",
title: "Test Task", title: "Test Task",
description: "A test task", description: "A test task",
column: "triage", column: "triage",
@@ -79,7 +79,7 @@ describe("TriageProcessor with semaphore", () => {
const triage = new TriageProcessor(store, "/tmp/test", { semaphore: sem }); const triage = new TriageProcessor(store, "/tmp/test", { semaphore: sem });
await triage.specifyTask({ await triage.specifyTask({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "triage", column: "triage",
@@ -111,7 +111,7 @@ describe("TriageProcessor with semaphore", () => {
}); });
await triage.specifyTask({ await triage.specifyTask({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "triage", column: "triage",
@@ -163,9 +163,9 @@ describe("TriageProcessor with semaphore", () => {
}); });
await Promise.all([ await Promise.all([
triage.specifyTask(task("HAI-001")), triage.specifyTask(task("KB-001")),
triage.specifyTask(task("HAI-002")), triage.specifyTask(task("KB-002")),
triage.specifyTask(task("HAI-003")), triage.specifyTask(task("KB-003")),
]); ]);
expect(maxConcurrent).toBe(1); expect(maxConcurrent).toBe(1);
@@ -223,7 +223,7 @@ describe("TriageProcessor paused tasks", () => {
it("skips paused triage tasks in poll()", async () => { it("skips paused triage tasks in poll()", async () => {
const pausedTask = { const pausedTask = {
id: "HAI-001", id: "KB-001",
title: "Paused", title: "Paused",
description: "Paused task", description: "Paused task",
column: "triage" as const, column: "triage" as const,
@@ -255,7 +255,7 @@ describe("TriageProcessor paused tasks", () => {
it("processes non-paused triage tasks normally", async () => { it("processes non-paused triage tasks normally", async () => {
const normalTask = { const normalTask = {
id: "HAI-002", id: "KB-002",
title: "Normal", title: "Normal",
description: "Normal task", description: "Normal task",
column: "triage" as const, column: "triage" as const,
@@ -280,14 +280,14 @@ describe("TriageProcessor paused tasks", () => {
await (triage as any).poll(); await (triage as any).poll();
// Agent should be created for a non-paused task // Agent should be created for a non-paused task
expect(store.updateTask).toHaveBeenCalledWith("HAI-002", { status: "specifying" }); expect(store.updateTask).toHaveBeenCalledWith("KB-002", { status: "specifying" });
}); });
}); });
describe("buildSpecificationPrompt", () => { describe("buildSpecificationPrompt", () => {
it("includes project commands when testCommand is set", () => { it("includes project commands when testCommand is set", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", { const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", {
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
pollIntervalMs: 15000, pollIntervalMs: 15000,
@@ -303,7 +303,7 @@ describe("buildSpecificationPrompt", () => {
it("includes project commands when buildCommand is set", () => { it("includes project commands when buildCommand is set", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", { const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", {
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
pollIntervalMs: 15000, pollIntervalMs: 15000,
@@ -318,7 +318,7 @@ describe("buildSpecificationPrompt", () => {
it("includes both commands when both are set", () => { it("includes both commands when both are set", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", { const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", {
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
pollIntervalMs: 15000, pollIntervalMs: 15000,
@@ -334,7 +334,7 @@ describe("buildSpecificationPrompt", () => {
it("omits project commands section when neither command is set", () => { it("omits project commands section when neither command is set", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", { const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", {
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
pollIntervalMs: 15000, pollIntervalMs: 15000,
@@ -347,7 +347,7 @@ describe("buildSpecificationPrompt", () => {
it("omits project commands section when settings is undefined", () => { it("omits project commands section when settings is undefined", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md"); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md");
expect(result).not.toContain("## Project Commands"); expect(result).not.toContain("## Project Commands");
}); });
@@ -357,7 +357,7 @@ describe("buildSpecificationPrompt", () => {
const attachmentContents: AttachmentContent[] = [ const attachmentContents: AttachmentContent[] = [
{ originalName: "error.log", mimeType: "text/plain", text: "ERROR: something broke\nStack trace here" }, { originalName: "error.log", mimeType: "text/plain", text: "ERROR: something broke\nStack trace here" },
]; ];
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", undefined, attachmentContents); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", undefined, attachmentContents);
expect(result).toContain("## Attachments"); expect(result).toContain("## Attachments");
expect(result).toContain("### error.log (text/plain)"); expect(result).toContain("### error.log (text/plain)");
@@ -369,7 +369,7 @@ describe("buildSpecificationPrompt", () => {
const attachmentContents: AttachmentContent[] = [ const attachmentContents: AttachmentContent[] = [
{ originalName: "screenshot.png", mimeType: "image/png", text: null }, { originalName: "screenshot.png", mimeType: "image/png", text: null },
]; ];
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", undefined, attachmentContents); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", undefined, attachmentContents);
expect(result).toContain("## Attachments"); expect(result).toContain("## Attachments");
expect(result).toContain("**screenshot.png** (image/png)"); expect(result).toContain("**screenshot.png** (image/png)");
@@ -382,7 +382,7 @@ describe("buildSpecificationPrompt", () => {
{ originalName: "screenshot.png", mimeType: "image/png", text: null }, { originalName: "screenshot.png", mimeType: "image/png", text: null },
{ originalName: "config.json", mimeType: "application/json", text: '{"key": "value"}' }, { originalName: "config.json", mimeType: "application/json", text: '{"key": "value"}' },
]; ];
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", undefined, attachmentContents); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", undefined, attachmentContents);
expect(result).toContain("**screenshot.png** (image/png)"); expect(result).toContain("**screenshot.png** (image/png)");
expect(result).toContain("### config.json (application/json)"); expect(result).toContain("### config.json (application/json)");
@@ -391,14 +391,14 @@ describe("buildSpecificationPrompt", () => {
it("omits attachments section when no attachments", () => { it("omits attachments section when no attachments", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", undefined, []); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md", undefined, []);
expect(result).not.toContain("## Attachments"); expect(result).not.toContain("## Attachments");
}); });
it("omits attachments section when attachmentContents is undefined", () => { it("omits attachments section when attachmentContents is undefined", () => {
const task = createMockTaskDetail(); const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md"); const result = buildSpecificationPrompt(task, ".kb/tasks/KB-001/PROMPT.md");
expect(result).not.toContain("## Attachments"); expect(result).not.toContain("## Attachments");
}); });
@@ -412,7 +412,7 @@ function createEnoentError(path = "/fake/path"): NodeJS.ErrnoException {
} }
const dummyTask = { const dummyTask = {
id: "HAI-099", id: "KB-099",
title: "Deleted task", title: "Deleted task",
description: "This task was deleted", description: "This task was deleted",
column: "triage" as const, column: "triage" as const,
@@ -485,7 +485,7 @@ describe("TriageProcessor deleted task handling", () => {
await triage.specifyTask(dummyTask); await triage.specifyTask(dummyTask);
// If processing Set was cleaned up, updateTask will be called again for "specifying" // If processing Set was cleaned up, updateTask will be called again for "specifying"
expect(store.updateTask).toHaveBeenCalledWith("HAI-099", { status: "specifying" }); expect(store.updateTask).toHaveBeenCalledWith("KB-099", { status: "specifying" });
expect(mockedCreateHaiAgent).toHaveBeenCalled(); expect(mockedCreateHaiAgent).toHaveBeenCalled();
}); });
}); });
@@ -515,7 +515,7 @@ describe("TriageProcessor agent log persistence", () => {
const triage = new TriageProcessor(store, "/tmp/test", {}); const triage = new TriageProcessor(store, "/tmp/test", {});
await triage.specifyTask({ await triage.specifyTask({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "triage", column: "triage",
@@ -528,7 +528,7 @@ describe("TriageProcessor agent log persistence", () => {
}); });
// Text buffer is flushed in finally block // Text buffer is flushed in finally block
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-001", "Hello world", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-001", "Hello world", "text");
}); });
it("logs tool invocations to store.appendAgentLog", async () => { it("logs tool invocations to store.appendAgentLog", async () => {
@@ -549,7 +549,7 @@ describe("TriageProcessor agent log persistence", () => {
const triage = new TriageProcessor(store, "/tmp/test", {}); const triage = new TriageProcessor(store, "/tmp/test", {});
await triage.specifyTask({ await triage.specifyTask({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "triage", column: "triage",
@@ -561,7 +561,7 @@ describe("TriageProcessor agent log persistence", () => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-001", "Read", "tool", "foo.ts"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-001", "Read", "tool", "foo.ts");
}); });
it("still fires onAgentText callback alongside logging", async () => { it("still fires onAgentText callback alongside logging", async () => {
@@ -583,7 +583,7 @@ describe("TriageProcessor agent log persistence", () => {
const triage = new TriageProcessor(store, "/tmp/test", { onAgentText }); const triage = new TriageProcessor(store, "/tmp/test", { onAgentText });
await triage.specifyTask({ await triage.specifyTask({
id: "HAI-001", id: "KB-001",
title: "Test", title: "Test",
description: "Test", description: "Test",
column: "triage", column: "triage",
@@ -595,7 +595,7 @@ describe("TriageProcessor agent log persistence", () => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
expect(onAgentText).toHaveBeenCalledWith("HAI-001", "hi"); expect(onAgentText).toHaveBeenCalledWith("KB-001", "hi");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-001", "hi", "text"); expect(store.appendAgentLog).toHaveBeenCalledWith("KB-001", "hi", "text");
}); });
}); });

View File

@@ -1,13 +1,13 @@
import type { TaskStore, Task, TaskDetail, TaskAttachment, Settings } from "@hai/core"; import type { TaskStore, Task, TaskDetail, TaskAttachment, Settings } from "@kb/core";
import type { ImageContent } from "@mariozechner/pi-ai"; import type { ImageContent } from "@mariozechner/pi-ai";
import { Type, type Static } from "@mariozechner/pi-ai"; import { Type, type Static } from "@mariozechner/pi-ai";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { createHaiAgent } from "./pi.js"; import { createKbAgent } from "./pi.js";
import type { AgentSemaphore } from "./concurrency.js"; import type { AgentSemaphore } from "./concurrency.js";
import { AgentLogger } from "./agent-logger.js"; import { AgentLogger } from "./agent-logger.js";
import { triageLog } from "./logger.js"; import { triageLog } from "./logger.js";
const TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "hai", an AI-orchestrated task board. const TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "kb", an AI-orchestrated task board.
Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation. Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation.
@@ -81,7 +81,7 @@ Follow this structure exactly:
### Step {N}: Documentation & Delivery ### Step {N}: Documentation & Delivery
- [ ] Update relevant documentation - [ ] Update relevant documentation
- [ ] Out-of-scope findings created as new tasks via \`hai task create\` - [ ] Out-of-scope findings created as new tasks via \`kb task create\`
## Documentation Requirements ## Documentation Requirements
@@ -250,7 +250,7 @@ export class TriageProcessor {
await this.store.updateTask(task.id, { status: "specifying" }); await this.store.updateTask(task.id, { status: "specifying" });
const detail = await this.store.getTask(task.id); const detail = await this.store.getTask(task.id);
const settings = await this.store.getSettings(); const settings = await this.store.getSettings();
const promptPath = `.hai/tasks/${task.id}/PROMPT.md`; const promptPath = `.kb/tasks/${task.id}/PROMPT.md`;
const agentWork = async () => { const agentWork = async () => {
const agentLogger = new AgentLogger({ const agentLogger = new AgentLogger({
@@ -264,7 +264,7 @@ export class TriageProcessor {
}, },
}); });
const { session } = await createHaiAgent({ const { session } = await createKbAgent({
cwd: this.rootDir, cwd: this.rootDir,
systemPrompt: TRIAGE_SYSTEM_PROMPT, systemPrompt: TRIAGE_SYSTEM_PROMPT,
tools: "coding", tools: "coding",
@@ -333,7 +333,7 @@ export class TriageProcessor {
const store = this.store; const store = this.store;
const taskGetParams = Type.Object({ const taskGetParams = Type.Object({
id: Type.String({ description: "Task ID (e.g. HAI-001)" }), id: Type.String({ description: "Task ID (e.g. KB-001)" }),
}); });
const taskList: ToolDefinition = { const taskList: ToolDefinition = {
@@ -431,7 +431,7 @@ export async function readAttachmentContents(
const { join } = await import("node:path"); const { join } = await import("node:path");
for (const att of attachments) { for (const att of attachments) {
const filePath = join(rootDir, ".hai", "tasks", taskId, "attachments", att.filename); const filePath = join(rootDir, ".kb", "tasks", taskId, "attachments", att.filename);
try { try {
if (IMAGE_MIME_TYPES.has(att.mimeType)) { if (IMAGE_MIME_TYPES.has(att.mimeType)) {

View File

@@ -8,7 +8,7 @@ describe("generateWorktreeName", () => {
let tempDir: string; let tempDir: string;
beforeEach(() => { beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "hai-wt-test-")); tempDir = mkdtempSync(join(tmpdir(), "kb-wt-test-"));
}); });
afterEach(() => { afterEach(() => {

View File

@@ -120,7 +120,7 @@ describe("WorktreePool", () => {
describe("prepareForTask", () => { describe("prepareForTask", () => {
it("cleans dirty working tree before checkout", () => { it("cleans dirty working tree before checkout", () => {
pool.prepareForTask("/tmp/wt", "hai/hai-042"); pool.prepareForTask("/tmp/wt", "kb/kb-042");
const calls = mockedExecSync.mock.calls.map((c) => c[0]); const calls = mockedExecSync.mock.calls.map((c) => c[0]);
expect(calls).toContain("git checkout -- ."); expect(calls).toContain("git checkout -- .");
@@ -128,18 +128,18 @@ describe("WorktreePool", () => {
}); });
it("creates branch from main with force-reset", () => { it("creates branch from main with force-reset", () => {
pool.prepareForTask("/tmp/wt", "hai/hai-042"); pool.prepareForTask("/tmp/wt", "kb/kb-042");
const checkoutCall = mockedExecSync.mock.calls.find( const checkoutCall = mockedExecSync.mock.calls.find(
(c) => typeof c[0] === "string" && (c[0] as string).includes("checkout -B"), (c) => typeof c[0] === "string" && (c[0] as string).includes("checkout -B"),
); );
expect(checkoutCall).toBeDefined(); expect(checkoutCall).toBeDefined();
expect(checkoutCall![0]).toBe('git checkout -B "hai/hai-042" main'); expect(checkoutCall![0]).toBe('git checkout -B "kb/kb-042" main');
expect(checkoutCall![1]).toMatchObject({ cwd: "/tmp/wt" }); expect(checkoutCall![1]).toMatchObject({ cwd: "/tmp/wt" });
}); });
it("runs all commands in the correct worktree directory", () => { it("runs all commands in the correct worktree directory", () => {
pool.prepareForTask("/tmp/my-worktree", "hai/hai-099"); pool.prepareForTask("/tmp/my-worktree", "kb/kb-099");
for (const call of mockedExecSync.mock.calls) { for (const call of mockedExecSync.mock.calls) {
expect(call[1]).toMatchObject({ cwd: "/tmp/my-worktree" }); expect(call[1]).toMatchObject({ cwd: "/tmp/my-worktree" });
@@ -153,12 +153,12 @@ describe("WorktreePool", () => {
}); });
// Should not throw // Should not throw
expect(() => pool.prepareForTask("/tmp/wt", "hai/hai-001")).not.toThrow(); expect(() => pool.prepareForTask("/tmp/wt", "kb/kb-001")).not.toThrow();
// Should still run clean and branch creation // Should still run clean and branch creation
const calls = mockedExecSync.mock.calls.map((c) => c[0]); const calls = mockedExecSync.mock.calls.map((c) => c[0]);
expect(calls).toContain("git clean -fd"); expect(calls).toContain("git clean -fd");
expect(calls).toContain('git checkout -B "hai/hai-001" main'); expect(calls).toContain('git checkout -B "kb/kb-001" main');
}); });
}); });
}); });

View File

@@ -82,7 +82,7 @@ export class WorktreePool {
* 3. `git checkout -B <branchName> main` — create/reset branch from main * 3. `git checkout -B <branchName> main` — create/reset branch from main
* *
* @param worktreePath — Absolute path to the recycled worktree * @param worktreePath — Absolute path to the recycled worktree
* @param branchName — Branch name for the new task (e.g., `hai/hai-042`) * @param branchName — Branch name for the new task (e.g., `kb/kb-042`)
*/ */
prepareForTask(worktreePath: string, branchName: string): void { prepareForTask(worktreePath: string, branchName: string): void {
// Clean tracked modifications // Clean tracked modifications

10
pnpm-lock.yaml generated
View File

@@ -20,13 +20,13 @@ importers:
packages/cli: packages/cli:
dependencies: dependencies:
'@hai/core': '@kb/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
'@hai/dashboard': '@kb/dashboard':
specifier: workspace:* specifier: workspace:*
version: link:../dashboard version: link:../dashboard
'@hai/engine': '@kb/engine':
specifier: workspace:* specifier: workspace:*
version: link:../engine version: link:../engine
'@mariozechner/pi-coding-agent': '@mariozechner/pi-coding-agent':
@@ -60,7 +60,7 @@ importers:
packages/dashboard: packages/dashboard:
dependencies: dependencies:
'@hai/core': '@kb/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
'@types/multer': '@types/multer':
@@ -124,7 +124,7 @@ importers:
packages/engine: packages/engine:
dependencies: dependencies:
'@hai/core': '@kb/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
'@mariozechner/pi-ai': '@mariozechner/pi-ai':