Compare commits

..

10 Commits

Author SHA1 Message Date
lanrtop
0552180e45 docs(blueprint): agent 友好基础设施方案 + 炼境自身接入包 + 蓝图规范 v1.8.0
- CONVENTIONS.md v1.8.0:蓝图定位为意图层,files 字段变可选,status/progress 从 git 推断
- 新增 agent-infrastructure 模块(concept):接入包 v2,扫描→确认→生成三件套
- AGENTS.md.tmpl 注入四支柱 + T1-T4 防腐元规则
- 炼境自身接入自己的包:AGENTS.md + docs/ai-context/ + .claude/rules/
- docs/templates/ 三件套模板:agents-meta-rules / project-brief / project-context
2026-06-29 16:38:45 +09:00
lanrtop
ecb5620abb chore(blueprint): onboarding-pack 模块完成(接入包 A-D 全部就绪) 2026-06-19 13:53:13 +09:00
lanrtop
58d2c79c74 feat(onboarding): 接入包数据可信度提示(commit 规范化比例)
get_commit_stats 暴露项目 commit 规范化统计(total/conventional/rework);治理面板常驻展示规范化比例 + 返工数,比例 <60% 提示种 lefthook。接入包 A/B/C1/C2/D 全部完成。
2026-06-19 13:52:09 +09:00
lanrtop
150535ffcf feat(onboarding): 接入包前端 UI——治理面板升级为应用接入包
治理面板 buff 区改为应用接入包:调 apply_onboarding_pack(种 3 模板 + 挂 buff),新增模板分发结果展示(written/skipped/failed)。接入包 A/B/C1/C2 就绪,剩 D 可信度提示。
2026-06-19 13:07:09 +09:00
lanrtop
ee2a37876e fix(dashboard): 移除 ProjectCard 未使用的 role 解构(typecheck) 2026-06-19 13:07:09 +09:00
lanrtop
5f8b2e8bec feat(onboarding): 接入包模板分发 + apply_onboarding_pack 后端
dispatch_templates 向项目种入 AGENTS.md/lefthook.yml/git-workflow.md 标准模板(蓝本取自 enterprise-system,已通用化),不存在才写、绝不覆盖用户已有;apply_onboarding_pack 组合模板分发 + apply_blueprint_buff 返回 OnboardingReport。含 3 内嵌模板 + 不覆盖单测 + commands.ts 封装。前端 UI 待接。
2026-06-19 13:00:22 +09:00
lanrtop
3befc50d20 feat(flywheel): L0 从 git 自吸执行质量(commit_metrics)
buff watcher 解析每条新 commit 的 conventional type/scope,将返工(fix)/CI自修信号写入 commit_metrics 表;append_usage_snapshot 的 rework_count 改由此表聚合,取代枯竭的复盘笔记解析。avg_rounds_M 暂留空(需 PR 边界,待档2)。含 parse_conventional 纯解析器 + 11 单测、project_id 索引、事务批量写入、首次限 50 条。
2026-06-19 12:23:40 +09:00
lanrtop
076bba7def chore(blueprint): 更新飞轮 usage.json 快照 2026-06-19 11:39:12 +09:00
lanrtop
2ed548556c docs(blueprint): 固化统一 Gitea 双层架构 + 接入包 architect 设计
战略转向统一自建 Gitea、冻结 GitHub;新增 onboarding-pack(两档接入包 + 基线 architect 设计 4 卡)与 gitea-integration(风筝线模型 + webhook);flywheel 转向 git 自吸 + 新增 L0 commit 解析层
2026-06-19 11:39:11 +09:00
lanrtop
5b28cb351c fix: dev 端口 1420→1430 避免占用冲突 2026-06-19 11:39:11 +09:00
34 changed files with 4946 additions and 61 deletions

View File

