commit 3172718a944e2b91f635bd4e7cd34e90adc270c9 Author: wangjia <809946525@qq.com> Date: Fri Jun 12 20:44:45 2026 +0800 feat: Phase1 核心——模型/状态机 + SQLite Store(守卫+审批闸) + REST/WS API + daemon - src/model: 复杂度分级、16 状态状态机、实体类型 - src/store: better-sqlite3 接 schema,transition 受 canTransition 守卫, decide 审批闸(reject 必带改进意见),事件订阅广播,nextExecutable - src/api: Fastify REST + ws 事件广播(/ws) - src/daemon: maestrod 入口(env 配置,默认 ~/.maestro :4517) - test: 9 个生命周期单测全过;typecheck/build 干净;REST+WS 端到端实跑验证 Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a69a851 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +data/ +*.sqlite +*.sqlite-* +*.log +.maestro/ +.DS_Store +_*.mjs diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..47286e1 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,100 @@ +# Maestro — 多项目 TODO 管理与 Agent 执行系统 · 设计方案 v0.1 + +本地优先的 daemon:管理多个本地 git 项目的任务,按复杂度分级驱动审批闸,并用后台 agent(Claude Code)自动执行可执行任务。 + +## Context(为什么做) +前身是 `~/.claude/skills/todo`(JSON + HTML 单机原型):单项目、无真正后端、确认靠临时 http hack、无自动执行。它验证了「复杂度分级 + 确认闸 + 看板」的形态,但不可扩展。Maestro 把它升级为正式系统:**多项目、任务层级化、复杂度驱动的审批闸、完整状态机、本地常驻 daemon、每项目一个后台执行 agent、与 Claude Code 深度集成(MCP + Agent SDK)**。 + +## 0. 关键决策 +| 维度 | 决策 | +|---|---| +| 运行时 | Node / TypeScript | +| 部署 | 本地优先 daemon(跑在用户机器,管理本地 git 项目) | +| Claude Code 集成 | MCP 服务(任务读写)+ Claude Agent SDK(headless 自动执行) | +| Easy 自动化边界 | 改动隔离到 git worktree/分支,**不自动合并** | + +## 1. 系统总览 +``` +maestrod(核心 daemon,常驻) + ├─ 任务存储 SQLite ├─ 项目注册表 + ├─ REST + WebSocket API ├─ MCP server(给任意 Claude Code 会话) + ├─ 编排器 Orchestrator └─ 每项目 worker 池(执行器) + │ │ + Web 看板(多项目·实时·页面内 accept/reject) 执行 agent:Agent SDK 在 worktree 跑 headless Claude Code +``` +闭环:建任务 →(Hard 分析拆解 / Medium 写方案 / Easy 写操作)→ 前置审批闸 → ready → 编排器领取 → worktree 起 headless CC 执行 → verify → 结果闸(审/合)→ done。 + +## 2. 复杂度分级 +Hard / Medium / Easy ↔ 旧 tier 1/2/3。每个任务(含子任务)都带 `complexity`,决定它走哪条「产出 + 审批」路径。(见 `src/model/complexity.ts`) + +## 3. 任务层级 +`parent_id` + `depth`;默认 ≤3 层,极复杂 ≤4 层(创建第 5 层时拒绝)。子任务各自带 complexity。**叶子任务才可执行**;被拆解的 Hard 是容器,进度由子任务汇总。 + +## 4. 三条产出路径与审批闸 +| 复杂度 | 产出(执行前) | 前置闸 | reject 规则 | +|---|---|---|---| +| **Hard** | 强制进 plan 分析 → 产出「分析 + 任务拆解(子任务,各带复杂度)」 | `plan_review`,accept 才进下一步 | **必须带改进意见** → 退回重做并附 feedback | +| **Medium** | 写「具体方案 = 改动内容 + 为什么这么做」 | `spec_review`,accept/reject | 同上(必带意见) | +| **Easy** | 写清「将执行的操作」记录在任务上 | 无前置闸,直接可执行 | — | + +**结果闸 `exec_review`**(所有产生改动的执行后、合并前):改动在隔离分支,用户审/合;reject 必带意见 → 返工。承载「不自动合并」边界。 + +## 5. 状态机(见 `src/model/status.ts`) +`init` · `analyzing` · `plan_review` · `decomposed` · `speccing` · `spec_review` · `ready` · `blocked` · `queued` · `executing` · `exec_review` · `failed` · `needs_attention` · `done` · `paused` · `cancelled` + +主要流转: +- `init` →(Hard)`analyzing` → `plan_review` —accept→ `decomposed` —子全 done→ `done`;—reject(+意见)→ `analyzing` +- `init` →(Medium)`speccing` → `spec_review` —accept→ `ready`;—reject(+意见)→ `speccing` +- `init` →(Easy,写完操作)→ `ready` +- `ready` —deps 满足→ `queued` → `executing` → `exec_review` —accept(合并)→ `done`;—reject(+意见)→ 返工 +- `executing` → `failed` —自动重试 n 次→ `executing` / `needs_attention` + +合法流转表与守卫在 `TRANSITIONS` / `canTransition()`。 + +## 6. 数据模型(见 `src/model/types.ts` 与 `src/store/schema.sql`) +- **Project**:repo_path、default_branch、verify_cmd、autonomy、model、concurrency、status +- **Task**:project_id、parent_id、depth、complexity、status、deps[]、plan/spec/operations、result、assignee +- **ApprovalRecord**:gate(plan/spec/exec)、action、actor、reason(reject 必填)、at +- **Run**:kind(planner/executor)、worktree、branch、status、transcript_ref、claude_session_id +- **Event**(append-only):审计 + 实时看板推送源 + +## 7. 编排器与执行 agent +- daemon 内编排循环 + 每项目 worker 池(并发可配,默认 1–2)。 +- 选「可执行任务」(叶子、`ready`、deps 满足):Easy 自动入队;Medium/Hard 子任务在 spec accept 后入队。 +- 执行 = Claude Agent SDK 起 headless Claude Code:`cwd`=worktree,附 MCP、权限模式(worktree 内 acceptEdits)、任务 spec/operations 作指令、项目 CLAUDE.md 作上下文。 +- 完成 → 跑 `verify_cmd` → `exec_review`(带分支 + diff 摘要);失败 → `failed` → 重试 n 次 → `needs_attention`。 +- Hard 拆解 / Medium 方案也可由 Agent SDK 起 **planner run** 自动产出 → 进前置闸:「自动产出,人工把关」。 + +## 8. Claude Code 集成 +- **MCP server(交互面)**:项目里的 Claude Code 配 `.mcp.json` 接 `maestro-mcp`。工具:list/get tasks、create、decompose、write_spec、write_operations、update_status、attach_result、request_approval、get_next_executable、get_pending_approvals。 +- **Agent SDK(自动面)**:编排器用 Claude Agent SDK 起 headless CC 跑可执行任务。 +- approve/reject 是用户动作,走看板按钮。 +- > 以官方 Claude Agent SDK / MCP 当前能力为准,落地前用 PoC 校准具体 API 与包版本。 + +## 9. 后端架构与项目绑定 +- 存储 SQLite(better-sqlite3,单文件零运维);run 转录日志存文件,Run 表引用。 +- API:REST(CRUD)+ WebSocket/SSE(实时看板)。 +- 项目绑定:`maestro project add ` 记录 repo 路径/默认分支/verify/autonomy/model/并发;repo 内 `.maestro/config.json` + daemon 中央 DB 行。 +- worktree:执行在 repo 的 git worktree(隔离分支),不自动合并。 + +## 10. Web 看板 +多项目切换 / 全局总览;任务树(层级折叠)、复杂度徽章、状态、内联审批闸(accept/reject,reject 必填意见)、实时执行状态、`exec_review` 的 diff/分支链接。实时 WS。 + +## 11. 从现有 skill 迁移 +旧 `todo.mjs`/JSON 作为 v0 原型被取代。importer:读旧 `todo/todo.json`,tier1/2/3 → hard/medium/easy,导入为一个项目。`/todo` 改用 MCP + 看板。 + +## 12. 分期实施 +- **Phase 1 核心(可控、无自动执行)**:daemon + SQLite + 项目注册 + 任务模型 + 状态机 + 看板 + MCP(读写 + 审批)。手动驱动。 +- **Phase 2 自动化**:编排器 + Agent SDK 执行器(Easy 自动跑 worktree,结果闸)+ planner runs。 +- **Phase 3 打磨**:并发/重试/通知/指标/可选远程看板。 + +## 13. 风险与未知 +- Agent SDK headless 执行 + MCP 回写的可靠性(PoC 先验证)。 +- 自动执行安全:worktree 隔离 + 不自动合并 + verify 把关 + `needs_attention` 人工兜底。 +- 长任务上下文超限、并发资源、worktree 清理。 +- Hard 自动拆解质量需人工把关(reject 带意见返工)。 + +## 14. 验证 / PoC +1. 最小执行闭环:注册项目 → 建 Easy 任务 → 编排器在 worktree 起 headless CC 改一个文件 → verify → 看板出现 `exec_review` → accept 合并。 +2. MCP 往返:交互 CC 会话里 list/create/update 跑通。 +3. 审批闸:Hard 自动拆解 → `plan_review` → 看板 reject(带意见) → 退回 `analyzing` → 重拆 → accept。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce2e37a --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# maestro + +多项目 TODO 管理与 Agent 执行系统(本地优先 daemon · Node/TypeScript)。 + +按任务复杂度(Hard / Medium / Easy)分级驱动审批闸,用后台 agent(Claude Code)在隔离 git worktree 自动执行可执行任务,改动不自动合并、等你审。 + +- 完整设计见 **[DESIGN.md](./DESIGN.md)**。 +- 当前状态:**脚手架 + 基础模型/状态机/schema**(Phase 1 起步)。 + +## 结构 +``` +src/model/ 复杂度、状态机、实体类型(已落地) +src/store/ SQLite schema + 访问层 +src/api/ REST + WebSocket +src/mcp/ MCP server(给 Claude Code) +src/executor/ Agent SDK 封装、worktree 管理、verify +src/daemon/ maestrod 入口、编排器、worker +web/ 看板 cli/ maestro CLI +``` + +## 开发 +```bash +npm install +npm run typecheck # tsc --noEmit +npm run build # → dist/ +npm run dev # 启动 daemon(开发) +``` + +> 依赖版本为初始值;MCP SDK 与 Claude Agent SDK 在 Phase 2 接入时按官方文档核定后再加。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b7f8575 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1574 @@ +{ + "name": "maestro", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maestro", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^11.3.0", + "fastify": "^4.28.1", + "nanoid": "^5.0.7", + "ws": "^8.18.0" + }, + "bin": { + "maestro": "dist/cli/index.js" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/node": "^20.14.0", + "@types/ws": "^8.5.12", + "tsx": "^4.16.5", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fa74bd --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "maestro", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "多项目 TODO 管理与 Agent 执行系统(本地优先 daemon)", + "engines": { "node": ">=20" }, + "bin": { "maestro": "dist/cli/index.js" }, + "scripts": { + "build": "tsc -p tsconfig.json && cp src/store/schema.sql dist/store/schema.sql", + "typecheck": "tsc -p tsconfig.json --noEmit", + "dev": "tsx src/daemon/index.ts", + "test": "tsx --test test/*.test.ts" + }, + "dependencies": { + "better-sqlite3": "^11.3.0", + "fastify": "^4.28.1", + "ws": "^8.18.0", + "nanoid": "^5.0.7" + }, + "devDependencies": { + "typescript": "^5.5.4", + "tsx": "^4.16.5", + "@types/node": "^20.14.0", + "@types/better-sqlite3": "^7.6.11", + "@types/ws": "^8.5.12" + } +} diff --git a/src/api/server.ts b/src/api/server.ts new file mode 100644 index 0000000..c776146 --- /dev/null +++ b/src/api/server.ts @@ -0,0 +1,160 @@ +import Fastify, { type FastifyInstance } from 'fastify'; +import { WebSocketServer, type WebSocket } from 'ws'; +import { Store, StoreError } from '../store/index.js'; +import type { Complexity } from '../model/complexity.js'; +import { isComplexity } from '../model/complexity.js'; +import type { TaskStatus } from '../model/status.js'; + +export interface ApiOptions { + store: Store; + logger?: boolean; +} + +/** + * REST + WebSocket API。所有写操作走 Store(带状态机守卫 / 审批闸)。 + * Store 的事件经 subscribe 广播到所有 WS 客户端,驱动看板实时刷新。 + */ +export function buildServer(opts: ApiOptions): FastifyInstance { + const { store } = opts; + const app = Fastify({ logger: opts.logger ?? false }); + + // Store 错误 → 400(业务校验),其余 → 500 + app.setErrorHandler((err, _req, reply) => { + if (err instanceof StoreError) return reply.code(400).send({ error: err.message }); + app.log.error(err); + return reply.code(500).send({ error: err.message ?? 'internal error' }); + }); + + // ---------- Projects ---------- + app.get('/api/projects', () => store.listProjects()); + + app.post('/api/projects', (req) => { + const b = req.body as Record; + if (!b?.name || !b?.repoPath) throw new StoreError('name 与 repoPath 必填'); + return store.createProject({ + name: String(b.name), repoPath: String(b.repoPath), + defaultBranch: b.defaultBranch ? String(b.defaultBranch) : undefined, + verifyCmd: b.verifyCmd === undefined ? undefined : (b.verifyCmd === null ? null : String(b.verifyCmd)), + autonomy: b.autonomy as never, model: b.model === undefined ? undefined : (b.model === null ? null : String(b.model)), + concurrency: b.concurrency === undefined ? undefined : Number(b.concurrency), + }); + }); + + app.get('/api/projects/:id', (req) => { + const { id } = req.params as { id: string }; + const p = store.getProject(id); + if (!p) throw new StoreError(`项目不存在: ${id}`); + return p; + }); + + app.get('/api/projects/:id/tasks', (req) => { + const { id } = req.params as { id: string }; + return store.listTasks(id); + }); + + app.get('/api/projects/:id/events', (req) => { + const { id } = req.params as { id: string }; + return store.listEvents(id); + }); + + app.get('/api/projects/:id/next', (req) => { + const { id } = req.params as { id: string }; + return store.nextExecutable(id) ?? { next: null }; + }); + + // ---------- Tasks ---------- + app.post('/api/projects/:id/tasks', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as Record; + if (!b?.title) throw new StoreError('title 必填'); + if (!isComplexity(b.complexity)) throw new StoreError('complexity 必须是 hard|medium|easy'); + return store.createTask({ + projectId: id, title: String(b.title), complexity: b.complexity as Complexity, + parentId: b.parentId ? String(b.parentId) : null, + priority: b.priority === undefined ? undefined : Number(b.priority), + deps: Array.isArray(b.deps) ? (b.deps as string[]) : undefined, + }); + }); + + app.get('/api/tasks/:id', (req) => { + const { id } = req.params as { id: string }; + const t = store.getTask(id); + if (!t) throw new StoreError(`任务不存在: ${id}`); + return t; + }); + + app.get('/api/tasks/:id/children', (req) => { + const { id } = req.params as { id: string }; + return store.childrenOf(id); + }); + + app.get('/api/tasks/:id/runs', (req) => { + const { id } = req.params as { id: string }; + return store.listRuns(id); + }); + + app.post('/api/tasks/:id/plan', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as { plan?: string }; + if (!b?.plan) throw new StoreError('plan 必填'); + return store.setPlan(id, b.plan); + }); + + app.post('/api/tasks/:id/spec', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as { spec?: string }; + if (!b?.spec) throw new StoreError('spec 必填'); + return store.setSpec(id, b.spec); + }); + + app.post('/api/tasks/:id/operations', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as { operations?: string }; + if (!b?.operations) throw new StoreError('operations 必填'); + return store.setOperations(id, b.operations); + }); + + app.post('/api/tasks/:id/transition', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as { to?: string; meta?: Record }; + if (!b?.to) throw new StoreError('to 必填'); + return store.transition(id, b.to as TaskStatus, b.meta ?? {}); + }); + + // 审批闸:accept / reject(reject 必带 reason) + app.post('/api/tasks/:id/decide', (req) => { + const { id } = req.params as { id: string }; + const b = req.body as { action?: string; actor?: string; reason?: string | null }; + if (b?.action !== 'accept' && b?.action !== 'reject') throw new StoreError('action 必须是 accept|reject'); + return store.decide(id, b.action, b.actor ?? 'user', b.reason ?? null); + }); + + // ---------- Approvals ---------- + app.get('/api/approvals', (req) => { + const q = req.query as { projectId?: string }; + return store.pendingApprovals(q.projectId); + }); + + return app; +} + +/** 在 Fastify 的底层 http server 上挂 WebSocket,把 Store 事件广播给所有客户端。 */ +export function attachWebSocket(app: FastifyInstance, store: Store): WebSocketServer { + const wss = new WebSocketServer({ server: app.server, path: '/ws' }); + const clients = new Set(); + + wss.on('connection', (ws) => { + clients.add(ws); + ws.on('close', () => clients.delete(ws)); + ws.on('error', () => clients.delete(ws)); + }); + + store.subscribe((evt) => { + const msg = JSON.stringify(evt); + for (const ws of clients) { + if (ws.readyState === ws.OPEN) ws.send(msg); + } + }); + + return wss; +} diff --git a/src/daemon/config.ts b/src/daemon/config.ts new file mode 100644 index 0000000..8c4adff --- /dev/null +++ b/src/daemon/config.ts @@ -0,0 +1,22 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { mkdirSync } from 'node:fs'; + +export interface DaemonConfig { + dataDir: string; // 数据库 + run 转录日志所在目录 + dbFile: string; + host: string; + port: number; +} + +/** 解析配置:环境变量覆盖默认值。首次访问时确保数据目录存在。 */ +export function loadConfig(): DaemonConfig { + const dataDir = process.env.MAESTRO_DATA_DIR ?? join(homedir(), '.maestro'); + mkdirSync(dataDir, { recursive: true }); + return { + dataDir, + dbFile: join(dataDir, 'maestro.sqlite'), + host: process.env.MAESTRO_HOST ?? '127.0.0.1', + port: Number(process.env.MAESTRO_PORT ?? 4517), + }; +} diff --git a/src/daemon/index.ts b/src/daemon/index.ts new file mode 100644 index 0000000..eeaa780 --- /dev/null +++ b/src/daemon/index.ts @@ -0,0 +1,29 @@ +import { Store } from '../store/index.js'; +import { buildServer, attachWebSocket } from '../api/server.js'; +import { loadConfig } from './config.js'; + +/** maestrod:核心 daemon。Phase 1 = Store + REST/WS API(手动驱动;编排器在 Phase 2 接入)。 */ +async function main(): Promise { + const cfg = loadConfig(); + const store = new Store(cfg.dbFile); + const app = buildServer({ store, logger: true }); + + attachWebSocket(app, store); + + await app.listen({ host: cfg.host, port: cfg.port }); + app.log.info(`maestrod 就绪 · db=${cfg.dbFile} · http://${cfg.host}:${cfg.port} · ws ${cfg.host}:${cfg.port}/ws`); + + const shutdown = async (): Promise => { + app.log.info('收到退出信号,关闭中…'); + await app.close(); + store.close(); + process.exit(0); + }; + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +main().catch((err) => { + console.error('maestrod 启动失败:', err); + process.exit(1); +}); diff --git a/src/model/complexity.ts b/src/model/complexity.ts new file mode 100644 index 0000000..75984d9 --- /dev/null +++ b/src/model/complexity.ts @@ -0,0 +1,23 @@ +/** 任务复杂度:Hard / Medium / Easy(对应旧 todo skill 的 tier 1/2/3) */ +export type Complexity = 'hard' | 'medium' | 'easy'; + +export const COMPLEXITY_ORDER: Record = { hard: 1, medium: 2, easy: 3 }; +export const COMPLEXITY_LABEL: Record = { hard: 'Hard', medium: 'Medium', easy: 'Easy' }; + +/** 旧 todo skill 的 tier(1/2/3) ↔ 复杂度,用于迁移导入 */ +export const TIER_TO_COMPLEXITY: Record = { 1: 'hard', 2: 'medium', 3: 'easy' }; +export const COMPLEXITY_TO_TIER: Record = { hard: 1, medium: 2, easy: 3 }; + +/** 前置闸:执行前是否需要用户确认(Hard / Medium 需要,Easy 不需要) */ +export function requiresPreGate(c: Complexity): boolean { + return c === 'hard' || c === 'medium'; +} + +/** Hard 必须先进 plan 分析 + 任务拆解 */ +export function mustDecompose(c: Complexity): boolean { + return c === 'hard'; +} + +export function isComplexity(v: unknown): v is Complexity { + return v === 'hard' || v === 'medium' || v === 'easy'; +} diff --git a/src/model/status.ts b/src/model/status.ts new file mode 100644 index 0000000..37b4e5d --- /dev/null +++ b/src/model/status.ts @@ -0,0 +1,95 @@ +import type { Complexity } from './complexity.js'; + +/** 任务执行状态机(设计见 DESIGN.md §5) */ +export type TaskStatus = + | 'init' // 新建,未分析 / 未定方案 + | 'analyzing' // (Hard)plan 分析 + 拆解中 + | 'plan_review' // (Hard)拆解完成,待 accept + | 'decomposed' // (Hard)已拆解,作为容器,进度由子任务汇总 + | 'speccing' // (Medium)写方案中 + | 'spec_review' // (Medium)方案完成,待 accept + | 'ready' // 可执行叶子 + | 'blocked' // 依赖未满足 + | 'queued' // 编排器已领取,等 worker 槽位 + | 'executing' // agent 在 worktree 跑 headless Claude Code + | 'exec_review' // 执行完成,改动在分支,待审 / 合 + | 'failed' // 执行失败 + | 'needs_attention' // 失败超阈值 / 冲突 / 歧义,需人工 + | 'done' // 已合并 / 已接受 + | 'paused' + | 'cancelled'; + +export const STATUS_LABEL: Record = { + init: '新建', + analyzing: '分析拆解中', + plan_review: '待确认拆解', + decomposed: '已拆解', + speccing: '写方案中', + spec_review: '待确认方案', + ready: '可执行', + blocked: '被依赖阻塞', + queued: '排队中', + executing: '执行中', + exec_review: '待审/合', + failed: '失败', + needs_attention: '需人工', + done: '完成', + paused: '暂停', + cancelled: '取消', +}; + +/** 审批闸类型 */ +export type GateKind = 'plan' | 'spec' | 'exec'; + +/** 哪些状态在等待用户审批,以及对应的闸类型 */ +export const GATE_STATUS: Partial> = { + plan_review: 'plan', + spec_review: 'spec', + exec_review: 'exec', +}; + +export function gateOf(s: TaskStatus): GateKind | null { + return GATE_STATUS[s] ?? null; +} + +export const TERMINAL_STATUS: ReadonlySet = new Set(['done', 'cancelled']); + +/** 合法状态流转表:from → 允许的 to[] */ +export const TRANSITIONS: Record = { + init: ['analyzing', 'speccing', 'ready', 'cancelled'], // 按复杂度分流 + analyzing: ['plan_review', 'cancelled', 'paused'], + plan_review: ['decomposed', 'analyzing', 'cancelled'], // accept / reject(+意见) + decomposed: ['done', 'analyzing', 'cancelled', 'paused'], // 子全 done / 重拆 + speccing: ['spec_review', 'cancelled', 'paused'], + spec_review: ['ready', 'speccing', 'cancelled'], // accept / reject(+意见) + ready: ['blocked', 'queued', 'speccing', 'analyzing', 'cancelled', 'paused'], + blocked: ['ready', 'cancelled', 'paused'], + queued: ['executing', 'ready', 'cancelled', 'paused'], + executing: ['exec_review', 'failed', 'cancelled'], + exec_review: ['done', 'ready', 'failed', 'cancelled'], // accept(合并) / reject(返工) + failed: ['queued', 'needs_attention', 'cancelled'], // 重试 / 升级 + needs_attention: ['ready', 'queued', 'analyzing', 'speccing', 'cancelled', 'paused'], + done: [], + paused: ['ready', 'analyzing', 'speccing', 'queued', 'cancelled'], + cancelled: [], +}; + +export function canTransition(from: TaskStatus, to: TaskStatus): boolean { + return TRANSITIONS[from]?.includes(to) ?? false; +} + +/** 新建任务按复杂度决定从 init 进入的下一个状态 */ +export function initialNextStatus(c: Complexity): TaskStatus { + if (c === 'hard') return 'analyzing'; // 强制分析 + 拆解 + if (c === 'medium') return 'speccing'; // 写方案 + return 'ready'; // Easy:写完 operations 即可执行 +} + +/** 可被执行器领取的状态(叶子 + 依赖满足由调用方另判) */ +export function isExecutable(s: TaskStatus): boolean { + return s === 'ready'; +} + +export function isGate(s: TaskStatus): boolean { + return s in GATE_STATUS; +} diff --git a/src/model/types.ts b/src/model/types.ts new file mode 100644 index 0000000..91d148c --- /dev/null +++ b/src/model/types.ts @@ -0,0 +1,92 @@ +import type { Complexity } from './complexity.js'; +import type { TaskStatus, GateKind } from './status.js'; + +export type Id = string; + +/** 层级上限:一般 ≤3 层,极复杂 ≤4 层 */ +export const DEFAULT_MAX_DEPTH = 3; +export const HARD_MAX_DEPTH = 4; + +/** 自动执行边界(Easy 任务由后台执行器自动跑到什么程度) */ +export type Autonomy = 'manual' | 'auto-easy' | 'auto-approved'; + +export interface Project { + id: Id; + name: string; + repoPath: string; + defaultBranch: string; + verifyCmd: string | null; // 执行后的校验命令(build/test/lint) + autonomy: Autonomy; + model: string | null; + concurrency: number; // 每项目并发执行上限 + status: 'active' | 'paused'; + createdAt: string; +} + +export interface ApprovalRecord { + gate: GateKind; + action: 'accept' | 'reject'; + actor: string; + reason: string | null; // reject 必填(改进意见) + at: string; +} + +export interface TaskResult { + branch: string | null; + worktree: string | null; + diffSummary: string | null; + commits: string[]; + prUrl: string | null; +} + +export interface Task { + id: Id; + projectId: Id; + parentId: Id | null; + depth: number; // 1..4 + title: string; + complexity: Complexity; + status: TaskStatus; + priority: number; + deps: Id[]; // 兄弟依赖(同层) + // 三选一产出(按复杂度) + plan: string | null; // Hard:分析 + 拆解说明 + spec: string | null; // Medium:改动内容 + 为什么 + operations: string | null; // Easy:将执行的操作 + approvals: ApprovalRecord[]; + result: TaskResult | null; + assignee: 'agent' | 'human' | null; + createdAt: string; + updatedAt: string; +} + +export type RunKind = 'planner' | 'executor'; +export type RunStatus = 'started' | 'succeeded' | 'failed' | 'cancelled'; + +export interface Run { + id: Id; + taskId: Id; + kind: RunKind; // planner(产出拆解/方案)| executor(执行改动) + worktree: string | null; + branch: string | null; + status: RunStatus; + startedAt: string; + endedAt: string | null; + transcriptRef: string | null; // agent 转录日志文件路径 + claudeSessionId: string | null; + error: string | null; +} + +export type EventType = + | 'task.created' | 'task.updated' | 'status.changed' + | 'approval.requested' | 'approval.granted' | 'approval.rejected' + | 'run.started' | 'run.finished'; + +export interface Event { + id: Id; + projectId: Id; + taskId: Id | null; + type: EventType; + payload: Record; + at: string; +} diff --git a/src/store/db.ts b/src/store/db.ts new file mode 100644 index 0000000..781e6f6 --- /dev/null +++ b/src/store/db.ts @@ -0,0 +1,18 @@ +import Database from 'better-sqlite3'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** 打开(或新建)数据库并应用 schema。schema.sql 在 dev(src) 与 build(dist) 两处都与本文件同目录。 */ +export function openDb(file: string): Database.Database { + const db = new Database(file); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + const schema = readFileSync(join(HERE, 'schema.sql'), 'utf8'); + db.exec(schema); + return db; +} + +export type DB = Database.Database; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..b93d8e2 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,4 @@ +export { Store, StoreError } from './store.js'; +export type { CreateProjectInput, CreateTaskInput } from './store.js'; +export { openDb } from './db.js'; +export type { DB } from './db.js'; diff --git a/src/store/mappers.ts b/src/store/mappers.ts new file mode 100644 index 0000000..8f75891 --- /dev/null +++ b/src/store/mappers.ts @@ -0,0 +1,71 @@ +import type { Project, Task, ApprovalRecord, Run, Event, TaskResult, Autonomy } from '../model/types.js'; +import type { Complexity } from '../model/complexity.js'; +import type { TaskStatus, GateKind } from '../model/status.js'; +import type { RunKind, RunStatus, EventType } from '../model/types.js'; + +/** SQLite 行类型(snake_case,文本/数字原样) */ +export interface ProjectRow { + id: string; name: string; repo_path: string; default_branch: string; + verify_cmd: string | null; autonomy: string; model: string | null; + concurrency: number; status: string; created_at: string; +} +export interface TaskRow { + id: string; project_id: string; parent_id: string | null; depth: number; + title: string; complexity: string; status: string; priority: number; + deps: string; plan: string | null; spec: string | null; operations: string | null; + result: string | null; assignee: string | null; created_at: string; updated_at: string; +} +export interface ApprovalRow { + id: string; task_id: string; gate: string; action: string; + actor: string; reason: string | null; at: string; +} +export interface RunRow { + id: string; task_id: string; kind: string; worktree: string | null; branch: string | null; + status: string; started_at: string; ended_at: string | null; + transcript_ref: string | null; claude_session_id: string | null; error: string | null; +} +export interface EventRow { + id: string; project_id: string; task_id: string | null; type: string; payload: string; at: string; +} + +export function rowToProject(r: ProjectRow): Project { + return { + id: r.id, name: r.name, repoPath: r.repo_path, defaultBranch: r.default_branch, + verifyCmd: r.verify_cmd, autonomy: r.autonomy as Autonomy, model: r.model, + concurrency: r.concurrency, status: r.status as Project['status'], createdAt: r.created_at, + }; +} + +export function rowToTask(r: TaskRow, approvals: ApprovalRecord[] = []): Task { + return { + id: r.id, projectId: r.project_id, parentId: r.parent_id, depth: r.depth, + title: r.title, complexity: r.complexity as Complexity, status: r.status as TaskStatus, + priority: r.priority, deps: JSON.parse(r.deps) as string[], + plan: r.plan, spec: r.spec, operations: r.operations, + approvals, result: r.result ? (JSON.parse(r.result) as TaskResult) : null, + assignee: r.assignee as Task['assignee'], createdAt: r.created_at, updatedAt: r.updated_at, + }; +} + +export function rowToApproval(r: ApprovalRow): ApprovalRecord { + return { + gate: r.gate as GateKind, action: r.action as ApprovalRecord['action'], + actor: r.actor, reason: r.reason, at: r.at, + }; +} + +export function rowToRun(r: RunRow): Run { + return { + id: r.id, taskId: r.task_id, kind: r.kind as RunKind, + worktree: r.worktree, branch: r.branch, status: r.status as RunStatus, + startedAt: r.started_at, endedAt: r.ended_at, + transcriptRef: r.transcript_ref, claudeSessionId: r.claude_session_id, error: r.error, + }; +} + +export function rowToEvent(r: EventRow): Event { + return { + id: r.id, projectId: r.project_id, taskId: r.task_id, + type: r.type as EventType, payload: JSON.parse(r.payload) as Record, at: r.at, + }; +} diff --git a/src/store/schema.sql b/src/store/schema.sql new file mode 100644 index 0000000..ed4e7cb --- /dev/null +++ b/src/store/schema.sql @@ -0,0 +1,74 @@ +-- maestro · SQLite schema v0.1(设计见 DESIGN.md §6/§9) +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + repo_path TEXT NOT NULL UNIQUE, + default_branch TEXT NOT NULL DEFAULT 'main', + verify_cmd TEXT, + autonomy TEXT NOT NULL DEFAULT 'manual', -- manual | auto-easy | auto-approved + model TEXT, + concurrency INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'active', -- active | paused + created_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE, + depth INTEGER NOT NULL DEFAULT 1, -- 1..4 + title TEXT NOT NULL, + complexity TEXT NOT NULL, -- hard | medium | easy + status TEXT NOT NULL DEFAULT 'init', + priority INTEGER NOT NULL DEFAULT 0, + deps TEXT NOT NULL DEFAULT '[]', -- JSON array of task ids + plan TEXT, -- Hard:分析 + 拆解 + spec TEXT, -- Medium:改动 + 理由 + operations TEXT, -- Easy:将执行的操作 + result TEXT, -- JSON TaskResult + assignee TEXT, -- agent | human + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id, status); +CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id); + +CREATE TABLE IF NOT EXISTS approvals ( + id TEXT PRIMARY KEY, + task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, + gate TEXT NOT NULL, -- plan | spec | exec + action TEXT NOT NULL, -- accept | reject + actor TEXT NOT NULL, + reason TEXT, -- reject 必填 + at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_approvals_task ON approvals(task_id, at); + +CREATE TABLE IF NOT EXISTS runs ( + id TEXT PRIMARY KEY, + task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, + kind TEXT NOT NULL, -- planner | executor + worktree TEXT, + branch TEXT, + status TEXT NOT NULL, -- started | succeeded | failed | cancelled + started_at TEXT NOT NULL, + ended_at TEXT, + transcript_ref TEXT, + claude_session_id TEXT, + error TEXT +); +CREATE INDEX IF NOT EXISTS idx_runs_task ON runs(task_id, started_at); + +-- 追加型事件日志:审计 + 实时看板推送源 +CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + task_id TEXT, + type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_events_project ON events(project_id, at); diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..a9f111b --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,285 @@ +import { nanoid } from 'nanoid'; +import { openDb, type DB } from './db.js'; +import { + rowToProject, rowToTask, rowToApproval, rowToRun, rowToEvent, + type ProjectRow, type TaskRow, type ApprovalRow, type RunRow, type EventRow, +} from './mappers.js'; +import type { Project, Task, ApprovalRecord, Run, Event, TaskResult, Autonomy, EventType } from '../model/types.js'; +import { DEFAULT_MAX_DEPTH, HARD_MAX_DEPTH } from '../model/types.js'; +import type { Complexity } from '../model/complexity.js'; +import { + type TaskStatus, type GateKind, canTransition, initialNextStatus, gateOf, STATUS_LABEL, +} from '../model/status.js'; + +const now = (): string => new Date().toISOString(); +const id = (prefix: string): string => `${prefix}_${nanoid(12)}`; + +export class StoreError extends Error {} + +export interface CreateProjectInput { + name: string; repoPath: string; defaultBranch?: string; verifyCmd?: string | null; + autonomy?: Autonomy; model?: string | null; concurrency?: number; +} +export interface CreateTaskInput { + projectId: string; title: string; complexity: Complexity; + parentId?: string | null; priority?: number; deps?: string[]; +} + +type EventListener = (e: Event) => void; + +/** + * 任务系统的唯一数据出入口。所有状态变更都经 transition() 守卫(canTransition), + * 审批 reject 强制带改进意见,每次变更追加 Event 并广播给监听者(供 WS 推送)。 + */ +export class Store { + readonly db: DB; + private listeners = new Set(); + + constructor(file: string) { + this.db = openDb(file); + } + + close(): void { this.db.close(); } + + /** 订阅事件流(看板 WS)。返回取消订阅函数。 */ + subscribe(fn: EventListener): () => void { + this.listeners.add(fn); + return () => this.listeners.delete(fn); + } + + private emit(projectId: string, taskId: string | null, type: EventType, payload: Record = {}): Event { + const row: EventRow = { + id: id('evt'), project_id: projectId, task_id: taskId, type, + payload: JSON.stringify(payload), at: now(), + }; + this.db.prepare( + `INSERT INTO events (id, project_id, task_id, type, payload, at) VALUES (@id,@project_id,@task_id,@type,@payload,@at)`, + ).run(row); + const evt = rowToEvent(row); + for (const l of this.listeners) { try { l(evt); } catch { /* listener 错误不影响主流程 */ } } + return evt; + } + + // ---------- Projects ---------- + createProject(input: CreateProjectInput): Project { + const row: ProjectRow = { + id: id('prj'), name: input.name, repo_path: input.repoPath, + default_branch: input.defaultBranch ?? 'main', verify_cmd: input.verifyCmd ?? null, + autonomy: input.autonomy ?? 'manual', model: input.model ?? null, + concurrency: input.concurrency ?? 1, status: 'active', created_at: now(), + }; + this.db.prepare( + `INSERT INTO projects (id,name,repo_path,default_branch,verify_cmd,autonomy,model,concurrency,status,created_at) + VALUES (@id,@name,@repo_path,@default_branch,@verify_cmd,@autonomy,@model,@concurrency,@status,@created_at)`, + ).run(row); + this.emit(row.id, null, 'task.created', { kind: 'project', name: row.name }); + return rowToProject(row); + } + + listProjects(): Project[] { + const rows = this.db.prepare(`SELECT * FROM projects ORDER BY created_at`).all() as ProjectRow[]; + return rows.map(rowToProject); + } + + getProject(projectId: string): Project | null { + const row = this.db.prepare(`SELECT * FROM projects WHERE id = ?`).get(projectId) as ProjectRow | undefined; + return row ? rowToProject(row) : null; + } + + // ---------- Tasks ---------- + createTask(input: CreateTaskInput): Task { + const project = this.getProject(input.projectId); + if (!project) throw new StoreError(`项目不存在: ${input.projectId}`); + + let depth = 1; + if (input.parentId) { + const parent = this.getTaskRow(input.parentId); + if (!parent) throw new StoreError(`父任务不存在: ${input.parentId}`); + if (parent.project_id !== input.projectId) throw new StoreError('子任务必须与父任务同项目'); + depth = parent.depth + 1; + const max = parent.complexity === 'hard' ? HARD_MAX_DEPTH : DEFAULT_MAX_DEPTH; + if (depth > max) throw new StoreError(`层级超限:最多 ${max} 层(父任务 ${parent.complexity})`); + } + + const status = initialNextStatus(input.complexity); + const row: TaskRow = { + id: id('tsk'), project_id: input.projectId, parent_id: input.parentId ?? null, depth, + title: input.title, complexity: input.complexity, status, priority: input.priority ?? 0, + deps: JSON.stringify(input.deps ?? []), plan: null, spec: null, operations: null, + result: null, assignee: null, created_at: now(), updated_at: now(), + }; + this.db.prepare( + `INSERT INTO tasks (id,project_id,parent_id,depth,title,complexity,status,priority,deps,plan,spec,operations,result,assignee,created_at,updated_at) + VALUES (@id,@project_id,@parent_id,@depth,@title,@complexity,@status,@priority,@deps,@plan,@spec,@operations,@result,@assignee,@created_at,@updated_at)`, + ).run(row); + this.emit(input.projectId, row.id, 'task.created', { title: row.title, complexity: row.complexity, status }); + return rowToTask(row); + } + + private getTaskRow(taskId: string): TaskRow | undefined { + return this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId) as TaskRow | undefined; + } + + getTask(taskId: string): Task | null { + const row = this.getTaskRow(taskId); + if (!row) return null; + return rowToTask(row, this.listApprovals(taskId)); + } + + listTasks(projectId: string): Task[] { + const rows = this.db.prepare( + `SELECT * FROM tasks WHERE project_id = ? ORDER BY depth, priority DESC, created_at`, + ).all(projectId) as TaskRow[]; + return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); + } + + childrenOf(taskId: string): Task[] { + const rows = this.db.prepare( + `SELECT * FROM tasks WHERE parent_id = ? ORDER BY priority DESC, created_at`, + ).all(taskId) as TaskRow[]; + return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); + } + + /** 写产出:Hard→plan / Medium→spec / Easy→operations。仅更新对应字段。 */ + setPlan(taskId: string, plan: string): Task { return this.patchField(taskId, 'plan', plan); } + setSpec(taskId: string, spec: string): Task { return this.patchField(taskId, 'spec', spec); } + setOperations(taskId: string, ops: string): Task { return this.patchField(taskId, 'operations', ops); } + + private patchField(taskId: string, field: 'plan' | 'spec' | 'operations', value: string): Task { + const row = this.getTaskRow(taskId); + if (!row) throw new StoreError(`任务不存在: ${taskId}`); + this.db.prepare(`UPDATE tasks SET ${field} = ?, updated_at = ? WHERE id = ?`).run(value, now(), taskId); + this.emit(row.project_id, taskId, 'task.updated', { field }); + return this.getTask(taskId)!; + } + + setResult(taskId: string, result: TaskResult): Task { + const row = this.getTaskRow(taskId); + if (!row) throw new StoreError(`任务不存在: ${taskId}`); + this.db.prepare(`UPDATE tasks SET result = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(result), now(), taskId); + this.emit(row.project_id, taskId, 'task.updated', { field: 'result' }); + return this.getTask(taskId)!; + } + + /** 受守卫的状态变更:非法流转抛错;记录 status.changed 事件。 */ + transition(taskId: string, to: TaskStatus, meta: Record = {}): Task { + const row = this.getTaskRow(taskId); + if (!row) throw new StoreError(`任务不存在: ${taskId}`); + const from = row.status as TaskStatus; + if (from === to) return this.getTask(taskId)!; + if (!canTransition(from, to)) { + throw new StoreError(`非法状态流转:${STATUS_LABEL[from]}(${from}) → ${STATUS_LABEL[to]}(${to})`); + } + this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`).run(to, now(), taskId); + this.emit(row.project_id, taskId, 'status.changed', { from, to, ...meta }); + return this.getTask(taskId)!; + } + + // ---------- Approvals(审批闸) ---------- + private listApprovals(taskId: string): ApprovalRecord[] { + const rows = this.db.prepare(`SELECT * FROM approvals WHERE task_id = ? ORDER BY at`).all(taskId) as ApprovalRow[]; + return rows.map(rowToApproval); + } + + /** 列出所有处于审批闸状态(plan_review/spec_review/exec_review)的任务。 */ + pendingApprovals(projectId?: string): Task[] { + const sql = projectId + ? `SELECT * FROM tasks WHERE project_id = ? AND status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at` + : `SELECT * FROM tasks WHERE status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at`; + const rows = (projectId + ? this.db.prepare(sql).all(projectId) + : this.db.prepare(sql).all()) as TaskRow[]; + return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); + } + + /** + * 处理一次审批。accept/reject 必须发生在闸状态上;reject 必须带 reason(改进意见)。 + * accept:plan_review→decomposed, spec_review→ready, exec_review→done。 + * reject:plan_review→analyzing, spec_review→speccing, exec_review→ready(返工)。 + */ + decide(taskId: string, action: 'accept' | 'reject', actor: string, reason?: string | null): Task { + const row = this.getTaskRow(taskId); + if (!row) throw new StoreError(`任务不存在: ${taskId}`); + const from = row.status as TaskStatus; + const gate: GateKind | null = gateOf(from); + if (!gate) throw new StoreError(`任务不在审批闸状态:当前 ${STATUS_LABEL[from]}(${from})`); + if (action === 'reject' && !reason?.trim()) { + throw new StoreError('reject 必须填写改进意见'); + } + + const to: TaskStatus = action === 'accept' + ? ({ plan: 'decomposed', spec: 'ready', exec: 'done' } as const)[gate] + : ({ plan: 'analyzing', spec: 'speccing', exec: 'ready' } as const)[gate]; + + const txn = this.db.transaction(() => { + const ap: ApprovalRow = { + id: id('apr'), task_id: taskId, gate, action, actor, reason: reason ?? null, at: now(), + }; + this.db.prepare( + `INSERT INTO approvals (id,task_id,gate,action,actor,reason,at) VALUES (@id,@task_id,@gate,@action,@actor,@reason,@at)`, + ).run(ap); + this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`).run(to, now(), taskId); + }); + txn(); + this.emit(row.project_id, taskId, action === 'accept' ? 'approval.granted' : 'approval.rejected', { gate, from, to, reason: reason ?? null }); + return this.getTask(taskId)!; + } + + // ---------- Runs ---------- + startRun(taskId: string, kind: Run['kind'], fields: Partial> = {}): Run { + const row = this.getTaskRow(taskId); + if (!row) throw new StoreError(`任务不存在: ${taskId}`); + const rr: RunRow = { + id: id('run'), task_id: taskId, kind, worktree: fields.worktree ?? null, branch: fields.branch ?? null, + status: 'started', started_at: now(), ended_at: null, transcript_ref: null, claude_session_id: null, error: null, + }; + this.db.prepare( + `INSERT INTO runs (id,task_id,kind,worktree,branch,status,started_at,ended_at,transcript_ref,claude_session_id,error) + VALUES (@id,@task_id,@kind,@worktree,@branch,@status,@started_at,@ended_at,@transcript_ref,@claude_session_id,@error)`, + ).run(rr); + this.emit(row.project_id, taskId, 'run.started', { runId: rr.id, kind }); + return rowToRun(rr); + } + + finishRun(runId: string, status: Run['status'], fields: Partial> = {}): Run { + const row = this.db.prepare(`SELECT * FROM runs WHERE id = ?`).get(runId) as RunRow | undefined; + if (!row) throw new StoreError(`run 不存在: ${runId}`); + this.db.prepare( + `UPDATE runs SET status = ?, ended_at = ?, transcript_ref = ?, claude_session_id = ?, error = ? WHERE id = ?`, + ).run(status, now(), fields.transcriptRef ?? row.transcript_ref, fields.claudeSessionId ?? row.claude_session_id, fields.error ?? row.error, runId); + const task = this.getTaskRow(row.task_id); + if (task) this.emit(task.project_id, row.task_id, 'run.finished', { runId, status }); + return rowToRun(this.db.prepare(`SELECT * FROM runs WHERE id = ?`).get(runId) as RunRow); + } + + listRuns(taskId: string): Run[] { + const rows = this.db.prepare(`SELECT * FROM runs WHERE task_id = ? ORDER BY started_at`).all(taskId) as RunRow[]; + return rows.map(rowToRun); + } + + // ---------- Events ---------- + listEvents(projectId: string, limit = 200): Event[] { + const rows = this.db.prepare( + `SELECT * FROM events WHERE project_id = ? ORDER BY at DESC LIMIT ?`, + ).all(projectId, limit) as EventRow[]; + return rows.map(rowToEvent).reverse(); + } + + /** + * 取下一个可执行任务(叶子、ready、依赖全部 done)。供编排器领取。 + * 被拆解的 Hard 容器任务不会是 ready(停在 decomposed),天然排除。 + */ + nextExecutable(projectId: string): Task | null { + const rows = this.db.prepare( + `SELECT * FROM tasks WHERE project_id = ? AND status = 'ready' ORDER BY priority DESC, created_at`, + ).all(projectId) as TaskRow[]; + for (const r of rows) { + if (this.childrenOf(r.id).length > 0) continue; // 非叶子跳过 + const deps = JSON.parse(r.deps) as string[]; + const depsDone = deps.every((d) => this.getTaskRow(d)?.status === 'done'); + if (!depsDone) continue; + return rowToTask(r, this.listApprovals(r.id)); + } + return null; + } +} diff --git a/test/store.test.ts b/test/store.test.ts new file mode 100644 index 0000000..dc5545f --- /dev/null +++ b/test/store.test.ts @@ -0,0 +1,128 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { Store, StoreError } from '../src/store/index.js'; + +function freshStore(): Store { + return new Store(':memory:'); +} + +test('project + task creation, complexity routes initial status', () => { + const s = freshStore(); + const p = s.createProject({ name: 'demo', repoPath: '/tmp/demo-' + Math.random() }); + assert.equal(p.status, 'active'); + + const hard = s.createTask({ projectId: p.id, title: 'big', complexity: 'hard' }); + const medium = s.createTask({ projectId: p.id, title: 'mid', complexity: 'medium' }); + const easy = s.createTask({ projectId: p.id, title: 'small', complexity: 'easy' }); + assert.equal(hard.status, 'analyzing'); // Hard 强制分析 + assert.equal(medium.status, 'speccing'); // Medium 写方案 + assert.equal(easy.status, 'ready'); // Easy 直接可执行 + s.close(); +}); + +test('Easy 路径:写操作 → ready → nextExecutable 领取', () => { + const s = freshStore(); + const p = s.createProject({ name: 'e', repoPath: '/tmp/e-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 'tweak', complexity: 'easy' }); + s.setOperations(t.id, '改 README 一行'); + const next = s.nextExecutable(p.id); + assert.equal(next?.id, t.id); + s.close(); +}); + +test('Medium 闸:spec_review accept → ready;reject 必带意见', () => { + const s = freshStore(); + const p = s.createProject({ name: 'm', repoPath: '/tmp/m-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 'feat', complexity: 'medium' }); + s.setSpec(t.id, '加一个接口,因为前端需要'); + const inReview = s.transition(t.id, 'spec_review'); + assert.equal(inReview.status, 'spec_review'); + + // reject 无意见 → 报错 + assert.throws(() => s.decide(t.id, 'reject', 'user'), StoreError); + // reject 带意见 → 回到 speccing + const back = s.decide(t.id, 'reject', 'user', '方案漏了鉴权'); + assert.equal(back.status, 'speccing'); + assert.equal(back.approvals.at(-1)?.reason, '方案漏了鉴权'); + + // 重新评审 → accept → ready + s.transition(t.id, 'spec_review'); + const ok = s.decide(t.id, 'accept', 'user'); + assert.equal(ok.status, 'ready'); + s.close(); +}); + +test('Hard 闸:plan_review accept → decomposed,子任务汇总', () => { + const s = freshStore(); + const p = s.createProject({ name: 'h', repoPath: '/tmp/h-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 'epic', complexity: 'hard' }); + s.setPlan(t.id, '分析…拆 2 个子任务'); + s.transition(t.id, 'plan_review'); + const decomposed = s.decide(t.id, 'accept', 'user'); + assert.equal(decomposed.status, 'decomposed'); + + const c1 = s.createTask({ projectId: p.id, parentId: t.id, title: '子1', complexity: 'easy' }); + assert.equal(c1.depth, 2); + // 拆解后容器不应被 nextExecutable 领取(它有子任务且非 ready) + s.setOperations(c1.id, '做子1'); + const next = s.nextExecutable(p.id); + assert.equal(next?.id, c1.id); // 取到叶子,不是容器 + s.close(); +}); + +test('层级上限:medium 默认 3 层,第 4 层拒绝', () => { + const s = freshStore(); + const p = s.createProject({ name: 'd', repoPath: '/tmp/d-' + Math.random() }); + const l1 = s.createTask({ projectId: p.id, title: 'l1', complexity: 'medium' }); + const l2 = s.createTask({ projectId: p.id, parentId: l1.id, title: 'l2', complexity: 'medium' }); + const l3 = s.createTask({ projectId: p.id, parentId: l2.id, title: 'l3', complexity: 'medium' }); + assert.equal(l3.depth, 3); + assert.throws(() => s.createTask({ projectId: p.id, parentId: l3.id, title: 'l4', complexity: 'medium' }), StoreError); + s.close(); +}); + +test('非法状态流转被守卫拒绝', () => { + const s = freshStore(); + const p = s.createProject({ name: 'g', repoPath: '/tmp/g-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); // ready + assert.throws(() => s.transition(t.id, 'done'), StoreError); // ready→done 非法 + s.close(); +}); + +test('exec_review 结果闸:accept → done', () => { + const s = freshStore(); + const p = s.createProject({ name: 'x', repoPath: '/tmp/x-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 'run', complexity: 'easy' }); + s.setOperations(t.id, 'op'); + s.transition(t.id, 'queued'); + s.transition(t.id, 'executing'); + s.setResult(t.id, { branch: 'maestro/run', worktree: '/wt', diffSummary: '+1 -0', commits: ['abc'], prUrl: null }); + s.transition(t.id, 'exec_review'); + const done = s.decide(t.id, 'accept', 'user'); + assert.equal(done.status, 'done'); + s.close(); +}); + +test('依赖未满足时不可领取', () => { + const s = freshStore(); + const p = s.createProject({ name: 'dep', repoPath: '/tmp/dep-' + Math.random() }); + const a = s.createTask({ projectId: p.id, title: 'A', complexity: 'easy' }); + const b = s.createTask({ projectId: p.id, title: 'B', complexity: 'easy', deps: [a.id] }); + s.setOperations(a.id, 'a'); s.setOperations(b.id, 'b'); + // 优先级相同时 A 先建,但 B 依赖 A 未 done → 只能取到 A + const next = s.nextExecutable(p.id); + assert.equal(next?.id, a.id); + s.close(); +}); + +test('事件订阅:状态变更广播', () => { + const s = freshStore(); + const got: string[] = []; + s.subscribe((e) => got.push(e.type)); + const p = s.createProject({ name: 'ev', repoPath: '/tmp/ev-' + Math.random() }); + const t = s.createTask({ projectId: p.id, title: 't', complexity: 'easy' }); + s.transition(t.id, 'queued'); + assert.ok(got.includes('task.created')); + assert.ok(got.includes('status.changed')); + s.close(); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..92814a9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +}