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:
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: hai-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.
|
||||
name: kb-board
|
||||
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
|
||||
|
||||
```bash
|
||||
hai dashboard
|
||||
kb dashboard
|
||||
```
|
||||
|
||||
Opens the kanban board at http://localhost:4040 and starts the AI engine.
|
||||
@@ -30,7 +30,7 @@ Run in two terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1: start the server + engine
|
||||
hai dashboard
|
||||
kb dashboard
|
||||
|
||||
# Terminal 2: watch-rebuild the React dashboard UI
|
||||
pnpm dev:ui
|
||||
@@ -38,7 +38,7 @@ pnpm dev:ui
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are in `.hai/config.json`:
|
||||
Settings are in `.kb/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -61,13 +61,13 @@ Settings are in `.hai/config.json`:
|
||||
|
||||
## Task storage
|
||||
|
||||
Tasks live in `.hai/tasks/`:
|
||||
Tasks live in `.kb/tasks/`:
|
||||
|
||||
```
|
||||
.hai/
|
||||
.kb/
|
||||
├── config.json
|
||||
└── tasks/
|
||||
└── HAI-001/
|
||||
└── KB-001/
|
||||
├── task.json # Metadata, steps, log
|
||||
└── PROMPT.md # Task specification
|
||||
```
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: hai-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.
|
||||
name: kb-task
|
||||
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**
|
||||
|
||||
## Commands
|
||||
@@ -13,9 +13,9 @@ hai is an AI-orchestrated task board. Tasks flow through columns:
|
||||
### Create a task
|
||||
|
||||
```bash
|
||||
hai task create "description of what needs to be done"
|
||||
hai task create "button is misaligned" --attach screenshot.png
|
||||
hai task create "server crash" --attach error.log --attach trace.txt
|
||||
kb task create "description of what needs to be done"
|
||||
kb task create "button is misaligned" --attach screenshot.png
|
||||
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
|
||||
@@ -25,7 +25,7 @@ move it to **todo**.
|
||||
Options:
|
||||
- `--attach <file>` — attach files (images, logs, configs). Repeatable.
|
||||
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:
|
||||
- Be descriptive — the triage agent uses this to write the spec
|
||||
@@ -37,7 +37,7 @@ Tips:
|
||||
### List tasks
|
||||
|
||||
```bash
|
||||
hai task list
|
||||
kb task list
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
hai task show HAI-001
|
||||
kb task show KB-001
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
hai task move HAI-001 <column>
|
||||
kb task move KB-001 <column>
|
||||
```
|
||||
|
||||
Columns: `triage`, `todo`, `in-progress`, `in-review`, `done`
|
||||
@@ -68,7 +68,7 @@ Transitions are validated:
|
||||
### Update step status
|
||||
|
||||
```bash
|
||||
hai task update HAI-001 <step-number> <status>
|
||||
kb task update KB-001 <step-number> <status>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```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.
|
||||
@@ -86,7 +86,7 @@ Adds a timestamped log entry visible on the task card.
|
||||
### Merge a completed task
|
||||
|
||||
```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.
|
||||
@@ -95,12 +95,12 @@ Cleans up the worktree and branch after merge.
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -108,14 +108,14 @@ A task can be anything from a rough idea to a detailed spec:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
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
|
||||
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
|
||||
hai task create "server crashes on startup" --attach crash.log
|
||||
kb task create "server crashes on startup" --attach crash.log
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run: pnpm test
|
||||
|
||||
- name: Build standalone binary
|
||||
run: pnpm --filter hai build:exe
|
||||
run: pnpm --filter @dustinbyrne/kb build:exe
|
||||
|
||||
- name: Verify binary exists
|
||||
run: test -f packages/cli/dist/hai
|
||||
run: test -f packages/cli/dist/kb
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -29,16 +29,16 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: bun-linux-x64
|
||||
binary: hai-linux-x64
|
||||
binary: kb-linux-x64
|
||||
- os: macos-latest
|
||||
target: bun-darwin-arm64
|
||||
binary: hai-darwin-arm64
|
||||
binary: kb-darwin-arm64
|
||||
- os: macos-13
|
||||
target: bun-darwin-x64
|
||||
binary: hai-darwin-x64
|
||||
binary: kb-darwin-x64
|
||||
- os: windows-latest
|
||||
target: bun-windows-x64
|
||||
binary: hai-windows-x64.exe
|
||||
binary: kb-windows-x64.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- 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
|
||||
shell: bash
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- name: Collect release files
|
||||
run: |
|
||||
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/
|
||||
|
||||
- name: Create GitHub Release
|
||||
|
||||
12
.github/workflows/test-release.yml
vendored
12
.github/workflows/test-release.yml
vendored
@@ -19,16 +19,16 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: bun-linux-x64
|
||||
binary: hai-linux-x64
|
||||
binary: kb-linux-x64
|
||||
- os: macos-latest
|
||||
target: bun-darwin-arm64
|
||||
binary: hai-darwin-arm64
|
||||
binary: kb-darwin-arm64
|
||||
- os: macos-13
|
||||
target: bun-darwin-x64
|
||||
binary: hai-darwin-x64
|
||||
binary: kb-darwin-x64
|
||||
- os: windows-latest
|
||||
target: bun-windows-x64
|
||||
binary: hai-windows-x64.exe
|
||||
binary: kb-windows-x64.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- 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
|
||||
shell: bash
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- name: Combine artifacts
|
||||
run: |
|
||||
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/
|
||||
|
||||
- name: Upload combined archive
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# hai runtime (local board state — don't commit)
|
||||
.kb/
|
||||
.hai/
|
||||
|
||||
# Taskplane runtime artifacts
|
||||
|
||||
56
README.md
56
README.md
@@ -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).
|
||||
|
||||
@@ -39,16 +39,16 @@ pnpm dev task create "Fix the login redirect bug"
|
||||
pnpm dev task list
|
||||
|
||||
# Move a task
|
||||
pnpm dev task move HAI-001 todo
|
||||
pnpm dev task move KB-001 todo
|
||||
|
||||
# Pause a task (stops all automation)
|
||||
pnpm dev task pause HAI-001
|
||||
pnpm dev task pause KB-001
|
||||
|
||||
# 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)
|
||||
pnpm dev task attach HAI-001 ./screenshot.png
|
||||
pnpm dev task attach KB-001 ./screenshot.png
|
||||
|
||||
# Create a task with attachments
|
||||
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`
|
||||
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
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@hai/core` | Domain model — tasks, board columns, file-based store |
|
||||
| `@hai/dashboard` | Web UI — Express server + kanban board with SSE |
|
||||
| `@hai/engine` | AI engine — triage (pi), execution (pi + worktrees), scheduling |
|
||||
| `hai` (cli) | CLI — `hai dashboard`, `hai task create/list/move/attach` |
|
||||
| `@kb/core` | Domain model — tasks, board columns, file-based store |
|
||||
| `@kb/dashboard` | Web UI — Express server + kanban board with SSE |
|
||||
| `@kb/engine` | AI engine — triage (pi), execution (pi + worktrees), scheduling |
|
||||
| `kb` (cli) | CLI — `kb dashboard`, `kb task create/list/move/attach` |
|
||||
|
||||
## Architecture
|
||||
|
||||
### 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
|
||||
└── tasks/
|
||||
└── HAI-001/
|
||||
└── KB-001/
|
||||
├── task.json # Metadata (column, deps, timestamps)
|
||||
├── PROMPT.md # Task specification
|
||||
└── attachments/ # File attachments — images & text files (optional)
|
||||
@@ -144,7 +144,7 @@ pnpm dev task list # CLI commands
|
||||
|
||||
## 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
|
||||
pnpm build:exe
|
||||
@@ -152,18 +152,18 @@ pnpm build:exe
|
||||
|
||||
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
|
||||
|
||||
Run the binary directly — no Node.js, pnpm, or workspace setup needed:
|
||||
|
||||
```bash
|
||||
./packages/cli/dist/hai --help
|
||||
./packages/cli/dist/hai task list
|
||||
./packages/cli/dist/hai dashboard
|
||||
./packages/cli/dist/kb --help
|
||||
./packages/cli/dist/kb task list
|
||||
./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
|
||||
|
||||
@@ -177,24 +177,24 @@ This produces binaries for all supported targets in `packages/cli/dist/`:
|
||||
|
||||
| Target | Output |
|
||||
|--------|--------|
|
||||
| `bun-linux-x64` | `hai-linux-x64` |
|
||||
| `bun-linux-arm64` | `hai-linux-arm64` |
|
||||
| `bun-darwin-x64` | `hai-darwin-x64` |
|
||||
| `bun-darwin-arm64` | `hai-darwin-arm64` |
|
||||
| `bun-windows-x64` | `hai-windows-x64.exe` |
|
||||
| `bun-linux-x64` | `kb-linux-x64` |
|
||||
| `bun-linux-arm64` | `kb-linux-arm64` |
|
||||
| `bun-darwin-x64` | `kb-darwin-x64` |
|
||||
| `bun-darwin-arm64` | `kb-darwin-arm64` |
|
||||
| `bun-windows-x64` | `kb-windows-x64.exe` |
|
||||
|
||||
To build for a specific platform:
|
||||
|
||||
```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).
|
||||
|
||||
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
|
||||
HAI_CLIENT_DIR=/path/to/client ./hai dashboard
|
||||
KB_CLIENT_DIR=/path/to/client ./kb dashboard
|
||||
```
|
||||
|
||||
**Prerequisites:** Bun ≥ 1.0 (`bun --version`)
|
||||
@@ -206,7 +206,7 @@ Packages are published to npm automatically via GitHub Actions and [changesets](
|
||||
### Installing from npm
|
||||
|
||||
```bash
|
||||
npm install -g hai
|
||||
npm install -g kb
|
||||
```
|
||||
|
||||
### Triggering a release
|
||||
|
||||
16
RELEASING.md
16
RELEASING.md
@@ -36,7 +36,7 @@ When you merge the Version Packages PR:
|
||||
|
||||
- The `version.yml` workflow detects that all changesets have been consumed
|
||||
- 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:
|
||||
- Builds platform-specific binaries for Linux x64, macOS x64, macOS arm64, and Windows x64
|
||||
- Signs macOS binaries (codesign + notarization) and Windows binaries (Authenticode)
|
||||
@@ -54,10 +54,10 @@ When you merge the Version Packages PR:
|
||||
|
||||
| Platform | Binary name | Signed |
|
||||
|----------|------------|--------|
|
||||
| Linux x64 | `hai-linux-x64` | — |
|
||||
| macOS arm64 | `hai-darwin-arm64` | ✓ (codesign + notarization) |
|
||||
| macOS x64 | `hai-darwin-x64` | ✓ (codesign + notarization) |
|
||||
| Windows x64 | `hai-windows-x64.exe` | ✓ (Authenticode) |
|
||||
| Linux x64 | `kb-linux-x64` | — |
|
||||
| macOS arm64 | `kb-darwin-arm64` | ✓ (codesign + notarization) |
|
||||
| macOS x64 | `kb-darwin-x64` | ✓ (codesign + notarization) |
|
||||
| Windows x64 | `kb-windows-x64.exe` | ✓ (Authenticode) |
|
||||
|
||||
## 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 status` | Check pending changesets |
|
||||
| `pnpm release:version` | Apply changesets and bump versions (used by CI) |
|
||||
| `pnpm --filter hai build:exe` | Build binary for current platform |
|
||||
| `pnpm --filter hai 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` | Build binary for current platform |
|
||||
| `pnpm --filter @dustinbyrne/kb build:exe -- --target <target>` | Cross-compile for a specific platform |
|
||||
| `pnpm --filter @dustinbyrne/kb build:exe:all` | Build binaries for all platforms |
|
||||
|
||||
## Tips
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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/)
|
||||
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
|
||||
|
||||
### 5. Determine Your Signing Identity
|
||||
|
||||
572
package-lock.json
generated
572
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "hai-workspace",
|
||||
"name": "kb-workspace",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"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:exe": "pnpm build && pnpm --filter hai build:exe",
|
||||
"build:exe:all": "pnpm build && pnpm --filter hai build:exe:all",
|
||||
"build:exe": "pnpm build && pnpm --filter @dustinbyrne/kb build:exe",
|
||||
"build:exe:all": "pnpm build && pnpm --filter @dustinbyrne/kb build:exe:all",
|
||||
"test": "pnpm -r test",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"changeset": "changeset",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/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/.
|
||||
*
|
||||
* Usage:
|
||||
@@ -38,20 +38,20 @@ type BunTarget = (typeof SUPPORTED_TARGETS)[number];
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// "bun-linux-x64" → "linux-x64"
|
||||
const suffix = target.replace(/^bun-/, "");
|
||||
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).
|
||||
*/
|
||||
function defaultBinaryName(): string {
|
||||
return process.platform === "win32" ? "hai.exe" : "hai";
|
||||
return process.platform === "win32" ? "kb.exe" : "kb";
|
||||
}
|
||||
|
||||
// ── Parse CLI arguments ───────────────────────────────────────────────
|
||||
@@ -146,7 +146,7 @@ const { targets } = parseArgs();
|
||||
copyClientAssets();
|
||||
|
||||
if (targets === null) {
|
||||
// Default: build for current platform → dist/hai
|
||||
// Default: build for current platform → dist/kb
|
||||
const outBinary = join(outDir, defaultBinaryName());
|
||||
const ok = compileBinary(outBinary, "bun");
|
||||
if (!ok) process.exit(1);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@dustinbyrne/hai",
|
||||
"name": "@dustinbyrne/kb",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"hai": "./dist/bin.js"
|
||||
"kb": "./dist/bin.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -18,9 +18,9 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hai/core": "workspace:*",
|
||||
"@hai/dashboard": "workspace:*",
|
||||
"@hai/engine": "workspace:*",
|
||||
"@kb/core": "workspace:*",
|
||||
"@kb/dashboard": "workspace:*",
|
||||
"@kb/engine": "workspace:*",
|
||||
"@mariozechner/pi-coding-agent": "^0.62.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@ const SUPPORTED_TARGETS = [
|
||||
function expectedBinaryName(target: string): string {
|
||||
const suffix = target.replace(/^bun-/, "");
|
||||
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);
|
||||
|
||||
it("produces dist/hai-linux-x64", () => {
|
||||
const bin = join(distDir, "hai-linux-x64");
|
||||
it("produces dist/kb-linux-x64", () => {
|
||||
const bin = join(distDir, "kb-linux-x64");
|
||||
expect(existsSync(bin)).toBe(true);
|
||||
expect(statSync(bin).size).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -64,8 +64,8 @@ describe("build-exe-cross: windows target has .exe extension", () => {
|
||||
});
|
||||
}, 180_000);
|
||||
|
||||
it("produces dist/hai-windows-x64.exe", () => {
|
||||
const bin = join(distDir, "hai-windows-x64.exe");
|
||||
it("produces dist/kb-windows-x64.exe", () => {
|
||||
const bin = join(distDir, "kb-windows-x64.exe");
|
||||
expect(existsSync(bin)).toBe(true);
|
||||
expect(statSync(bin).size).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -108,7 +108,7 @@ describe("build-exe-cross: --all builds all platforms", () => {
|
||||
timeout: 15_000,
|
||||
});
|
||||
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);
|
||||
|
||||
it("produces dist/hai (no platform suffix)", () => {
|
||||
const defaultName = process.platform === "win32" ? "hai.exe" : "hai";
|
||||
it("produces dist/kb (no platform suffix)", () => {
|
||||
const defaultName = process.platform === "win32" ? "kb.exe" : "kb";
|
||||
const bin = join(distDir, defaultName);
|
||||
expect(existsSync(bin)).toBe(true);
|
||||
expect(statSync(bin).size).toBeGreaterThan(0);
|
||||
|
||||
@@ -6,8 +6,8 @@ import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const cliRoot = join(import.meta.dirname!, "..", "..");
|
||||
const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "hai.exe" : "hai");
|
||||
const binaryName = process.platform === "win32" ? "hai.exe" : "hai";
|
||||
const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "kb.exe" : "kb");
|
||||
const binaryName = process.platform === "win32" ? "kb.exe" : "kb";
|
||||
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.
|
||||
*/
|
||||
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(clientDir, join(dir, "client"), { recursive: true });
|
||||
return {
|
||||
@@ -60,7 +60,7 @@ describe("build-exe", () => {
|
||||
timeout: 15_000,
|
||||
});
|
||||
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("task create");
|
||||
expect(result.stdout).toContain("task list");
|
||||
@@ -110,7 +110,7 @@ describe("build-exe", () => {
|
||||
resolve(out);
|
||||
});
|
||||
});
|
||||
expect(output).toContain("hai board");
|
||||
expect(output).toContain("kb board");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
|
||||
});
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -19,9 +19,9 @@ function loadWorkflowYaml(name: string): any {
|
||||
describe("CLI package.json publishing config", () => {
|
||||
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.hai).toBe("./dist/bin.js");
|
||||
expect(pkg.bin.kb).toBe("./dist/bin.js");
|
||||
});
|
||||
|
||||
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"];
|
||||
|
||||
for (const name of scopedPackages) {
|
||||
describe(`@hai/${name}`, () => {
|
||||
describe(`@kb/${name}`, () => {
|
||||
const pkg = loadPackageJson(name);
|
||||
|
||||
it('has publishConfig with access "public"', () => {
|
||||
|
||||
@@ -24,11 +24,11 @@ if (isBunBinary) {
|
||||
|
||||
if (!existsSync(localPkg)) {
|
||||
// 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(
|
||||
join(tmp, "package.json"),
|
||||
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,
|
||||
2,
|
||||
) + "\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 HELP = `
|
||||
hai — AI-orchestrated task board
|
||||
kb — AI-orchestrated task board
|
||||
|
||||
Usage:
|
||||
hai dashboard Start the board web UI
|
||||
hai task create [desc] [--attach f] Create a new task (goes to triage)
|
||||
hai task list List all tasks
|
||||
hai task show <id> Show task details, steps, log
|
||||
hai task move <id> <col> Move a task to a column
|
||||
hai task update <id> <step> <status> Update step status (pending|in-progress|done|skipped)
|
||||
hai task log <id> <message> Add a log entry
|
||||
hai task merge <id> Merge an in-review task and close it
|
||||
hai task attach <id> <file> Attach a file to a task
|
||||
hai task pause <id> Pause a task (stops all automation)
|
||||
hai task unpause <id> Unpause a task (resumes automation)
|
||||
kb dashboard Start the board web UI
|
||||
kb task create [desc] [--attach f] Create a new task (goes to triage)
|
||||
kb task list List all tasks
|
||||
kb task show <id> Show task details, steps, log
|
||||
kb task move <id> <col> Move a task to a column
|
||||
kb task update <id> <step> <status> Update step status (pending|in-progress|done|skipped)
|
||||
kb task log <id> <message> Add a log entry
|
||||
kb task merge <id> Merge an in-review task and close it
|
||||
kb task attach <id> <file> Attach a file to a task
|
||||
kb task pause <id> Pause a task (stops all automation)
|
||||
kb task unpause <id> Unpause a task (resumes automation)
|
||||
|
||||
Options:
|
||||
--port, -p <port> Dashboard port (default: 4040)
|
||||
@@ -118,7 +118,7 @@ async function main() {
|
||||
const id = args[2];
|
||||
const column = args[3];
|
||||
if (!id || !column) {
|
||||
console.error("Usage: hai task move <id> <column>");
|
||||
console.error("Usage: kb task move <id> <column>");
|
||||
process.exit(1);
|
||||
}
|
||||
await runTaskMove(id, column);
|
||||
@@ -126,14 +126,14 @@ async function main() {
|
||||
}
|
||||
case "show": {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
const id = args[2], step = args[3], status = args[4];
|
||||
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");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -142,20 +142,20 @@ async function main() {
|
||||
}
|
||||
case "log": {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case "merge": {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case "attach": {
|
||||
const id = args[2], file = args[3];
|
||||
if (!id || !file) {
|
||||
console.error("Usage: hai task attach <id> <file>");
|
||||
console.error("Usage: kb task attach <id> <file>");
|
||||
process.exit(1);
|
||||
}
|
||||
await runTaskAttach(id, file);
|
||||
@@ -163,19 +163,19 @@ async function main() {
|
||||
}
|
||||
case "pause": {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case "unpause": {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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()),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/dashboard ─────────────────────────────────────────────
|
||||
// ── Mock @kb/dashboard ─────────────────────────────────────────────
|
||||
|
||||
const mockListen = vi.fn();
|
||||
vi.mock("@hai/dashboard", () => ({
|
||||
vi.mock("@kb/dashboard", () => ({
|
||||
createServer: vi.fn(() => ({ listen: mockListen })),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/engine ────────────────────────────────────────────────
|
||||
// ── Mock @kb/engine ────────────────────────────────────────────────
|
||||
|
||||
vi.mock("@hai/engine", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("@hai/engine")>();
|
||||
vi.mock("@kb/engine", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("@kb/engine")>();
|
||||
return {
|
||||
...original,
|
||||
WorktreePool: original.WorktreePool,
|
||||
@@ -81,12 +81,12 @@ const { runDashboard } = await import("../dashboard.js");
|
||||
describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const { TaskStore } = await import("@hai/core");
|
||||
const { TaskStore } = await import("@kb/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
|
||||
@@ -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()),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/dashboard ─────────────────────────────────────────────
|
||||
// ── Mock @kb/dashboard ─────────────────────────────────────────────
|
||||
|
||||
const mockListen = vi.fn();
|
||||
vi.mock("@hai/dashboard", () => ({
|
||||
vi.mock("@kb/dashboard", () => ({
|
||||
createServer: vi.fn(() => ({ listen: mockListen })),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/engine ────────────────────────────────────────────────
|
||||
// ── Mock @kb/engine ────────────────────────────────────────────────
|
||||
|
||||
// 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) => {
|
||||
const original = await importOriginal<typeof import("@hai/engine")>();
|
||||
vi.mock("@kb/engine", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("@kb/engine")>();
|
||||
return {
|
||||
...original,
|
||||
// Keep real WorktreePool & AgentSemaphore
|
||||
@@ -81,10 +81,10 @@ describe("runDashboard — WorktreePool wiring", () => {
|
||||
capturedExecutorOpts = undefined;
|
||||
vi.clearAllMocks();
|
||||
// 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());
|
||||
// Re-set engine mocks
|
||||
const engine = await import("@hai/engine");
|
||||
const engine = await import("@kb/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
);
|
||||
@@ -104,8 +104,8 @@ describe("runDashboard — WorktreePool wiring", () => {
|
||||
});
|
||||
|
||||
it("passes a WorktreePool instance to aiMergeTask via rawMerge", async () => {
|
||||
const { aiMergeTask } = await import("@hai/engine");
|
||||
const { createServer } = await import("@hai/dashboard");
|
||||
const { aiMergeTask } = await import("@kb/engine");
|
||||
const { createServer } = await import("@kb/dashboard");
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("runDashboard — WorktreePool wiring", () => {
|
||||
const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise<unknown> };
|
||||
|
||||
// Invoke the merge handler
|
||||
await serverOpts.onMerge("HAI-TEST");
|
||||
await serverOpts.onMerge("KB-TEST");
|
||||
|
||||
expect(aiMergeTask).toHaveBeenCalled();
|
||||
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 () => {
|
||||
const { aiMergeTask } = await import("@hai/engine");
|
||||
const { createServer } = await import("@hai/dashboard");
|
||||
const { aiMergeTask } = await import("@kb/engine");
|
||||
const { createServer } = await import("@kb/dashboard");
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
// Trigger merger via onMerge
|
||||
const createServerCall = (createServer as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
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 mergerPool = (aiMergeTask as ReturnType<typeof vi.fn>).mock.calls[0][3].pool;
|
||||
@@ -148,9 +148,9 @@ describe("runDashboard — auto-merge pause exclusion", () => {
|
||||
capturedExecutorOpts = undefined;
|
||||
vi.clearAllMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@hai/core");
|
||||
const { TaskStore } = await import("@kb/core");
|
||||
(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(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
);
|
||||
@@ -172,11 +172,11 @@ describe("runDashboard — auto-merge pause exclusion", () => {
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
const { aiMergeTask } = await import("@hai/engine");
|
||||
const { aiMergeTask } = await import("@kb/engine");
|
||||
|
||||
// Emit task:moved with a paused task
|
||||
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",
|
||||
to: "in-review",
|
||||
});
|
||||
@@ -195,11 +195,11 @@ describe("runDashboard — auto-merge pause exclusion", () => {
|
||||
pollIntervalMs: 60_000,
|
||||
});
|
||||
mockStore.listTasks.mockResolvedValue([
|
||||
{ id: "HAI-PAUSED", column: "in-review", paused: true },
|
||||
{ id: "HAI-ACTIVE", column: "in-review", paused: false },
|
||||
{ id: "KB-PAUSED", column: "in-review", paused: true },
|
||||
{ id: "KB-ACTIVE", column: "in-review", paused: false },
|
||||
]);
|
||||
|
||||
const { aiMergeTask } = await import("@hai/engine");
|
||||
const { aiMergeTask } = await import("@kb/engine");
|
||||
// Reset after import
|
||||
(aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
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(
|
||||
(call: any[]) => call[2],
|
||||
);
|
||||
expect(mergedIds).not.toContain("HAI-PAUSED");
|
||||
expect(mergedIds).not.toContain("KB-PAUSED");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { TaskStore } from "@hai/core";
|
||||
import { createServer } from "@hai/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask } from "@hai/engine";
|
||||
import { TaskStore } from "@kb/core";
|
||||
import { createServer } from "@kb/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask } from "@kb/engine";
|
||||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
@@ -228,11 +228,11 @@ export async function runDashboard(port: number, opts: { open?: boolean } = {})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log();
|
||||
console.log(` hai board`);
|
||||
console.log(` kb board`);
|
||||
console.log(` ────────────────────────`);
|
||||
console.log(` → http://localhost:${port}`);
|
||||
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(` AI engine: ✓ active`);
|
||||
console.log(` • triage: auto-specifying tasks`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mock @hai/core before importing the module under test
|
||||
vi.mock("@hai/core", () => {
|
||||
// Mock @kb/core before importing the module under test
|
||||
vi.mock("@kb/core", () => {
|
||||
const COLUMNS = ["triage", "specified", "in-progress", "review", "done"];
|
||||
const COLUMN_LABELS: Record<string, string> = {
|
||||
triage: "Triage",
|
||||
@@ -18,15 +18,15 @@ vi.mock("@hai/core", () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock @hai/engine
|
||||
vi.mock("@hai/engine", () => ({ aiMergeTask: vi.fn() }));
|
||||
// Mock @kb/engine
|
||||
vi.mock("@kb/engine", () => ({ aiMergeTask: vi.fn() }));
|
||||
|
||||
import { TaskStore } from "@hai/core";
|
||||
import { TaskStore } from "@kb/core";
|
||||
import { runTaskShow, runTaskCreate } from "./task.js";
|
||||
|
||||
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "A short description",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
@@ -59,16 +59,16 @@ describe("runTaskShow", () => {
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
}));
|
||||
|
||||
await runTaskShow("HAI-001");
|
||||
await runTaskShow("KB-001");
|
||||
|
||||
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![0]).toContain(longDesc);
|
||||
// Ensure no truncation happened
|
||||
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 () => {
|
||||
@@ -82,10 +82,10 @@ describe("runTaskShow", () => {
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
}));
|
||||
|
||||
await runTaskShow("HAI-001");
|
||||
await runTaskShow("KB-001");
|
||||
|
||||
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![0]).toContain("My Task Title");
|
||||
@@ -119,7 +119,7 @@ describe("runTaskCreate with --attach", () => {
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn(),
|
||||
createTask: vi.fn().mockResolvedValue({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
description: "test task",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
@@ -146,7 +146,7 @@ describe("runTaskCreate with --attach", () => {
|
||||
|
||||
expect(mockAddAttachment).toHaveBeenCalledOnce();
|
||||
expect(mockAddAttachment).toHaveBeenCalledWith(
|
||||
"HAI-002",
|
||||
"KB-002",
|
||||
"test.png",
|
||||
expect.any(Buffer),
|
||||
"image/png",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus } from "@hai/core";
|
||||
import { aiMergeTask } from "@hai/engine";
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus } from "@kb/core";
|
||||
import { aiMergeTask } from "@kb/engine";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
|
||||
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(` ✓ Created ${task.id}: ${label}`);
|
||||
console.log(` Column: triage`);
|
||||
console.log(` Path: .hai/tasks/${task.id}/`);
|
||||
console.log(` Path: .kb/tasks/${task.id}/`);
|
||||
|
||||
if (attachFiles && attachFiles.length > 0) {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
@@ -73,7 +73,7 @@ export async function runTaskList() {
|
||||
const tasks = await store.listTasks();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ export async function runTaskAttach(id: string, filePath: string) {
|
||||
console.log();
|
||||
console.log(` ✓ Attached to ${id}: ${attachment.originalName}`);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hai/core",
|
||||
"name": "@kb/core",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
|
||||
import type { Task } from "./types.js";
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return mkdtempSync(join(tmpdir(), "hai-store-test-"));
|
||||
return mkdtempSync(join(tmpdir(), "kb-store-test-"));
|
||||
}
|
||||
|
||||
describe("TaskStore", () => {
|
||||
@@ -32,7 +32,7 @@ describe("TaskStore", () => {
|
||||
async function createTaskWithSteps(): Promise<Task> {
|
||||
const task = await store.createTask({ description: "Task with steps" });
|
||||
// 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(
|
||||
join(dir, "PROMPT.md"),
|
||||
`# ${task.id}: Task with steps
|
||||
@@ -63,7 +63,7 @@ describe("TaskStore", () => {
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// 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
|
||||
const count = detail.prompt.split("Fix the login bug").length - 1;
|
||||
expect(count).toBe(1);
|
||||
@@ -76,7 +76,7 @@ describe("TaskStore", () => {
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("TaskStore", () => {
|
||||
const detail = await store.getTask(task.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)
|
||||
const count = detail.prompt.split("Implement caching layer").length - 1;
|
||||
expect(count).toBe(1);
|
||||
@@ -102,7 +102,7 @@ describe("TaskStore", () => {
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe("TaskStore", () => {
|
||||
await Promise.all(promises);
|
||||
|
||||
// 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 result = JSON.parse(raw) as Task;
|
||||
|
||||
@@ -144,7 +144,7 @@ describe("TaskStore", () => {
|
||||
describe("defensive JSON parsing", () => {
|
||||
it("throws on corrupted task.json with trailing duplicate content (atomic writes prevent this)", async () => {
|
||||
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
|
||||
const validJson = await readFile(taskJsonPath, "utf-8");
|
||||
@@ -157,7 +157,7 @@ describe("TaskStore", () => {
|
||||
|
||||
it("throws a clear error when JSON is completely unrecoverable", async () => {
|
||||
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
|
||||
await writeFile(taskJsonPath, "not json at all {{{");
|
||||
@@ -171,7 +171,7 @@ describe("TaskStore", () => {
|
||||
describe("atomic writes", () => {
|
||||
it("produces valid JSON after write with no .tmp files left behind", async () => {
|
||||
const task = await createTestTask();
|
||||
const dir = join(rootDir, ".hai", "tasks", task.id);
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
|
||||
// Perform a write
|
||||
await store.logEntry(task.id, "atomic test");
|
||||
@@ -200,18 +200,18 @@ describe("TaskStore", () => {
|
||||
const ids = tasks.map((t) => t.id);
|
||||
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();
|
||||
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
|
||||
const configPath = join(rootDir, ".hai", "config.json");
|
||||
const configPath = join(rootDir, ".kb", "config.json");
|
||||
const raw = await readFile(configPath, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
expect(config.nextId).toBe(6);
|
||||
|
||||
// No .tmp files left behind
|
||||
const haiDir = join(rootDir, ".hai");
|
||||
const haiDir = join(rootDir, ".kb");
|
||||
const files = await readdir(haiDir);
|
||||
expect(files.filter((f) => f.endsWith(".tmp"))).toHaveLength(0);
|
||||
});
|
||||
@@ -240,7 +240,7 @@ describe("TaskStore", () => {
|
||||
expect(updated.attachments![0].filename).toBe(attachment.filename);
|
||||
|
||||
// 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);
|
||||
expect(content).toEqual(TINY_PNG);
|
||||
});
|
||||
@@ -296,7 +296,7 @@ describe("TaskStore", () => {
|
||||
expect(updated.attachments).toBeUndefined();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
@@ -369,48 +369,48 @@ describe("TaskStore", () => {
|
||||
const task = await createTestTask();
|
||||
expect(task.dependencies).toEqual([]);
|
||||
|
||||
const updated = await store.updateTask(task.id, { dependencies: ["HAI-001", "HAI-002"] });
|
||||
expect(updated.dependencies).toEqual(["HAI-001", "HAI-002"]);
|
||||
const updated = await store.updateTask(task.id, { dependencies: ["KB-001", "KB-002"] });
|
||||
expect(updated.dependencies).toEqual(["KB-001", "KB-002"]);
|
||||
|
||||
// Verify persistence
|
||||
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 () => {
|
||||
const task = await store.createTask({ description: "Dep task", dependencies: ["HAI-001"] });
|
||||
expect(task.dependencies).toEqual(["HAI-001"]);
|
||||
const task = await store.createTask({ description: "Dep task", dependencies: ["KB-001"] });
|
||||
expect(task.dependencies).toEqual(["KB-001"]);
|
||||
|
||||
const updated = await store.updateTask(task.id, { dependencies: ["HAI-002", "HAI-003"] });
|
||||
expect(updated.dependencies).toEqual(["HAI-002", "HAI-003"]);
|
||||
const updated = await store.updateTask(task.id, { dependencies: ["KB-002", "KB-003"] });
|
||||
expect(updated.dependencies).toEqual(["KB-002", "KB-003"]);
|
||||
});
|
||||
|
||||
it("clears dependencies with empty array", async () => {
|
||||
const task = await store.createTask({ description: "Dep task", dependencies: ["HAI-001"] });
|
||||
expect(task.dependencies).toEqual(["HAI-001"]);
|
||||
const task = await store.createTask({ description: "Dep task", dependencies: ["KB-001"] });
|
||||
expect(task.dependencies).toEqual(["KB-001"]);
|
||||
|
||||
const updated = await store.updateTask(task.id, { dependencies: [] });
|
||||
expect(updated.dependencies).toEqual([]);
|
||||
});
|
||||
|
||||
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" });
|
||||
expect(updated.dependencies).toEqual(["HAI-001"]);
|
||||
expect(updated.dependencies).toEqual(["KB-001"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTask — blockedBy", () => {
|
||||
it("sets blockedBy to a string value", async () => {
|
||||
const task = await store.createTask({ title: "Blocked task", description: "A task" });
|
||||
const updated = await store.updateTask(task.id, { blockedBy: "HAI-999" });
|
||||
expect(updated.blockedBy).toBe("HAI-999");
|
||||
const updated = await store.updateTask(task.id, { blockedBy: "KB-999" });
|
||||
expect(updated.blockedBy).toBe("KB-999");
|
||||
});
|
||||
|
||||
it("clears blockedBy when set to null", async () => {
|
||||
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 });
|
||||
expect(updated.blockedBy).toBeUndefined();
|
||||
});
|
||||
@@ -419,9 +419,9 @@ describe("TaskStore", () => {
|
||||
// ── Task prefix tests ──────────────────────────────────────────
|
||||
|
||||
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" });
|
||||
expect(task.id).toBe("HAI-001");
|
||||
expect(task.id).toBe("KB-001");
|
||||
});
|
||||
|
||||
it("custom prefix produces PROJ-001 IDs", async () => {
|
||||
@@ -433,8 +433,8 @@ describe("TaskStore", () => {
|
||||
it("prefix change mid-stream continues sequence", async () => {
|
||||
const t1 = await store.createTask({ description: "First" });
|
||||
const t2 = await store.createTask({ description: "Second" });
|
||||
expect(t1.id).toBe("HAI-001");
|
||||
expect(t2.id).toBe("HAI-002");
|
||||
expect(t1.id).toBe("KB-001");
|
||||
expect(t2.id).toBe("KB-002");
|
||||
|
||||
await store.updateSettings({ taskPrefix: "PROJ" });
|
||||
const t3 = await store.createTask({ description: "Third" });
|
||||
@@ -448,7 +448,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const tasks = await store.listTasks();
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface TaskStoreEvents {
|
||||
}
|
||||
|
||||
export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private haiDir: string;
|
||||
private kbDir: string;
|
||||
private tasksDir: string;
|
||||
private configPath: string;
|
||||
|
||||
@@ -37,9 +37,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
constructor(private rootDir: string) {
|
||||
super();
|
||||
this.haiDir = join(rootDir, ".hai");
|
||||
this.tasksDir = join(this.haiDir, "tasks");
|
||||
this.configPath = join(this.haiDir, "config.json");
|
||||
this.kbDir = join(rootDir, ".kb");
|
||||
this.tasksDir = join(this.kbDir, "tasks");
|
||||
this.configPath = join(this.kbDir, "config.json");
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
@@ -160,7 +160,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private async allocateId(): Promise<string> {
|
||||
return this.withConfigLock(async () => {
|
||||
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")}`;
|
||||
config.nextId++;
|
||||
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 result: MergeResult = {
|
||||
task,
|
||||
@@ -556,7 +556,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
`Merge conflict merging '${branch}'. Resolve manually:\n` +
|
||||
` cd ${this.rootDir}\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).
|
||||
* 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.
|
||||
*
|
||||
* @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 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)
|
||||
@@ -903,7 +903,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
* Read all historical agent log entries for a task from its agent log file.
|
||||
* 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
|
||||
*/
|
||||
async getAgentLogs(taskId: string): Promise<AgentLogEntry[]> {
|
||||
|
||||
@@ -94,12 +94,12 @@ export interface Settings {
|
||||
* of being deleted. New tasks acquire a warm worktree from the pool,
|
||||
* preserving build caches (node_modules, target/, dist/). Default: false. */
|
||||
recycleWorktrees?: boolean;
|
||||
/** Prefix for generated task IDs (e.g. `"HAI"` produces `HAI-001`).
|
||||
* Defaults to `"HAI"`. Only affects new tasks — existing tasks retain
|
||||
/** Prefix for generated task IDs (e.g. `"KB"` produces `KB-001`).
|
||||
* Defaults to `"KB"`. Only affects new tasks — existing tasks retain
|
||||
* their original IDs. */
|
||||
taskPrefix?: string;
|
||||
/** 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. */
|
||||
includeTaskIdInCommit?: boolean;
|
||||
/** Default AI model provider name (e.g. `"anthropic"`, `"openai"`).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Header } from "./components/Header";
|
||||
import { Board } from "./components/Board";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
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 = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
@@ -12,7 +12,7 @@ const FAKE_DETAIL: TaskDetail = {
|
||||
log: [],
|
||||
createdAt: "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) {
|
||||
@@ -38,9 +38,9 @@ describe("fetchTaskDetail", () => {
|
||||
it("returns data on first success", async () => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -49,9 +49,9 @@ describe("fetchTaskDetail", () => {
|
||||
.mockReturnValueOnce(mockFetchResponse(false, { error: "Transient error" }))
|
||||
.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);
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("fetchTaskDetail", () => {
|
||||
globalThis.fetch = vi.fn()
|
||||
.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
|
||||
});
|
||||
});
|
||||
@@ -72,10 +72,10 @@ describe("updateTask", () => {
|
||||
});
|
||||
|
||||
const FAKE_TASK: Task = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: ["HAI-002"],
|
||||
dependencies: ["KB-002"],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
@@ -86,20 +86,20 @@ describe("updateTask", () => {
|
||||
it("sends PATCH with dependencies and returns updated task", async () => {
|
||||
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(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/HAI-001", {
|
||||
expect(result.dependencies).toEqual(["KB-002"]);
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ dependencies: ["HAI-002"] }),
|
||||
body: JSON.stringify({ dependencies: ["KB-002"] }),
|
||||
});
|
||||
});
|
||||
|
||||
it("throws on error response", async () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
|
||||
interface AgentLogViewerProps {
|
||||
entries: AgentLogEntry[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
|
||||
import { COLUMNS } from "@kb/core";
|
||||
import { Column } from "./Column";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core";
|
||||
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
|
||||
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@kb/core";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { WorktreeGroup } from "./WorktreeGroup";
|
||||
import { InlineCreateCard } from "./InlineCreateCard";
|
||||
|
||||
@@ -8,8 +8,8 @@ export function Header({ onOpenSettings }: HeaderProps) {
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<img src="/logo.svg" alt="hai logo" className="header-logo" width={24} height={24} />
|
||||
<h1 className="logo">hai</h1>
|
||||
<img src="/logo.svg" alt="kb logo" className="header-logo" width={24} height={24} />
|
||||
<h1 className="logo">kb</h1>
|
||||
<span className="logo-sub">board</span>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "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 { uploadAttachment } from "../api";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { AuthProvider, ModelInfo } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -185,7 +185,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
<input
|
||||
id="taskPrefix"
|
||||
type="text"
|
||||
placeholder="HAI"
|
||||
placeholder="KB"
|
||||
value={form.taskPrefix || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
@@ -198,7 +198,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
}}
|
||||
/>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
@@ -412,7 +412,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
/>
|
||||
Include task ID in commit scope
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
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
|
||||
vi.mock("lucide-react", () => ({
|
||||
@@ -19,7 +19,7 @@ import { uploadAttachment } from "../api";
|
||||
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test task",
|
||||
column: "in-progress",
|
||||
status: undefined as any,
|
||||
@@ -35,7 +35,7 @@ const noop = () => {};
|
||||
describe("TaskCard", () => {
|
||||
it("renders the card ID text", () => {
|
||||
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", () => {
|
||||
@@ -126,7 +126,7 @@ describe("TaskCard", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-001", file);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-001", file);
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Attached test.png"),
|
||||
"success",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from "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 type { ToastType } from "../hooks/useToast";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@hai/core";
|
||||
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@kb/core";
|
||||
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@kb/core";
|
||||
import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useAgentLogs } from "../hooks/useAgentLogs";
|
||||
|
||||
@@ -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 { TaskCard } from "./TaskCard";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { AgentLogViewer } from "../AgentLogViewer";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
|
||||
function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry {
|
||||
return {
|
||||
timestamp: "2026-01-01T00:00:00Z",
|
||||
taskId: "HAI-001",
|
||||
taskId: "KB-001",
|
||||
text: "Hello world",
|
||||
type: "text",
|
||||
...overrides,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
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
|
||||
vi.mock("../Column", () => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
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
|
||||
vi.mock("../TaskCard", () => ({
|
||||
@@ -43,7 +43,7 @@ const defaultProps = {
|
||||
|
||||
describe("Column count-flash", () => {
|
||||
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} />);
|
||||
|
||||
const badge = screen.getByText("1");
|
||||
@@ -52,10 +52,10 @@ describe("Column count-flash", () => {
|
||||
});
|
||||
|
||||
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 moreTasks = [makeTask("HAI-001"), makeTask("HAI-002")];
|
||||
const moreTasks = [makeTask("KB-001"), makeTask("KB-002")];
|
||||
rerender(<Column {...defaultProps} tasks={moreTasks} />);
|
||||
|
||||
const badge = screen.getByText("2");
|
||||
@@ -63,10 +63,10 @@ describe("Column count-flash", () => {
|
||||
});
|
||||
|
||||
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 fewerTasks = [makeTask("HAI-001")];
|
||||
const fewerTasks = [makeTask("KB-001")];
|
||||
rerender(<Column {...defaultProps} tasks={fewerTasks} />);
|
||||
|
||||
const badge = screen.getByText("1");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Header } from "../Header";
|
||||
describe("Header", () => {
|
||||
it("renders a logo image with correct src and alt", () => {
|
||||
render(<Header />);
|
||||
const logo = screen.getByAltText("hai logo");
|
||||
const logo = screen.getByAltText("kb logo");
|
||||
expect(logo).toBeDefined();
|
||||
expect(logo.tagName).toBe("IMG");
|
||||
expect((logo as HTMLImageElement).src).toContain("/logo.svg");
|
||||
@@ -13,7 +13,7 @@ describe("Header", () => {
|
||||
|
||||
it("renders the logo before the h1 element", () => {
|
||||
render(<Header />);
|
||||
const logo = screen.getByAltText("hai logo");
|
||||
const logo = screen.getByAltText("kb logo");
|
||||
const h1 = screen.getByRole("heading", { level: 1 });
|
||||
// Logo should be a preceding sibling of the h1
|
||||
expect(logo.compareDocumentPosition(h1) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("../../api", () => ({
|
||||
function renderCard() {
|
||||
const props = {
|
||||
tasks: [],
|
||||
onSubmit: vi.fn().mockResolvedValue({ id: "HAI-001" }),
|
||||
onSubmit: vi.fn().mockResolvedValue({ id: "KB-001" }),
|
||||
onCancel: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { SettingsModal } from "../SettingsModal";
|
||||
import type { Settings } from "@hai/core";
|
||||
import type { Settings } from "@kb/core";
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
@@ -139,11 +139,11 @@ describe("TaskCard dependency tooltip", () => {
|
||||
}
|
||||
|
||||
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", () => {
|
||||
expect(computeDepTooltip(["HAI-010"])).toBe("HAI-010");
|
||||
expect(computeDepTooltip(["KB-010"])).toBe("KB-010");
|
||||
});
|
||||
|
||||
it("returns undefined when dependencies array is empty", () => {
|
||||
@@ -151,12 +151,12 @@ describe("TaskCard dependency tooltip", () => {
|
||||
});
|
||||
|
||||
it("handles many dependencies", () => {
|
||||
const deps = ["HAI-001", "HAI-002", "HAI-003", "HAI-004"];
|
||||
expect(computeDepTooltip(deps)).toBe("HAI-001, HAI-002, HAI-003, HAI-004");
|
||||
const deps = ["KB-001", "KB-002", "KB-003", "KB-004"];
|
||||
expect(computeDepTooltip(deps)).toBe("KB-001, KB-002, KB-003, KB-004");
|
||||
});
|
||||
|
||||
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);
|
||||
expect(tooltip).toBeDefined();
|
||||
// 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", () => {
|
||||
expect(shouldShowScopeBadge("HAI-003")).toBe(true);
|
||||
expect(shouldShowScopeBadge("KB-003")).toBe(true);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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", () => {
|
||||
@@ -200,7 +200,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
|
||||
}
|
||||
|
||||
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)");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
|
||||
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", () => ({
|
||||
uploadAttachment: vi.fn(),
|
||||
@@ -16,7 +16,7 @@ vi.mock("../../hooks/useAgentLogs", () => ({
|
||||
|
||||
function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "HAI-099",
|
||||
id: "KB-099",
|
||||
description: "Test task",
|
||||
column: "in-progress" as Column,
|
||||
dependencies: [],
|
||||
@@ -157,7 +157,7 @@ describe("TaskDetailModal", () => {
|
||||
task={makeTask({
|
||||
title: undefined,
|
||||
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}
|
||||
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");
|
||||
expect(markdownBody?.innerHTML).not.toContain("HAI-099");
|
||||
expect(markdownBody?.innerHTML).not.toContain("KB-099");
|
||||
// Description appears in the markdown body
|
||||
expect(markdownBody?.textContent).toContain("Fix the login bug");
|
||||
// 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
|
||||
const h2 = container.querySelector("h2.detail-title");
|
||||
expect(h2?.textContent).toBe("Fix the login bug");
|
||||
@@ -210,7 +210,7 @@ describe("TaskDetailModal", () => {
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(withTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
expect(withTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
|
||||
|
||||
// Without title
|
||||
const { container: withoutTitle } = render(
|
||||
@@ -223,7 +223,7 @@ describe("TaskDetailModal", () => {
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
|
||||
});
|
||||
|
||||
describe("paste image upload", () => {
|
||||
@@ -267,7 +267,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -394,7 +394,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -418,7 +418,7 @@ describe("TaskDetailModal", () => {
|
||||
it("renders dependency list when dependencies exist", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })}
|
||||
task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -427,16 +427,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("HAI-001")).toBeTruthy();
|
||||
expect(screen.getByText("HAI-002")).toBeTruthy();
|
||||
expect(screen.getByText("KB-001")).toBeTruthy();
|
||||
expect(screen.getByText("KB-002")).toBeTruthy();
|
||||
expect(screen.queryByText("(no dependencies)")).toBeNull();
|
||||
});
|
||||
|
||||
it("can add a dependency via the dropdown", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const allTasks: Task[] = [
|
||||
{ id: "HAI-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-001", description: "Dep 1", column: "todo" 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(
|
||||
@@ -452,16 +452,16 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
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")!;
|
||||
expect(dropdown).toBeTruthy();
|
||||
expect(dropdown.textContent).toContain("HAI-001");
|
||||
expect(dropdown.textContent).toContain("KB-001");
|
||||
expect(dropdown.querySelectorAll(".dep-dropdown-item")).toHaveLength(1);
|
||||
|
||||
fireEvent.click(screen.getByText("HAI-001"));
|
||||
fireEvent.click(screen.getByText("KB-001"));
|
||||
|
||||
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(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })}
|
||||
task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -480,10 +480,10 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const removeButtons = screen.getAllByTitle(/Remove dependency/);
|
||||
fireEvent.click(removeButtons[0]); // Remove HAI-001
|
||||
fireEvent.click(removeButtons[0]); // Remove KB-001
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("HAI-099", { dependencies: ["HAI-002"] });
|
||||
expect(updateTask).toHaveBeenCalledWith("KB-099", { dependencies: ["KB-002"] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ afterEach(() => {
|
||||
|
||||
describe("useAgentLogs", () => {
|
||||
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(MockEventSource.instances).toHaveLength(0);
|
||||
@@ -60,27 +60,27 @@ describe("useAgentLogs", () => {
|
||||
|
||||
it("fetches historical logs and opens SSE when enabled=true", async () => {
|
||||
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);
|
||||
|
||||
const { result } = renderHook(() => useAgentLogs("HAI-001", true));
|
||||
const { result } = renderHook(() => useAgentLogs("KB-001", true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.entries).toEqual(historicalLogs);
|
||||
});
|
||||
|
||||
expect(mockFetchAgentLogs).toHaveBeenCalledWith("HAI-001");
|
||||
expect(mockFetchAgentLogs).toHaveBeenCalledWith("KB-001");
|
||||
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 () => {
|
||||
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(() => {
|
||||
expect(result.current.entries).toHaveLength(1);
|
||||
@@ -90,7 +90,7 @@ describe("useAgentLogs", () => {
|
||||
act(() => {
|
||||
es._emit("agent:log", {
|
||||
timestamp: "2026-01-01T00:01:00Z",
|
||||
taskId: "HAI-001",
|
||||
taskId: "KB-001",
|
||||
text: "new",
|
||||
type: "text",
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe("useAgentLogs", () => {
|
||||
mockFetchAgentLogs.mockResolvedValueOnce([]);
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ enabled }) => useAgentLogs("HAI-001", enabled),
|
||||
({ enabled }) => useAgentLogs("KB-001", enabled),
|
||||
{ initialProps: { enabled: true } },
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("useAgentLogs", () => {
|
||||
it("closes SSE on unmount", async () => {
|
||||
mockFetchAgentLogs.mockResolvedValueOnce([]);
|
||||
|
||||
const { unmount } = renderHook(() => useAgentLogs("HAI-001", true));
|
||||
const { unmount } = renderHook(() => useAgentLogs("KB-001", true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
import { fetchAgentLogs } from "../api";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export function useTasks() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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 {
|
||||
return {
|
||||
@@ -18,8 +18,8 @@ function makeTask(overrides: Partial<Task> & { id: string }): Task {
|
||||
|
||||
describe("getWorktreeLabel", () => {
|
||||
it("extracts last path segment", () => {
|
||||
expect(getWorktreeLabel(".worktrees/HAI-001")).toBe("HAI-001");
|
||||
expect(getWorktreeLabel("/path/to/hai/hai-001")).toBe("hai-001");
|
||||
expect(getWorktreeLabel(".worktrees/KB-001")).toBe("KB-001");
|
||||
expect(getWorktreeLabel("/path/to/kb/kb-001")).toBe("kb-001");
|
||||
});
|
||||
|
||||
it("extracts humanized worktree names", () => {
|
||||
@@ -31,8 +31,8 @@ describe("getWorktreeLabel", () => {
|
||||
|
||||
describe("groupByWorktree", () => {
|
||||
it("groups active in-progress tasks by worktree", () => {
|
||||
const t1 = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const t2 = makeTask({ id: "HAI-002", worktree: ".worktrees/quiet-robin" });
|
||||
const t1 = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const t2 = makeTask({ id: "KB-002", worktree: ".worktrees/quiet-robin" });
|
||||
|
||||
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", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const queued = makeTask({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -74,11 +74,11 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
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({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
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);
|
||||
@@ -87,10 +87,10 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("respects maxConcurrent limit on queued tasks shown", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const q1 = makeTask({ id: "HAI-010", column: "todo" });
|
||||
const q2 = makeTask({ id: "HAI-011", column: "todo" });
|
||||
const q3 = makeTask({ id: "HAI-012", column: "todo" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const q1 = makeTask({ id: "KB-010", column: "todo" });
|
||||
const q2 = makeTask({ id: "KB-011", column: "todo" });
|
||||
const q3 = makeTask({ id: "KB-012", column: "todo" });
|
||||
|
||||
const groups = groupByWorktree([active], [active, q1, q2, q3], 2);
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -110,15 +110,15 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
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({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
paused: true,
|
||||
});
|
||||
const normal = makeTask({
|
||||
id: "HAI-003",
|
||||
id: "KB-003",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
});
|
||||
@@ -127,16 +127,16 @@ describe("groupByWorktree", () => {
|
||||
|
||||
const upNext = groups.find((g) => g.label === "Up Next");
|
||||
expect(upNext).toBeDefined();
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["HAI-003"]);
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("HAI-002");
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["KB-003"]);
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("KB-002");
|
||||
});
|
||||
|
||||
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({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: ["HAI-001"],
|
||||
dependencies: ["KB-001"],
|
||||
});
|
||||
|
||||
const groups = groupByWorktree([], [done, queued], 2);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Task } from "@hai/core";
|
||||
import type { Task } from "@kb/core";
|
||||
|
||||
export interface WorktreeGroupData {
|
||||
label: string;
|
||||
@@ -8,7 +8,7 @@ export interface WorktreeGroupData {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Take the last segment of the path
|
||||
@@ -18,8 +18,8 @@ export function getWorktreeLabel(worktreePath: string): string {
|
||||
|
||||
/**
|
||||
* Topological sort of tasks by dependency order.
|
||||
* Mirrors resolveDependencyOrder from @hai/core but inlined to avoid
|
||||
* build alias issues (Vite aliases @hai/core to types.ts only).
|
||||
* Mirrors resolveDependencyOrder from @kb/core but inlined to avoid
|
||||
* build alias issues (Vite aliases @kb/core to types.ts only).
|
||||
*/
|
||||
function resolveDependencyOrder(tasks: Task[]): string[] {
|
||||
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hai/dashboard",
|
||||
"name": "@kb/dashboard",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hai/core": "workspace:*",
|
||||
"@kb/core": "workspace:*",
|
||||
"@types/multer": "^2.1.0",
|
||||
"express": "^5.1.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="logo">hai</h1>
|
||||
<h1 class="logo">kb</h1>
|
||||
<span class="logo-sub">board</span>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="add-task-btn">+ New Task</button>
|
||||
@@ -96,7 +96,7 @@
|
||||
>Dependencies
|
||||
<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 class="modal-actions">
|
||||
<button type="button" class="btn" data-close="create-modal">
|
||||
|
||||
@@ -2,8 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import http from "node:http";
|
||||
import { createApiRoutes } from "./routes.js";
|
||||
import type { TaskStore, TaskAttachment } from "@hai/core";
|
||||
import type { TaskDetail } from "@hai/core";
|
||||
import type { TaskStore, TaskAttachment } from "@kb/core";
|
||||
import type { TaskDetail } from "@kb/core";
|
||||
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
|
||||
|
||||
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
@@ -24,7 +24,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
}
|
||||
|
||||
const FAKE_TASK_DETAIL: TaskDetail = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
@@ -33,7 +33,7 @@ const FAKE_TASK_DETAIL: TaskDetail = {
|
||||
log: [],
|
||||
createdAt: "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 } */
|
||||
@@ -117,11 +117,11 @@ describe("GET /tasks/:id", () => {
|
||||
it("returns task detail on success", async () => {
|
||||
(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.body.id).toBe("HAI-001");
|
||||
expect(res.body.prompt).toBe("# HAI-001\n\nTest task");
|
||||
expect(res.body.id).toBe("KB-001");
|
||||
expect(res.body.prompt).toBe("# KB-001\n\nTest task");
|
||||
});
|
||||
|
||||
it("returns 404 when task genuinely does not exist (ENOENT)", async () => {
|
||||
@@ -129,7 +129,7 @@ describe("GET /tasks/:id", () => {
|
||||
err.code = "ENOENT";
|
||||
(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.body.error).toContain("not found");
|
||||
@@ -139,7 +139,7 @@ describe("GET /tasks/:id", () => {
|
||||
const err = new Error("Unexpected end of JSON input");
|
||||
(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.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.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",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: undefined });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
});
|
||||
|
||||
it("returns 400 when task is not in failed state", async () => {
|
||||
const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" };
|
||||
(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",
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
const doneTask = { ...FAKE_TASK_DETAIL, column: "done", status: "failed" };
|
||||
(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",
|
||||
});
|
||||
|
||||
@@ -216,32 +216,32 @@ describe("PATCH /tasks/:id", () => {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
title: undefined,
|
||||
description: 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 () => {
|
||||
(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",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
title: "New",
|
||||
description: undefined,
|
||||
prompt: undefined,
|
||||
@@ -280,14 +280,14 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("fake 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}`,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.filename).toBe("1234-screenshot.png");
|
||||
expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
|
||||
"HAI-001",
|
||||
"KB-001",
|
||||
"screenshot.png",
|
||||
expect.any(Buffer),
|
||||
"image/png",
|
||||
@@ -302,7 +302,7 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("not an image");
|
||||
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}`,
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("small but store rejects");
|
||||
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}`,
|
||||
});
|
||||
|
||||
@@ -327,10 +327,10 @@ describe("Attachment routes", () => {
|
||||
});
|
||||
|
||||
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((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 () => {
|
||||
@@ -338,29 +338,29 @@ describe("Attachment routes", () => {
|
||||
err.code = "ENOENT";
|
||||
(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);
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns agent logs", async () => {
|
||||
const fakeLogs = [
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "Hello", type: "text" },
|
||||
{ timestamp: "2026-01-01T00:00:01Z", taskId: "HAI-001", text: "Read", type: "tool" },
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "Hello", type: "text" },
|
||||
{ timestamp: "2026-01-01T00:00:01Z", taskId: "KB-001", text: "Read", type: "tool" },
|
||||
];
|
||||
(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.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 () => {
|
||||
(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.body).toEqual([]);
|
||||
@@ -369,7 +369,7 @@ describe("Attachment routes", () => {
|
||||
it("GET /tasks/:id/logs — returns 500 on store error", async () => {
|
||||
(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.body.error).toBe("disk error");
|
||||
@@ -631,27 +631,27 @@ describe("Pause/Unpause endpoints", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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 () => {
|
||||
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.body).toEqual({ id: "HAI-001", paused: true });
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("HAI-001", true);
|
||||
expect(res.body).toEqual({ id: "KB-001", paused: true });
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("KB-001", true);
|
||||
});
|
||||
|
||||
it("POST /tasks/:id/unpause — unpauses a task", async () => {
|
||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "HAI-001" });
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/unpause");
|
||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "KB-001" });
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/unpause");
|
||||
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 () => {
|
||||
(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.body.error).toBe("not found");
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { createReadStream } from "node:fs";
|
||||
import type { TaskStore, Column, MergeResult } from "@hai/core";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
import type { TaskStore, Column, MergeResult } from "@kb/core";
|
||||
import { COLUMNS } from "@kb/core";
|
||||
import type { ServerOptions } from "./server.js";
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import express from "express";
|
||||
import { join, dirname } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
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 { createApiRoutes } from "./routes.js";
|
||||
import { createSSE } from "./sse.js";
|
||||
@@ -27,14 +27,14 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
|
||||
// Serve built React app
|
||||
// Resolution order:
|
||||
// 1. HAI_CLIENT_DIR env override (explicit)
|
||||
// 2. Next to process.execPath (bun-compiled binary: dist/hai + dist/client/)
|
||||
// 1. KB_CLIENT_DIR env override (explicit)
|
||||
// 2. Next to process.execPath (bun-compiled binary: dist/kb + dist/client/)
|
||||
// 3. __dirname/../dist/client (running from src/ via tsx/ts-node)
|
||||
// 4. __dirname/../client (running from dist/ after tsc)
|
||||
// 5. __dirname/../public (fallback for dev)
|
||||
const execDir = dirname(process.execPath);
|
||||
const clientDir = process.env.HAI_CLIENT_DIR
|
||||
? process.env.HAI_CLIENT_DIR
|
||||
const clientDir = process.env.KB_CLIENT_DIR
|
||||
? process.env.KB_CLIENT_DIR
|
||||
: existsSync(join(execDir, "client", "index.html"))
|
||||
? join(execDir, "client")
|
||||
: existsSync(join(__dirname, "..", "dist", "client"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { TaskStore } from "@hai/core";
|
||||
import type { TaskStore } from "@kb/core";
|
||||
|
||||
export function createSSE(store: TaskStore) {
|
||||
return (_req: Request, res: Response) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@hai/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
"@kb/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@hai/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
"@kb/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hai/engine",
|
||||
"name": "@kb/engine",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hai/core": "workspace:*",
|
||||
"@kb/core": "workspace:*",
|
||||
"@mariozechner/pi-ai": "^0.62.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.62.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { AgentLogger, summarizeToolArgs } from "./agent-logger.js";
|
||||
import type { TaskStore } from "@hai/core";
|
||||
import type { TaskStore } from "@kb/core";
|
||||
|
||||
// ── summarizeToolArgs tests ──────────────────────────────────────────
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("AgentLogger", () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-001",
|
||||
taskId: "KB-001",
|
||||
flushSizeBytes: 10,
|
||||
flushIntervalMs: 500,
|
||||
});
|
||||
@@ -75,14 +75,14 @@ describe("AgentLogger", () => {
|
||||
logger.onText("worldextra");
|
||||
// Allow async flush
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-002",
|
||||
taskId: "KB-002",
|
||||
flushSizeBytes: 1024,
|
||||
flushIntervalMs: 500,
|
||||
});
|
||||
@@ -91,14 +91,14 @@ describe("AgentLogger", () => {
|
||||
expect(store.appendAgentLog).not.toHaveBeenCalled();
|
||||
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-003",
|
||||
taskId: "KB-003",
|
||||
flushSizeBytes: 1024,
|
||||
});
|
||||
|
||||
@@ -110,36 +110,36 @@ describe("AgentLogger", () => {
|
||||
const calls = (store.appendAgentLog as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(calls.length).toBe(2);
|
||||
// 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
|
||||
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 () => {
|
||||
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" });
|
||||
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 () => {
|
||||
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 });
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-006",
|
||||
taskId: "KB-006",
|
||||
flushSizeBytes: 1024,
|
||||
flushIntervalMs: 500,
|
||||
});
|
||||
@@ -147,12 +147,12 @@ describe("AgentLogger", () => {
|
||||
logger.onText("remaining");
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({ store, taskId: "HAI-007" });
|
||||
const logger = new AgentLogger({ store, taskId: "KB-007" });
|
||||
|
||||
await logger.flush();
|
||||
expect(store.appendAgentLog).not.toHaveBeenCalled();
|
||||
@@ -164,23 +164,23 @@ describe("AgentLogger", () => {
|
||||
const onAgentTool = vi.fn();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-008",
|
||||
taskId: "KB-008",
|
||||
onAgentText,
|
||||
onAgentTool,
|
||||
});
|
||||
|
||||
logger.onText("delta");
|
||||
expect(onAgentText).toHaveBeenCalledWith("HAI-008", "delta");
|
||||
expect(onAgentText).toHaveBeenCalledWith("KB-008", "delta");
|
||||
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "HAI-009",
|
||||
taskId: "KB-009",
|
||||
flushSizeBytes: 1024,
|
||||
flushIntervalMs: 500,
|
||||
});
|
||||
@@ -193,6 +193,6 @@ describe("AgentLogger", () => {
|
||||
|
||||
// All text should be flushed in a single call
|
||||
expect(store.appendAgentLog).toHaveBeenCalledTimes(1);
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-009", "abc", "text");
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-009", "abc", "text");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TaskStore } from "@hai/core";
|
||||
import type { TaskStore } from "@kb/core";
|
||||
|
||||
/** Default byte threshold before an automatic flush. */
|
||||
const FLUSH_SIZE_BYTES = 1024;
|
||||
@@ -56,12 +56,12 @@ export interface AgentLoggerOptions {
|
||||
* detailed argument summaries via {@link summarizeToolArgs}.
|
||||
*
|
||||
* Produces `onText` and `onToolStart` callbacks compatible with
|
||||
* `createHaiAgent`'s `AgentOptions` interface.
|
||||
* `createKbAgent`'s `AgentOptions` interface.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const logger = new AgentLogger({ store, taskId, onAgentText, onAgentTool });
|
||||
* const { session } = await createHaiAgent({
|
||||
* const { session } = await createKbAgent({
|
||||
* cwd: worktreePath,
|
||||
* onText: logger.onText,
|
||||
* onToolStart: logger.onToolStart,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AgentSemaphore } from "./concurrency.js";
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock("./pi.js", () => ({
|
||||
createHaiAgent: vi.fn(),
|
||||
createKbAgent: vi.fn(),
|
||||
}));
|
||||
vi.mock("./reviewer.js", () => ({
|
||||
reviewStep: vi.fn(),
|
||||
@@ -28,14 +28,14 @@ vi.mock("node:fs", () => ({
|
||||
}));
|
||||
|
||||
import { TaskExecutor, buildExecutionPrompt } from "./executor.js";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
import { execSync } from "node:child_process";
|
||||
import { findWorktreeUser, aiMergeTask } from "./merger.js";
|
||||
import { WorktreePool } from "./worktree-pool.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() {
|
||||
const listeners = new Map<string, Function[]>();
|
||||
@@ -52,7 +52,7 @@ function createMockStore() {
|
||||
emit: vi.fn(),
|
||||
listTasks: vi.fn().mockResolvedValue([]),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
@@ -102,7 +102,7 @@ describe("TaskExecutor with semaphore", () => {
|
||||
const executor = new TaskExecutor(store, "/tmp/test", { semaphore: sem });
|
||||
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -132,7 +132,7 @@ describe("TaskExecutor with semaphore", () => {
|
||||
});
|
||||
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -157,7 +157,7 @@ describe("TaskExecutor with semaphore", () => {
|
||||
const executor = new TaskExecutor(store, "/tmp/test", { onError });
|
||||
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -169,7 +169,7 @@ describe("TaskExecutor with semaphore", () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: "failed" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: "failed" });
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -209,9 +209,9 @@ describe("TaskExecutor with semaphore", () => {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
executor.execute(task("HAI-001")),
|
||||
executor.execute(task("HAI-002")),
|
||||
executor.execute(task("HAI-003")),
|
||||
executor.execute(task("KB-001")),
|
||||
executor.execute(task("KB-002")),
|
||||
executor.execute(task("KB-003")),
|
||||
]);
|
||||
|
||||
expect(maxConcurrent).toBe(1);
|
||||
@@ -224,7 +224,7 @@ const { existsSync: mockedExistsSyncRaw } = await import("node:fs");
|
||||
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
|
||||
|
||||
describe("TaskExecutor worktreeInitCommand", () => {
|
||||
const makeTask = (id = "HAI-010") => ({
|
||||
const makeTask = (id = "KB-010") => ({
|
||||
id,
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
@@ -275,7 +275,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
|
||||
|
||||
// Should log success
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"HAI-010",
|
||||
"KB-010",
|
||||
"Worktree init command completed",
|
||||
"pnpm install",
|
||||
);
|
||||
@@ -322,7 +322,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
|
||||
|
||||
// Should log the failure
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"HAI-010",
|
||||
"KB-010",
|
||||
expect.stringContaining("Worktree init command failed"),
|
||||
);
|
||||
|
||||
@@ -356,7 +356,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
|
||||
});
|
||||
|
||||
describe("TaskExecutor worktree naming", () => {
|
||||
const makeTask = (id = "HAI-030", worktree?: string) => ({
|
||||
const makeTask = (id = "KB-030", worktree?: string) => ({
|
||||
id,
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
@@ -391,7 +391,7 @@ describe("TaskExecutor worktree naming", () => {
|
||||
await executor.execute(makeTask());
|
||||
|
||||
// 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",
|
||||
});
|
||||
expect(mockedGenerateWorktreeName).toHaveBeenCalledWith("/tmp/test");
|
||||
@@ -401,7 +401,7 @@ describe("TaskExecutor worktree naming", () => {
|
||||
const store = createMockStore();
|
||||
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
|
||||
const updateCalls = store.updateTask.mock.calls;
|
||||
@@ -409,7 +409,7 @@ describe("TaskExecutor worktree naming", () => {
|
||||
(call: any[]) => call[1]?.worktree !== undefined,
|
||||
);
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -420,7 +420,7 @@ describe("TaskExecutor worktree naming", () => {
|
||||
const store = createMockStore();
|
||||
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
|
||||
expect(mockedGenerateWorktreeName).not.toHaveBeenCalled();
|
||||
@@ -428,7 +428,7 @@ describe("TaskExecutor worktree naming", () => {
|
||||
});
|
||||
|
||||
describe("TaskExecutor worktree pool integration", () => {
|
||||
const makeTask = (id = "HAI-020") => ({
|
||||
const makeTask = (id = "KB-020") => ({
|
||||
id,
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
@@ -482,7 +482,7 @@ describe("TaskExecutor worktree pool integration", () => {
|
||||
|
||||
// Should log pool acquisition
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"HAI-020",
|
||||
"KB-020",
|
||||
expect.stringContaining("Acquired worktree from pool"),
|
||||
);
|
||||
|
||||
@@ -515,7 +515,7 @@ describe("TaskExecutor worktree pool integration", () => {
|
||||
|
||||
// Should log worktree creation, NOT pool acquisition
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"HAI-020",
|
||||
"KB-020",
|
||||
expect.stringContaining("Worktree created at"),
|
||||
);
|
||||
});
|
||||
@@ -597,12 +597,12 @@ describe("Merger worktree pool integration", () => {
|
||||
}),
|
||||
emit: vi.fn(),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: "HAI-050",
|
||||
id: "KB-050",
|
||||
title: "Test merge",
|
||||
description: "Test",
|
||||
column: "in-review",
|
||||
dependencies: [],
|
||||
worktree: "/tmp/test/.worktrees/HAI-050",
|
||||
worktree: "/tmp/test/.worktrees/KB-050",
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
@@ -612,7 +612,7 @@ describe("Merger worktree pool integration", () => {
|
||||
}),
|
||||
updateTask: vi.fn().mockResolvedValue({}),
|
||||
moveTask: vi.fn().mockResolvedValue({
|
||||
id: "HAI-050",
|
||||
id: "KB-050",
|
||||
column: "done",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
@@ -659,10 +659,10 @@ describe("Merger worktree pool integration", () => {
|
||||
},
|
||||
} 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
|
||||
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);
|
||||
|
||||
// git worktree remove should NOT have been called
|
||||
@@ -686,7 +686,7 @@ describe("Merger worktree pool integration", () => {
|
||||
},
|
||||
} 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
|
||||
expect(pool.size).toBe(0);
|
||||
@@ -702,7 +702,7 @@ describe("Merger worktree pool integration", () => {
|
||||
|
||||
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test Task",
|
||||
description: "A test task",
|
||||
column: "in-progress",
|
||||
@@ -728,7 +728,7 @@ describe("buildExecutionPrompt", () => {
|
||||
|
||||
expect(result).toContain("## Attachments");
|
||||
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", () => {
|
||||
@@ -742,7 +742,7 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Attachments");
|
||||
expect(result).toContain("**error.log** (text/plain)");
|
||||
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", () => {
|
||||
@@ -853,7 +853,7 @@ describe("buildExecutionPrompt", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -929,7 +929,7 @@ describe("TaskExecutor pause behavior", () => {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
// 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)
|
||||
throw new Error("Session terminated");
|
||||
}),
|
||||
@@ -940,7 +940,7 @@ describe("TaskExecutor pause behavior", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -953,8 +953,8 @@ describe("TaskExecutor pause behavior", () => {
|
||||
});
|
||||
|
||||
// Should move to todo, NOT mark as failed
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo");
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("HAI-001", { status: "failed" });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("KB-001", { status: "failed" });
|
||||
});
|
||||
|
||||
it("does not move to in-review when paused during execution (graceful session end)", async () => {
|
||||
@@ -965,7 +965,7 @@ describe("TaskExecutor pause behavior", () => {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
// 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(),
|
||||
},
|
||||
@@ -974,7 +974,7 @@ describe("TaskExecutor pause behavior", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.execute({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -987,14 +987,14 @@ describe("TaskExecutor pause behavior", () => {
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
const store = createMockStore();
|
||||
store.listTasks.mockResolvedValue([
|
||||
{ id: "HAI-001", column: "in-progress", paused: true, title: "Paused task" },
|
||||
{ id: "HAI-002", column: "in-progress", paused: false, title: "Active task" },
|
||||
{ id: "KB-001", column: "in-progress", paused: true, title: "Paused task" },
|
||||
{ id: "KB-002", column: "in-progress", paused: false, title: "Active task" },
|
||||
]);
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
@@ -1007,8 +1007,8 @@ describe("TaskExecutor pause behavior", () => {
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
|
||||
// Only HAI-002 should be resumed (HAI-001 is paused)
|
||||
expect(store.logEntry).toHaveBeenCalledWith("HAI-002", "Resumed after engine restart");
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith("HAI-001", expect.anything());
|
||||
// Only KB-002 should be resumed (KB-001 is paused)
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-002", "Resumed after engine restart");
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith("KB-001", expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
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 { generateWorktreeName } from "./worktree-names.js";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
import { reviewStep } from "./reviewer.js";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import type { AgentSemaphore } from "./concurrency.js";
|
||||
@@ -36,7 +36,7 @@ const taskLogParams = Type.Object({
|
||||
const taskCreateParams = Type.Object({
|
||||
description: Type.String({ description: "What needs to be done" }),
|
||||
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.
|
||||
|
||||
@@ -239,7 +239,7 @@ export class TaskExecutor {
|
||||
* than being named after the task ID. This decouples directory names from
|
||||
* tasks, enabling worktree reuse across dependency chains. When resuming
|
||||
* 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> {
|
||||
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
|
||||
const branchName = `hai/${task.id.toLowerCase()}`;
|
||||
const branchName = `kb/${task.id.toLowerCase()}`;
|
||||
// Use generateWorktreeName for human-friendly directory names (adjective-noun pattern)
|
||||
// instead of task.id, so worktrees are named like ".worktrees/swift-falcon"
|
||||
let worktreePath = task.worktree || join(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
|
||||
@@ -343,7 +343,7 @@ export class TaskExecutor {
|
||||
});
|
||||
|
||||
const agentWork = async () => {
|
||||
const { session } = await createHaiAgent({
|
||||
const { session } = await createKbAgent({
|
||||
cwd: worktreePath,
|
||||
systemPrompt: EXECUTOR_SYSTEM_PROMPT,
|
||||
tools: "coding",
|
||||
@@ -664,7 +664,7 @@ git log --oneline
|
||||
const IMAGE_MIMES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
||||
const lines = ["## 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)) {
|
||||
lines.push(`- **${att.originalName}** (screenshot): \`${absPath}\``);
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,6 @@ export { TaskExecutor, type TaskExecutorOptions } from "./executor.js";
|
||||
export { Scheduler, type SchedulerOptions } from "./scheduler.js";
|
||||
export { aiMergeTask, type MergerOptions } from "./merger.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 { createLogger, type Logger } from "./logger.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Lightweight structured logger for the `@hai/engine` package.
|
||||
* Lightweight structured logger for the `@kb/engine` package.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock("./pi.js", () => ({
|
||||
createHaiAgent: vi.fn(),
|
||||
createKbAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
@@ -14,23 +14,23 @@ vi.mock("node:fs", () => ({
|
||||
}));
|
||||
|
||||
import { aiMergeTask, findWorktreeUser } from "./merger.js";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
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 { existsSync: mockedExistsSyncRaw } = await import("node:fs");
|
||||
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
|
||||
|
||||
function createMockStore(taskOverrides: Partial<Task> = {}, allTasks: Task[] = []) {
|
||||
const baseTask: Task = {
|
||||
id: "HAI-050",
|
||||
id: "KB-050",
|
||||
title: "Test task",
|
||||
description: "Test",
|
||||
column: "in-review",
|
||||
dependencies: [],
|
||||
worktree: "/tmp/root/.worktrees/HAI-050",
|
||||
worktree: "/tmp/root/.worktrees/KB-050",
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
@@ -73,27 +73,27 @@ function setupHappyPathExecSync() {
|
||||
describe("findWorktreeUser", () => {
|
||||
it("returns null when no other task uses the worktree", async () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it("returns task ID when another non-done task uses the worktree", async () => {
|
||||
const store = createMockStore({}, [
|
||||
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task,
|
||||
{ id: "HAI-051", worktree: "/tmp/wt", column: "in-progress" } as Task,
|
||||
{ id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
|
||||
{ id: "KB-051", worktree: "/tmp/wt", column: "in-progress" } as Task,
|
||||
]);
|
||||
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050");
|
||||
expect(result).toBe("HAI-051");
|
||||
const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
|
||||
expect(result).toBe("KB-051");
|
||||
});
|
||||
|
||||
it("ignores done tasks", async () => {
|
||||
const store = createMockStore({}, [
|
||||
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task,
|
||||
{ id: "HAI-051", worktree: "/tmp/wt", column: "done" } as Task,
|
||||
{ id: "KB-050", 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();
|
||||
});
|
||||
});
|
||||
@@ -112,16 +112,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
|
||||
});
|
||||
|
||||
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(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[
|
||||
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
|
||||
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task,
|
||||
{ id: "KB-050", worktree: worktreePath, column: "in-review" } 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
|
||||
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 () => {
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
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(
|
||||
(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 () => {
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[
|
||||
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
|
||||
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task,
|
||||
{ id: "KB-050", worktree: worktreePath, column: "in-review" } 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
|
||||
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 () => {
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[
|
||||
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
|
||||
{ id: "HAI-051", worktree: worktreePath, column: "todo" } as Task,
|
||||
{ id: "KB-050", worktree: worktreePath, column: "in-review" } 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.merged).toBe(true);
|
||||
});
|
||||
@@ -200,11 +200,11 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
|
||||
it("includes task ID in system prompt by default (includeTaskIdInCommit: true)", async () => {
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ 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;
|
||||
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 () => {
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
|
||||
);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
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;
|
||||
expect(agentCall.systemPrompt).toContain("<type>: <summary>");
|
||||
@@ -245,17 +245,17 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
});
|
||||
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ 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(
|
||||
(call) => String(call[0]).includes("git commit"),
|
||||
);
|
||||
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 () => {
|
||||
@@ -273,22 +273,22 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
|
||||
});
|
||||
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
|
||||
);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
includeTaskIdInCommit: false,
|
||||
});
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "HAI-050");
|
||||
await aiMergeTask(store, "/tmp/root", "KB-050");
|
||||
|
||||
const commitCall = mockedExecSync.mock.calls.find(
|
||||
(call) => String(call[0]).includes("git commit"),
|
||||
);
|
||||
expect(commitCall).toBeDefined();
|
||||
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);
|
||||
});
|
||||
|
||||
it("passes defaultProvider and defaultModelId from settings to createHaiAgent", async () => {
|
||||
it("passes defaultProvider and defaultModelId from settings to createKbAgent", async () => {
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
|
||||
);
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
@@ -316,7 +316,7 @@ describe("aiMergeTask — model settings threading", () => {
|
||||
defaultModelId: "gpt-4o",
|
||||
});
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "HAI-050");
|
||||
await aiMergeTask(store, "/tmp/root", "KB-050");
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
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 () => {
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
|
||||
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
|
||||
[{ 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;
|
||||
expect(opts.defaultProvider).toBeUndefined();
|
||||
@@ -361,15 +361,15 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
} as any;
|
||||
});
|
||||
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[{ 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 () => {
|
||||
@@ -387,15 +387,15 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
} as any;
|
||||
});
|
||||
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[{ 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 () => {
|
||||
@@ -414,15 +414,15 @@ describe("aiMergeTask — agent log persistence", () => {
|
||||
} as any;
|
||||
});
|
||||
|
||||
const worktreePath = "/tmp/root/.worktrees/HAI-050";
|
||||
const worktreePath = "/tmp/root/.worktrees/KB-050";
|
||||
const store = createMockStore(
|
||||
{ id: "HAI-050", worktree: worktreePath },
|
||||
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
|
||||
{ id: "KB-050", worktree: worktreePath },
|
||||
[{ 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(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "hi", "text");
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "hi", "text");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { TaskStore, Task, MergeResult } from "@hai/core";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import type { TaskStore, Task, MergeResult } from "@kb/core";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
import type { WorktreePool } from "./worktree-pool.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import { mergerLog } from "./logger.js";
|
||||
@@ -19,13 +19,13 @@ git commit -m "<type>(<scope>): <summary>" -m "<body>"
|
||||
|
||||
Message format:
|
||||
- **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)
|
||||
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
|
||||
|
||||
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
|
||||
- Add profile image resizing via sharp
|
||||
- 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"
|
||||
\`\`\``;
|
||||
|
||||
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.
|
||||
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 result: MergeResult = {
|
||||
task,
|
||||
@@ -231,7 +231,7 @@ export async function aiMergeTask(
|
||||
});
|
||||
|
||||
// Forward model settings from store so the merger honours the user's model choice
|
||||
const { session } = await createHaiAgent({
|
||||
const { session } = await createKbAgent({
|
||||
cwd: rootDir,
|
||||
systemPrompt: buildMergeSystemPrompt(includeTaskId),
|
||||
tools: "coding",
|
||||
|
||||
@@ -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).
|
||||
* 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.
|
||||
*/
|
||||
export async function createHaiAgent(options: AgentOptions): Promise<AgentResult> {
|
||||
export async function createKbAgent(options: AgentOptions): Promise<AgentResult> {
|
||||
const authStorage = AuthStorage.create();
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AgentSemaphore } from "./concurrency.js";
|
||||
// ── Module-level mocks (matching existing test patterns) ──────────────────
|
||||
|
||||
vi.mock("./pi.js", () => ({
|
||||
createHaiAgent: vi.fn(),
|
||||
createKbAgent: vi.fn(),
|
||||
}));
|
||||
vi.mock("./reviewer.js", () => ({
|
||||
reviewStep: vi.fn(),
|
||||
@@ -31,12 +31,12 @@ import { TaskExecutor } from "./executor.js";
|
||||
import { TriageProcessor } from "./triage.js";
|
||||
import { Scheduler } from "./scheduler.js";
|
||||
import { aiMergeTask } from "./merger.js";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
import { execSync } from "node:child_process";
|
||||
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 mockedExistsSync = vi.mocked(existsSync);
|
||||
|
||||
@@ -61,7 +61,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
|
||||
}),
|
||||
emit: vi.fn(),
|
||||
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({}),
|
||||
moveTask: vi.fn().mockImplementation(async (id: string, col: Column) => {
|
||||
return makeTask(id, col);
|
||||
@@ -74,7 +74,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
|
||||
return makeTaskDetail(id, "in-progress");
|
||||
}),
|
||||
createTask: vi.fn().mockImplementation(async (input: any) => {
|
||||
return makeTask("HAI-NEW", "triage");
|
||||
return makeTask("KB-NEW", "triage");
|
||||
}),
|
||||
deleteTask: vi.fn().mockResolvedValue(undefined),
|
||||
_listeners: listeners,
|
||||
@@ -139,9 +139,9 @@ beforeEach(() => {
|
||||
describe("In-progress task resume after restart", () => {
|
||||
it("resumeOrphaned() calls execute() for each in-progress task not already executing", async () => {
|
||||
const store = createMockStore();
|
||||
const task1 = makeTask("HAI-001", "in-progress");
|
||||
const task2 = makeTask("HAI-002", "in-progress");
|
||||
const taskDone = makeTask("HAI-003", "done");
|
||||
const task1 = makeTask("KB-001", "in-progress");
|
||||
const task2 = makeTask("KB-002", "in-progress");
|
||||
const taskDone = makeTask("KB-003", "done");
|
||||
store.listTasks.mockResolvedValue([task1, task2, taskDone]);
|
||||
|
||||
mockAgentSuccess();
|
||||
@@ -152,18 +152,18 @@ describe("In-progress task resume after restart", () => {
|
||||
// Wait for async execute calls to complete
|
||||
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);
|
||||
});
|
||||
|
||||
it("resumed task reuses existing worktree — no git worktree add called", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask("HAI-010", "in-progress", {
|
||||
worktree: "/tmp/wt/HAI-010",
|
||||
const task = makeTask("KB-010", "in-progress", {
|
||||
worktree: "/tmp/wt/KB-010",
|
||||
});
|
||||
store.listTasks.mockResolvedValue([task]);
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-010", "in-progress", {
|
||||
worktree: "/tmp/wt/HAI-010",
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-010", "in-progress", {
|
||||
worktree: "/tmp/wt/KB-010",
|
||||
}));
|
||||
|
||||
// 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 () => {
|
||||
const store = createMockStore();
|
||||
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.getTask.mockResolvedValue(makeTaskDetail("HAI-020", "in-progress", {
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-020", "in-progress", {
|
||||
steps,
|
||||
currentStep: 3,
|
||||
}));
|
||||
@@ -217,9 +217,9 @@ describe("In-progress task resume after restart", () => {
|
||||
...DEFAULT_SETTINGS,
|
||||
worktreeInitCommand: "pnpm install",
|
||||
});
|
||||
const task = makeTask("HAI-030", "in-progress");
|
||||
const task = makeTask("KB-030", "in-progress");
|
||||
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
|
||||
mockAgentSuccess();
|
||||
@@ -240,8 +240,8 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
it("resumeOrphaned() logs 'Resumed after engine restart' for each orphaned task", async () => {
|
||||
const store = createMockStore();
|
||||
const task1 = makeTask("HAI-040", "in-progress");
|
||||
const task2 = makeTask("HAI-041", "in-progress");
|
||||
const task1 = makeTask("KB-040", "in-progress");
|
||||
const task2 = makeTask("KB-041", "in-progress");
|
||||
store.listTasks.mockResolvedValue([task1, task2]);
|
||||
store.getTask.mockImplementation(async (id: string) =>
|
||||
makeTaskDetail(id, "in-progress"),
|
||||
@@ -253,25 +253,25 @@ describe("In-progress task resume after restart", () => {
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
expect(store.logEntry).toHaveBeenCalledWith("HAI-040", "Resumed after engine restart");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("HAI-041", "Resumed after engine restart");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-040", "Resumed after engine restart");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-041", "Resumed after engine restart");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Step 3: In-review merge re-queue tests ────────────────────────────────
|
||||
//
|
||||
// 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,
|
||||
// and error handling with git reset --merge cleanup.
|
||||
|
||||
describe("In-review merge handling after restart", () => {
|
||||
it("aiMergeTask validates task is in 'in-review' before merging", async () => {
|
||||
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(
|
||||
"Cannot merge HAI-050: task is in 'in-progress', must be in 'in-review'",
|
||||
await expect(aiMergeTask(store, "/tmp/root", "KB-050")).rejects.toThrow(
|
||||
"Cannot merge KB-050: task is in 'in-progress', must be in 'in-review'",
|
||||
);
|
||||
|
||||
// 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 () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-051", "in-review"));
|
||||
store.moveTask.mockResolvedValue(makeTask("HAI-051", "done"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-051", "in-review"));
|
||||
store.moveTask.mockResolvedValue(makeTask("KB-051", "done"));
|
||||
|
||||
// Branch exists, merge succeeds, no conflicts
|
||||
mockedExecSync.mockImplementation((cmd: any) => {
|
||||
@@ -294,16 +294,16 @@ describe("In-review merge handling after restart", () => {
|
||||
|
||||
mockAgentSuccess();
|
||||
|
||||
await aiMergeTask(store, "/tmp/root", "HAI-051");
|
||||
await aiMergeTask(store, "/tmp/root", "KB-051");
|
||||
|
||||
// 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)
|
||||
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 () => {
|
||||
const taskIds = ["HAI-052", "HAI-053", "HAI-054"];
|
||||
const taskIds = ["KB-052", "KB-053", "KB-054"];
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
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 () => {
|
||||
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
|
||||
mockedExecSync.mockImplementation((cmd: any) => {
|
||||
@@ -343,8 +343,8 @@ describe("In-review merge handling after restart", () => {
|
||||
},
|
||||
} as any);
|
||||
|
||||
await expect(aiMergeTask(store, "/tmp/root", "HAI-055")).rejects.toThrow(
|
||||
"AI merge failed for HAI-055: merge agent crashed",
|
||||
await expect(aiMergeTask(store, "/tmp/root", "KB-055")).rejects.toThrow(
|
||||
"AI merge failed for KB-055: merge agent crashed",
|
||||
);
|
||||
|
||||
// Should have attempted git reset --merge cleanup
|
||||
@@ -354,13 +354,13 @@ describe("In-review merge handling after restart", () => {
|
||||
expect(resetCalls.length).toBeGreaterThan(0);
|
||||
|
||||
// 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 () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-056", "in-review"));
|
||||
store.moveTask.mockResolvedValue(makeTask("HAI-056", "done"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-056", "in-review"));
|
||||
store.moveTask.mockResolvedValue(makeTask("KB-056", "done"));
|
||||
|
||||
// git rev-parse --verify throws (branch not found)
|
||||
mockedExecSync.mockImplementation((cmd: any) => {
|
||||
@@ -370,11 +370,11 @@ describe("In-review merge handling after restart", () => {
|
||||
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.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", () => {
|
||||
it("TriageProcessor.start() after restart picks up triage tasks (processing set is fresh)", async () => {
|
||||
const store = createMockStore();
|
||||
const triageTask1 = makeTask("HAI-060", "triage");
|
||||
const triageTask2 = makeTask("HAI-061", "triage");
|
||||
const triageTask1 = makeTask("KB-060", "triage");
|
||||
const triageTask2 = makeTask("KB-061", "triage");
|
||||
store.listTasks.mockResolvedValue([triageTask1, triageTask2]);
|
||||
store.getTask.mockImplementation(async (id: string) =>
|
||||
makeTaskDetail(id, "triage"),
|
||||
@@ -402,15 +402,15 @@ describe("Triage re-pick after restart", () => {
|
||||
triage.stop();
|
||||
|
||||
// Both triage tasks should have been picked up for specification
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-060", { status: "specifying" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-061", { status: "specifying" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-060", { status: "specifying" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-061", { status: "specifying" });
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("specifyTask() skips task already in processing set (no double-specification)", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask("HAI-062", "triage");
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-062", "triage"));
|
||||
const task = makeTask("KB-062", "triage");
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-062", "triage"));
|
||||
|
||||
// Slow agent to keep task in processing
|
||||
let resolvePrompt: Function;
|
||||
@@ -444,7 +444,7 @@ describe("Triage re-pick after restart", () => {
|
||||
describe("Scheduler after restart", () => {
|
||||
it("schedule() moves todo tasks to in-progress when deps are satisfied", async () => {
|
||||
const store = createMockStore();
|
||||
const todoTask = makeTask("HAI-070", "todo");
|
||||
const todoTask = makeTask("KB-070", "todo");
|
||||
store.listTasks.mockResolvedValue([todoTask]);
|
||||
store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
|
||||
|
||||
@@ -463,16 +463,16 @@ describe("Scheduler after restart", () => {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
scheduler.stop();
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-070", "in-progress");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-070", { status: null, blockedBy: null });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-070", "in-progress");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-070", { status: null, blockedBy: null });
|
||||
expect(onSchedule).toHaveBeenCalledWith(todoTask);
|
||||
});
|
||||
|
||||
it("schedule() respects dependency ordering — blocked tasks stay in todo", async () => {
|
||||
const store = createMockStore();
|
||||
const depTask = makeTask("HAI-071", "in-progress");
|
||||
const blockedTask = makeTask("HAI-072", "todo", {
|
||||
dependencies: ["HAI-071"],
|
||||
const depTask = makeTask("KB-071", "in-progress");
|
||||
const blockedTask = makeTask("KB-072", "todo", {
|
||||
dependencies: ["KB-071"],
|
||||
});
|
||||
store.listTasks.mockResolvedValue([depTask, blockedTask]);
|
||||
store.getSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
|
||||
@@ -489,19 +489,19 @@ describe("Scheduler after restart", () => {
|
||||
scheduler.stop();
|
||||
|
||||
// Task should NOT have been moved
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith("HAI-072", "in-progress");
|
||||
expect(onBlocked).toHaveBeenCalledWith(blockedTask, ["HAI-071"]);
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith("KB-072", "in-progress");
|
||||
expect(onBlocked).toHaveBeenCalledWith(blockedTask, ["KB-071"]);
|
||||
});
|
||||
|
||||
it("full column coverage: restart with tasks in every column", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
// Tasks across all columns
|
||||
const triageTask = makeTask("HAI-080", "triage");
|
||||
const todoTask = makeTask("HAI-081", "todo");
|
||||
const inProgressTask = makeTask("HAI-082", "in-progress");
|
||||
const inReviewTask = makeTask("HAI-083", "in-review");
|
||||
const doneTask = makeTask("HAI-084", "done");
|
||||
const triageTask = makeTask("KB-080", "triage");
|
||||
const todoTask = makeTask("KB-081", "todo");
|
||||
const inProgressTask = makeTask("KB-082", "in-progress");
|
||||
const inReviewTask = makeTask("KB-083", "in-review");
|
||||
const doneTask = makeTask("KB-084", "done");
|
||||
|
||||
const allTasks = [triageTask, todoTask, inProgressTask, inReviewTask, doneTask];
|
||||
store.listTasks.mockResolvedValue(allTasks);
|
||||
@@ -521,7 +521,7 @@ describe("Scheduler after restart", () => {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
triage.stop();
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-080", { status: "specifying" });
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-080", { status: "specifying" });
|
||||
|
||||
// 2. Scheduler moves todo → in-progress
|
||||
vi.clearAllMocks();
|
||||
@@ -533,7 +533,7 @@ describe("Scheduler after restart", () => {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
scheduler.stop();
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-081", "in-progress");
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-081", "in-progress");
|
||||
|
||||
// 3. Executor resumes in-progress tasks
|
||||
vi.clearAllMocks();
|
||||
@@ -548,13 +548,13 @@ describe("Scheduler after restart", () => {
|
||||
await executor.resumeOrphaned();
|
||||
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 = [
|
||||
...store.updateTask.mock.calls,
|
||||
...store.moveTask.mock.calls,
|
||||
].filter((call) => call[0] === "HAI-084");
|
||||
].filter((call) => call[0] === "KB-084");
|
||||
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 () => {
|
||||
const sem = new AgentSemaphore(2);
|
||||
const store = createMockStore();
|
||||
const task = makeTask("HAI-090", "in-progress");
|
||||
const task = makeTask("KB-090", "in-progress");
|
||||
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)
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
@@ -596,7 +596,7 @@ describe("Crash scenario edge cases", () => {
|
||||
// Verify by calling resumeOrphaned again — it should try to execute again
|
||||
vi.clearAllMocks();
|
||||
store.listTasks.mockResolvedValue([task]);
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-090", "in-progress"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-090", "in-progress"));
|
||||
mockAgentSuccess();
|
||||
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-091", "in-review"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-091", "in-review"));
|
||||
|
||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||
|
||||
@@ -620,7 +620,7 @@ describe("Crash scenario edge cases", () => {
|
||||
},
|
||||
} 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
|
||||
const resetCalls = mockedExecSync.mock.calls.filter(
|
||||
@@ -629,17 +629,17 @@ describe("Crash scenario edge cases", () => {
|
||||
expect(resetCalls.length).toBeGreaterThan(0);
|
||||
|
||||
// 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
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask("HAI-092", "in-progress");
|
||||
const task = makeTask("KB-092", "in-progress");
|
||||
store.listTasks.mockResolvedValue([task]);
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-092", "in-progress"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-092", "in-progress"));
|
||||
|
||||
let resolvePrompt: Function;
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
@@ -676,9 +676,9 @@ describe("Crash scenario edge cases", () => {
|
||||
await sem.acquire();
|
||||
expect(sem.activeCount).toBe(1);
|
||||
|
||||
const task = makeTask("HAI-093", "in-progress");
|
||||
const task = makeTask("KB-093", "in-progress");
|
||||
store.listTasks.mockResolvedValue([task]);
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("HAI-093", "in-progress"));
|
||||
store.getTask.mockResolvedValue(makeTaskDetail("KB-093", "in-progress"));
|
||||
|
||||
// Agent creation itself fails
|
||||
mockedCreateHaiAgent.mockRejectedValue(new Error("cannot create agent"));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("./pi.js", () => ({
|
||||
createHaiAgent: vi.fn(),
|
||||
createKbAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
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) {
|
||||
return {
|
||||
@@ -30,13 +30,13 @@ describe("reviewStep — model settings threading", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("passes defaultProvider and defaultModelId to createHaiAgent when provided", async () => {
|
||||
it("passes defaultProvider and defaultModelId to createKbAgent when provided", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
await reviewStep(
|
||||
"/tmp/worktree", "HAI-100", 1, "Test Step", "plan", "# prompt",
|
||||
"/tmp/worktree", "KB-100", 1, "Test Step", "plan", "# prompt",
|
||||
undefined,
|
||||
{
|
||||
defaultProvider: "anthropic",
|
||||
@@ -56,7 +56,7 @@ describe("reviewStep — model settings threading", () => {
|
||||
);
|
||||
|
||||
await reviewStep(
|
||||
"/tmp/worktree", "HAI-100", 1, "Test Step", "plan", "# prompt",
|
||||
"/tmp/worktree", "KB-100", 1, "Test Step", "plan", "# prompt",
|
||||
undefined,
|
||||
{},
|
||||
);
|
||||
@@ -73,7 +73,7 @@ describe("reviewStep — model settings threading", () => {
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - 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.
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function reviewStep(
|
||||
);
|
||||
|
||||
// Spawn a reviewer agent with read-only tools
|
||||
const { session } = await createHaiAgent({
|
||||
const { session } = await createKbAgent({
|
||||
cwd,
|
||||
systemPrompt: REVIEWER_SYSTEM_PROMPT,
|
||||
tools: "readonly",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Scheduler } from "./scheduler.js";
|
||||
|
||||
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test Task",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
@@ -49,40 +49,40 @@ describe("Scheduler concurrency", () => {
|
||||
|
||||
it("respects maxConcurrent with only in-progress tasks", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "in-progress" }),
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("counts specifying tasks toward concurrency", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("blocks all todo tasks when specifying fills all slots", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "HAI-004", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
makeTask({ id: "KB-004", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
@@ -94,9 +94,9 @@ describe("Scheduler concurrency", () => {
|
||||
|
||||
it("allows scheduling when mixed slots leave room", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "triage", status: "specifying" }),
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -111,14 +111,14 @@ describe("Scheduler concurrency", () => {
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// 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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "triage" }), // no status: "specifying"
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "triage" }), // no status: "specifying"
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
@@ -126,7 +126,7 @@ describe("Scheduler concurrency", () => {
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// 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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
// Start with maxConcurrent: 1 — no room
|
||||
@@ -159,7 +159,7 @@ describe("Scheduler dynamic settings reload", () => {
|
||||
await runSchedule(scheduler);
|
||||
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({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
@@ -169,14 +169,14 @@ describe("Scheduler dynamic settings reload", () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "in-review", worktree: "/tmp/wt" }),
|
||||
makeTask({ id: "HAI-003", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "in-review", worktree: "/tmp/wt" }),
|
||||
makeTask({ id: "KB-003", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
// Start with maxWorktrees: 2 — no room (2 active worktrees)
|
||||
@@ -192,7 +192,7 @@ describe("Scheduler dynamic settings reload", () => {
|
||||
await runSchedule(scheduler);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
|
||||
// Bump maxWorktrees to 3 — room for HAI-003
|
||||
// Bump maxWorktrees to 3 — room for KB-003
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 4,
|
||||
maxWorktrees: 3,
|
||||
@@ -202,7 +202,7 @@ describe("Scheduler dynamic settings reload", () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
@@ -254,8 +254,8 @@ describe("Scheduler file-scope overlap", () => {
|
||||
|
||||
it("sets status 'queued' for a todo task deferred due to file scope overlap", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
// Enable file scope grouping
|
||||
@@ -268,24 +268,24 @@ describe("Scheduler file-scope overlap", () => {
|
||||
});
|
||||
// Both tasks share overlapping file scopes
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "HAI-002") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-002") return ["packages/shared/utils.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
|
||||
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();
|
||||
// HAI-002 should have status set to "queued" with blockedBy
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-002", { status: "queued", blockedBy: "HAI-001" });
|
||||
// KB-002 should have status set to "queued" with blockedBy
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -296,20 +296,20 @@ describe("Scheduler file-scope overlap", () => {
|
||||
autoMerge: false,
|
||||
});
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "HAI-002") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-002") return ["packages/shared/utils.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -323,15 +323,15 @@ describe("Scheduler file-scope overlap", () => {
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
await runSchedule(scheduler);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: null, blockedBy: null });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "in-progress");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: null, blockedBy: null });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "in-progress");
|
||||
});
|
||||
|
||||
it("does not emit console.log when deferring due to file overlap", async () => {
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -342,8 +342,8 @@ describe("Scheduler file-scope overlap", () => {
|
||||
autoMerge: false,
|
||||
});
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "HAI-002") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "KB-002") return ["packages/shared/utils.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -358,8 +358,8 @@ describe("Scheduler file-scope overlap", () => {
|
||||
|
||||
it("does not set status 'queued' when file scopes do not overlap", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -370,16 +370,16 @@ describe("Scheduler file-scope overlap", () => {
|
||||
autoMerge: false,
|
||||
});
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/a/file.ts"];
|
||||
if (id === "HAI-002") return ["packages/b/file.ts"];
|
||||
if (id === "KB-001") return ["packages/a/file.ts"];
|
||||
if (id === "KB-002") return ["packages/b/file.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// HAI-002 should be moved (no overlap)
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress");
|
||||
// KB-002 should be moved (no overlap)
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-002", "in-progress");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -395,7 +395,7 @@ describe("Scheduler paused tasks", () => {
|
||||
|
||||
it("does not schedule paused todo tasks", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "todo", paused: true }),
|
||||
makeTask({ id: "KB-001", column: "todo", paused: true }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
@@ -407,20 +407,20 @@ describe("Scheduler paused tasks", () => {
|
||||
|
||||
it("schedules non-paused todo tasks normally", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "todo", paused: false }),
|
||||
makeTask({ id: "KB-001", column: "todo", paused: false }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2 });
|
||||
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "triage", status: "specifying", paused: true }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
makeTask({ id: "KB-001", column: "triage", status: "specifying", paused: true }),
|
||||
makeTask({ id: "KB-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
@@ -434,8 +434,8 @@ describe("Scheduler paused tasks", () => {
|
||||
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// The paused specifying task doesn't consume a slot, so HAI-002 should be scheduled
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress");
|
||||
// The paused specifying task doesn't consume a slot, so KB-002 should be scheduled
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "in-progress" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "in-progress" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
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 () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "in-progress" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "in-progress" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
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 () => {
|
||||
const maxedTasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "in-progress" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-002", column: "in-progress" }),
|
||||
];
|
||||
const freeTasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "KB-001", column: "in-progress" }),
|
||||
];
|
||||
|
||||
const store = createMockStore(maxedTasks);
|
||||
|
||||
@@ -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 { schedulerLog } from "./logger.js";
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { AgentSemaphore } from "./concurrency.js";
|
||||
|
||||
// Mock createHaiAgent before importing TriageProcessor
|
||||
// Mock createKbAgent before importing TriageProcessor
|
||||
vi.mock("./pi.js", () => ({
|
||||
createHaiAgent: vi.fn(),
|
||||
createKbAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
import { TriageProcessor, buildSpecificationPrompt, type AttachmentContent } from "./triage.js";
|
||||
import { createHaiAgent } from "./pi.js";
|
||||
import type { TaskDetail } from "@hai/core";
|
||||
import { createKbAgent } from "./pi.js";
|
||||
import type { TaskDetail } from "@kb/core";
|
||||
|
||||
const mockedCreateHaiAgent = vi.mocked(createHaiAgent);
|
||||
const mockedCreateHaiAgent = vi.mocked(createKbAgent);
|
||||
|
||||
function createMockStore(tasks: any[] = []) {
|
||||
return {
|
||||
listTasks: vi.fn().mockResolvedValue(tasks),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "triage",
|
||||
@@ -43,7 +43,7 @@ function createMockStore(tasks: any[] = []) {
|
||||
|
||||
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test Task",
|
||||
description: "A test task",
|
||||
column: "triage",
|
||||
@@ -79,7 +79,7 @@ describe("TriageProcessor with semaphore", () => {
|
||||
const triage = new TriageProcessor(store, "/tmp/test", { semaphore: sem });
|
||||
|
||||
await triage.specifyTask({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "triage",
|
||||
@@ -111,7 +111,7 @@ describe("TriageProcessor with semaphore", () => {
|
||||
});
|
||||
|
||||
await triage.specifyTask({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "triage",
|
||||
@@ -163,9 +163,9 @@ describe("TriageProcessor with semaphore", () => {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
triage.specifyTask(task("HAI-001")),
|
||||
triage.specifyTask(task("HAI-002")),
|
||||
triage.specifyTask(task("HAI-003")),
|
||||
triage.specifyTask(task("KB-001")),
|
||||
triage.specifyTask(task("KB-002")),
|
||||
triage.specifyTask(task("KB-003")),
|
||||
]);
|
||||
|
||||
expect(maxConcurrent).toBe(1);
|
||||
@@ -223,7 +223,7 @@ describe("TriageProcessor paused tasks", () => {
|
||||
|
||||
it("skips paused triage tasks in poll()", async () => {
|
||||
const pausedTask = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Paused",
|
||||
description: "Paused task",
|
||||
column: "triage" as const,
|
||||
@@ -255,7 +255,7 @@ describe("TriageProcessor paused tasks", () => {
|
||||
|
||||
it("processes non-paused triage tasks normally", async () => {
|
||||
const normalTask = {
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
title: "Normal",
|
||||
description: "Normal task",
|
||||
column: "triage" as const,
|
||||
@@ -280,14 +280,14 @@ describe("TriageProcessor paused tasks", () => {
|
||||
await (triage as any).poll();
|
||||
|
||||
// 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", () => {
|
||||
it("includes project commands when testCommand is set", () => {
|
||||
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,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
@@ -303,7 +303,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
|
||||
it("includes project commands when buildCommand is set", () => {
|
||||
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,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
@@ -318,7 +318,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
|
||||
it("includes both commands when both are set", () => {
|
||||
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,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
@@ -334,7 +334,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
|
||||
it("omits project commands section when neither command is set", () => {
|
||||
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,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
@@ -347,7 +347,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
|
||||
it("omits project commands section when settings is undefined", () => {
|
||||
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");
|
||||
});
|
||||
@@ -357,7 +357,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
const attachmentContents: AttachmentContent[] = [
|
||||
{ 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("### error.log (text/plain)");
|
||||
@@ -369,7 +369,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
const attachmentContents: AttachmentContent[] = [
|
||||
{ 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("**screenshot.png** (image/png)");
|
||||
@@ -382,7 +382,7 @@ describe("buildSpecificationPrompt", () => {
|
||||
{ originalName: "screenshot.png", mimeType: "image/png", text: null },
|
||||
{ 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("### config.json (application/json)");
|
||||
@@ -391,14 +391,14 @@ describe("buildSpecificationPrompt", () => {
|
||||
|
||||
it("omits attachments section when no attachments", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
it("omits attachments section when attachmentContents is undefined", () => {
|
||||
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");
|
||||
});
|
||||
@@ -412,7 +412,7 @@ function createEnoentError(path = "/fake/path"): NodeJS.ErrnoException {
|
||||
}
|
||||
|
||||
const dummyTask = {
|
||||
id: "HAI-099",
|
||||
id: "KB-099",
|
||||
title: "Deleted task",
|
||||
description: "This task was deleted",
|
||||
column: "triage" as const,
|
||||
@@ -485,7 +485,7 @@ describe("TriageProcessor deleted task handling", () => {
|
||||
await triage.specifyTask(dummyTask);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
@@ -515,7 +515,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
|
||||
const triage = new TriageProcessor(store, "/tmp/test", {});
|
||||
await triage.specifyTask({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "triage",
|
||||
@@ -528,7 +528,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
@@ -549,7 +549,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
|
||||
const triage = new TriageProcessor(store, "/tmp/test", {});
|
||||
await triage.specifyTask({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "triage",
|
||||
@@ -561,7 +561,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
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 () => {
|
||||
@@ -583,7 +583,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
|
||||
const triage = new TriageProcessor(store, "/tmp/test", { onAgentText });
|
||||
await triage.specifyTask({
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "triage",
|
||||
@@ -595,7 +595,7 @@ describe("TriageProcessor agent log persistence", () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
expect(onAgentText).toHaveBeenCalledWith("HAI-001", "hi");
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-001", "hi", "text");
|
||||
expect(onAgentText).toHaveBeenCalledWith("KB-001", "hi");
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-001", "hi", "text");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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, type Static } from "@mariozechner/pi-ai";
|
||||
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 { AgentLogger } from "./agent-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.
|
||||
|
||||
@@ -81,7 +81,7 @@ Follow this structure exactly:
|
||||
### Step {N}: Documentation & Delivery
|
||||
|
||||
- [ ] 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
|
||||
|
||||
@@ -250,7 +250,7 @@ export class TriageProcessor {
|
||||
await this.store.updateTask(task.id, { status: "specifying" });
|
||||
const detail = await this.store.getTask(task.id);
|
||||
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 agentLogger = new AgentLogger({
|
||||
@@ -264,7 +264,7 @@ export class TriageProcessor {
|
||||
},
|
||||
});
|
||||
|
||||
const { session } = await createHaiAgent({
|
||||
const { session } = await createKbAgent({
|
||||
cwd: this.rootDir,
|
||||
systemPrompt: TRIAGE_SYSTEM_PROMPT,
|
||||
tools: "coding",
|
||||
@@ -333,7 +333,7 @@ export class TriageProcessor {
|
||||
const store = this.store;
|
||||
|
||||
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 = {
|
||||
@@ -431,7 +431,7 @@ export async function readAttachmentContents(
|
||||
const { join } = await import("node:path");
|
||||
|
||||
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 {
|
||||
if (IMAGE_MIME_TYPES.has(att.mimeType)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("generateWorktreeName", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "hai-wt-test-"));
|
||||
tempDir = mkdtempSync(join(tmpdir(), "kb-wt-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("WorktreePool", () => {
|
||||
|
||||
describe("prepareForTask", () => {
|
||||
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]);
|
||||
expect(calls).toContain("git checkout -- .");
|
||||
@@ -128,18 +128,18 @@ describe("WorktreePool", () => {
|
||||
});
|
||||
|
||||
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(
|
||||
(c) => typeof c[0] === "string" && (c[0] as string).includes("checkout -B"),
|
||||
);
|
||||
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" });
|
||||
});
|
||||
|
||||
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) {
|
||||
expect(call[1]).toMatchObject({ cwd: "/tmp/my-worktree" });
|
||||
@@ -153,12 +153,12 @@ describe("WorktreePool", () => {
|
||||
});
|
||||
|
||||
// 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
|
||||
const calls = mockedExecSync.mock.calls.map((c) => c[0]);
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export class WorktreePool {
|
||||
* 3. `git checkout -B <branchName> main` — create/reset branch from main
|
||||
*
|
||||
* @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 {
|
||||
// Clean tracked modifications
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -20,13 +20,13 @@ importers:
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
'@hai/core':
|
||||
'@kb/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@hai/dashboard':
|
||||
'@kb/dashboard':
|
||||
specifier: workspace:*
|
||||
version: link:../dashboard
|
||||
'@hai/engine':
|
||||
'@kb/engine':
|
||||
specifier: workspace:*
|
||||
version: link:../engine
|
||||
'@mariozechner/pi-coding-agent':
|
||||
@@ -60,7 +60,7 @@ importers:
|
||||
|
||||
packages/dashboard:
|
||||
dependencies:
|
||||
'@hai/core':
|
||||
'@kb/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@types/multer':
|
||||
@@ -124,7 +124,7 @@ importers:
|
||||
|
||||
packages/engine:
|
||||
dependencies:
|
||||
'@hai/core':
|
||||
'@kb/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@mariozechner/pi-ai':
|
||||
|
||||
Reference in New Issue
Block a user