@ -1,6 +1,6 @@
---
version: 1.7.0
updated: 2026-05-06
version: 1.8.0
updated: 2026-06-29
source: dev-manager-tauri
---
@ -22,6 +22,26 @@ source: dev-manager-tauri
└── <module-id>.md ← 每个模块的详情 + 任务卡
```
## 蓝图定位(意图层)
蓝图只记录 git 永远无法自动产生的三类信息:
| 类型 | 内容 | 为什么 git 做不到 |
|------|------|----------------|
| **结构** | 模块列表 + 关系图dependency/related | git 只知道文件,不知道模块意图和边界 |
| **意图** | 为什么做 + 验收标准 + 复杂度 + 复盘笔记 | commit 记录"做了什么",不记录"为什么" |
| **规划** | concept / planned 方向 | 还没有代码git 里什么都没有 |
**不属于蓝图的**(交给 git 信号,蓝图不手动维护):
- 任务状态in_progress / done→ 从 branch / commit 推断
- 涉及文件files→ 从 git diff 读取,蓝图里为可选提示
- 进度数字progress→ 从任务卡完成比例自动算
- 更新时间updated→ 从 git log 读取
> 蓝图是项目的"架构意图层",不是任务追踪系统。任务追踪的职责交还给 git。
---
## 何时更新蓝图
| 事件 | 操作 |
@ -69,7 +89,7 @@ modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
### 📋 任务标题
- status: todo
- complexity: M
- files: src/path/to/file.tsx, src-tauri/src/commands/xxx.rs
- files: src/path/to/file.tsx(可选,规划时填预期路径;实际文件由 git diff 追踪,无需回填)
- depends: other-module-id
- acceptance: 简要描述完成标准
- blocked_reason: (仅 status: blocked 时填写)执行受阻的具体原因
@ -79,8 +99,6 @@ modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
详细说明(可选)。
```
> **files 回填规则**:任务卡的 `files` 在规划时填入预期路径。实现完成后,若实际文件与规划有出入(合并、拆分、重命名),须在标记 ✅ done 之前将 `files` 更新为实际路径——这保证下次续接或派发时上下文准确。
### 状态前缀
| 前缀 | status | 含义 |
@ -95,9 +113,10 @@ modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
任务卡同时满足以下条件时标记为 📋:
1. status 为 `todo`
2. 有明确的 `files`(涉及哪些文件)
3. 有明确的 `acceptance`(验收标准)
4. complexity 为 `S``M`
2. 有明确的 `acceptance`(验收标准)
3. complexity 为 `S``M`
`files` 为可选提示字段,有则帮助 agent 更快定位,无则从 git diff 推断,不作派发门槛。
不满足条件的 todo 任务保持无前缀,说明还需补充信息。
@ -130,8 +149,8 @@ updated: YYYY-MM-DD
- id: kebab-case-id # 唯一标识,同时是 modules/ 下的文件名
name: 显示名称
area: frontend # 所属领域 ID
status: done # done / in_progress / planned / concept
progress: 100 # 0~100done 自动视为 100
status: done # planned/concept 手动设in_progress/done 优先从 git 信号推断,人工确认
progress: 100 # 由任务卡完成比例自动算无需手填App 根据 done 卡比例渲染
position: [x, y] # React Flow 画布坐标(可选)
```
@ -247,7 +266,7 @@ Claude 在每次对话中应遵循以下流程:
|----------|--------------|
| "我想加个 XX 功能" | 确认需求后manifest 加模块 (status: concept),创建 modules/<id>.md |
| "XX 功能需要 A、B、C 三步" | 在模块文件中拆分为 3 张任务卡 |
| "这个方案确定了" | 模块 status → planned补充任务卡的 files/acceptance |
| "这个方案确定了" | 模块 status → planned补充任务卡的 acceptancefiles 可选,有则更准确) |
| "开始做 XX" | 模块 status → in_progress |
| "为什么选这个方案" | 记录到模块的 `## 决策记录` |
@ -402,7 +421,7 @@ rule_effectiveness: R10=未验证(任务开始前未触发,事后归因)
}
```
- `dispatched` = 当前满足📋可派发条件的任务数(有 files + acceptance + complexity S/M
- `dispatched` = 当前满足📋可派发条件的任务数(有 acceptance + complexity S/Mfiles 可选
- 节流:同一 UTC 日期只写一条,避免多次打开导致数据膨胀
- 文件存于项目本地,不上传服务器
@ -529,17 +548,17 @@ rule_effectiveness: R10=未验证(任务开始前未触发,事后归因)
2. 按以下原则拆解:
- **粒度**:每张卡 complexity 必须是 S 或 M不允许 L
- **依赖顺序**:数据层 → 后端命令 → 前端封装 → UI 组件
- **可派发标准**:每张卡必须有 `files` + `acceptance` + `complexity`
- **可派发标准**:每张卡必须有 `acceptance` + `complexity``files` 建议填入预期路径,有则更准确
3. 将任务卡追加到 `modules/<id>.md`
4. 更新 `manifest.yaml`status → `planned`,更新 `updated` 日期
5. 输出拆解摘要卡名、complexity、执行顺序
### 质量检查
- [ ] 每张卡有 `files`
- [ ] 每张卡有 `acceptance`
- [ ] complexity 全部是 S 或 M
- [ ] 有依赖的卡填了 `depends`
- [ ] 任务卡按执行顺序排列
- [ ] `files` 尽量填入(建议项,有则帮助 agent 更快定位)
---

View File

@ -6,7 +6,7 @@ description: >
成为每个项目的贴身导师。炼境本身亦被纳入其中,
形成可自我进化的开发智能闭环。
iteration: 1
updated: 2026-04-19
updated: 2026-06-29
@ -106,14 +106,14 @@ modules:
- id: github-auth
name: GitHub OAuth
area: backend
status: done
status: deprecated
progress: 100
position: [250, 350]
- id: github-publish
name: GitHub 发布
area: backend
status: done
status: deprecated
progress: 100
position: [500, 350]
@ -363,6 +363,28 @@ modules:
progress: 100
position: [3250, -150]
# ── 统一 Gitea 接入包2026-06-19 战略转向)──────
- id: onboarding-pack
name: 接入包
area: backend
status: done
progress: 100
position: [3500, 0]
- id: gitea-integration
name: Gitea 集成
area: backend
status: concept
progress: 0
position: [3500, 150]
- id: agent-infrastructure
name: Agent 友好基础设施生成
area: backend
status: concept
progress: 0
position: [3750, 0]
edges:
# 前端依赖
- from: workspace-mgmt
@ -572,3 +594,29 @@ edges:
- from: cloud-products
to: cloud-db-instances
type: dependency
# 接入包(统一 Gitea 双层架构)
- from: blueprint-buff
to: onboarding-pack
type: dependency
- from: mcp-config-inject
to: onboarding-pack
type: dependency
- from: blueprint-onboarding
to: onboarding-pack
type: dependency
- from: onboarding-pack
to: gitea-integration
type: dependency
- from: onboarding-pack
to: agent-infrastructure
type: dependency
- from: devtools-scan
to: agent-infrastructure
type: dependency
- from: project-env-scan
to: agent-infrastructure
type: dependency
- from: gitea-integration
to: flywheel-intelligence
type: related

View File

@ -0,0 +1,104 @@
# Agent 友好基础设施生成
接入包 v2 的核心扩展:从"分发固定模板"升级为"根据技术栈定制生成 Agent 友好基础设施"。
炼境扫描项目技术栈 → 用户确认 → 生成三件套(元规则 + 契约映射 + 工具派生格式)。
## 定位
现有接入包v1解决了"让项目被炼境感知"的问题。
本模块解决"让 agent 在项目里真正高效工作"的问题。
两者的关系:
- v1 是注册入炼境飞轮的门票
- v2 是项目内部 agent 工作环境的基础设施
## 生成的三件套
| 产物 | 用途 | 单一来源 |
|------|------|---------|
| `AGENTS.md`(元规则段落) | 跨工具通用的防腐约定 | 炼境统一版本,接入时写入 |
| `docs/ai-context/project-context.yaml` | 技术栈特定的契约路径映射 | 炼境扫描后生成,技术栈变更时重新生成 |
| `.claude/rules/doc-freshness.md` | Claude Code 的路径注入派生版 | 从 AGENTS.md 元规则自动派生 |
## 接入流程(扫描 → 确认 → 生成)
```
① 炼境扫描
package.json / Cargo.toml / go.mod / Dockerfile
→ 识别框架、ORM、API 层、monorepo 结构
② 展示给用户确认(不跳过此步)
"检测到React 19 + Drizzledrizzle/schema.ts+ Honosrc/routes/
是否正确?"
③ 用户确认或手动修正
④ 定制化生成三件套
```
## 技术栈 → 契约路径映射规则
炼境内置识别表(示例,可扩展):
| 识别条件 | 契约类型 | 默认路径 |
|---------|---------|---------|
| `prisma` in dependencies | 数据库 schema | `prisma/schema.prisma` |
| `drizzle-orm` in dependencies | 数据库 schema | `drizzle/schema.ts` |
| `hono` / `express` | API 接口 | `src/routes/**` |
| `openapi` / `swagger` | API 接口 | `openapi.yaml` |
| Rust + `axum` | API 接口 | `src/routes/` |
| pnpm workspace | 共享类型 | `packages/types/**` |
## 腐化监控(接入后持续运行)
炼境定期读取 git log对比文档文件 vs 代码文件的最后修改时间:
| 风险级别 | 判断条件 |
|---------|---------|
| 🟢 正常 | 文档与代码同步更新 |
| 🟡 注意 | 核心代码改了,文档 14 天未动 |
| 🔴 高风险 | 外部契约类文件改了AGENTS.md 30 天未动 |
技术栈变更感知:`package.json` / `Cargo.toml` 变化时主动提示"建议重新运行接入包生成"。
## 设计原则
- 元规则不含具体路径(技术栈无关),`project-context.yaml` 含路径(技术栈特定)
- 炼境生成,不要求用户手动维护多份格式
- 修复腐化永远是本地 agent 的事,炼境只检测和提醒
## 决策记录
- 元规则选用四支柱 + T1-T4 触发类型的双层结构:四支柱给 agent 判断"这个项目 ok 吗"T1-T4 给 agent 判断"这次改动需要更新什么"
- project-context.yaml 选 YAML 而非写进 AGENTS.md技术栈映射是易变信息独立文件符合防腐铁律不写死进常驻文档
- 确认步骤不可跳过:项目可能正在迁移技术栈,扫描结果不一定是当前权威状态
## 参考验证
2026-06-29 用 enterprise-system 做模拟验证:
- 技术栈识别准确React + Drizzle + Hono + PostgreSQL
- 最近 git log 有 "chore(db): 彻底移除 Prisma全面统一 Drizzle",完美复现 T3 工具链变更场景
- project-context.yaml 不存在,验证了它需要炼境主动生成
- .claude/rules/ 已有 6 个路径注入规则,验证了派生格式的可行性
## 任务卡
> 本模块为 L 级,需先走 /architect 设计再拆分。以下为 concept 阶段的方向记录。
### 💭 技术栈扫描与确认 UI
- status: concept
### 💭 project-context.yaml 生成器
- status: concept
### 💭 AGENTS.md 元规则段落注入
- status: concept
### 💭 .claude/rules/ 派生生成
- status: concept
### 💭 腐化风险看板(文档新鲜度评分)
- status: concept
### 💭 技术栈变更感知与重新生成提示
- status: concept

View File

@ -10,10 +10,16 @@
本模块分三个阶段逐步打通数据→改善的完整闭环。
## 方向修订2026-06-19
阶段一/二依赖「AI 主动写结构化复盘笔记」作为执行质量数据源。实测发现该进料口几乎枯竭70 条笔记仅炼境自身 4 条结构化,外部项目存量为旧格式,`rework_count`/`avg_rounds_M` 长期为 0/null。
**根本转向**执行质量数据从「AI 主动上报」改为「炼境从 git 被动自吸」——解析 conventional commit 的 typefix=返工)/scope=模块)自动计算,客户端无关、工作流无关。新增 **L0 解析层**(见任务卡)作为阶段三预测层的数据地基。详见 memory [[project_gitea_unified_architecture]] 及 onboarding-pack / gitea-integration 模块。
## 阶段触发条件
- **阶段二启动条件**:阶段一上线后,梦核输出中稳定出现"高频陷阱摘要"板块,且跨项目复盘笔记数量 ≥ 20 条
- **阶段三启动条件**:阶段二运行 ≥ 3 个月accumulated 数据覆盖 ≥ 15 个项目usage.json 携带 rework_count 和 avg_rounds 字段
- **阶段三启动条件(已修订)**L0 解析层上线后conventional commit 执行质量数据稳定覆盖 ≥ 10 个项目(不再依赖复盘笔记积累时长,见上「方向修订」)
---
@ -93,6 +99,17 @@
---
### ✅ L0Conventional Commit 执行质量解析层(数据地基,先于阶段三)
- status: done2026-06-19实现见 onboarding-pack 卡 C1+C2
- complexity: L
- depends: onboarding-pack项目须先种 lefthook + conventional 规范)
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/commands/buff.rs, src-tauri/src/commands/blueprint.rs, src-tauri/src/db.rs
- acceptance: 炼境后端从本地 git 解析 conventional commit —— type=fix 计返工、scope 映射模块、commit 数≈轮数;自动填充 usage.json 的 rework_count/avg_rounds_M/complexity_dist取代「从复盘笔记解析」的枯竭路径CI 自修 commit`[DeepSeek-V3]`须从返工率剔除squash merge 项目须降级处理
> 背景实测复盘笔记进料口枯竭70 条仅 4 条结构化)。改由 git 自吸,客户端无关、工作流无关,是阶段三预测层的真实数据基础。
---
### 阶段三:执行质量预测层
- status: todo
- complexity: L

View File

@ -0,0 +1,55 @@
# Gitea 集成
对接已有的自建 Gitea 实例,作为所有被管理项目的统一 git 后端。接入包「增配档」的核心。
## 风筝线模型(炼境与 Gitea 的关系)
🪁 **风筝 = Gitea 本体**git 存储 + CI 执行):独立部署、独立运行,炼境**不托管其进程、不背运维**。
🧵 **线 = 炼境的掌控**(控制面):炼境是**唯一持线人**——Gitea 连接/token 统一存炼境,建仓/关联/迁移/配置 lefthook&CI 的操控全经炼境,子项目对 Gitea 的动作都从它手里过。
👀 **关注风筝的动态 = 观测面**(飞轮燃料):炼境持续感知每个子项目 ↔ Gitea 的连接动态push 的 commit type/scope、PR、CI 三阶段、review、停滞项目
> 「线」≠ 松散 API 对接:炼境是**主动掌控者**而非被动客户端。风筝越多 → 线越多 → 动态越丰富 → 飞轮越强(契合核心愿景)。
## 其他边界
- 替代炼境现有 GitHub 能力:`github-auth` / `github-publish` 已冻结,`cicd.rs` 的 GitHub Actions 生成改造为 Gitea Actions
- 炼境自身仓库也迁 Gitea发布产物仍走 OSS updater 不变)
## 参考实现
`enterprise-system` 已趟通完整链路,属「照搬」而非「从零探索」:
- Gitea origin`git.yljt.info`+ Gitee 镜像(备份)
- lefthook 强制 conventional commit + typecheckpre-commit
- Gitea Actions 三阶段 CI静态检查 → 测试DeepSeek-V3 自修)→ PR code reviewDeepSeek
- Gitea Actions 语法兼容 GitHub Actions迁移成本低
> 详见 `E:/code-project/enterprise-system/docs/ai-context/git-workflow.md``AGENTS.md`
## 任务卡concept待 /architect
### 💭 Gitea API 对接(握线基础)
- status: concept
- complexity: L
- acceptance: TBD —— 建仓/关联、推送、读 CI 状态、读 PR review 结果
### 💭 观测面Gitea webhook 接入(持线人感知动态)
- status: concept
- complexity: L
- files: src-tauri/src/mcp_server.rs内嵌 HTTP server 加 webhook 端点), src-tauri/src/commands/blueprint.rs
- acceptance: TBD —— 复用炼境内嵌 HTTP server 新增 Gitea webhook 接收端点;实时接住各子项目 push/PR/CI/review 事件 → 解析 conventional commit对接 flywheel-intelligence 的 L0 解析层)→ 写飞轮快照 + 刷新连接动态webhook 不可达时降级为定时轮询 Gitea API
### 💭 Gitea Actions workflow 生成
- status: concept
- complexity: M
- files: src-tauri/src/commands/cicd.rs改造
- acceptance: TBD —— 生成 Gitea Actions 三阶段 CI 模板(语法兼容 GitHub Actions
### 💭 DeepSeek CI review 集成(可选插件)
- status: concept
- complexity: L
- acceptance: TBD —— 静态检查 + 测试自修 + PR review作为复杂项目可选增配不进基线档
### 💭 旧项目 GitHub → Gitea 迁移
- status: concept
- complexity: M
- acceptance: TBD —— remote 切换 + 推送,安全增量,不丢历史

View File

@ -1,10 +1,12 @@
---
id: github-auth
name: GitHub OAuth
status: done
status: deprecated
progress: 100
---
> ❄️ **已冻结2026-06-19**:全面统一自建 Gitea弃用 GitHub。功能保留不删但不再演进新接入走 onboarding-pack / gitea-integration 模块。
## 概述
GitHub OAuth Device Flow 认证,获取 token 后支持 API 操作和 Git 推送。

View File

@ -1,5 +1,7 @@
# GitHub 发布
> ❄️ **已冻结2026-06-19**:全面统一自建 Gitea弃用 GitHub。功能保留不删不再演进项目发布走 Gitea见 gitea-integration 模块)。
在炼境内通过 GitHub OAuth 直接将本地项目推送到远程仓库,支持新建仓库或关联已有仓库,完成发布后自动回填 remote URL。
## 决策记录

View File

@ -0,0 +1,99 @@
# 接入包Onboarding Pack
炼境向被管理项目一键分发的标准化接入套件,让任意新旧项目成为「飞轮合格公民」。
统一以自建 Gitea 为 git 数据底座,缝合 git 规范、AI 约定、蓝图语义、炼境登记四层。
## 双层架构
- **自建 Gitea**:统一 git 数据底座稳定、自主、CI + review 自控)
- **炼境**:底座之上的「项目飞轮控制台」——读 git 数据做跨项目飞轮聚合、分发接入包、维护蓝图语义层
> 决策2026-06-19全面统一自建 Gitea弃用 GitHub。数据主权 = 飞轮燃料自主可控,契合「管理越多→数据越多→飞轮越强」愿景。详见 memory [[project_gitea_unified_architecture]]。
## 蓝图重新定位git 与蓝图分工)
> 完整定义见 `.blueprint/CONVENTIONS.md` §「蓝图定位(意图层)」。
核心分工git 记录「做了什么」,蓝图只保留 git 给不了的三类信息(结构关系图 / 意图与验收标准 / 未来规划)。状态/进度/涉及文件从 git 信号推断,蓝图不手动双写。
## 接入包分档(底座统一,上层按需)
| 档 | 内容 | 适用 |
|----|------|------|
| **基线**(所有项目) | Gitea 建仓/迁移 + origin=Gitea + lefthook(强制 conventional) + AGENTS.md + git-workflow.md + 炼境登记 + buff 解析 commit | 全部新旧项目 |
| **增配**(复杂项目) | + Gitea Actions CI + DeepSeek review + 瘦蓝图 | 长周期/多业务线 |
## 决策记录
- 飞轮升格与 Gitea API 对接「分步实施」而非耦合buff 读本地 `.git`commit type/scope与远程平台无关可先做Gitea APICI 状态/PR review随后
- 「完整分发」是陷阱,正确形态是分级菜单:新项目种全套,旧项目增量接入(复用 mcp_inject 的「命名空间隔离 + 合并不覆盖」语义,不动用户已有配置)
- 接入动作:现有 `apply_blueprint_buff` 升格为 `apply_onboarding_pack`,按档编排注入
- 语义层进料口隐患:决策/验收/踩坑仍需主动写(与饿死复盘笔记同源),须配「从 PR 标题/commit body/review 评论半自动提取」,不再单纯指望人自觉
## 复用的现有能力(~60% 地基已具备)
| 能力 | 现状 |
|------|------|
| `.claude/settings.json` 注入 | ✅ `mcp_inject`(命名空间隔离 + 合并不覆盖 + JSON 校验) |
| 生成接入 prompt / 扫描项目 | ✅ `generate_blueprint_prompt`(已读 AGENTS.md + monorepo 感知) |
| git 轮询 → 状态同步 | 🟡 `buff`(仅匹配文件路径,需补 conventional 解析,见 flywheel-intelligence L0 |
| CONVENTIONS 规则同步 | ✅ `sync_blueprint_rules` |
| 项目模板 | ✅ `templates` |
| CI workflow 生成 | 🟡 `cicd.rs`(仅 GitHub需改造 Gitea Actions见 gitea-integration |
## 风险
- 旧项目存量 git 数据脏(无 scope、非 conventional→ 数据质量是光谱,需「采用规范后切起算」或标不可信
- 跨项目聚合可比性:不同项目 scope 粒度/复杂度标准不同fix 率直接相加会误导梦核
- 模板自身维护成本AGENTS.md/lefthook/git-workflow.md 模板要随最佳实践演进
## 设计architect 2026-06-19
基线档范围:模板分发 + 炼境登记 + L0 commit 解析,**全部本地、不依赖 Gitea API**Gitea 建仓/Actions/webhook 属 gitea-integration 档2
关键决策:
- **新建 `apply_onboarding_pack`**(不扩展 buffbuff=挂 git 观测onboarding_pack=完整接入,分层清晰
- **模板"存在则跳过 + 报告"**markdown/yaml 不强行整文件合并,绝不覆盖用户已有(复用 mcp_inject 的路径解析 + 不存在才写)
- **L0 commit 解析挂在现有 buff watcher**`last_commit..HEAD` 取 message → parse_conventional → 写新表 `commit_metrics`;与文件路径匹配两条路径并行互不干扰
- **rework_count 来源**从枯竭的 `parse_notes_metrics` 换成查 `commit_metrics`fix 且非 CI 自修);`avg_rounds_M` 基线暂为 null需 PR 边界留档2
- **scope 原样聚合**module 映射后置
新增:`commands/onboarding.rs`、`commands/commit_metrics.rs`、`resources/onboarding/*.tmpl`、`commit_metrics` 表
修改:`buff.rs`(check_project 加解析)、`blueprint.rs`(append_usage_snapshot)、`db.rs`、`lib.rs`、前端入口
## 任务卡
> 已过 /architect + /splitter。A→B、C1→C2 两条链可并行D 依赖 C1+B**C1飞轮地基建议优先开工**。
### ✅ A 模板分发引擎
- status: done
- complexity: M
- files: src-tauri/src/commands/onboarding.rs, src-tauri/resources/onboarding/{AGENTS.md,lefthook.yml,git-workflow.md}.tmpl
- acceptance: include_str! 内嵌 3 个标准模板(蓝本取自 enterprise-system分发时文件不存在→写入、已存在→跳过并在报告标注绝不覆盖复用 mcp_inject 的路径解析(含 WSL返回每个模板的 written/skipped/failed
### ✅ B apply_onboarding_pack 编排 + 前端入口
- status: done
- complexity: M
- depends: onboarding-pack卡A
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/lib.rs, src/lib/commands.ts, src/components/dashboard/ProjectCard.tsx
- acceptance: apply_onboarding_pack(project_path, project_id) 组合「模板分发 + apply_blueprint_buff」返回 OnboardingReport看板卡片有"接入包"入口展示分发报告written/skipped/failed
### ✅ C1 commit_metrics 表 + Conventional 解析器
- status: done
- complexity: M
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/db.rs
- acceptance: 新增 commit_metrics 表sha PK / project_id / commit_type / scope / is_rework / is_ci_auto / committed_at / parsed_at`parse_conventional(message)` 解析 type/scopetype=fix→is_rework`[DeepSeek-V3]`/`[R1]` 等标记→is_ci_auto非 conventional→commit_type=null 单列;提供聚合查询 `rework_count(project_id)`;解析逻辑纯函数可单测(覆盖 feat/fix/带scope/无scope/CI自修/非规范 六种输入)
### ✅ C2 挂接 buff watcher + 飞轮快照
- status: done
- complexity: M
- depends: onboarding-pack卡 C1
- files: src-tauri/src/commands/buff.rs, src-tauri/src/commands/blueprint.rs
- acceptance: check_project 用 `last_commit..HEAD` 取 commit messagegit log复用现有 WSL 路径处理)→ 调 C1 解析写 commit_metrics首次接入last_commit 空)限解析最近 N 条append_usage_snapshot 的 rework_count 改查 commit_metricsis_rework=1 AND is_ci_auto=0取代 parse_notes_metrics
### ✅ D 数据可信度提示
- status: done
- complexity: S
- depends: onboarding-pack卡 C1、卡 B
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/commands/commit_metrics.rs
- acceptance: OnboardingReport 与飞轮展示标注该项目 commit 规范化比例conventional / 总数);比例低时提示"种 lefthook 后数据才准"避免脏数据被误读为可信CI 过滤 / 非规范单列已在 C1 落实)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
---
globs: src-tauri/src/db.rs
---
修改 `db.rs` 新增表或字段时,确认已在 `migrate()` 函数中补充对应 migration 语句;缺失会导致旧版本用户升级后启动报错(对应蓝图 R05 规则)。

View File

@ -0,0 +1,4 @@
---
globs: src-tauri/resources/onboarding/**
---
修改接入包模板时,检查 `docs/templates/agents-meta-rules.md` 是否需要同步更新——两者描述同一套元规则必须保持一致单一事实来源tmpl 是分发实体docs/templates/ 是设计草案)。

View File

@ -0,0 +1,4 @@
---
globs: src-tauri/src/commands/**
---
新增或修改 Tauri command 时,检查 `src/lib/commands.ts` 是否有对应前端封装;缺失则补写,否则前端无法调用(对应蓝图 R01 规则)。

111
AGENTS.md Normal file
View File

@ -0,0 +1,111 @@
# 炼境 — AI Agent 上下文
> 本文件遵循 [AGENTS.md 开放规范](https://agents.md/),供 Claude Code、Qwen Code、Cursor、Copilot 等 AI 工具读取。
## 项目定位
Git 多空间开发管理器,同时也是**项目 Agent 化改造引擎**。
以蓝图为神经系统,以 Claude 为智能层,感知所管理项目的功能运转、质量状态与成长方向。
炼境本身亦被纳入其中,形成可自我进化的开发智能闭环。
## 技术栈
- 前端React 19 + TypeScript + TailwindCSS + React Query + Zustand + React Flow
- 后端Rust + Tauri v2 + SQLiterusqlite + r2d2
- MCP Server内嵌于 Tauri 进程HTTP供 Claude Code 调用炼境数据
- 包管理pnpm禁止 npm/yarn/npx/bun
## 核心开发约束
**包管理**pnpm 唯一,禁止 npm / yarn / npx / bun。
**TypeScript**:优先 TypeScript避免 `any`;函数/变量 camelCase组件 PascalCaseasync/await不用裸 Promise 链try/catch 不吞异常。
**Rust 业务函数**:禁止在函数内部调用 `pool().get()`,连接由调用方传入(依赖注入,便于 in-memory DB 测试。Tauri command 层负责获取连接并调用(薄包装,无需测试)。
## 架构速查
```
src/lib/commands.ts 所有 Tauri command 的前端封装(唯一调用入口)
src-tauri/src/commands/ Rust 后端实现(每功能一个 .rs 文件)
src-tauri/src/db.rs SQLite 建表 + migration 逻辑
src-tauri/src/mcp_server.rs 内嵌 MCP Serveragent 调用炼境的入口)
src-tauri/src/mcp_inject.rs MCP 配置注入(写入被管理项目的 .claude/settings.json
src-tauri/resources/onboarding/ 接入包分发模板AGENTS.md/lefthook/git-workflow
.blueprint/ 项目蓝图manifest + 模块详情 + 任务卡)
docs/ai-context/ AI 上下文文档(速查卡、契约映射)
docs/templates/ 炼境接入包模板草案(设计层)
```
## 关键约定
1. **新增 Tauri command → 必须同步更新 `src/lib/commands.ts`**,否则前端无法调用
2. **SQLite 新增表或字段 → 必须在 `db.rs` 的 `migrate()` 里补 migration 语句**,否则旧版本升级后启动报错
3. **修改接入包模板(`resources/onboarding/`)→ 检查 `docs/templates/agents-meta-rules.md` 是否需要同步**
## 多 AI CLI 协作
本项目可由多个 AI CLI 协同。交接时附上:
```
已完成:<修改了哪些文件 + 核心决策>
待验证:<需要对方确认的地方>
约束继承:<影响后续工作的关键限制>
```
---
## Agent 友好工程四支柱
修改代码前,确认这个项目满足四支柱:
**可导航**:凭文件名/目录名可在 30 秒内定位某功能的代码。通用能力有独立家,不寄生在业务模块下。
**可推理**:公共类型只在一处定义。改一个接口,类型检查能报出所有受影响处。
**可验证**typecheck / lint / test 各有一条命令真能跑。关键路径有冒烟测试。
**可记忆**:规则按场景注入(不是一份巨型文档)。决策只写"为什么",代码自己回答"是什么"。
---
## 文档防腐元规则
做任何改动时,判断它属于哪类变更:
### T1 外部契约变更(最高优先级)
其他代码、服务、或 agent 依赖的定义——Tauri command 接口、MCP 接口、数据库 schema、共享类型。
→ 查 `docs/ai-context/project-context.yaml` 找到对应文档,检查并同步更新。
### T2 架构边界变更
模块划分、目录结构、依赖方向、命令分组变化。
→ 检查本文件架构速查和 `docs/ai-context/project-brief.md` 是否需要同步。
### T3 工具链变更
引入或替换库、Tauri 版本升级、Rust 依赖变更。
→ 更新本文件技术栈声明;更新 `docs/ai-context/project-context.yaml`
### T4 常驻上下文过期
本文件AGENTS.md`docs/ai-context/project-brief.md` 描述的现实已经改变。
→ 立即更新。这两份文档常驻 agent 上下文,过期危害最大。
### 不触发检查
内部重构、性能优化、bug fix外部行为不变、测试文件、UI 样式调整。
---
## 文档防腐铁律
- 文档只写稳定的:为什么、铁律、意图、架构决策。
- 易变的(端口、表名、命令清单)交给工具自省,不写死进常驻文档。
- 单一事实来源:两处说同一事时,删掉派生的那处。
- 两份文档打架时,信代码,不信文档。
---
## 定期防腐自查
- [ ] 技术栈声明还和 `package.json` / `Cargo.toml` 一致吗?
- [ ] `project-brief.md` 最近 30 天有没有随架构改动更新?
- [ ] `src/lib/commands.ts` 的封装覆盖了所有 `src-tauri/src/commands/` 下的 command 吗?
- [ ] 接入包模板(`resources/onboarding/`)和 `docs/templates/` 内容一致吗?

View File

@ -13,13 +13,17 @@ Git 多空间开发管理器Tauri v2 + React + TypeScript + Rust。
## 目录结构
```
src/ 前端源码
src/components/ React 组件(按功能分目录)
src/lib/commands.ts Tauri command 调用封装
src-tauri/src/ Rust 后端
src-tauri/src/commands/ Tauri command 实现
.blueprint/ 项目蓝图manifest + 模块详情 + 任务卡)
docs/ 部署和更新指南
src/ 前端源码
src/components/ React 组件(按功能分目录)
src/lib/commands.ts Tauri command 调用封装
src-tauri/src/ Rust 后端
src-tauri/src/commands/ Tauri command 实现
src-tauri/resources/onboarding/ 接入包分发模板AGENTS.md、git-workflow、lefthook
.blueprint/ 项目蓝图manifest + 模块详情 + 任务卡)
.claude/rules/ 路径注入规则tauri-contract / db-schema / onboarding-sync
AGENTS.md 跨工具 AI 上下文(四支柱 + 元规则,单一来源)
docs/ai-context/ AI 速查文档project-brief.md、project-context.yaml
docs/templates/ 炼境接入包模板草案agents-meta-rules 等)
```
## 命令执行规则
@ -59,7 +63,7 @@ docs/ 部署和更新指南
|----------|--------------|
| "我想加个 XX 功能" | 确认需求后manifest 新增模块status: concept创建 `.blueprint/modules/<id>.md` |
| "XX 功能需要 A、B、C" | 在模块文件中拆分为任务卡 |
| "这个方案确定了" | 模块 status → planned补充任务卡 files/acceptance |
| "这个方案确定了" | 模块 status → planned补充任务卡 acceptancefiles 可选) |
| "开始做 XX" | 模块 status → in_progress |
| 没提蓝图但在讨论功能 | 主动提议更新蓝图 |
@ -67,7 +71,7 @@ docs/ 部署和更新指南
| complexity | 执行流程 |
|-----------|---------|
| S / M | 直接拆任务卡,确保每张卡有 `files` + `acceptance` + `complexity` → 标记 📋 |
| S / M | 直接拆任务卡,确保每张卡有 `acceptance` + `complexity` → 标记 📋`files` 可选) |
| L复杂系统设计 | 先 `/architect` 输出设计文档,用户确认后 `/splitter` 拆成 S/M 任务卡 |
> L 级任务禁止直接写代码,必须先经过 `/architect``/splitter` 流程。

View File

@ -0,0 +1,41 @@
# 炼境 — 速查卡
**定位**Git 多空间开发管理器 + 项目 Agent 化改造引擎。Tauri v2 桌面 App管理多个开发项目为每个项目注入 Agent 友好基础设施,形成智能成长飞轮。
## 技术栈
| 层 | 技术 |
|----|------|
| 前端 | React 19 + TypeScript + TailwindCSS + React Query + Zustand + React Flow |
| 后端 | Rust + Tauri v2 |
| 数据 | SQLiterusqlite + r2d2 |
| MCP | 内嵌 HTTP Server供 Claude Code 调用) |
| 包管理 | pnpm |
## 关键约定(改代码前必看)
1. **Tauri command ↔ 前端封装**:新增 command 必须同步更新 `src/lib/commands.ts`
2. **DB 变更必须有 migration**:在 `src-tauri/src/db.rs``migrate()` 里补语句,否则旧版本升级报错
3. **Rust 业务函数依赖注入**:禁止内部 `pool().get()`,连接由调用方传入
4. **接入包模板 = 分发给被管理项目的规则**:改 `resources/onboarding/` 务必同步 `docs/templates/`
## 目录速查
```
src/lib/commands.ts 前后端契约(唯一前端入口)
src-tauri/src/commands/ Rust 后端实现(一功能一文件)
src-tauri/src/db.rs 数据库 schema + migration
src-tauri/src/mcp_server.rs MCP Serveragent 调用炼境的入口)
src-tauri/resources/onboarding/ 接入包分发模板
.blueprint/ 项目蓝图
docs/ai-context/ AI 上下文文档(本文件、契约映射)
docs/templates/ 接入包模板草案(设计层)
```
## 验收命令
```bash
pnpm typecheck # TypeScript 类型检查
pnpm tauri dev # 启动开发环境
cargo test # Rust 单元测试src-tauri/ 目录下运行)
```

View File

@ -0,0 +1,36 @@
# 炼境契约路径映射
# 炼境自身版本,手工维护(等 agent-infrastructure 模块完成后由炼境自动生成)
# 供 AGENTS.md 元规则 T1 查询:「哪个文件是这个项目的契约?」
contracts:
- label: Tauri command 接口(前后端契约)
paths:
- "src-tauri/src/commands/**/*.rs"
- "src/lib/commands.ts"
doc: "新增 command 时两端必须同步src/lib/commands.ts 是唯一前端调用入口"
- label: 数据库 schema
paths:
- "src-tauri/src/db.rs"
doc: "SQLite 表结构变更必须在 migrate() 函数中补 migration 语句"
- label: MCP Server 接口
paths:
- "src-tauri/src/mcp_server.rs"
- "src-tauri/src/mcp_inject.rs"
doc: "MCP 接口变更影响 Claude Code 对炼境的调用,接口路由和参数需同步"
- label: 接入包分发模板
paths:
- "src-tauri/resources/onboarding/**"
doc: "模板变更需同步检查 docs/templates/agents-meta-rules.md设计层来源"
architecture_docs:
- "AGENTS.md"
- "CLAUDE.md"
- ".blueprint/manifest.yaml"
agent_docs:
- "AGENTS.md"
- "docs/ai-context/project-brief.md"

