From 53c54b97e361323739b6d19fc6fd41f7fc3c32f6 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Fri, 28 Aug 2026 16:19:39 +0100 Subject: [PATCH 1/2] (MOT-4610) feat(kanban): add multi-worker Kanban runs --- .gitignore | 1 + README.md | 1 + iii-permissions.yaml | 11 + kanban/README.md | 79 ++ kanban/iii-permissions.yaml | 11 + kanban/iii.worker.yaml | 20 + kanban/package.json | 31 + kanban/pnpm-lock.yaml | 1809 +++++++++++++++++++++++++++++++ kanban/pnpm-workspace.yaml | 7 + kanban/scripts/build-bundle.mjs | 73 ++ kanban/skills/SKILL.md | 72 ++ kanban/src/index.ts | 985 +++++++++++++++++ kanban/src/model.ts | 317 ++++++ kanban/src/virtual-ui.d.ts | 4 + kanban/tests/model.test.ts | 195 ++++ kanban/tsconfig.json | 12 + kanban/ui/build.mjs | 18 + kanban/ui/package.json | 19 + kanban/ui/page.tsx | 1271 ++++++++++++++++++++++ kanban/ui/styles.css | 150 +++ kanban/ui/tsconfig.json | 13 + kanban/vitest.config.ts | 5 + pnpm-lock.yaml | 16 + pnpm-workspace.yaml | 1 + 24 files changed, 5121 insertions(+) create mode 100644 kanban/README.md create mode 100644 kanban/iii-permissions.yaml create mode 100644 kanban/iii.worker.yaml create mode 100644 kanban/package.json create mode 100644 kanban/pnpm-lock.yaml create mode 100644 kanban/pnpm-workspace.yaml create mode 100644 kanban/scripts/build-bundle.mjs create mode 100644 kanban/skills/SKILL.md create mode 100644 kanban/src/index.ts create mode 100644 kanban/src/model.ts create mode 100644 kanban/src/virtual-ui.d.ts create mode 100644 kanban/tests/model.test.ts create mode 100644 kanban/tsconfig.json create mode 100644 kanban/ui/build.mjs create mode 100644 kanban/ui/package.json create mode 100644 kanban/ui/page.tsx create mode 100644 kanban/ui/styles.css create mode 100644 kanban/ui/tsconfig.json create mode 100644 kanban/vitest.config.ts diff --git a/.gitignore b/.gitignore index 08bd46973..3061fa419 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ __pycache__/ .omx/ .serena/ data/ +worktree/worktrees/ *.pid *.zip iii.lock diff --git a/README.md b/README.md index d25002678..4793cd6ea 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ npx skills add iii-hq/iii --all | [`editor`](editor/) | Rust | A shared code workspace — open buffers, file tree, unified diffs, fuzzy find and conflict-safe saves, held in `state` so an agent and a person see one editor. Files and git go through `shell`; ships a console editor page. | | [`vscode`](vscode/) | Node | VS Code as an iii worker — `vscode::*` runs the VS Code Server through the `code` CLI per workspace, and a Console page embeds the Workbench for the working directory. | | [`compose-ui`](compose-ui/) | Node | The compose daemon in the Console — a **Compose** page over `compose::*` with live container state, lifecycle actions, worker packages, and per-container log tails, plus a `compose-ui::changed` trigger type for supervisor changes. | +| [`kanban`](kanban/) | Node | Repository-aware multi-worker Kanban runs — launch Harness and external worker tasks under one root session, isolate them in managed Git worktrees, gate dependencies, review results, and land approved branches from the Console. | | [`iii-directory`](iii-directory/) | Rust | Engine introspection, workers-registry proxy, filesystem-backed skills, system prompts, and agent profiles, plus one-shot lexical function search — `directory::search_functions` returns compact candidates for the relevant functions (BM25 + coverage pruning, installed + installable-from-registry) and directs callers to batch selected ids through `engine::functions::info`; its search hint is injected at most once per turn. | | [`lsp`](lsp/) | Rust | Language Server for iii function ids, trigger configs, and worker discovery. Autocomplete / hover across JS/TS, Python, Rust. | | [`lsp-vscode`](lsp-vscode/) | Node | VS Code extension package `iii-lsp`, embedding the `lsp` server. | diff --git a/iii-permissions.yaml b/iii-permissions.yaml index 6cc44fc0f..8ac5f3872 100644 --- a/iii-permissions.yaml +++ b/iii-permissions.yaml @@ -125,6 +125,14 @@ rules: - '!harness::stop' - '!harness::on-config-change' - '!harness::sweep-pending' + # kanban: Console asset delivery and lifecycle sinks are engine plumbing. + # Run/task mutations stay at the needs_approval default; read-only board, + # model, and executor discovery are allowlisted below. + - '!kanban::ui-content' + - '!kanban::on-agent-task' + - '!kanban::on-harness-completed' + - '!kanban::on-session-changed' + - '!kanban::on-worktree-changed' # eval: durable orchestration internals are trigger/queue targets only. # Starting, cancelling, and deleting evaluations remain approval-gated by # the default. Status/result are read-only and the exact evaluator is pure. @@ -299,6 +307,9 @@ rules: - worktree::get - worktree::status - worktree::validate + - kanban::board + - kanban::executors::list + - kanban::models::list # editor: the reading half. editor::diff is pure (two strings in, a patch # out — no I/O at all); the rest read through the shell worker, so they reach # nothing shell::fs::read and shell::exec could not already reach. diff --git a/kanban/README.md b/kanban/README.md new file mode 100644 index 000000000..a5ed1ab12 --- /dev/null +++ b/kanban/README.md @@ -0,0 +1,79 @@ +# kanban + +Repository-aware multi-worker runs in the Console. The **Kanban** page combines +a root Harness session, native or external child executors, dependency gates, +isolated Git worktrees, review, and explicit landing in one Kanban board. +Existing Harness conversations with child sessions appear automatically, while +runs created from Kanban receive a real root session so every task remains +visible in the Console's native conversation hierarchy. + +Harness and the session manager remain the execution source of truth. Kanban +stores only run definitions, task dispatch metadata, dependencies, workspace +references, and review decisions. The worktree Worker owns Git lifecycle and +status. Reactive triggers keep the page current without another scheduler. + +## Install + +```bash +iii compose add kanban +``` + +The page appears in the Console as **Kanban** (`#/ext/kanban`). It discovers +available executors from the live function registry. Harness is supported +natively; workers that expose a `::task` function using the durable +`agent_tasks` result contract are added automatically. + +## What the page controls + +- Select any live run and open its root Harness conversation. +- Create up to 24 tasks in one run and choose an executor for each task. +- Give every task its own managed worktree and branch, or use one shared directory. +- Gate tasks on reviewed prerequisites, then dispatch them automatically. +- Mix native Harness agents with external task-contract executors under one root. +- Dispatch immediately or hold tasks in Ready. +- Open the child session or a Shell rooted in the task worktree. +- Stop supported running tasks, retry failed tasks, and accept reviewed work. +- Inspect live Git status and explicitly queue reviewed work for serialized landing. +- Observe existing parent and child Harness sessions without importing or + duplicating them. + +## Functions + +| Function | Purpose | +|---|---| +| `kanban::board` | Return runs, tasks, executor topology, managed-worktree Git status, and capabilities. | +| `kanban::executors::list` | List currently available Harness and task-contract executors. | +| `kanban::runs::create` | Create a root session plus one or more durable tasks, optionally dispatching them immediately. | +| `kanban::tasks::dispatch` | Dispatch a Ready or attention-blocked task. | +| `kanban::tasks::retry` | Retry an attention-blocked task, preserving the claimed session for a managed worktree. | +| `kanban::tasks::stop` | Stop a running task when its executor exposes a stop function. | +| `kanban::tasks::accept` | Move a reviewed task to Done. | +| `kanban::tasks::land` | Queue a reviewed task's worktree for tested, serialized landing. | + +## Reactive updates + +Bind `kanban::changed` with an empty config and re-read `kanban::board` when it +fires. The trigger is emitted for Kanban state writes and relevant session +creation, status, and metadata events. + +```ts +iii.registerTrigger({ + type: 'kanban::changed', + function_id: 'my-worker::on-kanban-change', + config: {}, +}) +``` + +## Run from source + +```bash +pnpm install +pnpm test +pnpm typecheck +pnpm build +III_URL=ws://127.0.0.1:49134 III_NAMESPACE=my-project pnpm start +``` + +A standalone process must use the same namespace as the Console and Harness. +Set `III_KANBAN_UI_WATCH=1` to serve page assets from `ui/dist` while developing +the injected UI. diff --git a/kanban/iii-permissions.yaml b/kanban/iii-permissions.yaml new file mode 100644 index 000000000..d38eb461b --- /dev/null +++ b/kanban/iii-permissions.yaml @@ -0,0 +1,11 @@ +version: 1 + +rules: + - '!kanban::ui-content' + - '!kanban::on-agent-task' + - '!kanban::on-harness-completed' + - '!kanban::on-session-changed' + - '!kanban::on-worktree-changed' + - kanban::board + - kanban::executors::list + - kanban::models::list diff --git a/kanban/iii.worker.yaml b/kanban/iii.worker.yaml new file mode 100644 index 000000000..e06ee742b --- /dev/null +++ b/kanban/iii.worker.yaml @@ -0,0 +1,20 @@ +iii: v1 +name: kanban +language: javascript +deploy: bundle +manifest: package.json +license: Apache-2.0 +tags: [kanban, workers, tasks, runs, console, ui] +description: Repository-aware multi-worker runs with Harness sessions, isolated worktrees, and Git review. + +runtime: + kind: javascript + +scripts: + start: node ./index.mjs + +dependencies: + console: "^1.9.11" + state: "^0.22.3" + harness: "^1.8.8" + worktree: "^0.3.4-rc.1" diff --git a/kanban/package.json b/kanban/package.json new file mode 100644 index 000000000..96507f397 --- /dev/null +++ b/kanban/package.json @@ -0,0 +1,31 @@ +{ + "name": "kanban", + "version": "0.1.0", + "private": true, + "description": "Repository-aware multi-worker runs with Harness sessions, isolated worktrees, and Git review.", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": ">=22" + }, + "packageManager": "pnpm@11.13.1", + "scripts": { + "build": "pnpm --dir ui build && node scripts/build-bundle.mjs", + "build:bundle": "node scripts/build-bundle.mjs", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "typecheck": "tsc -p tsconfig.json --noEmit && pnpm --dir ui typecheck", + "test": "vitest run", + "start": "node dist/bundle/index.mjs" + }, + "dependencies": { + "iii-sdk": "0.22.1-alpha.25" + }, + "devDependencies": { + "@biomejs/biome": "2.4.10", + "@types/node": "^22.10.0", + "esbuild": "^0.25.0", + "typescript": "^5.9.2", + "vitest": "^3.0.0" + } +} diff --git a/kanban/pnpm-lock.yaml b/kanban/pnpm-lock.yaml new file mode 100644 index 000000000..728db0b21 --- /dev/null +++ b/kanban/pnpm-lock.yaml @@ -0,0 +1,1809 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + iii-sdk: + specifier: 0.22.1-alpha.25 + version: 0.22.1-alpha.25 + devDependencies: + '@biomejs/biome': + specifier: 2.4.10 + version: 2.4.10 + '@types/node': + specifier: ^22.10.0 + version: 22.20.1 + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.7(@types/node@22.20.1) + +packages: + + '@biomejs/biome@2.4.10': + resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.10': + resolution: {integrity: sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.10': + resolution: {integrity: sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.10': + resolution: {integrity: sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.10': + resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.10': + resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.10': + resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.10': + resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.10': + resolution: {integrity: sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@iii-dev/helpers@0.22.1-alpha.25': + resolution: {integrity: sha512-q3XQZ4i+eMA4mi1iRaNHlTcIGJVvhCYkmfiIPn0rvlFjLYU32vEqjbxKeNsy9YyRbAhGbjF+kzXp2cUpycVhJw==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rollup/rollup-android-arm-eabi@4.63.0': + resolution: {integrity: sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.0': + resolution: {integrity: sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.0': + resolution: {integrity: sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.0': + resolution: {integrity: sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.0': + resolution: {integrity: sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.0': + resolution: {integrity: sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.0': + resolution: {integrity: sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.0': + resolution: {integrity: sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.0': + resolution: {integrity: sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.0': + resolution: {integrity: sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.0': + resolution: {integrity: sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.0': + resolution: {integrity: sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.0': + resolution: {integrity: sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.0': + resolution: {integrity: sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.0': + resolution: {integrity: sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.0': + resolution: {integrity: sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.0': + resolution: {integrity: sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.0': + resolution: {integrity: sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.0': + resolution: {integrity: sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.0': + resolution: {integrity: sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.0': + resolution: {integrity: sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.0': + resolution: {integrity: sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.0': + resolution: {integrity: sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.0': + resolution: {integrity: sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.0': + resolution: {integrity: sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + iii-sdk@0.22.1-alpha.25: + resolution: {integrity: sha512-WUDzAydJacutn5IQz3Tcx296fAn88UnUt8S5/2slRsZK7WCadMS9o3qXmkgKAUAmPHsdCOAwVolBWNnAz4imqQ==} + + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.63.0: + resolution: {integrity: sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@biomejs/biome@2.4.10': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.10 + '@biomejs/cli-darwin-x64': 2.4.10 + '@biomejs/cli-linux-arm64': 2.4.10 + '@biomejs/cli-linux-arm64-musl': 2.4.10 + '@biomejs/cli-linux-x64': 2.4.10 + '@biomejs/cli-linux-x64-musl': 2.4.10 + '@biomejs/cli-win32-arm64': 2.4.10 + '@biomejs/cli-win32-x64': 2.4.10 + + '@biomejs/cli-darwin-arm64@2.4.10': + optional: true + + '@biomejs/cli-darwin-x64@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64@2.4.10': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-x64@2.4.10': + optional: true + + '@biomejs/cli-win32-arm64@2.4.10': + optional: true + + '@biomejs/cli-win32-x64@2.4.10': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@iii-dev/helpers@0.22.1-alpha.25': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.8.5 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + protobufjs: 7.6.6 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + semver: 7.8.5 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@rollup/rollup-android-arm-eabi@4.63.0': + optional: true + + '@rollup/rollup-android-arm64@4.63.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.0': + optional: true + + '@rollup/rollup-darwin-x64@4.63.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.0': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/shimmer@1.2.0': {} + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-import-attributes@1.9.5(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + cjs-module-lexer@1.4.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + iii-sdk@0.22.1-alpha.25: + dependencies: + '@iii-dev/helpers': 0.22.1-alpha.25 + '@opentelemetry/api': 1.9.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + js-tokens@9.0.1: {} + + long@5.3.2: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + module-details-from-path@1.0.4: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + protobufjs@7.6.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.1 + long: 5.3.2 + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.63.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.0 + '@rollup/rollup-android-arm64': 4.63.0 + '@rollup/rollup-darwin-arm64': 4.63.0 + '@rollup/rollup-darwin-x64': 4.63.0 + '@rollup/rollup-freebsd-arm64': 4.63.0 + '@rollup/rollup-freebsd-x64': 4.63.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.0 + '@rollup/rollup-linux-arm-musleabihf': 4.63.0 + '@rollup/rollup-linux-arm64-gnu': 4.63.0 + '@rollup/rollup-linux-arm64-musl': 4.63.0 + '@rollup/rollup-linux-loong64-gnu': 4.63.0 + '@rollup/rollup-linux-loong64-musl': 4.63.0 + '@rollup/rollup-linux-ppc64-gnu': 4.63.0 + '@rollup/rollup-linux-ppc64-musl': 4.63.0 + '@rollup/rollup-linux-riscv64-gnu': 4.63.0 + '@rollup/rollup-linux-riscv64-musl': 4.63.0 + '@rollup/rollup-linux-s390x-gnu': 4.63.0 + '@rollup/rollup-linux-x64-gnu': 4.63.0 + '@rollup/rollup-linux-x64-musl': 4.63.0 + '@rollup/rollup-openbsd-x64': 4.63.0 + '@rollup/rollup-openharmony-arm64': 4.63.0 + '@rollup/rollup-win32-arm64-msvc': 4.63.0 + '@rollup/rollup-win32-ia32-msvc': 4.63.0 + '@rollup/rollup-win32-x64-gnu': 4.63.0 + '@rollup/rollup-win32-x64-msvc': 4.63.0 + fsevents: 2.3.3 + + semver@7.8.5: {} + + shimmer@1.2.1: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-preserve-symlinks-flag@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite-node@3.2.4(@types/node@22.20.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.20.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.20.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.26 + rollup: 4.63.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@22.20.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.20.1) + vite-node: 3.2.4(@types/node@22.20.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} diff --git a/kanban/pnpm-workspace.yaml b/kanban/pnpm-workspace.yaml new file mode 100644 index 000000000..5c938dd39 --- /dev/null +++ b/kanban/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +minimumReleaseAgeExclude: + - '@iii-dev/helpers@0.22.1-alpha.25' + - iii-sdk@0.22.1-alpha.25 + +allowBuilds: + esbuild: true + protobufjs: false diff --git a/kanban/scripts/build-bundle.mjs b/kanban/scripts/build-bundle.mjs new file mode 100644 index 000000000..5fdd2d400 --- /dev/null +++ b/kanban/scripts/build-bundle.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') + +await build({ + entryPoints: [join(root, 'ui/page.tsx'), join(root, 'ui/styles.css')], + bundle: true, + format: 'esm', + jsx: 'automatic', + outdir: join(root, 'ui/dist'), + external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@iii-dev/console-ui'], + logLevel: 'info', +}) + +const packageJsonRequirePattern = /createRequire\(\s*import\.meta\.url\s*\)\s*\(\s*["']\.\.\/package\.json["']\s*\)/ +const uiNamespace = 'kanban-ui-assets' + +const inlineAssets = { + name: 'inline-kanban-ui-assets', + setup(builder) { + builder.onResolve({ filter: /^virtual:kanban-ui-assets$/ }, () => ({ path: 'assets', namespace: uiNamespace })) + builder.onLoad({ filter: /.*/, namespace: uiNamespace }, async () => { + const page = await readFile(join(root, 'ui/dist/page.js'), 'utf8') + const styles = await readFile(join(root, 'ui/dist/styles.css'), 'utf8') + return { + contents: `export const uiPage = ${JSON.stringify(page)};\nexport const uiStyles = ${JSON.stringify(styles)};`, + loader: 'js', + } + }) + }, +} + +const inlinePackageJson = { + name: 'inline-sdk-package-json', + setup(builder) { + builder.onLoad({ filter: /iii-sdk[\\/]dist[\\/]index\.mjs$/ }, async (args) => { + const [source, pkg] = await Promise.all([ + readFile(args.path, 'utf8'), + readFile(join(root, 'node_modules/iii-sdk/package.json'), 'utf8'), + ]) + const { version } = JSON.parse(pkg) + if (!packageJsonRequirePattern.test(source)) { + throw new Error('iii-sdk package.json lookup pattern was not found during bundling') + } + return { + contents: source.replace(new RegExp(packageJsonRequirePattern.source, 'g'), JSON.stringify({ version })), + loader: 'js', + } + }) + }, +} + +await build({ + entryPoints: [join(root, 'src/index.ts')], + outfile: join(root, 'dist/bundle/index.mjs'), + bundle: true, + platform: 'node', + target: 'node22', + format: 'esm', + legalComments: 'none', + external: ['fsevents'], + banner: { + js: "import{createRequire as __iiiCR}from'module';const require=__iiiCR(import.meta.url);", + }, + define: { 'process.env.NODE_ENV': '"production"' }, + plugins: [inlineAssets, inlinePackageJson], + logLevel: 'info', +}) diff --git a/kanban/skills/SKILL.md b/kanban/skills/SKILL.md new file mode 100644 index 000000000..f1be03c5b --- /dev/null +++ b/kanban/skills/SKILL.md @@ -0,0 +1,72 @@ +--- +name: kanban +description: >- + Plan, launch, inspect, review, and land multiple Harness or external-worker + tasks as one repository-aware run with dependencies and isolated worktrees. +--- + +# kanban + +Kanban is a visual control plane over the live Harness session tree. Harness +and the session manager own execution, messages, status, and parent-child +relationships. The worktree Worker owns isolated Git directories, branches, +status, and landing. Kanban binds them through durable run definitions, +executor selection, dependencies, retries, and explicit review acceptance. + +## When to use + +- A task has independent workstreams that should execute concurrently. +- A person wants to see existing Harness child sessions as cards and open the + underlying conversation from the board. +- External workers exposing the shared task contract should participate in the + same run as native Harness sessions. +- Parallel implementation tasks need separate branches and filesystem roots. +- A dependent task should wait until its prerequisite reaches Review. +- Completed worker output needs an explicit Review to Done gate. + +## Workflow + +1. Call `kanban::executors::list` and select only executors whose `available` + field is true. +2. Create one coherent run with `kanban::runs::create`. Set `repo_path` and + `isolation: worktree_per_task` for parallel Git work. Give every task a + stable `key`, bounded instruction, executor ID, and descriptive title. Use + `depends_on` keys for ordering and `auto_dispatch: false` only when the + person wants a staging gate. +3. Read `kanban::board` after `kanban::changed` events. Do not poll it on a + timer. +4. Open `root_session_id` or a task's `external_session_id` when the detailed + transcript is needed. +5. Retry only `needs_you` tasks. Accept only tasks in `review` after checking + their result. +6. Call `kanban::tasks::land` only after review and only with the intended + target branch. Landing is an explicit operator action, never automatic. + +## Boundaries + +- Do not create a second message bus or scheduler. Use the existing session, + state, and Harness functions. +- Do not invent an executor from its name. Executor availability comes from + the live function registry. +- Do not create ad-hoc directories for isolated work. Use `worktree::create` + through the run isolation contract and pass its path to the child executor. +- Harness-projected runs are read-only board projections. Their session tree + remains authoritative. +- Kanban-owned task controls must use the `kanban::tasks::*` functions so + durable metadata and reactive updates remain consistent. + +## Functions + +| Function | Use | +|---|---| +| `kanban::board` | Read live runs, task routes, worktrees, Git status, and capabilities. | +| `kanban::executors::list` | Discover usable executors and capabilities. | +| `kanban::runs::create` | Create a native root session and durable tasks. | +| `kanban::tasks::dispatch` | Start a held task. | +| `kanban::tasks::retry` | Re-run an attention-blocked task. | +| `kanban::tasks::stop` | Stop a task when supported. | +| `kanban::tasks::accept` | Approve a reviewed result. | +| `kanban::tasks::land` | Queue reviewed Git work onto the run target branch. | + +The Console page ID is `kanban`. Open it from another injected page with +`host.panels.open({ pageId: 'kanban' })`. diff --git a/kanban/src/index.ts b/kanban/src/index.ts new file mode 100644 index 000000000..350b3a092 --- /dev/null +++ b/kanban/src/index.ts @@ -0,0 +1,985 @@ +import { randomUUID } from 'node:crypto' +import { watch } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { uiPage, uiStyles } from 'virtual:kanban-ui-assets' +import { registerWorker } from 'iii-sdk' +import { + dependenciesSatisfied, + deriveRunStatus, + type Executor, + executorsFromCatalog, + type FunctionCatalogRow, + type GitStatus, + isTaskStatus, + type RunRecord, + type RunView, + resultText, + runtimeCapabilities, + singleFlight, + type TaskRecord, + type TaskView, + taskCounts, + validateCreateRun, +} from './model.js' + +const WORKER = 'kanban' +const RUNS_SCOPE = 'kanban_runs' +const TASKS_SCOPE = 'kanban_tasks' +const AGENT_TASKS_SCOPE = 'agent_tasks' +const CHANGED_TRIGGER = 'kanban::changed' +const TASK_OUTCOME_FN = 'kanban::on-agent-task' +const HARNESS_COMPLETED_FN = 'kanban::on-harness-completed' +const SESSION_CHANGED_FN = 'kanban::on-session-changed' +const WORKTREE_CHANGED_FN = 'kanban::on-worktree-changed' + +const iii = registerWorker(process.env.III_URL ?? process.env.III_ENGINE_URL, { + workerName: WORKER, + workerDescription: 'Repository-aware multi-worker runs with Harness sessions, isolated worktrees, and Git review.', +}) + +const object = (properties: Record = {}, required: string[] = []) => ({ + type: 'object' as const, + properties, + ...(required.length ? { required } : {}), +}) +const string = { type: 'string' } +const nullableString = { type: ['string', 'null'] } + +type FunctionList = { functions?: FunctionCatalogRow[] } + +type SessionRecord = { + session_id: string + title?: string + description?: string + status?: string + status_reason?: string + message_count?: number + metadata?: Record + created_at?: number + updated_at?: number +} + +type SessionList = { sessions?: SessionRecord[]; next_cursor?: string | null } + +type WorktreeCreateResponse = { + worktree_id: string + path: string + branch: string + base_ref: string + base_sha: string + dev_port: number +} + +type WorktreeStatusResponse = GitStatus & { + worktree_id: string + branch: string + lifecycle: 'active' | 'claimed' | 'landing' | 'land-blocked' | 'orphaned' + dev_port: number +} + +type WorktreeLandResponse = { job_id: string; queued: boolean } + +type CatalogModel = { + id: string + provider: string + display_name?: string + supports_tools?: boolean +} + +type ModelOption = { id: string; label: string; provider: string } + +let modelCache: { expiresAt: number; models: ModelOption[] } | null = null +const activeDispatches = new Map>() + +async function listFunctionRows(search?: string): Promise { + const response = await iii.trigger, FunctionList>({ + function_id: 'engine::functions::list', + payload: { include_internal: false, ...(search ? { search } : {}) }, + timeoutMs: 10_000, + }) + return Array.isArray(response?.functions) ? response.functions : [] +} + +async function discoverExecutors(): Promise { + return executorsFromCatalog(await listFunctionRows()) +} + +async function triggerCatalogFunction, O>( + catalog: FunctionCatalogRow[], + functionId: string, + payload: I, + timeoutMs = 15_000, +): Promise { + const row = catalog.find((candidate) => candidate.function_id === functionId) + if (!row) throw new Error(`FUNCTION_UNAVAILABLE: ${functionId}`) + return iii.trigger({ + function_id: functionId, + payload, + timeoutMs, + ...(row.namespace ? { namespace: row.namespace } : {}), + }) +} + +async function discoverModels(catalog: FunctionCatalogRow[]): Promise { + if (modelCache && modelCache.expiresAt > Date.now()) return modelCache.models + if (!catalog.some((row) => row.function_id === 'router::models::list')) return [] + const response = await triggerCatalogFunction, { models?: CatalogModel[] }>( + catalog, + 'router::models::list', + {}, + ) + const models = (response.models ?? []) + .filter((model) => model.supports_tools !== false && model.provider && model.id) + .map((model) => ({ + id: `${model.provider}::${model.id}`, + label: model.display_name?.trim() || model.id, + provider: model.provider, + })) + modelCache = { expiresAt: Date.now() + 30_000, models } + return models +} + +async function stateGet(scope: string, key: string): Promise { + const value = await iii.trigger, T | null>({ + function_id: 'state::get', + payload: { scope, key }, + timeoutMs: 10_000, + }) + return value ?? null +} + +async function stateList(scope: string): Promise { + const value = await iii.trigger, T[] | null>({ + function_id: 'state::list', + payload: { scope }, + timeoutMs: 10_000, + }) + return Array.isArray(value) ? value : [] +} + +async function stateSet(scope: string, key: string, value: T): Promise { + await iii.trigger({ + function_id: 'state::set', + payload: { scope, key, value }, + timeoutMs: 10_000, + }) +} + +type ChangeBinding = { id: string; function_id: string; namespace?: string } +const changeBindings = new Map() + +iii.registerTriggerType>( + { + id: CHANGED_TRIGGER, + description: 'Fires when a Kanban run or task changes. Bind with an empty config.', + }, + { + async registerTrigger({ id, function_id, namespace }) { + changeBindings.set(id, { id, function_id, namespace }) + }, + async unregisterTrigger({ id }) { + changeBindings.delete(id) + }, + }, +) + +async function emitChanged(kind: 'run' | 'task', id: string, runId: string): Promise { + const payload = { kind, id, run_id: runId, updated_at_ms: Date.now() } + await Promise.all( + [...changeBindings.values()].map((binding) => + iii + .trigger({ + function_id: binding.function_id, + payload, + timeoutMs: 10_000, + ...(binding.namespace ? { namespace: binding.namespace } : {}), + }) + .catch((error) => + console.error(`[${WORKER}] change delivery to ${binding.function_id} failed: ${String(error)}`), + ), + ), + ) +} + +async function saveRun(run: RunRecord): Promise { + await stateSet(RUNS_SCOPE, run.id, run) + await emitChanged('run', run.id, run.id) +} + +async function saveTask(task: TaskRecord): Promise { + await stateSet(TASKS_SCOPE, task.id, task) + await emitChanged('task', task.id, task.run_id) +} + +function validRun(value: unknown): value is RunRecord { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false + const run = value as Partial + return typeof run.id === 'string' && typeof run.title === 'string' && Array.isArray(run.task_ids) +} + +function validTask(value: unknown): value is TaskRecord { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false + const task = value as Partial + return ( + typeof task.id === 'string' && + typeof task.run_id === 'string' && + typeof task.title === 'string' && + typeof task.instruction === 'string' && + isTaskStatus(task.status) + ) +} + +async function loadTask(id: string): Promise { + const task = await stateGet(TASKS_SCOPE, id) + if (!validTask(task)) throw new Error(`TASK_NOT_FOUND: ${id}`) + return task +} + +function sessionParent(session: SessionRecord): string | undefined { + const value = session.metadata?.parent_session_id + return typeof value === 'string' && value ? value : undefined +} + +function sessionTitle(session: SessionRecord): string { + const display = session.metadata?.subagent_display + if (display && typeof display === 'object' && !Array.isArray(display)) { + const name = (display as Record).name + if (typeof name === 'string' && name.trim()) return name.trim() + } + return session.title?.trim() || session.session_id +} + +function sessionTaskStatus(status?: string): TaskRecord['status'] { + if (status === 'working') return 'running' + if (status === 'done') return 'done' + if (status === 'error') return 'needs_you' + if (status === 'idle') return 'ready' + return 'queued' +} + +function sessionFilesystemRoot(session: SessionRecord): string | undefined { + const scope = session.metadata?.fs_scope + if (!scope || typeof scope !== 'object' || Array.isArray(scope)) return undefined + const root = (scope as Record).root + return typeof root === 'string' && root ? root : undefined +} + +function rootSession(session: SessionRecord, byId: Map): SessionRecord { + let current = session + const seen = new Set([current.session_id]) + while (true) { + const parentId = sessionParent(current) + if (!parentId || seen.has(parentId)) return current + const parent = byId.get(parentId) + if (!parent) return current + seen.add(parentId) + current = parent + } +} + +async function board() { + const [rawRuns, rawTasks, rawSessions, catalog] = await Promise.all([ + stateList(RUNS_SCOPE), + stateList(TASKS_SCOPE), + iii.trigger, SessionList>({ + function_id: 'session::list', + payload: { limit: 200, order: 'updated_desc' }, + timeoutMs: 10_000, + }), + listFunctionRows(), + ]) + const executors = executorsFromCatalog(catalog) + const models = await discoverModels(catalog) + const runs = rawRuns.filter(validRun) + const tasks = rawTasks.filter(validTask) + const runById = new Map(runs.map((run) => [run.id, run])) + const runViews: RunView[] = runs.map((run) => ({ + ...run, + source: 'kanban', + status: 'ready', + counts: taskCounts([]), + })) + const taskViews: TaskView[] = tasks.map((task) => ({ + ...task, + source: 'kanban', + run_title: runById.get(task.run_id)?.title ?? task.run_id, + })) + + if (catalog.some((row) => row.function_id === 'worktree::status')) { + await Promise.all( + taskViews.map(async (task) => { + if (!task.worktree_id) return + try { + const status = await triggerCatalogFunction, WorktreeStatusResponse>( + catalog, + 'worktree::status', + { worktree_id: task.worktree_id }, + ) + task.git_status = status + task.dev_port = status.dev_port + if (status.lifecycle === 'land-blocked') task.land_status = 'blocked' + } catch {} + }), + ) + } + + const sessions = Array.isArray(rawSessions?.sessions) ? rawSessions.sessions : [] + const sessionById = new Map(sessions.map((session) => [session.session_id, session])) + const ownedExternalSessions = new Set(tasks.map((task) => task.external_session_id).filter(Boolean)) + const ownedRunByRoot = new Map( + runs.filter((run) => run.root_session_id).map((run) => [run.root_session_id as string, run]), + ) + const projectedRunByRoot = new Map() + + for (const session of sessions) { + if (!sessionParent(session) || ownedExternalSessions.has(session.session_id)) continue + const root = rootSession(session, sessionById) + const ownedRun = ownedRunByRoot.get(root.session_id) + const runId = ownedRun?.id ?? `harness:${root.session_id}` + const runTitle = ownedRun?.title ?? sessionTitle(root) + const status = sessionTaskStatus(session.status) + const instruction = + session.status_reason?.trim() || + session.description?.trim() || + `Harness session with ${session.message_count ?? 0} messages` + taskViews.push({ + id: `harness:${session.session_id}`, + run_id: runId, + run_title: runTitle, + title: sessionTitle(session), + instruction, + executor_id: 'harness', + executor_function: 'harness::spawn', + executor_kind: 'harness', + status, + attempt: 1, + external_session_id: session.session_id, + ...(sessionFilesystemRoot(session) ? { cwd: sessionFilesystemRoot(session) } : {}), + ...(status === 'needs_you' ? { error: session.status_reason || 'Harness session failed' } : {}), + created_at_ms: session.created_at ?? Date.now(), + updated_at_ms: session.updated_at ?? session.created_at ?? Date.now(), + source: 'harness', + }) + if (!ownedRun && !projectedRunByRoot.has(root.session_id)) { + const projected: RunView = { + id: runId, + title: runTitle, + ...(root.description?.trim() ? { goal: root.description.trim() } : {}), + root_session_id: root.session_id, + task_ids: [], + created_at_ms: root.created_at ?? Date.now(), + updated_at_ms: root.updated_at ?? root.created_at ?? Date.now(), + source: 'harness', + status: 'ready', + counts: taskCounts([]), + } + projectedRunByRoot.set(root.session_id, projected) + runViews.push(projected) + } + } + + for (const run of runViews) { + const children = taskViews.filter((task) => task.run_id === run.id) + run.task_ids = children.map((task) => task.id) + run.status = deriveRunStatus(children) + run.counts = taskCounts(children) + run.updated_at_ms = Math.max(run.updated_at_ms, ...children.map((task) => task.updated_at_ms)) + } + runViews.sort((a, b) => b.updated_at_ms - a.updated_at_ms) + taskViews.sort((a, b) => b.updated_at_ms - a.updated_at_ms) + return { + runs: runViews, + tasks: taskViews, + executors, + models, + capabilities: runtimeCapabilities(catalog, executors), + updated_at_ms: Date.now(), + } +} + +function dispatchTask(taskId: string, retry = false): Promise { + return singleFlight(activeDispatches, taskId, () => dispatchTaskOnce(taskId, retry)) +} + +async function dispatchTaskOnce(taskId: string, retry = false): Promise { + const task = await loadTask(taskId) + if (!['ready', 'queued', 'needs_you'].includes(task.status)) { + throw new Error(`TASK_NOT_DISPATCHABLE: ${task.id} is ${task.status}`) + } + const executor = (await discoverExecutors()).find((candidate) => candidate.id === task.executor_id) + if (!executor) throw new Error(`EXECUTOR_UNAVAILABLE: ${task.executor_id}`) + const run = await stateGet(RUNS_SCOPE, task.run_id) + const parentSessionId = validRun(run) ? run.root_session_id : undefined + const runTasks = (await stateList(TASKS_SCOPE)).filter( + (candidate): candidate is TaskRecord => validTask(candidate) && candidate.run_id === task.run_id, + ) + if (!dependenciesSatisfied(task, runTasks)) { + throw new Error(`TASK_DEPENDENCIES_PENDING: ${task.id}`) + } + + const sessionId = (retry && !task.worktree_id) || !task.external_session_id ? randomUUID() : task.external_session_id + let next: TaskRecord = { + ...task, + status: 'running', + executor_function: executor.function_id, + executor_kind: executor.kind, + external_session_id: sessionId, + external_turn_id: undefined, + result: undefined, + error: undefined, + attempt: retry ? task.attempt + 1 : task.attempt, + updated_at_ms: Date.now(), + } + await saveTask(next) + + try { + if (executor.kind === 'harness') { + const response = await iii.trigger< + Record, + { child_session_id?: string; child_turn_id?: string } + >({ + function_id: executor.function_id, + payload: { + task: task.instruction, + session_id: sessionId, + ...(parentSessionId ? { parent_session_id: parentSessionId } : {}), + display: { name: task.title, icon: 'code', color: 'blue' }, + ...(task.worktree_path ? { options: { filesystem_root: task.worktree_path } } : {}), + ...(task.model ? { model: task.model } : {}), + ...(task.profile ? { agent: task.profile } : {}), + }, + timeoutMs: 15_000, + ...(executor.namespace ? { namespace: executor.namespace } : {}), + }) + next = { + ...next, + external_session_id: response?.child_session_id ?? sessionId, + ...(response?.child_turn_id ? { external_turn_id: response.child_turn_id } : {}), + updated_at_ms: Date.now(), + } + } else { + const response = await iii.trigger< + Record, + { session_id?: string; started?: boolean; reason?: string } + >({ + function_id: executor.function_id, + payload: { + task: task.instruction, + session_id: sessionId, + ...(parentSessionId ? { parent_session_id: parentSessionId } : {}), + ...(task.worktree_path || task.cwd ? { cwd: task.worktree_path ?? task.cwd } : {}), + ...(task.model ? { model: task.model } : {}), + }, + timeoutMs: 15_000, + ...(executor.namespace ? { namespace: executor.namespace } : {}), + }) + if (response?.started === false) throw new Error(response.reason || 'executor did not start the task') + next = { + ...next, + external_session_id: response?.session_id ?? sessionId, + updated_at_ms: Date.now(), + } + } + await saveTask(next) + return next + } catch (error) { + next = { + ...next, + status: 'needs_you', + error: error instanceof Error ? error.message : String(error), + updated_at_ms: Date.now(), + } + await saveTask(next) + return next + } +} + +async function releaseDependents(runId: string): Promise { + const tasks = (await stateList(TASKS_SCOPE)).filter( + (task): task is TaskRecord => validTask(task) && task.run_id === runId, + ) + const eligible = tasks.filter((task) => task.status === 'queued' && dependenciesSatisfied(task, tasks)) + await Promise.all(eligible.map((task) => dispatchTask(task.id))) +} + +async function prepareTaskWorktree( + task: TaskRecord, + run: RunRecord, + catalog: FunctionCatalogRow[], +): Promise { + if (run.isolation !== 'worktree_per_task') return task + if (!run.repo_path) throw new Error('RUN_REPOSITORY_MISSING') + try { + const created = await triggerCatalogFunction, WorktreeCreateResponse>( + catalog, + 'worktree::create', + { + repo_path: run.repo_path, + ...(run.base_ref ? { base_ref: run.base_ref } : {}), + ...(task.external_session_id ? { session_id: task.external_session_id } : {}), + }, + 30_000, + ) + return { + ...task, + cwd: created.path, + worktree_id: created.worktree_id, + worktree_path: created.path, + branch: created.branch, + base_ref: created.base_ref, + base_sha: created.base_sha, + dev_port: created.dev_port, + updated_at_ms: Date.now(), + } + } catch (error) { + return { + ...task, + status: 'needs_you', + error: `Worktree setup failed: ${error instanceof Error ? error.message : String(error)}`, + updated_at_ms: Date.now(), + } + } +} + +iii.registerFunction( + TASK_OUTCOME_FN, + async (event: { scope?: string; key?: string; new_value?: unknown }) => { + if (event.scope !== AGENT_TASKS_SCOPE || !event.key || !event.new_value || typeof event.new_value !== 'object') { + return { updated: false } + } + const outcome = event.new_value as Record + const sessionId = typeof outcome.session_id === 'string' ? outcome.session_id : event.key + const tasks = (await stateList(TASKS_SCOPE)).filter(validTask) + const task = tasks.find((candidate) => candidate.external_session_id === sessionId) + if (!task || task.status === 'done') return { updated: false } + const ok = outcome.status === 'done' + const next: TaskRecord = { + ...task, + status: ok ? 'review' : 'needs_you', + ...(ok ? { result: outcome.result } : { error: resultText(outcome.error) ?? 'Task failed' }), + updated_at_ms: typeof outcome.updated_at_ms === 'number' ? outcome.updated_at_ms : Date.now(), + } + await saveTask(next) + if (ok) await releaseDependents(next.run_id) + return { updated: true, task_id: next.id, status: next.status } + }, + { + description: 'Internal completion sink for task-contract executors.', + metadata: { internal: true }, + request_format: object(), + response_format: object({ updated: { type: 'boolean' }, task_id: string, status: string }, ['updated']), + }, +) + +iii.registerFunction( + HARNESS_COMPLETED_FN, + async (event: Record) => { + if (event.terminal !== true || typeof event.session_id !== 'string') return { updated: false } + const tasks = (await stateList(TASKS_SCOPE)).filter(validTask) + const task = tasks.find( + (candidate) => candidate.executor_kind === 'harness' && candidate.external_session_id === event.session_id, + ) + if (!task || task.status === 'done') return { updated: false } + const ok = event.status === 'completed' + const next: TaskRecord = { + ...task, + status: ok ? 'review' : 'needs_you', + ...(typeof event.turn_id === 'string' ? { external_turn_id: event.turn_id } : {}), + ...(ok + ? { result: event.result } + : { error: resultText(event.result_error ?? event.reason) ?? `Harness task ${String(event.status)}` }), + updated_at_ms: typeof event.timestamp === 'number' ? event.timestamp : Date.now(), + } + await saveTask(next) + if (ok) await releaseDependents(next.run_id) + return { updated: true, task_id: next.id, status: next.status } + }, + { + description: 'Internal completion sink for Harness tasks.', + metadata: { internal: true }, + request_format: object(), + response_format: object({ updated: { type: 'boolean' }, task_id: string, status: string }, ['updated']), + }, +) + +iii.registerFunction( + SESSION_CHANGED_FN, + async (event: Record) => { + const sessionId = typeof event.session_id === 'string' ? event.session_id : 'sessions' + const parentSessionId = typeof event.parent_session_id === 'string' ? event.parent_session_id : sessionId + await emitChanged('run', sessionId, parentSessionId) + return { updated: true } + }, + { + description: 'Internal refresh sink for Harness session-tree changes.', + metadata: { internal: true }, + request_format: object(), + response_format: object({ updated: { type: 'boolean' } }, ['updated']), + }, +) + +iii.registerFunction( + WORKTREE_CHANGED_FN, + async (event: Record) => { + if (typeof event.worktree_id !== 'string') return { updated: false } + const tasks = (await stateList(TASKS_SCOPE)).filter(validTask) + const task = tasks.find((candidate) => candidate.worktree_id === event.worktree_id) + if (!task) return { updated: false } + const next: TaskRecord = { + ...task, + ...(typeof event.merged_sha === 'string' ? { land_status: 'landed' as const } : {}), + ...(typeof event.reason === 'string' + ? { land_status: 'blocked' as const, error: `Landing blocked: ${event.reason}` } + : {}), + updated_at_ms: typeof event.timestamp === 'number' ? event.timestamp : Date.now(), + } + await saveTask(next) + return { updated: true, task_id: next.id, land_status: next.land_status } + }, + { + description: 'Internal refresh sink for managed worktree landing outcomes.', + metadata: { internal: true }, + request_format: object(), + response_format: object({ updated: { type: 'boolean' }, task_id: string, land_status: string }, ['updated']), + }, +) + +iii.registerTrigger({ type: 'state', function_id: TASK_OUTCOME_FN, config: { scope: AGENT_TASKS_SCOPE } }) +iii.registerTrigger({ type: 'harness::turn-completed', function_id: HARNESS_COMPLETED_FN, config: {} }) +iii.registerTrigger({ type: 'session::created', function_id: SESSION_CHANGED_FN, config: {} }) +iii.registerTrigger({ type: 'session::status-changed', function_id: SESSION_CHANGED_FN, config: {} }) +iii.registerTrigger({ type: 'session::meta-updated', function_id: SESSION_CHANGED_FN, config: {} }) +iii.registerTrigger({ type: 'worktree::landed', function_id: WORKTREE_CHANGED_FN, config: {} }) +iii.registerTrigger({ type: 'worktree::land-blocked', function_id: WORKTREE_CHANGED_FN, config: {} }) + +iii.registerFunction('kanban::executors::list', () => discoverExecutors(), { + description: 'List live executors that satisfy the Kanban task contract, including Harness when available.', + request_format: object(), + response_format: { type: 'array', items: object() }, +}) + +iii.registerFunction('kanban::models::list', async () => discoverModels(await listFunctionRows()), { + description: 'List live tool-capable models usable by top-level Harness child tasks.', + request_format: object(), + response_format: { type: 'array', items: object() }, +}) + +iii.registerFunction('kanban::board', () => board(), { + description: 'Project Harness session trees, executor topology, managed worktrees, and Git status into durable runs.', + request_format: object(), + response_format: object( + { + runs: { type: 'array', items: object() }, + tasks: { type: 'array', items: object() }, + executors: { type: 'array', items: object() }, + models: { type: 'array', items: object() }, + capabilities: object(), + }, + ['runs', 'tasks', 'executors', 'models', 'capabilities'], + ), +}) + +iii.registerFunction( + 'kanban::runs::create', + async (input: unknown) => { + const parsed = validateCreateRun(input) + const catalog = await listFunctionRows() + const executors = executorsFromCatalog(catalog) + const executorById = new Map(executors.map((executor) => [executor.id, executor])) + for (const task of parsed.tasks) { + const executor = executorById.get(task.executor) + if (!executor) throw new Error(`EXECUTOR_UNAVAILABLE: ${task.executor}`) + if (executor.kind === 'harness' && !task.model) { + throw new Error(`HARNESS_MODEL_REQUIRED: task ${task.key} must name a live model`) + } + } + if (parsed.isolation === 'worktree_per_task' && !runtimeCapabilities(catalog, executors).worktree) { + throw new Error('WORKTREE_UNAVAILABLE: start the worktree Worker before creating an isolated run') + } + + const now = Date.now() + const runId = randomUUID() + const taskIds = parsed.tasks.map(() => randomUUID()) + const taskIdByKey = new Map(parsed.tasks.map((task, index) => [task.key, taskIds[index]])) + const tasks: TaskRecord[] = parsed.tasks.map((inputTask, index) => { + const executor = executorById.get(inputTask.executor) as Executor + const instruction = inputTask.instruction.trim() + return { + id: taskIds[index], + key: inputTask.key, + run_id: runId, + title: inputTask.title?.trim() || instruction.split('\n')[0].slice(0, 72) || `Task ${index + 1}`, + instruction, + executor_id: executor.id, + executor_function: executor.function_id, + executor_kind: executor.kind, + cwd: inputTask.cwd ?? parsed.cwd, + depends_on: inputTask.depends_on.map((key) => taskIdByKey.get(key) as string), + model: inputTask.model, + profile: inputTask.profile, + status: parsed.auto_dispatch === false ? 'ready' : 'queued', + attempt: 1, + external_session_id: randomUUID(), + created_at_ms: now, + updated_at_ms: now, + } + }) + const runTitle = parsed.title?.trim() || tasks[0].title + const rootSession = await iii.trigger, { session_id: string }>({ + function_id: 'session::create', + payload: { + title: runTitle, + description: parsed.goal ?? `${tasks.length} coordinated task${tasks.length === 1 ? '' : 's'}`, + metadata: { + surface: 'kanban', + mode: 'orchestration', + kanban_run_id: runId, + isolation: parsed.isolation, + ...(parsed.repo_path ? { repo_path: parsed.repo_path } : {}), + }, + }, + timeoutMs: 10_000, + }) + const run: RunRecord = { + id: runId, + title: runTitle, + goal: parsed.goal, + cwd: parsed.cwd, + repo_path: parsed.repo_path, + base_ref: parsed.base_ref, + target_branch: parsed.target_branch, + isolation: parsed.isolation, + root_session_id: rootSession.session_id, + task_ids: tasks.map((task) => task.id), + created_at_ms: now, + updated_at_ms: now, + } + const prepared = await Promise.all(tasks.map((task) => prepareTaskWorktree(task, run, catalog))) + await Promise.all([saveRun(run), ...prepared.map(saveTask)]) + if (parsed.auto_dispatch !== false) { + await Promise.all( + prepared + .filter((task) => task.status === 'queued' && !task.depends_on?.length) + .map((task) => dispatchTask(task.id)), + ) + } + return { run, tasks: (await stateList(TASKS_SCOPE)).filter((task) => task.run_id === runId) } + }, + { + description: 'Create a durable run with one or more tasks and optionally dispatch them immediately.', + request_format: object( + { + title: nullableString, + goal: nullableString, + cwd: nullableString, + repo_path: nullableString, + base_ref: nullableString, + target_branch: nullableString, + isolation: { type: 'string', enum: ['shared', 'worktree_per_task'] }, + auto_dispatch: { type: 'boolean' }, + tasks: { + type: 'array', + minItems: 1, + maxItems: 24, + items: object( + { + title: nullableString, + instruction: string, + executor: string, + key: nullableString, + depends_on: { type: 'array', items: string }, + cwd: nullableString, + model: nullableString, + profile: nullableString, + }, + ['instruction', 'executor'], + ), + }, + }, + ['tasks'], + ), + response_format: object({ run: object(), tasks: { type: 'array', items: object() } }, ['run', 'tasks']), + }, +) + +iii.registerFunction('kanban::tasks::dispatch', (input: { task_id: string }) => dispatchTask(input.task_id), { + description: 'Dispatch one ready or attention-blocked task to its live executor.', + request_format: object({ task_id: string }, ['task_id']), + response_format: object(), +}) + +iii.registerFunction( + 'kanban::tasks::retry', + async (input: { task_id: string; model?: string; profile?: string }) => { + const task = await loadTask(input.task_id) + if (input.model?.trim() || input.profile?.trim()) { + await saveTask({ + ...task, + ...(input.model?.trim() ? { model: input.model.trim() } : {}), + ...(input.profile?.trim() ? { profile: input.profile.trim() } : {}), + updated_at_ms: Date.now(), + }) + } + return dispatchTask(input.task_id, true) + }, + { + description: 'Retry an attention-blocked task, preserving the claimed session for managed worktrees.', + request_format: object({ task_id: string, model: nullableString, profile: nullableString }, ['task_id']), + response_format: object(), + }, +) + +iii.registerFunction( + 'kanban::tasks::stop', + async (input: { task_id: string }) => { + const task = await loadTask(input.task_id) + if (task.status !== 'running') throw new Error(`TASK_NOT_RUNNING: ${task.id}`) + if (!task.external_session_id) throw new Error(`TASK_SESSION_MISSING: ${task.id}`) + const executor = (await discoverExecutors()).find((candidate) => candidate.id === task.executor_id) + if (!executor?.stop_function) throw new Error(`STOP_UNSUPPORTED: ${task.executor_id}`) + await iii.trigger({ + function_id: executor.stop_function, + payload: { session_id: task.external_session_id }, + timeoutMs: 15_000, + ...(executor.namespace ? { namespace: executor.namespace } : {}), + }) + const next: TaskRecord = { + ...task, + status: 'needs_you', + error: 'Stopped by operator', + updated_at_ms: Date.now(), + } + await saveTask(next) + return next + }, + { + description: 'Stop a running task when its executor exposes a stop function.', + request_format: object({ task_id: string }, ['task_id']), + response_format: object(), + }, +) + +iii.registerFunction( + 'kanban::tasks::accept', + async (input: { task_id: string }) => { + const task = await loadTask(input.task_id) + if (task.status !== 'review') throw new Error(`TASK_NOT_IN_REVIEW: ${task.id}`) + const next: TaskRecord = { ...task, status: 'done', updated_at_ms: Date.now() } + await saveTask(next) + return next + }, + { + description: 'Accept a reviewed task and move it to Done.', + request_format: object({ task_id: string }, ['task_id']), + response_format: object(), + }, +) + +iii.registerFunction( + 'kanban::tasks::land', + async (input: { task_id: string; target_branch?: string; test_cmd?: string; keep?: boolean }) => { + const task = await loadTask(input.task_id) + if (!task.worktree_id) throw new Error(`TASK_WORKTREE_MISSING: ${task.id}`) + if (!['review', 'done'].includes(task.status)) throw new Error(`TASK_NOT_REVIEWED: ${task.id}`) + const run = await stateGet(RUNS_SCOPE, task.run_id) + if (!validRun(run)) throw new Error(`RUN_NOT_FOUND: ${task.run_id}`) + const targetBranch = input.target_branch?.trim() || run.target_branch + if (!targetBranch) throw new Error('TARGET_BRANCH_REQUIRED') + const catalog = await listFunctionRows() + const landed = await triggerCatalogFunction, WorktreeLandResponse>( + catalog, + 'worktree::land', + { + worktree_id: task.worktree_id, + target_branch: targetBranch, + ...(input.test_cmd?.trim() ? { test_cmd: input.test_cmd.trim() } : {}), + keep: input.keep === true, + }, + 30_000, + ) + const next: TaskRecord = { + ...task, + land_job_id: landed.job_id, + land_status: 'landing', + updated_at_ms: Date.now(), + } + await saveTask(next) + return next + }, + { + description: 'Queue reviewed work for tested, serialized landing onto the run target branch.', + request_format: object( + { + task_id: string, + target_branch: nullableString, + test_cmd: nullableString, + keep: { type: 'boolean' }, + }, + ['task_id'], + ), + response_format: object(), + }, +) + +type UiAsset = { file: string; type: 'console:script' | 'console:style'; content_type: string; content: string } + +const uiAssets: Record = { + 'kanban/page.js': { file: 'page.js', type: 'console:script', content_type: 'text/javascript', content: uiPage }, + 'kanban/styles.css': { file: 'styles.css', type: 'console:style', content_type: 'text/css', content: uiStyles }, +} +const uiWatch = process.env.III_KANBAN_UI_WATCH +const uiWatchEnabled = Boolean(uiWatch) +const uiWatchDir = + uiWatchEnabled && uiWatch !== '1' && uiWatch !== 'true' + ? (uiWatch as string) + : join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'ui', 'dist') + +async function uiContent(path: string) { + const asset = uiAssets[path] + if (!asset) throw new Error(`unknown ui asset: ${path}`) + const content = uiWatchEnabled ? await readFile(join(uiWatchDir, asset.file), 'utf8') : asset.content + return { content, content_type: asset.content_type } +} + +iii.registerFunction('kanban::ui-content', (input: { path: string }) => uiContent(input.path), { + description: 'Serve the injectable Kanban Console page assets.', + metadata: { internal: true }, + request_format: object({ path: string }, ['path']), + response_format: object({ content: string, content_type: string }, ['content', 'content_type']), +}) + +function registerUiAsset(path: string) { + return iii.registerTrigger({ type: uiAssets[path].type, function_id: 'kanban::ui-content', config: { path } }) +} + +const uiTriggers = new Map(Object.keys(uiAssets).map((path) => [path, registerUiAsset(path)])) + +if (uiWatchEnabled) { + const pending = new Map() + watch(uiWatchDir, (_event, file) => { + const path = Object.keys(uiAssets).find((key) => uiAssets[key].file === file) + if (!path) return + clearTimeout(pending.get(path)) + pending.set( + path, + setTimeout(() => { + const previous = uiTriggers.get(path) + uiTriggers.set(path, registerUiAsset(path)) + previous?.unregister() + console.error(`[${WORKER}] reloaded ui asset ${path}`) + }, 150), + ) + }) + console.error(`[${WORKER}] serving ui assets from ${uiWatchDir}`) +} + +console.log(`${WORKER} worker connected`) + +const shutdown = async () => { + await iii.shutdown() + process.exit(0) +} +process.on('SIGINT', shutdown) +process.on('SIGTERM', shutdown) diff --git a/kanban/src/model.ts b/kanban/src/model.ts new file mode 100644 index 000000000..ea606475f --- /dev/null +++ b/kanban/src/model.ts @@ -0,0 +1,317 @@ +export const TASK_STATUSES = ['needs_you', 'queued', 'running', 'review', 'ready', 'done'] as const + +export type TaskStatus = (typeof TASK_STATUSES)[number] +export type RunStatus = 'needs_you' | 'active' | 'review' | 'ready' | 'done' + +export type ExecutorKind = 'task' | 'harness' +export type IsolationMode = 'shared' | 'worktree_per_task' + +export interface GitStatus { + clean: boolean + ahead: number + behind: number + staged: number + unstaged: number + untracked: number + conflicted: number + diffstat: string + unpushed: number + in_rebase: boolean + head_sha: string + integrated?: boolean + integration_reason?: string +} + +export interface Executor { + id: string + label: string + kind: ExecutorKind + function_id: string + worker_name: string + namespace?: string + stop_function?: string + status_function?: string + available: boolean +} + +export interface FunctionCatalogRow { + function_id?: string + worker_name?: string + namespace?: string + description?: string | null +} + +export interface RuntimeCapabilities { + harness: boolean + worktree: boolean + external_executors: number +} + +export interface RunRecord { + id: string + title: string + goal?: string + cwd?: string + repo_path?: string + base_ref?: string + target_branch?: string + isolation?: IsolationMode + root_session_id?: string + task_ids: string[] + created_at_ms: number + updated_at_ms: number +} + +export interface TaskRecord { + id: string + key?: string + run_id: string + title: string + instruction: string + executor_id: string + executor_function: string + executor_kind: ExecutorKind + cwd?: string + depends_on?: string[] + model?: string + profile?: string + worktree_id?: string + worktree_path?: string + branch?: string + base_ref?: string + base_sha?: string + dev_port?: number + land_job_id?: string + land_status?: 'landing' | 'landed' | 'blocked' + status: TaskStatus + attempt: number + external_session_id?: string + external_turn_id?: string + result?: unknown + error?: string + created_at_ms: number + updated_at_ms: number +} + +export interface CreateTaskInput { + key: string + title?: string + instruction: string + executor: string + depends_on: string[] + cwd?: string + model?: string + profile?: string +} + +export interface CreateRunInput { + title?: string + goal?: string + cwd?: string + repo_path?: string + base_ref?: string + target_branch?: string + isolation: IsolationMode + auto_dispatch?: boolean + tasks: CreateTaskInput[] +} + +export interface RunView extends RunRecord { + source: 'kanban' | 'harness' + status: RunStatus + counts: Record +} + +export interface TaskView extends TaskRecord { + source: 'kanban' | 'harness' + run_title: string + git_status?: GitStatus +} + +export function isTaskStatus(value: unknown): value is TaskStatus { + return typeof value === 'string' && (TASK_STATUSES as readonly string[]).includes(value) +} + +export function taskCounts(tasks: TaskRecord[]): Record { + const counts = Object.fromEntries(TASK_STATUSES.map((status) => [status, 0])) as Record + for (const task of tasks) counts[task.status] += 1 + return counts +} + +export function deriveRunStatus(tasks: TaskRecord[]): RunStatus { + if (tasks.some((task) => task.status === 'needs_you')) return 'needs_you' + if (tasks.some((task) => task.status === 'queued' || task.status === 'running')) return 'active' + if (tasks.some((task) => task.status === 'review')) return 'review' + if (tasks.some((task) => task.status === 'ready')) return 'ready' + return 'done' +} + +export function dependenciesSatisfied(task: TaskRecord, tasks: TaskRecord[]): boolean { + if (!task.depends_on?.length) return true + const byId = new Map(tasks.map((candidate) => [candidate.id, candidate])) + return task.depends_on.every((id) => { + const dependency = byId.get(id) + return dependency?.status === 'review' || dependency?.status === 'done' + }) +} + +export function singleFlight(inFlight: Map>, key: K, work: () => Promise): Promise { + const existing = inFlight.get(key) + if (existing) return existing + const pending = Promise.resolve() + .then(work) + .finally(() => { + if (inFlight.get(key) === pending) inFlight.delete(key) + }) + inFlight.set(key, pending) + return pending +} + +export function validateCreateRun(input: unknown): CreateRunInput { + if (!input || typeof input !== 'object' || Array.isArray(input)) { + throw new Error('INVALID_RUN: expected an object') + } + const raw = input as Record + if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) { + throw new Error('INVALID_RUN: tasks must contain at least one task') + } + if (raw.tasks.length > 24) throw new Error('INVALID_RUN: a run supports at most 24 tasks') + const tasks = raw.tasks.map((value, index): CreateTaskInput => { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`INVALID_TASK: tasks[${index}] must be an object`) + } + const task = value as Record + const instruction = text(task.instruction) + const executor = text(task.executor) + if (!instruction) throw new Error(`INVALID_TASK: tasks[${index}].instruction is required`) + if (!executor) throw new Error(`INVALID_TASK: tasks[${index}].executor is required`) + return { + key: text(task.key) ?? `task-${index + 1}`, + instruction, + executor, + depends_on: Array.isArray(task.depends_on) + ? [...new Set(task.depends_on.map(text).filter((key): key is string => Boolean(key)))] + : [], + ...(text(task.title) ? { title: text(task.title) } : {}), + ...(text(task.cwd) ? { cwd: text(task.cwd) } : {}), + ...(text(task.model) ? { model: text(task.model) } : {}), + ...(text(task.profile) ? { profile: text(task.profile) } : {}), + } + }) + const keys = new Set() + for (const [index, task] of tasks.entries()) { + if (keys.has(task.key)) throw new Error(`INVALID_TASK: tasks[${index}].key must be unique`) + keys.add(task.key) + } + for (const [index, task] of tasks.entries()) { + if (task.depends_on.includes(task.key)) { + throw new Error(`INVALID_TASK: tasks[${index}] cannot depend on itself`) + } + const missing = task.depends_on.find((key) => !keys.has(key)) + if (missing) throw new Error(`INVALID_TASK: tasks[${index}].depends_on references unknown key ${missing}`) + } + const taskByKey = new Map(tasks.map((task) => [task.key, task])) + const visiting = new Set() + const visited = new Set() + const visit = (key: string) => { + if (visiting.has(key)) throw new Error(`INVALID_TASK: dependency cycle includes ${key}`) + if (visited.has(key)) return + visiting.add(key) + for (const dependency of taskByKey.get(key)?.depends_on ?? []) visit(dependency) + visiting.delete(key) + visited.add(key) + } + for (const task of tasks) visit(task.key) + const isolation: IsolationMode = raw.isolation === 'worktree_per_task' ? 'worktree_per_task' : 'shared' + const repoPath = text(raw.repo_path) ?? text(raw.cwd) + if (isolation === 'worktree_per_task' && !repoPath) { + throw new Error('INVALID_RUN: repo_path is required for per-task worktrees') + } + return { + tasks, + isolation, + auto_dispatch: raw.auto_dispatch !== false, + ...(text(raw.title) ? { title: text(raw.title) } : {}), + ...(text(raw.goal) ? { goal: text(raw.goal) } : {}), + ...(text(raw.cwd) ? { cwd: text(raw.cwd) } : {}), + ...(repoPath ? { repo_path: repoPath } : {}), + ...(text(raw.base_ref) ? { base_ref: text(raw.base_ref) } : {}), + ...(text(raw.target_branch) ? { target_branch: text(raw.target_branch) } : {}), + } +} + +function text(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + return trimmed || undefined +} + +export function executorLabel(functionId: string, workerName: string): string { + const base = workerName || functionId.split('::')[0] || functionId + return base + .split(/[-_]/g) + .filter(Boolean) + .map((part) => part[0]?.toUpperCase() + part.slice(1)) + .join(' ') +} + +function catalogHas(catalog: FunctionCatalogRow[], functionId: string, namespace?: string): boolean { + return catalog.some((row) => row.function_id === functionId && row.namespace === namespace) +} + +export function executorsFromCatalog(catalog: FunctionCatalogRow[]): Executor[] { + const executors: Executor[] = [] + + for (const row of catalog) { + const functionId = row.function_id + if (!functionId?.endsWith('::task')) continue + if (!row.description?.includes('agent_tasks')) continue + const prefix = functionId.slice(0, -'::task'.length) + executors.push({ + id: prefix, + label: executorLabel(functionId, row.worker_name ?? prefix), + kind: 'task', + function_id: functionId, + worker_name: row.worker_name ?? prefix, + ...(row.namespace ? { namespace: row.namespace } : {}), + ...(catalogHas(catalog, `${prefix}::stop`, row.namespace) ? { stop_function: `${prefix}::stop` } : {}), + ...(catalogHas(catalog, `${prefix}::status`, row.namespace) ? { status_function: `${prefix}::status` } : {}), + available: true, + }) + } + + for (const harness of catalog.filter((row) => row.function_id === 'harness::spawn')) { + executors.push({ + id: 'harness', + label: 'Harness', + kind: 'harness', + function_id: 'harness::spawn', + worker_name: harness.worker_name ?? 'harness', + ...(harness.namespace ? { namespace: harness.namespace } : {}), + ...(catalogHas(catalog, 'harness::stop', harness.namespace) ? { stop_function: 'harness::stop' } : {}), + ...(catalogHas(catalog, 'harness::status', harness.namespace) ? { status_function: 'harness::status' } : {}), + available: true, + }) + } + + return executors.sort((a, b) => a.label.localeCompare(b.label)) +} + +export function runtimeCapabilities(catalog: FunctionCatalogRow[], executors: Executor[]): RuntimeCapabilities { + const ids = new Set(catalog.map((row) => row.function_id)) + return { + harness: ids.has('harness::spawn'), + worktree: ids.has('worktree::create') && ids.has('worktree::status') && ids.has('worktree::land'), + external_executors: executors.filter((executor) => executor.kind === 'task').length, + } +} + +export function resultText(value: unknown): string | undefined { + if (value === undefined || value === null) return undefined + if (typeof value === 'string') return value + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } +} diff --git a/kanban/src/virtual-ui.d.ts b/kanban/src/virtual-ui.d.ts new file mode 100644 index 000000000..48b79e9d2 --- /dev/null +++ b/kanban/src/virtual-ui.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:kanban-ui-assets' { + export const uiPage: string + export const uiStyles: string +} diff --git a/kanban/tests/model.test.ts b/kanban/tests/model.test.ts new file mode 100644 index 000000000..e00312b74 --- /dev/null +++ b/kanban/tests/model.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it } from 'vitest' +import { + dependenciesSatisfied, + deriveRunStatus, + executorLabel, + executorsFromCatalog, + runtimeCapabilities, + singleFlight, + type TaskRecord, + taskCounts, + validateCreateRun, +} from '../src/model.js' + +function task(status: TaskRecord['status']): TaskRecord { + return { + id: status, + run_id: 'run', + title: status, + instruction: status, + executor_id: 'worker', + executor_function: 'worker::task', + executor_kind: 'task', + status, + attempt: 1, + created_at_ms: 1, + updated_at_ms: 1, + } +} + +describe('deriveRunStatus', () => { + it('keeps attention and active work ahead of review', () => { + expect(deriveRunStatus([task('review'), task('needs_you')])).toBe('needs_you') + expect(deriveRunStatus([task('review'), task('running')])).toBe('active') + }) + + it('marks a fully accepted run done', () => { + expect(deriveRunStatus([task('done'), task('done')])).toBe('done') + }) +}) + +describe('taskCounts', () => { + it('returns every board column including empty columns', () => { + expect(taskCounts([task('running'), task('running')])).toEqual({ + needs_you: 0, + queued: 0, + running: 2, + review: 0, + ready: 0, + done: 0, + }) + }) +}) + +describe('validateCreateRun', () => { + it('normalizes text and defaults auto dispatch', () => { + expect( + validateCreateRun({ title: ' Release ', tasks: [{ instruction: ' inspect ', executor: ' worker ' }] }), + ).toEqual({ + title: 'Release', + isolation: 'shared', + auto_dispatch: true, + tasks: [{ key: 'task-1', instruction: 'inspect', executor: 'worker', depends_on: [] }], + }) + }) + + it('rejects empty task lists', () => { + expect(() => validateCreateRun({ tasks: [] })).toThrow('tasks must contain at least one task') + }) + + it('validates a per-task worktree dependency graph', () => { + expect( + validateCreateRun({ + repo_path: '/repo', + isolation: 'worktree_per_task', + tasks: [ + { key: 'research', instruction: 'inspect', executor: 'harness' }, + { key: 'build', instruction: 'implement', executor: 'pi', depends_on: ['research'] }, + ], + }), + ).toMatchObject({ + repo_path: '/repo', + isolation: 'worktree_per_task', + tasks: [ + { key: 'research', depends_on: [] }, + { key: 'build', depends_on: ['research'] }, + ], + }) + }) + + it('requires a repository for isolated work', () => { + expect(() => + validateCreateRun({ + isolation: 'worktree_per_task', + tasks: [{ instruction: 'inspect', executor: 'harness' }], + }), + ).toThrow('repo_path is required') + }) + + it('rejects dependency cycles before any work is dispatched', () => { + expect(() => + validateCreateRun({ + tasks: [ + { key: 'a', instruction: 'first', executor: 'harness', depends_on: ['b'] }, + { key: 'b', instruction: 'second', executor: 'harness', depends_on: ['a'] }, + ], + }), + ).toThrow('dependency cycle') + }) +}) + +describe('dependenciesSatisfied', () => { + it('releases a dependent after its prerequisite reaches review', () => { + const first = { ...task('review'), id: 'first' } + const second = { ...task('queued'), id: 'second', depends_on: ['first'] } + expect(dependenciesSatisfied(second, [first, second])).toBe(true) + expect(dependenciesSatisfied(second, [{ ...first, status: 'running' }, second])).toBe(false) + }) +}) + +describe('singleFlight', () => { + it('coalesces concurrent work for the same task and releases the key afterward', async () => { + const inFlight = new Map>() + let calls = 0 + const work = async () => { + calls += 1 + await Promise.resolve() + return `run-${calls}` + } + + const first = singleFlight(inFlight, 'task-1', work) + const second = singleFlight(inFlight, 'task-1', work) + expect(second).toBe(first) + await expect(Promise.all([first, second])).resolves.toEqual(['run-1', 'run-1']) + await expect(singleFlight(inFlight, 'task-1', work)).resolves.toBe('run-2') + }) +}) + +it('turns worker ids into readable labels', () => { + expect(executorLabel('code-runner::task', 'code-runner')).toBe('Code Runner') +}) + +describe('executor discovery', () => { + it('discovers task-contract workers and preserves their namespace', () => { + expect( + executorsFromCatalog([ + { + function_id: 'claude-code::task', + worker_name: 'claude-code', + namespace: 'agents', + description: 'Writes completion records to agent_tasks state.', + }, + { function_id: 'claude-code::stop', namespace: 'agents' }, + { function_id: 'claude-code::status', namespace: 'other' }, + ]), + ).toEqual([ + expect.objectContaining({ + id: 'claude-code', + namespace: 'agents', + stop_function: 'claude-code::stop', + }), + ]) + }) + + it('only advertises Harness controls registered in the same namespace', () => { + expect( + executorsFromCatalog([ + { function_id: 'harness::spawn', worker_name: 'harness', namespace: 'runtime' }, + { function_id: 'harness::stop', namespace: 'other' }, + ]), + ).toEqual([ + expect.not.objectContaining({ + stop_function: 'harness::stop', + }), + ]) + }) + + it('reports the live runtime capabilities used by the board', () => { + const catalog = [ + { function_id: 'harness::spawn' }, + { function_id: 'worktree::create' }, + { function_id: 'worktree::status' }, + { function_id: 'worktree::land' }, + { + function_id: 'pi::task', + worker_name: 'pi', + description: 'Writes completion records to agent_tasks state.', + }, + ] + expect(runtimeCapabilities(catalog, executorsFromCatalog(catalog))).toEqual({ + harness: true, + worktree: true, + external_executors: 1, + }) + }) +}) diff --git a/kanban/tsconfig.json b/kanban/tsconfig.json new file mode 100644 index 000000000..2b590ff76 --- /dev/null +++ b/kanban/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/kanban/ui/build.mjs b/kanban/ui/build.mjs new file mode 100644 index 000000000..8c4075564 --- /dev/null +++ b/kanban/ui/build.mjs @@ -0,0 +1,18 @@ +import esbuild from 'esbuild' + +const options = { + entryPoints: ['page.tsx', 'styles.css'], + bundle: true, + format: 'esm', + jsx: 'automatic', + outdir: 'dist', + external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@iii-dev/console-ui'], + logLevel: 'info', +} + +if (process.argv.includes('--watch')) { + const context = await esbuild.context(options) + await context.watch() +} else { + await esbuild.build(options) +} diff --git a/kanban/ui/package.json b/kanban/ui/package.json new file mode 100644 index 000000000..8b4659795 --- /dev/null +++ b/kanban/ui/package.json @@ -0,0 +1,19 @@ +{ + "name": "@iii-workers/kanban-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "tsc --noEmit && node build.mjs", + "typecheck": "tsc --noEmit", + "watch": "node build.mjs --watch" + }, + "dependencies": { + "@iii-dev/console-ui": "workspace:*" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "esbuild": "^0.25.0", + "typescript": "^5.9.2" + } +} diff --git a/kanban/ui/page.tsx b/kanban/ui/page.tsx new file mode 100644 index 000000000..a29a288d3 --- /dev/null +++ b/kanban/ui/page.tsx @@ -0,0 +1,1271 @@ +import { + Badge, + Button, + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + EmptyState, + type Host, + Input, + List, + ListGroup, + ListGroupLabel, + ListItem, + PageBody, + PageHeader, + PageMain, + type PageRenderProps, + PageShell, + PageSidebar, + Select, + Skeleton, + StatusDot, + StatusPanel, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + uiClasses, +} from '@iii-dev/console-ui' +import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' + +type TaskStatus = 'needs_you' | 'queued' | 'running' | 'review' | 'ready' | 'done' +type RunStatus = 'needs_you' | 'active' | 'review' | 'ready' | 'done' +type IsolationMode = 'shared' | 'worktree_per_task' + +type Executor = { + id: string + label: string + kind: 'task' | 'harness' + function_id: string + worker_name: string + stop_function?: string + available: boolean +} + +type GitStatus = { + clean: boolean + ahead: number + behind: number + staged: number + unstaged: number + untracked: number + conflicted: number + diffstat: string + unpushed: number + in_rebase: boolean + head_sha: string + integrated?: boolean +} + +type Run = { + id: string + title: string + goal?: string + cwd?: string + repo_path?: string + base_ref?: string + target_branch?: string + isolation?: IsolationMode + root_session_id?: string + task_ids: string[] + source: 'kanban' | 'harness' + status: RunStatus + counts: Record + created_at_ms: number + updated_at_ms: number +} + +type Task = { + id: string + key?: string + run_id: string + run_title: string + title: string + instruction: string + executor_id: string + executor_function: string + executor_kind: 'task' | 'harness' + cwd?: string + depends_on?: string[] + model?: string + profile?: string + worktree_id?: string + worktree_path?: string + branch?: string + base_ref?: string + base_sha?: string + dev_port?: number + land_job_id?: string + land_status?: 'landing' | 'landed' | 'blocked' + git_status?: GitStatus + status: TaskStatus + attempt: number + external_session_id?: string + external_turn_id?: string + source: 'kanban' | 'harness' + result?: unknown + error?: string + created_at_ms: number + updated_at_ms: number +} + +type Capabilities = { harness: boolean; worktree: boolean; external_executors: number } +type ModelOption = { id: string; label: string; provider: string } +type Board = { + runs: Run[] + tasks: Task[] + executors: Executor[] + models: ModelOption[] + capabilities: Capabilities + updated_at_ms: number +} +type ChangedEvent = { kind?: 'run' | 'task'; id?: string; run_id?: string; updated_at_ms?: number } +type Props = PageRenderProps & { host: Host } +type Feedback = { kind: 'success' | 'error'; text: string } +type DraftTask = { + id: string + key: string + title: string + instruction: string + executor: string + dependsOn: string + model: string + profile: string +} + +const EVENTS_FN = 'iii::kanban::changed' +const STATUS_LABEL: Record = { + needs_you: 'Needs you', + queued: 'Queued', + running: 'Running', + review: 'Review', + ready: 'Ready', + done: 'Done', +} + +function describe(cause: unknown): string { + if (cause instanceof Error) return cause.message.replace(/^handler error:\s*/i, '') + if (cause && typeof cause === 'object') { + const message = (cause as { message?: unknown }).message + if (typeof message === 'string') return message.replace(/^handler error:\s*/i, '') + try { + return JSON.stringify(cause) + } catch { + return String(cause) + } + } + return String(cause) +} + +function resultText(result: unknown): string { + if (result === undefined || result === null) return '' + if (typeof result === 'string') return result + try { + return JSON.stringify(result, null, 2) + } catch { + return String(result) + } +} + +function relativeTime(timestamp: number): string { + const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000)) + if (seconds < 10) return 'now' + if (seconds < 60) return `${seconds}s` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h` + return `${Math.floor(hours / 24)}d` +} + +function statusBadge(status: TaskStatus | RunStatus): 'default' | 'ok' | 'warn' | 'alert' | 'accent' { + if (status === 'needs_you') return 'alert' + if (status === 'running' || status === 'active') return 'accent' + if (status === 'review') return 'warn' + if (status === 'done') return 'ok' + return 'default' +} + +function statusTone(status: TaskStatus | RunStatus): 'accent' | 'alert' | 'warn' | 'ink' { + if (status === 'needs_you') return 'alert' + if (status === 'running' || status === 'active') return 'accent' + if (status === 'review') return 'warn' + return 'ink' +} + +function shortPath(path?: string): string { + if (!path) return 'Shared directory' + const parts = path.split('/').filter(Boolean) + return parts.length > 2 ? `…/${parts.slice(-2).join('/')}` : path +} + +function Field({ id, label, hint, children }: { id: string; label: string; hint?: string; children: ReactNode }) { + return ( +
+ + {children} + {hint ? {hint} : null} +
+ ) +} + +function newDraft(index: number, executors: Executor[], defaultModel?: string | null): DraftTask { + const executor = executors[index % Math.max(1, executors.length)] + const id = globalThis.crypto.randomUUID() + return { + id, + key: `task-${id.slice(0, 8)}`, + title: '', + instruction: '', + executor: executor?.id ?? '', + dependsOn: '', + model: executor?.kind === 'harness' ? (defaultModel ?? '') : '', + profile: '', + } +} + +function NewRunDialog({ + open, + executors, + models, + defaultModel, + capabilities, + initialRepo, + recentDirectories, + busy, + onOpenChange, + onCreate, +}: { + open: boolean + executors: Executor[] + models: ModelOption[] + defaultModel?: string | null + capabilities: Capabilities + initialRepo?: string | null + recentDirectories: string[] + busy: boolean + onOpenChange: (open: boolean) => void + onCreate: (input: Record) => Promise +}) { + const [title, setTitle] = useState('') + const [goal, setGoal] = useState('') + const [repoPath, setRepoPath] = useState(initialRepo ?? recentDirectories[0] ?? '') + const [baseRef, setBaseRef] = useState('HEAD') + const [targetBranch, setTargetBranch] = useState('') + const [isolation, setIsolation] = useState( + capabilities.worktree && Boolean(initialRepo) ? 'worktree_per_task' : 'shared', + ) + const [autoDispatch, setAutoDispatch] = useState(true) + const [tasks, setTasks] = useState(() => [newDraft(0, executors, defaultModel)]) + const autoIsolation = useRef(false) + + useEffect(() => { + const first = executors[0]?.id + if (!first) return + const firstExecutor = executors[0] + setTasks((current) => + current.map((task) => { + if (task.executor) { + const executor = executors.find((candidate) => candidate.id === task.executor) + return executor?.kind === 'harness' && !task.model && defaultModel ? { ...task, model: defaultModel } : task + } + return { + ...task, + executor: first, + ...(firstExecutor.kind === 'harness' && defaultModel ? { model: defaultModel } : {}), + } + }), + ) + }, [defaultModel, executors]) + + useEffect(() => { + if (!capabilities.worktree && isolation === 'worktree_per_task') setIsolation('shared') + }, [capabilities.worktree, isolation]) + + useEffect(() => { + if (!open || repoPath.trim()) return + const nextRepo = initialRepo?.trim() || recentDirectories[0]?.trim() + if (nextRepo) setRepoPath(nextRepo) + }, [initialRepo, open, recentDirectories, repoPath]) + + useEffect(() => { + if (!open) { + autoIsolation.current = false + return + } + if (!autoIsolation.current && capabilities.worktree && repoPath.trim()) { + autoIsolation.current = true + setIsolation('worktree_per_task') + } + }, [capabilities.worktree, open, repoPath]) + + const updateTask = (id: string, patch: Partial) => { + setTasks((current) => current.map((task) => (task.id === id ? { ...task, ...patch } : task))) + } + + const submit = async (event: FormEvent) => { + event.preventDefault() + await onCreate({ + title, + goal, + cwd: repoPath, + repo_path: repoPath, + base_ref: baseRef, + target_branch: targetBranch, + isolation, + auto_dispatch: autoDispatch, + tasks: tasks.map((task) => ({ + key: task.key, + title: task.title, + instruction: task.instruction, + executor: task.executor, + depends_on: task.dependsOn ? [task.dependsOn] : [], + model: task.model, + profile: task.profile, + })), + }) + } + + const invalid = + executors.length === 0 || + tasks.some( + (task) => + !task.instruction.trim() || + !task.executor || + (executors.find((executor) => executor.id === task.executor)?.kind === 'harness' && !task.model), + ) || + (isolation === 'worktree_per_task' && !repoPath.trim()) + + return ( + + + New Kanban run + + One root session, child executors, dependency gates, and optional isolated Git worktrees. + +
+
+ + + + + + + + + + {recentDirectories.map((path) => ( + + + + + + + + +
+ +
+
+ Tasks + Each task becomes a child session under this run. +
+ +
+
+ {tasks.map((task, index) => { + const previous = tasks.slice(0, index) + return ( +
+
{String(index + 1).padStart(2, '0')}
+
+
+ updateTask(task.id, { title: value })} + placeholder="Task title" + aria-label={`Task ${index + 1} title`} + /> + ({ + value: candidate.key, + label: `After ${candidate.title || candidate.key}`, + }))} + onChange={(dependsOn) => updateTask(task.id, { dependsOn })} + onClear={() => updateTask(task.id, { dependsOn: '' })} + allowEmpty + emptyLabel="Starts immediately" + placeholder="Starts immediately" + disabled={previous.length === 0} + aria-label={`Task ${index + 1} dependency`} + /> + {tasks.length > 1 ? ( + + ) : null} +
+