81
docs/templates/agents-meta-rules.md vendored Normal file
View File

@ -0,0 +1,81 @@
---
version: 1.0.0
updated: 2026-06-29
usage: 炼境接入包分发时写入被管理项目 AGENTS.md 的元规则标准内容
---
# Agent 友好工程元规则
> 本节由炼境接入包分发,规则技术栈无关,适用于所有项目和所有 AI 工具。
> 项目特定的路径映射见 `docs/ai-context/project-context.yaml`
---
## Agent 友好工程四支柱
修改代码前,确认这个项目满足四支柱:
**可导航**:凭文件名/目录名可在 30 秒内定位某功能的代码。通用能力有独立家,不寄生在业务模块下。
**可推理**:公共类型只在一处定义。改一个接口,类型检查能报出所有受影响处。
**可验证**typecheck / lint / test 各有一条命令真能跑。关键路径有冒烟测试。
**可记忆**:规则按场景注入(不是一份巨型文档)。决策只写"为什么",代码自己回答"是什么"。
---
## 文档防腐元规则
做任何改动时,判断它属于哪类变更:
### T1 外部契约变更(最高优先级)
其他代码、服务、或 agent 依赖的定义——接口、共享类型、数据结构、配置键名、环境变量名。
→ 查 `docs/ai-context/project-context.yaml` 找到记录该契约的文档,检查并同步更新。
### T2 架构边界变更
模块划分、目录结构、依赖方向、包边界调整。
→ 检查架构文档是否需要同步。
### T3 工具链变更
引入或替换库、框架、构建工具。
→ 更新 AGENTS.md 技术栈声明;通知炼境重新扫描并更新 `project-context.yaml`
### T4 常驻上下文过期
AGENTS.md 或 `docs/ai-context/project-brief.md` 描述的现实已经改变。
→ 立即更新。这两份文档常驻 agent 上下文,过期危害最大。
### 不触发检查
内部重构、性能优化、bug fix外部行为不变、测试文件、临时脚本。
---
## 文档防腐铁律
- 文档只写稳定的:为什么、铁律、意图、架构决策。
- 易变的(端口、路由、表名、命令清单)交给工具自省,不写死进常驻文档。写死 = 定时炸弹。
- 单一事实来源:两处说同一事时,删掉派生的那处。
- 两份文档打架时,信代码,不信文档。
---
## 跨 AI 工具交接模板
完成一段工作并交接给下一个 agent 时,附上:
```
已完成:<修改了哪些文件 + 核心决策>
待验证:<需要对方确认的地方>
约束继承:<影响对方工作的关键限制>
```
---
## 定期防腐自查
每隔一阵或大改后,对照以下清单:
- [ ] AGENTS.md 里的技术栈声明还和代码一致吗?
- [ ] `project-brief.md` 最近 30 天有没有随架构改动更新?
- [ ] 有没有两份文档说同一件事且打架?
- [ ] `project-context.yaml` 的契约路径还准确吗?

44
docs/templates/project-brief.md.tmpl vendored Normal file
View File

@ -0,0 +1,44 @@
# <!-- 项目名 --> — 速查卡
> 由炼境接入包生成。请补全 TODO 段落后提交到仓库。
> 目标:新 agent 30 秒内能定向,不超过 300 字。
**定位**<!-- TODO: 一句话描述项目定位和核心功能 -->
## 技术栈
| 层 | 技术 |
|----|------|
<!-- TODO: 填入技术栈,例:
| 前端 | React 19 + TypeScript |
| 后端 | Node.js + Hono |
| 数据 | PostgreSQL + Drizzle ORM |
| 包管理 | pnpm |
-->
## 关键约定(改代码前必看)
<!-- TODO: 列出 3-5 条最重要的开发约束,例:
1. **类型单一来源**:跨模块共享类型只在 `packages/types/` 定义
2. **ORM 唯一**:使用 Drizzle禁止 Prisma
3. **API 层禁止写统计逻辑**:一律用 SQL view
-->
## 目录速查
```
<!-- TODO: 列出关键目录及职责,例:
src/routes/ API 路由
src/types/ 共享类型(单一来源)
drizzle/schema.ts 数据库 schema
-->
```
## 验收命令
```bash
# TODO: 填入项目的一键验证命令
# pnpm typecheck # 类型检查
# pnpm test # 测试
# pnpm lint # lint
```

View File

@ -0,0 +1,35 @@
# 项目契约路径映射
# 由炼境接入包根据技术栈扫描生成,用户确认后写入
# 供 AGENTS.md 元规则 T1 查询:「哪个文件是这个项目的外部契约?」
# 技术栈变更后重新运行接入包生成
# contracts 由炼境扫描技术栈后自动填入,也可手动补充。
# 常见示例(取消注释并按实际修改):
contracts:
# - label: 数据库 schemaDrizzle
# paths: ["drizzle/schema.ts"]
# doc: "schema 变更需同步更新数据库文档"
#
# - label: 数据库 schemaPrisma
# paths: ["prisma/schema.prisma"]
# doc: "schema 变更需同步更新数据库文档"
#
# - label: API 接口Hono / Express
# paths: ["src/routes/**"]
# doc: "接口变更需同步更新 API 文档或 openapi.yaml"
#
# - label: 共享类型
# paths: ["packages/types/**", "src/types/**"]
# doc: "类型只在此处定义,禁止手抄到其他模块"
#
# - label: 环境变量声明
# paths: [".env.example"]
# doc: "新增环境变量必须在 .env.example 中同步声明"
architecture_docs:
- "AGENTS.md"
# - "ARCHITECTURE.md"
agent_docs:
- "AGENTS.md"
- "docs/ai-context/project-brief.md"

View File

@ -0,0 +1,103 @@
<!-- 项目名 --> — AI Agent 上下文
> 本文件遵循 [AGENTS.md 开放规范](https://agents.md/),供 Claude Code、Qwen Code、Cursor、Copilot 等 AI 工具读取。
> 由炼境接入包分发。请按项目实际补全下方 TODO 段落。
## 项目概况
<!-- TODO: 一句话描述项目 + 技术栈。例pnpm monorepoReact 19 + TypeScript 前端 + Hono API。 -->
## 核心开发约束
### 包管理
- **pnpm 唯一**:禁止 npm / yarn / npx / bun
- lock 文件只有 `pnpm-lock.yaml`,不生成其他
### 代码规范
- TypeScript 优先,避免 `any`
- 函数/变量 camelCase组件 PascalCase
- 异步统一 `async/await`,不用裸 Promise 链
- 错误处理明确 `try/catch`,不吞异常
- 优先编辑现有文件,避免创建不必要的新文件
## Git 工作流
见 `docs/ai-context/git-workflow.md`(分支策略 + Conventional Commits + PR 规范)。
**commit 必须符合 Conventional Commits**lefthook 强制),炼境据此采集飞轮数据。
## 架构与模块
<!-- TODO: 列出主要目录/模块及职责,帮助 AI 快速定位「改哪里」。 -->
## 多 AI CLI 协作
本项目可由多个 AI CLI 协同。交接时把结果总结成 3 条传给下一个:
```
1. 已完成:<改了哪些文件 + 核心决策>
2. 待验证:<需要确认的地方>
3. 约束继承:<影响后续的关键限制>
```
## 关键文档索引
<!-- TODO: 列出 README / ARCHITECTURE.md / 其他关键文档及用途。 -->
---
## Agent 友好工程四支柱
修改代码前,确认这个项目满足四支柱:
**可导航**:凭文件名/目录名可在 30 秒内定位某功能的代码。通用能力有独立家,不寄生在业务模块下。
**可推理**:公共类型只在一处定义。改一个接口,类型检查能报出所有受影响处。
**可验证**typecheck / lint / test 各有一条命令真能跑。关键路径有冒烟测试。
**可记忆**:规则按场景注入(不是一份巨型文档)。决策只写"为什么",代码自己回答"是什么"。
---
## 文档防腐元规则
做任何改动时,判断它属于哪类变更:
### T1 外部契约变更(最高优先级)
其他代码、服务、或 agent 依赖的定义——接口、共享类型、数据结构、配置键名、环境变量名。
→ 查 `docs/ai-context/project-context.yaml`(若存在)找到记录该契约的文档,检查并同步更新。
### T2 架构边界变更
模块划分、目录结构、依赖方向、包边界调整。
→ 检查架构文档是否需要同步。
### T3 工具链变更
引入或替换库、框架、构建工具。
→ 更新本文件AGENTS.md技术栈声明通知炼境重新扫描并更新 `project-context.yaml`。
### T4 常驻上下文过期
本文件AGENTS.md或 `docs/ai-context/project-brief.md` 描述的现实已经改变。
→ 立即更新。这两份文档常驻 agent 上下文,过期危害最大。
### 不触发检查
内部重构、性能优化、bug fix外部行为不变、测试文件、临时脚本。
---
## 文档防腐铁律
- 文档只写稳定的:为什么、铁律、意图、架构决策。
- 易变的(端口、路由、表名、命令清单)交给工具自省,不写死进常驻文档。写死 = 定时炸弹。
- 单一事实来源:两处说同一事时,删掉派生的那处。
- 两份文档打架时,信代码,不信文档。
---
## 定期防腐自查
每隔一阵或大改后,对照以下清单:
- [ ] AGENTS.md 里的技术栈声明还和代码一致吗?
- [ ] `docs/ai-context/project-brief.md` 最近 30 天有没有随架构改动更新?
- [ ] 有没有两份文档说同一件事且打架?
- [ ] `docs/ai-context/project-context.yaml` 的契约路径还准确吗?

View File

@ -0,0 +1,63 @@
# Git 工作流 — AI Agent 操作指南
> 把此文件粘贴给任何 AI Agent让它了解本项目的 git 工作方式。由炼境接入包分发。
## 分支策略
```
dev ← 日常开发在这里 commit/push
main ← 稳定分支,通过 PR 合入(合入时触发 CI code review
```
- 小改动可直接 push `dev`
- 功能完整后开 PRdev → main
- 不要直接 push `main`,除非紧急热修复
## Commit 规范lefthook 强制)
格式:`<type>(<scope>): <描述>`
| type | 用途 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `chore` | 配置、依赖、脚本 |
| `docs` | 文档 |
| `refactor` | 重构(不改功能) |
| `ci` | CI 配置 |
| `perf` | 性能优化 |
| `style` | 代码格式 |
| `test` | 测试 |
`scope` 用「模块/子模块」二级结构(如 `auth/login`、`api/user`、`db/order`),对应项目功能模块。
```bash
git commit -m "feat(auth/login): 新增手机号登录"
git commit -m "fix(api/user): 修正分页 off-by-one"
```
> 炼境会解析这些 commit 的 type/scope 作为飞轮数据:`fix` 计入返工率、`scope` 归到模块。
> **commit 写得越规范,飞轮数据越准。**
## 一个 commit = 一件独立的事
不要 `git add -A` 一次提交所有。按功能分批:
```bash
git add <相关文件>
git commit -m "feat(模块): 一件事"
```
## PR 大小控制
PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否则 CI code review 易超时失效。
「所有检测通过」≠ 代码被审查过——大 PR 即使 CI 全绿review 可能已静默失效。
## 常用命令
```bash
git status --short
git log --oneline -10
git reset --soft HEAD~1 # 撤销最后一次 commit保留改动
```

View File

@ -0,0 +1,22 @@
# 接入包标准 lefthook 配置(炼境分发)
# 作用:强制 Conventional Commits + 提交前类型检查
# 安装pnpm add -D lefthook && pnpm exec lefthook install
pre-commit:
parallel: true
commands:
# TODO: 按项目调整 typecheck 命令与 globmonorepo 用 --filter <pkg> typecheck
typecheck:
glob: "*.{ts,tsx}"
run: pnpm typecheck
commit-msg:
commands:
conventional:
run: |
head -1 "{1}" | grep -qE '^(feat|fix|docs|chore|refactor|ci|perf|style|test|build|revert)(\(.+\))?!?: .+' || {
echo "✗ commit message 必须符合 Conventional Commits<type>(<scope>): <描述>"
echo " type: feat|fix|docs|chore|refactor|ci|perf|style|test|build|revert"
echo " 例feat(auth/login): 新增手机号登录"
exit 1
}

View File

@ -2010,6 +2010,10 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
/// 从项目的 project_notes 解析返工次数和 M 级任务平均实际轮数。
/// 返回 (rework_count, avg_rounds_m)avg_rounds_m 为 None 表示无 M 级复盘数据。
///
/// 旧路径:飞轮 L0 起 rework_count 改由 commit_metricsgit 自吸)提供,
/// 此函数保留作参考与未来回退,暂不调用。
#[allow(dead_code)]
fn parse_notes_metrics(bp_dir: &Path) -> (u32, Option<f64>) {
let project_path = match bp_dir.parent().and_then(|p| p.to_str()) {
Some(p) => p.to_string(),
@ -2121,6 +2125,20 @@ fn parse_notes_metrics(bp_dir: &Path) -> (u32, Option<f64>) {
(rework_count, avg)
}
/// 从项目路径反查 project_id再聚合 commit_metrics 的有效返工次数。
/// 项目未施加 buff查不到 project_id时返回 None。
fn commit_rework_for_path(project_path: &str) -> Option<u32> {
let conn = crate::db::pool().get().ok()?;
let pid: String = conn
.query_row(
"SELECT project_id FROM blueprint_buffs WHERE project_path = ?1",
rusqlite::params![project_path.trim_end_matches(['/', '\\'])],
|r| r.get(0),
)
.ok()?;
crate::commands::commit_metrics::rework_count(&conn, &pid).ok()
}
/// 向项目 .blueprint/usage.json 追加一条每日飞轮快照。
/// 节流:同一 UTC 日期只追加一次;写入失败静默忽略(不影响主流程)。
fn append_usage_snapshot(bp_dir: &Path, manifest: &BlueprintManifest, stats: &BlueprintStats) {
@ -2201,8 +2219,14 @@ fn append_usage_snapshot(bp_dir: &Path, manifest: &BlueprintManifest, stats: &Bl
}
}
// 从 project_notes 解析返工次数和 M 级任务平均轮数
let (rework_count, avg_rounds_m) = parse_notes_metrics(bp_dir);
// 飞轮 L0rework_count 从 commit_metricsgit 自吸)取,取代枯竭的复盘笔记解析;
// avg_rounds_M 需 PR 边界,基线档暂不出(见 gitea-integration 档2
let rework_count = bp_dir
.parent()
.and_then(|p| p.to_str())
.and_then(commit_rework_for_path)
.unwrap_or(0);
let avg_rounds_m: Option<f64> = None;
// 停滞模块in_progress 且任务全为 in_progress/blocked没有 todo 可推进)
let stalled_modules: Vec<String> = manifest.modules.iter()

View File

@ -131,47 +131,72 @@ fn check_project(app: &AppHandle, project_path: &str) -> Result<(), String> {
Err(_) => return Ok(()), // 不是 git 仓库或无提交,跳过
};
// 查询上次已处理的 commit
// 查询 project_id 与上次已处理的 commit
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
let last_commit: Option<String> = conn
let (project_id, last_commit): (String, Option<String>) = conn
.query_row(
"SELECT last_commit FROM blueprint_buffs WHERE project_path = ?1",
"SELECT project_id, last_commit FROM blueprint_buffs WHERE project_path = ?1",
rusqlite::params![project_path],
|row| row.get(0),
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap_or(None);
.unwrap_or_else(|_| (String::new(), None));
// 没有变化,跳过
if last_commit.as_deref() == Some(current_commit.as_str()) {
return Ok(());
}
// 获取变更文件列表
let changed = get_changed_files(root, last_commit.as_deref())?;
let mut synced = false;
if !changed.is_empty() {
let updated = update_task_statuses(root, &changed)?;
if updated > 0 {
// 触发飞轮快照(调用 get_blueprint 会触发内部 append_usage_snapshot
let _ = crate::commands::blueprint::get_blueprint(project_path.to_string());
// 通知前端刷新
use tauri::Emitter;
let _ = app.emit(
"blueprint://changed",
serde_json::json!({ "project_path": project_path }),
// 飞轮 L0解析新增 commit 的 conventional 指标写 commit_metrics与下方文件匹配互不干扰
if !project_id.is_empty() {
let first_time = last_commit.is_none();
let commits = get_new_commits(root, last_commit.as_deref());
if first_time && commits.len() >= FIRST_TIME_LIMIT {
crate::db::log_event(
"buff",
"info",
&format!("首次接入仅解析最近 {FIRST_TIME_LIMIT} 条 commit更早历史跳过"),
Some(project_path),
);
synced = true;
}
// 批量写入用事务包裹(首次可达数十条,避免逐条 fsync
let tx = conn.unchecked_transaction();
for (sha, subject, date) in &commits {
let _ = crate::commands::commit_metrics::record_commit(
&conn,
&project_id,
sha,
subject,
Some(date.as_str()),
);
}
if let Ok(tx) = tx {
let _ = tx.commit();
}
}
// 文件路径匹配 → 标记命中的任务卡 🔵
let changed = get_changed_files(root, last_commit.as_deref())?;
let mut tasks_updated = false;
if !changed.is_empty() && update_task_statuses(root, &changed)? > 0 {
tasks_updated = true;
}
// 有新 commit 即刷新飞轮快照rework_count 可能已变);任务卡有变动则通知前端重绘
let _ = crate::commands::blueprint::get_blueprint(project_path.to_string());
if tasks_updated {
use tauri::Emitter;
let _ = app.emit(
"blueprint://changed",
serde_json::json!({ "project_path": project_path }),
);
}
// 更新 DB记录已处理的 commit
let _ = conn.execute(
"UPDATE blueprint_buffs
SET last_commit = ?1, last_sync_at = CASE WHEN ?3 THEN datetime('now') ELSE last_sync_at END
SET last_commit = ?1, last_sync_at = datetime('now')
WHERE project_path = ?2",
rusqlite::params![current_commit, project_path, synced],
rusqlite::params![current_commit, project_path],
);
Ok(())
@ -262,6 +287,64 @@ fn get_changed_files(root: &Path, last_commit: Option<&str>) -> Result<Vec<Strin
}
}
// ── 辅助:新增 commit 解析(飞轮 L0─────────────────────────────────────────
/// 首次接入时只解析最近这么多条 commit避免一次性解析整个历史。
const FIRST_TIME_LIMIT: usize = 50;
/// 获取 last_commit..HEAD 之间的新 commitsha, subject, committed_at ISO
/// 首次接入last_commit 为 None只取最近 FIRST_TIME_LIMIT 条。
fn get_new_commits(root: &Path, last_commit: Option<&str>) -> Vec<(String, String, String)> {
// sha \x1f subject \x1f committer-date(ISO),每条 commit 一行
const FMT: &str = "--format=%H%x1f%s%x1f%cI";
let path_str = root.to_string_lossy();
let out = if is_wsl_path(&path_str) {
let linux = match unc_to_linux(&path_str) {
Some(l) => l,
None => return vec![],
};
let range = match last_commit {
Some(prev) => format!("{prev}..HEAD"),
None => format!("-n {FIRST_TIME_LIMIT}"),
};
let cmd = format!("git -C {linux} log {range} {FMT}");
crate::silent_cmd("wsl").args(["bash", "-c", &cmd]).output()
} else {
let mut cmd = crate::silent_cmd("git");
cmd.arg("log");
match last_commit {
Some(prev) => {
cmd.arg(format!("{prev}..HEAD"));
}
None => {
cmd.arg("-n").arg(FIRST_TIME_LIMIT.to_string());
}
}
cmd.arg(FMT);
cmd.current_dir(root).output()
};
let out = match out {
Ok(o) if o.status.success() => o,
_ => return vec![],
};
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| {
let mut parts = line.split('\u{1f}');
let sha = parts.next()?.trim().to_string();
if sha.is_empty() {
return None;
}
let subject = parts.next().unwrap_or("").to_string();
let date = parts.next().unwrap_or("").to_string();
Some((sha, subject, date))
})
.collect()
}
// ── 辅助:任务状态更新 ────────────────────────────────────────────────────────
fn update_task_statuses(root: &Path, changed_files: &[String]) -> Result<usize, String> {

View File

@ -0,0 +1,252 @@
//! Conventional Commit 解析与执行质量指标(飞轮 L0 数据地基)。
//!
//! 从 git commit message 解析出 type/scope/返工/CI 自修信号,写入 commit_metrics 表,
//! 取代枯竭的「复盘笔记解析」路径。纯解析逻辑(`parse_conventional`)与 DB 无关,可独立单测。
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
/// 一条 commit 的解析结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCommit {
/// conventional typefeat/fix/chore...None 表示非 conventional commit。
pub commit_type: Option<String>,
/// scope如 estate1/cadNone 表示无 scope。
pub scope: Option<String>,
/// 是否返工type == fix。
pub is_rework: bool,
/// 是否 CI 自动修复(含 [DeepSeek-V3]/[R1] 等标记),聚合返工率时剔除。
pub is_ci_auto: bool,
}
/// CI 自动修复 commit 的标记(来自 enterprise-system CI`fix(ci): auto-fix ... [DeepSeek-V3]`)。
const CI_AUTO_MARKERS: [&str; 3] = ["[DeepSeek", "[R1]", "[V3]"];
/// 解析一条 commit message 为 Conventional Commit 结构。
///
/// 纯函数,不触碰 DB。无法识别为 conventional 的merge commit、随手写的
/// `commit_type` 记为 None聚合时单列、不污染 fix 率。
pub fn parse_conventional(message: &str) -> ParsedCommit {
let is_ci_auto = CI_AUTO_MARKERS.iter().any(|m| message.contains(m));
// 只看首行subject
let subject = message.lines().next().unwrap_or("").trim();
// 必须有 "<prefix>: <desc>" 结构
let prefix = match subject.split_once(':') {
Some((p, _desc)) => p.trim(),
None => return non_conventional(is_ci_auto),
};
// 去掉 breaking change 的末尾 '!'
let prefix = prefix.trim_end_matches('!');
// 拆出 type 与可选 scopetype(scope)
let (typ, scope) = match prefix.split_once('(') {
Some((t, rest)) => {
let scope = rest
.strip_suffix(')')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
(t.trim(), scope)
}
None => (prefix, None),
};
// type 必须是纯小写字母feat/fix/...),否则视为非 conventional如 "Merge pull request"
if typ.is_empty() || !typ.chars().all(|c| c.is_ascii_lowercase()) {
return non_conventional(is_ci_auto);
}
ParsedCommit {
is_rework: typ == "fix",
commit_type: Some(typ.to_string()),
scope,
is_ci_auto,
}
}
fn non_conventional(is_ci_auto: bool) -> ParsedCommit {
ParsedCommit {
commit_type: None,
scope: None,
is_rework: false,
is_ci_auto,
}
}
/// 解析并写入一条 commit 记录sha 为主键,重复写入忽略)。
pub fn record_commit(
conn: &Connection,
project_id: &str,
sha: &str,
message: &str,
committed_at: Option<&str>,
) -> Result<(), String> {
let p = parse_conventional(message);
conn.execute(
"INSERT OR IGNORE INTO commit_metrics
(sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
sha,
project_id,
p.commit_type,
p.scope,
p.is_rework as i32,
p.is_ci_auto as i32,
committed_at,
],
)
.map_err(|e| e.to_string())?;
Ok(())
}
/// 项目的有效返工次数type=fix 且非 CI 自动修复。
pub fn rework_count(conn: &Connection, project_id: &str) -> Result<u32, String> {
conn.query_row(
"SELECT COUNT(*) FROM commit_metrics
WHERE project_id = ?1 AND is_rework = 1 AND is_ci_auto = 0",
rusqlite::params![project_id],
|r| r.get::<_, i64>(0),
)
.map(|n| n as u32)
.map_err(|e| e.to_string())
}
/// commit 规范化统计:(总数, conventional 数)。供数据可信度提示(卡 D使用。
pub fn conventional_stats(conn: &Connection, project_id: &str) -> Result<(u32, u32), String> {
let total: i64 = conn
.query_row(
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1",
rusqlite::params![project_id],
|r| r.get(0),
)
.map_err(|e| e.to_string())?;
let conv: i64 = conn
.query_row(
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND commit_type IS NOT NULL",
rusqlite::params![project_id],
|r| r.get(0),
)
.map_err(|e| e.to_string())?;
Ok((total as u32, conv as u32))
}
/// 项目 commit 执行质量统计(供前端数据可信度展示)。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommitStats {
pub total: u32,
pub conventional: u32,
pub rework: u32,
}
/// 查询某项目的 commit 执行质量统计(总数 / conventional 数 / 有效返工数)。
#[tauri::command]
pub fn get_commit_stats(project_id: String) -> Result<CommitStats, String> {
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
let (total, conventional) = conventional_stats(&conn, &project_id)?;
let rework = rework_count(&conn, &project_id)?;
Ok(CommitStats {
total,
conventional,
rework,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_feat_with_scope() {
let p = parse_conventional("feat(hub/rpc): add shared type system");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("hub/rpc"));
assert!(!p.is_rework);
assert!(!p.is_ci_auto);
}
#[test]
fn parses_fix_as_rework() {
let p = parse_conventional("fix(estate1/view): correct sort order");
assert_eq!(p.commit_type.as_deref(), Some("fix"));
assert_eq!(p.scope.as_deref(), Some("estate1/view"));
assert!(p.is_rework);
assert!(!p.is_ci_auto);
}
#[test]
fn parses_type_without_scope() {
let p = parse_conventional("chore: update AGENTS.md");
assert_eq!(p.commit_type.as_deref(), Some("chore"));
assert_eq!(p.scope, None);
assert!(!p.is_rework);
}
#[test]
fn detects_ci_auto_fix() {
let p = parse_conventional("fix(ci): auto-fix type/lint errors [DeepSeek-V3]");
assert_eq!(p.commit_type.as_deref(), Some("fix"));
assert!(p.is_rework);
assert!(p.is_ci_auto, "应识别 [DeepSeek-V3] 为 CI 自修");
}
#[test]
fn breaking_change_marker_stripped() {
let p = parse_conventional("feat(api)!: drop legacy endpoint");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("api"));
}
#[test]
fn merge_commit_is_non_conventional() {
let p = parse_conventional("Merge pull request 'xxx' (#1) from dev into main");
assert_eq!(p.commit_type, None);
assert!(!p.is_rework);
}
#[test]
fn freeform_message_is_non_conventional() {
let p = parse_conventional("update stuff");
assert_eq!(p.commit_type, None);
assert_eq!(p.scope, None);
}
#[test]
fn uses_first_line_only() {
let p = parse_conventional("feat(x): title\n\nbody line with: colon");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("x"));
}
#[test]
fn rework_count_excludes_ci_auto_and_non_fix() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "sha1", "fix(a): real bug", None).unwrap();
record_commit(&conn, "proj", "sha2", "fix(ci): auto-fix [DeepSeek-V3]", None).unwrap();
record_commit(&conn, "proj", "sha3", "feat(a): feature", None).unwrap();
record_commit(&conn, "proj", "sha4", "update stuff", None).unwrap();
// 仅 sha1 算有效返工sha2 是 CI 自修、sha3 是 feat、sha4 非规范)
assert_eq!(rework_count(&conn, "proj").unwrap(), 1);
}
#[test]
fn record_commit_is_idempotent() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
let (total, _) = conventional_stats(&conn, "proj").unwrap();
assert_eq!(total, 1, "同 sha 重复写入应被忽略");
}
#[test]
fn conventional_stats_counts_ratio() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "s1", "feat: a", None).unwrap();
record_commit(&conn, "proj", "s2", "random text", None).unwrap();
let (total, conv) = conventional_stats(&conn, "proj").unwrap();
assert_eq!(total, 2);
assert_eq!(conv, 1);
}
}

View File

@ -18,6 +18,8 @@ pub mod tags;
pub mod cicd;
pub mod blueprint;
pub mod buff;
pub mod commit_metrics;
pub mod onboarding;
pub mod canvas;
pub mod claude_config;
pub mod mentor;

View File

@ -0,0 +1,115 @@
//! 接入包模板分发(基线档)。
//!
//! 向项目种入 AGENTS.md / lefthook.yml / git-workflow.md 标准模板:
//! 文件不存在才写、绝不覆盖用户已有;返回每个模板的处理结果。
//! 模板蓝本取自 enterprise-system已通用化。
use serde::{Deserialize, Serialize};
use std::path::Path;
// 标准模板(编译期内嵌)
const TMPL_AGENTS: &str = include_str!("../../resources/onboarding/AGENTS.md.tmpl");
const TMPL_LEFTHOOK: &str = include_str!("../../resources/onboarding/lefthook.yml.tmpl");
const TMPL_GIT_WORKFLOW: &str = include_str!("../../resources/onboarding/git-workflow.md.tmpl");
/// 单个模板的分发结果。
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct TemplateResult {
/// 相对项目根的目标路径
pub name: String,
/// "written" | "skipped" | "failed"
pub action: String,
pub detail: String,
}
/// 分发单个模板:存在则跳过(绝不覆盖),不存在则写入。
fn dispatch_one(root: &Path, relative: &str, content: &str) -> TemplateResult {
let target = root.join(relative);
if target.exists() {
return TemplateResult {
name: relative.to_string(),
action: "skipped".into(),
detail: "已存在,未覆盖".into(),
};
}
if let Some(parent) = target.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
return TemplateResult {
name: relative.to_string(),
action: "failed".into(),
detail: format!("建目录失败: {e}"),
};
}
}
match std::fs::write(&target, content) {
Ok(_) => TemplateResult {
name: relative.to_string(),
action: "written".into(),
detail: "已写入".into(),
},
Err(e) => TemplateResult {
name: relative.to_string(),
action: "failed".into(),
detail: format!("写入失败: {e}"),
},
}
}
/// 向项目分发全部基线模板,返回每个的处理结果。
/// project_path 为项目根WSL 项目为 \\wsl.localhost\... UNC 路径,可直接写)。
pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
let root = Path::new(project_path.trim_end_matches(['/', '\\']));
vec![
dispatch_one(root, "AGENTS.md", TMPL_AGENTS),
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
dispatch_one(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
]
}
/// 接入包应用报告:模板分发结果 + buff 状态。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OnboardingReport {
pub templates: Vec<TemplateResult>,
pub buff: crate::commands::buff::BuffStatus,
}
/// 应用接入包(基线档):分发标准模板 + 挂 buff炼境登记 + git 观测/飞轮采集)。
#[tauri::command]
pub fn apply_onboarding_pack(
project_path: String,
project_id: String,
) -> Result<OnboardingReport, String> {
let templates = dispatch_templates(&project_path);
let buff = crate::commands::buff::apply_blueprint_buff(project_path, project_id)?;
Ok(OnboardingReport { templates, buff })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_then_skips_never_overwrites() {
let base = std::env::temp_dir()
.join(format!("lianjing_onboarding_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let pp = base.to_str().unwrap();
// 首次:全部写入
let r1 = dispatch_templates(pp);
assert!(r1.iter().all(|t| t.action == "written"), "首次应全部写入");
assert!(base.join("AGENTS.md").exists());
assert!(base.join("lefthook.yml").exists());
assert!(base.join("docs/ai-context/git-workflow.md").exists());
// 改一个文件,验证再次分发不覆盖
std::fs::write(base.join("AGENTS.md"), "用户自己的内容").unwrap();
let r2 = dispatch_templates(pp);
assert!(r2.iter().all(|t| t.action == "skipped"), "已存在应全部跳过");
let kept = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
assert_eq!(kept, "用户自己的内容", "绝不覆盖用户已有内容");
let _ = std::fs::remove_dir_all(&base);
}
}

View File

@ -520,6 +520,21 @@ fn migrate(conn: &rusqlite::Connection) {
);
").expect("create blueprint_buffs table");
// 飞轮 L0从 git 解析的 conventional commit 执行质量指标
conn.execute_batch("
CREATE TABLE IF NOT EXISTS commit_metrics (
sha TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
commit_type TEXT,
scope TEXT,
is_rework INTEGER NOT NULL DEFAULT 0,
is_ci_auto INTEGER NOT NULL DEFAULT 0,
committed_at TEXT,
parsed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_commit_metrics_project ON commit_metrics(project_id);
").expect("create commit_metrics table");
// 仪表盘全局快启应用(无项目 FK 约束)
conn.execute_batch("
CREATE TABLE IF NOT EXISTS global_tools (

View File

@ -265,6 +265,9 @@ pub fn run() {
remove_blueprint_buff,
get_buff_status,
list_buffed_projects,
// onboarding pack
commands::onboarding::apply_onboarding_pack,
commands::commit_metrics::get_commit_stats,
// mentor
get_mentor_context,
get_project_notes,

View File

@ -5,7 +5,7 @@
"identifier": "com.yangzhixiang.dev-manager-tauri",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:1430",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},

View File

@ -22,7 +22,8 @@ import {
getFlywheelStats,
generateMuhePrompt,
archiveProjectDocs,
applyBlueprintBuff,
applyOnboardingPack,
getCommitStats,
removeBlueprintBuff,
getBuffStatus,
getUserConventionsStatus,
@ -38,6 +39,8 @@ import {
type FlywheelStats,
type StalledProject,
type BuffStatus,
type TemplateResult,
type CommitStats,
type UserConventionsStatus,
} from "../../lib/commands";
@ -1060,6 +1063,13 @@ function GovernancePanel({
staleTime: 5000,
});
const { data: commitStats } = useQuery<CommitStats | null>({
queryKey: ["commit-stats-governance", projectId],
queryFn: () => (projectId ? getCommitStats(projectId) : Promise.resolve(null)),
enabled: !!projectId,
staleTime: 5000,
});
const [syncing, setSyncing] = useState(false);
const [syncResult, setSyncResult] = useState<SyncResult | null>(null);
const [syncError, setSyncError] = useState<string | null>(null);
@ -1068,6 +1078,7 @@ function GovernancePanel({
const [buffError, setBuffError] = useState<string | null>(null);
const [buffInitPrompt, setBuffInitPrompt] = useState<string | null>(null);
const [buffInitCopied, setBuffInitCopied] = useState(false);
const [packTemplates, setPackTemplates] = useState<TemplateResult[] | null>(null);
const [promptMode, setPromptMode] = useState<"init" | "sync" | null>(null);
const [prompt, setPrompt] = useState("");
@ -1091,8 +1102,10 @@ function GovernancePanel({
setBuffLoading(true);
setBuffError(null);
setBuffInitPrompt(null);
setPackTemplates(null);
try {
await applyBlueprintBuff(projectPath, projectId);
const report = await applyOnboardingPack(projectPath, projectId);
setPackTemplates(report.templates);
refetchBuff();
// 只有尚无蓝图时才展示 init prompt已有蓝图则 Buff 直接激活无需初始化
if (bpStatus?.status === "none") {
@ -1113,6 +1126,7 @@ function GovernancePanel({
await removeBlueprintBuff(projectPath);
refetchBuff();
setBuffInitPrompt(null);
setPackTemplates(null);
} catch (e) {
setBuffError(String(e));
} finally {
@ -1435,13 +1449,13 @@ function GovernancePanel({
{/* ── 蓝图 Buff ─────────────────────────────────────── */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"> Buff</p>
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
{buffStatus?.status === "active" && (
<span className="text-[10px] text-purple-600 bg-purple-50 border border-purple-200 px-1.5 py-0.5 rounded-full"> </span>
)}
</div>
<p className="text-[11px] text-gray-500 leading-relaxed">
Buff git
AGENTS.md / lefthook / git-workflow buff git
</p>
{buffStatus?.last_sync_at && (
<p className="text-[10px] text-gray-400">{buffStatus.last_sync_at}</p>
@ -1449,6 +1463,30 @@ function GovernancePanel({
{buffError && (
<p className="text-[11px] text-red-600 font-mono break-all">{buffError}</p>
)}
{packTemplates && (
<div className="space-y-0.5 pt-0.5">
<p className="text-[10px] text-gray-500 font-semibold"></p>
{packTemplates.map((t) => (
<p key={t.name} className="text-[10px] font-mono flex items-center gap-1">
<span
className={
t.action === "written"
? "text-green-600"
: t.action === "skipped"
? "text-gray-400"
: "text-red-600"
}
>
{t.action === "written" ? "✓" : t.action === "skipped" ? "○" : "✗"}
</span>
<span className="text-gray-600 truncate flex-1">{t.name}</span>
<span className="text-gray-400">
{t.action === "written" ? "已写入" : t.action === "skipped" ? "已存在" : t.detail}
</span>
</p>
))}
</div>
)}
{buffStatus?.status === "active" ? (
<button
onClick={handleRemoveBuff}
@ -1492,6 +1530,19 @@ function GovernancePanel({
</button>
</div>
)}
{buffStatus?.status === "active" && commitStats && commitStats.total > 0 && (
<div className="pt-1.5 border-t border-gray-100 space-y-0.5">
<p className="text-[10px] text-gray-500">
commit {commitStats.conventional}/{commitStats.total}
{Math.round((commitStats.conventional / commitStats.total) * 100)}% · {commitStats.rework}
</p>
{commitStats.conventional / commitStats.total < 0.6 && (
<p className="text-[10px] text-amber-600 leading-relaxed">
lefthook
</p>
)}
</div>
)}
</div>
{/* 项目信息 */}

View File

@ -60,7 +60,7 @@ interface Props {
spacesJson?: SpacesJson | null;
}
export function ProjectCard({ project: p, role, spacesJson }: Props) {
export function ProjectCard({ project: p, spacesJson }: Props) {
const [expanded, setExpanded] = useState(false);
const [showToolsMgr, setShowToolsMgr] = useState(false);
const [toolsMgrDefaultAdd, setToolsMgrDefaultAdd] = useState(false);

View File

@ -1599,6 +1599,31 @@ export const getBuffStatus = (projectPath: string) =>
export const listBuffedProjects = () =>
invoke<BuffStatus[]>("list_buffed_projects");
// ── Onboarding Pack接入包──────────────────────────────────────────────────
export interface TemplateResult {
name: string;
action: "written" | "skipped" | "failed";
detail: string;
}
export interface OnboardingReport {
templates: TemplateResult[];
buff: BuffStatus;
}
export const applyOnboardingPack = (projectPath: string, projectId: string) =>
invoke<OnboardingReport>("apply_onboarding_pack", { projectPath, projectId });
export interface CommitStats {
total: number;
conventional: number;
rework: number;
}
export const getCommitStats = (projectId: string) =>
invoke<CommitStats>("get_commit_stats", { projectId });
// ── Beast Global Status (read-only) ───────────────────────────────────────────
export interface BeastGlobalStatus {

View File

@ -15,14 +15,14 @@ export default defineConfig(async () => ({
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
port: 1430,
strictPort: true,
host: host || "127.0.0.1",
hmr: host
? {
protocol: "ws",
host,
port: 1421,
port: 1431,
}
: undefined,
watch: {