diff --git a/.claude/agents/game-pm.md b/.claude/agents/game-pm.md index 57e2c18..cda69d5 100644 --- a/.claude/agents/game-pm.md +++ b/.claude/agents/game-pm.md @@ -33,7 +33,7 @@ memory: project ### 步骤 1:获取项目状态 -通过项目管理工具(`gitea-collab` 技能)执行: +通过项目管理工具(`gitea-pm` 技能)执行: 1. **读取 Pipeline Tracker** — 仅获取「当前阶段」(草图/总路线/迭代) 2. **列出激活里程碑** — `milestone list --state open` diff --git a/.claude/refs/development-workflow.md b/.claude/refs/development-workflow.md index dbc68fd..833501e 100644 --- a/.claude/refs/development-workflow.md +++ b/.claude/refs/development-workflow.md @@ -3,7 +3,7 @@ > 本文档定义游戏项目的开发阶段、任务管理、角色职责、产出标准和质量检查规则。 > 项目采用 3 阶段开发模型:草图 → 总路线 → 迭代。每个阶段有独立的技能文件管理任务序列。 > -> 项目管理工具当前使用 Gitea。所有操作命令和标签定义详见 `.claude/skills/gitea-collab.md`。 +> 项目管理工具当前使用 Gitea。PM 操作详见 `gitea-pm` 技能,通用协作详见 `gitea-collab` 技能。 > 阶段技能文件: > - `.claude/skills/project-initializer.md` — 项目初始化 > - `.claude/skills/stage-sketch.md` — 草图阶段 @@ -26,7 +26,7 @@ - **策划先行**:设计文档是其他角色的需求源 - **程序读设计**:实现前必须先读取对应设计文档和配置表 -- **文档即交接**:Agent 之间不直接通信,通过共享文档目录 + 任务 Issue 评论交接(参见 `.claude/skills/gitea-collab.md`) +- **文档即交接**:Agent 之间不直接通信,通过共享文档目录 + 任务 Issue 评论交接(参见 `gitea-collab` 技能) - **冲突时用户说了算**:玩法/数值/叙事的最终决定权在用户 - **PM 是进度调度器**:PM 管理进度和任务分配,不做审查 - **审查是子代理**:角色完成工作后启动 game-reviewer 子代理审查,审查结果返回给角色,角色向用户汇报 @@ -36,7 +36,7 @@ ### 进度汇报规则 -- 进度管理唯一真实来源是 **项目管理工具**(当前为 Gitea Issues + Milestones)。具体操作参见 `.claude/skills/gitea-collab.md` +- 进度管理唯一真实来源是 **项目管理工具**(当前为 Gitea Issues + Milestones)。具体操作参见 `gitea-pm` / `gitea-collab` 技能 - 各角色完成工作后在对应 Issue 下评论汇报,不更新本地 report 文件 - PM 通过项目管理工具读取任务和评论,给出行动建议 - 角色完成工作后自行启动 game-reviewer 子代理审查,审查通过后 PM 请示用户关闭 Issue diff --git a/.claude/skills/gitea-collab.md b/.claude/skills/gitea-collab.md deleted file mode 100644 index 7b39e2a..0000000 --- a/.claude/skills/gitea-collab.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -name: "gitea-collab" -description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee 映射、标签体系、Issue/Milestone/Pipeline 操作规范、角色汇报模板" ---- - -# Gitea 协作工具 - -> 本 skill 封装 ChildHood 项目基于 Gitea 的进度管理全部实现细节。 -> Agent 文件只描述项目管理的逻辑(做什么),本文件描述实现细节(怎么做)。 -> 未来若更换项目管理后端,只需替换本 skill,Agent 逻辑无需修改。 - ---- - -## 1. CLI 工具 - -**工具路径**: `node scripts/gitea.js [subcommand] [options]` - -**API 封装**: `scripts/gitea-api.js`(GiteaAPI 类,HTTP REST 调用) - -**服务端**: `micecola.top:8418`,仓库 `starmen1265/ChildHood` - -**`--json` 标志**: 所有命令支持 `--json` 输出纯 JSON,供脚本解析。 - ---- - -## 2. 认证机制 - -通过 `--role <角色>` 参数自动从 `scripts/.gitea-tokens.json` 读取对应 Token,无需手动传 `GITEA_TOKEN`。 - -| 角色 | `--role` 值 | 适用 Agent | -|------|------------|-----------| -| 策划 | `designer` | game-designer | -| 程序 | `dev` | game-dev | -| 美术 | `artist` | (暂无独立 Agent) | -| PM | `pm` | game-pm | -| 审查 | `reviewer` | game-reviewer | - ---- - -## 3. Assignee 映射 - -创建任务时通过 `--assignees` 指派给对应角色的 Gitea 用户。 - -| 角色 | `--assignees` 值 | 任务 ID 前缀 | -|------|-----------------|-------------| -| 策划 | `ai-designer` | `D-XXX` | -| 程序 | `ai-programmer` | `P-XXX` | -| 美术 | `ai-artist` | `A-XXX` | -| PM | `ai-pm` | (管理任务) | -| 审查 | `ai-reviewer` | (审查操作,不拥有任务) | - ---- - -## 4. 标签体系 - -19 个标准标签,通过 `node scripts/gitea.js label init` 初始化。 - -### status(任务状态) -| 标签 | 含义 | -|------|------| -| `status:backlog` | 待处理 | -| `status:ready` | 已就绪,可开始 | -| `status:in-progress` | 进行中 | -| `status:blocked` | 被阻塞 | -| `status:in-review` | 待审查(角色完成工作后设置) | -| `status:done` | 已完成 | - -### gate(质量门判定 — 旧标签,保留用于历史 Issue) -| 标签 | 含义 | -|------|------| -| `gate:execution-pass/fail` | 执行案质量门 | -| `gate:tech-pass/fail` | 程序设计质量门 | -| `gate:art-pass/fail` | 美术资产验收门 | -| `gate:impl-pass/fail` | 程序实现验收门 | - -> 新工作流使用 `review:*` 标签替代 `gate:*`。旧 Issue 上的 gate 标签保留不动。 - -### review(审查结果 — 新标签) -| 标签 | 含义 | -|------|------| -| `review:pass` | 审查已通过 ✅ | -| `review:fail` | 审查未通过 ❌ | - -### stage(开发阶段 — 新标签) -| 标签 | 含义 | -|------|------| -| `stage:sketch` | 草图阶段任务 | -| `stage:roadmap` | 总路线阶段任务 | -| `stage:iteration` | 迭代阶段任务 | - -> **注**:`pipeline:*` 和 `role:*` 标签已废弃并从 Gitea 上删除。如旧 Issue 上仍残留这些标签,忽略即可。 - ---- - -## 5. 命令速查 - -### issue - -```bash -# 列出 Issue(支持按 assignee 过滤) -node scripts/gitea.js --role issue list --milestone <标题> [--state open|closed|all] [--labels ] [--assigned <用户名>] [--json] - -# 查看 Issue 详情 -node scripts/gitea.js --role issue get [--json] - -# 创建 Issue(必须指定 assignees) -node scripts/gitea.js --role issue create --title "..." --body "..." --milestone --assignees <用户名,...> - -# 更新 Issue -node scripts/gitea.js --role issue update --title "..." --body "..." - -# 关闭 Issue -node scripts/gitea.js --role issue close [--body "关闭说明"] - -# 重新打开 Issue -node scripts/gitea.js --role issue reopen - -# 评论 -node scripts/gitea.js --role issue comment --body "评论内容" - -# 查看评论 -node scripts/gitea.js --role issue comments [--json] - -# 设置标签 -node scripts/gitea.js --role issue labels --set - -# 依赖关系 -node scripts/gitea.js --role issue deps - -# 阻塞关系 -node scripts/gitea.js --role issue block -node scripts/gitea.js --role issue unblock -``` - -### milestone - -```bash -# 列出里程碑 -node scripts/gitea.js --role milestone list [--state open|closed|all] [--json] - -# 创建里程碑 -node scripts/gitea.js --role milestone create --title "..." --description "..." - -# 更新里程碑 -node scripts/gitea.js --role milestone update --title "..." --description "..." --due "YYYY-MM-DD" -``` - -### label - -```bash -# 列出标签 -node scripts/gitea.js --role label list [--json] - -# 创建标签 -node scripts/gitea.js --role label create --name "..." --color "#hex" - -# 初始化标准标签体系 -node scripts/gitea.js --role label init -``` - -### pipeline - -```bash -# 初始化 Pipeline Tracker Meta Issue -node scripts/gitea.js --role pipeline init - -# 查看 Pipeline 状态 -node scripts/gitea.js --role pipeline status - -# 更新 Pipeline 步骤状态 -node scripts/gitea.js --role pipeline update --step <1-7> --status "状态描述" -``` - -### test - -```bash -# 运行 API 连通性测试(P0 + P1) -node scripts/gitea.js test -``` - ---- - -## 6. 数据映射 - -| 概念 | Gitea 对应 | 说明 | -|------|-----------|------| -| 任务 (P-002) | Issue | 标题含任务 ID 前缀 | -| 阶段/版本 | Milestone | 阶段名或版本号作为里程碑标题 | -| 任务状态 | Label (`status:*`) | 6 种状态标签 | -| 审查结果 | Label (`review:*`) | 通过/未通过 | -| 开发阶段 | Label (`stage:*`) | 草图/总路线/迭代 | -| 质量门结果 | Label (`gate:*`) | 旧标签,保留用于历史 Issue | -| 角色归属 | Assignee | 通过 `--assignees` 指派 | -| 当前进度 | Pipeline Tracker | Meta Issue 记录当前阶段和进度 | -| 角色汇报 | Issue 评论 | 各角色在对应 Issue 下评论 | -| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地文件名格式 `<任务ID>-审查记录.md` | -| 任务归档 | close Issue | 用户确认后由 PM 关闭 | - ---- - -## 7. 任务 ID 规范 - -格式:`[角色前缀]-[三位数字]` - -| 前缀 | 角色 | 示例 | -|------|------|------| -| `D` | 策划 (Design) | `D-001` | -| `P` | 程序 (Programmer) | `P-002` | -| `A` | 美术 (Artist) | `A-003` | - -每个前缀从 001 独立编号。Issue 标题需包含任务 ID 前缀。 - ---- - -## 8. Pipeline Tracker - -Pipeline Tracker 是一个 **Meta Issue**,仅记录**无法从 Issue/Milestone 系统直接推导**的信息。 - -### Tracker 正文只保留 - -```markdown -# 流水线进度追踪 - -## 当前阶段 -草图阶段 / 总路线阶段 / 迭代阶段 - -## 当前迭代 -v1.4(迭代阶段时填写,其他阶段留空) - -## 阶段历史 -| 阶段 | 里程碑 | 进入日期 | 完成日期 | -|------|--------|----------|----------| -| 草图阶段 | 草图阶段 | YYYY-MM-DD | YYYY-MM-DD | -| 总路线阶段 | 总路线阶段 | YYYY-MM-DD | — | -``` - -### 不记录的信息(从 Issue/Milestone 直接查询) - -- ~~当前里程碑详情~~ → `milestone list --state open` -- ~~Open/Closed Issues 计数~~ → `milestone list` 自带 -- ~~7 步流水线步骤状态~~ → 从任务的 status/review 标签聚合 -- ~~相关任务索引~~ → `issue list --state open` -- ~~当前阻塞~~ → `issue list --labels status:blocked` - -### 相关命令 - -- 初始化:`pipeline init` -- 查看状态:`pipeline status` -- 阶段转换时直接编辑 Issue 正文更新「当前阶段」和「阶段历史」 - ---- - -## 9. 角色汇报模板 - -各角色完成工作后,在对应 Issue 下评论汇报。 - -### 策划汇报 - -```bash -node scripts/gitea.js --role designer issue comment --body "## 策划汇报 — YYYY-MM-DD - -**任务ID**: D-XXX -**状态**: 进行中 / 已完成 -**产出**: path/to/output - -### 完成内容 -- ... - -### 待确认 -- ..." -``` - -### 程序汇报 - -```bash -node scripts/gitea.js --role dev issue comment --body "## 程序汇报 — YYYY-MM-DD - -**任务ID**: P-XXX -**状态**: 进行中 / 已完成 -**产出**: path/to/output - -### 完成内容 -- ... - -### 偏差/问题 -- ..." -``` - -### 审查修改后汇报(审查退回修改后使用) - -```bash -node scripts/gitea.js --role issue comment --body "## 修改汇报 — YYYY-MM-DD - -**任务ID**: -**修改原因**: 质量门审查退回 - -### 修改内容 -- [对应审查意见第X条]:[具体修改内容] - -### 修改后产出 -- path/to/output" -``` - -### PM 确认关闭任务(审查通过后) - -```bash -# 用户确认后关闭 Issue -node scripts/gitea.js --role pm issue close --body "## 任务完成 ✔ - -**完成日期**:YYYY-MM-DD -**产出**:path/to/output -**审查记录**:docs/plans/reviews/-审查记录.md" -``` - ---- - -## 10. PM 会话启动 - -PM 每次启动时: - -1. **读取当前阶段**:`pipeline status`(仅获取当前阶段) -2. **列出激活里程碑**:`milestone list --state open` -3. **列出所有活跃任务**:`issue list --state open`,展示摘要(任务 ID、标题、状态标签、assignee) -4. **展开需要关注任务的详情**:仅对 `status:in-progress`、`status:blocked`、`review:pass` 的任务执行 `issue get` 获取详情 -5. **按阶段调用技能**:无进度 → project-initializer / 草图 → stage-sketch / 总路线 → stage-roadmap / 迭代 → stage-iteration - ---- - -## 11. PM 任务管理命令序列 - -### 创建任务 - -```bash -# 创建 Issue(必须指定 assignees 和 stage 标签) -node scripts/gitea.js --role pm issue create \ - --title "D-001 概念设计" \ - --body "## 任务信息\n- **任务ID**: D-001\n- **所属阶段**: 草图阶段\n- **角色**: 策划\n- **预期产出**: docs/design/概念草案.md\n..." \ - --milestone \ - --assignees ai-designer - -# 设置标签(status + stage + role) -node scripts/gitea.js --role pm issue labels --set ,, - -# 设置依赖 -node scripts/gitea.js --role pm issue block -``` - -### 关闭任务(用户确认后) - -```bash -# 审查通过 + 用户确认关闭后 -node scripts/gitea.js --role pm issue close --body "## 任务完成 ✔\n**完成日期**: YYYY-MM-DD\n**产出**: path/to/output\n**审查记录**: docs/plans/reviews/-审查记录.md" -``` - ---- - -## 12. 角色(非 PM)任务查询 - -非 PM 角色启动时只需三步,**不需要**读取 Pipeline Tracker: - -```bash -# 1. 查看当前激活里程碑 -node scripts/gitea.js --role milestone list --state open - -# 2. 查看分配给自己的活跃任务 -node scripts/gitea.js --role issue list --assigned <自己的用户名> --state open - -# 3. 查看具体任务详情(仅在需要时) -node scripts/gitea.js --role issue get -``` - -### 角色参数速查 - -| 角色 | `--role` | `--assigned` | -|------|----------|-------------| -| 策划 | `designer` | `ai-designer` | -| 程序 | `dev` | `ai-programmer` | -| 美术 | `artist` | `ai-artist` | -| 审查 | `reviewer` | (不拥有任务) | diff --git a/.claude/skills/gitea-collab/SKILL.md b/.claude/skills/gitea-collab/SKILL.md new file mode 100644 index 0000000..41cbc25 --- /dev/null +++ b/.claude/skills/gitea-collab/SKILL.md @@ -0,0 +1,201 @@ +--- +name: "gitea-collab" +description: "通用协作技能 — 任务查询、进度汇报、审查操作。非 PM 角色使用。" +--- + +# Gitea 协作技能 + +> 通用协作技能,供策划、程序、美术、审查角色使用。 +> 所有配置从 `gitea-config.json` 读取。 +> 本文档不含 PM 专属命令。 + +--- + +## 1. CLI 工具 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> [subcommand] [options] +``` + +**`--json` 标志**:所有命令支持 `--json` 输出纯 JSON。 + +--- + +## 2. 角色参数 + +使用 `--role <角色名>` 参数,自动解析为 Token 和身份: + +| 角色 | `--role` 值 | +|------|------------| +| 策划 | `designer` | +| 程序 | `dev` | +| 美术 | `artist` | +| 审查 | `reviewer` | + +**不需要记住 Gitea 用户名** — `--role` 自动处理认证和身份。 + +--- + +## 3. 任务查询(启动时使用) + +### 查看激活里程碑 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> milestone list --state open +``` + +### 查看分配给自己的活跃任务 + +```bash +# --assigned 用角色名,自动映射 +node .claude/skills/gitea-collab/gitea.js --role <角色名> issue list --assigned <角色名> --state open +``` + +### 查看任务详情 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> issue get +``` + +--- + +## 4. 进度汇报 + +### 添加进度评论 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> issue comment --body "汇报内容" +``` + +### 查看已有评论 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> issue comments +``` + +### 查看任务依赖 + +```bash +node .claude/skills/gitea-collab/gitea.js --role <角色名> issue deps +``` + +--- + +## 5. 审查操作(reviewer 使用) + +### 评论审查结果 + +```bash +node .claude/skills/gitea-collab/gitea.js --role reviewer issue comment --body "## 审查通过 ✅ + +**审查类型**:[执行案/程序设计/美术资产/程序实现] +**审查日期**:YYYY-MM-DD +**审查记录**:docs/plans/reviews/-审查记录.md +**审查者**:审查 Agent (ai-reviewer) + +### 结论 +[一句话总结] + +### 检查通过项 / 问题清单 +- ..." +``` + +### 更新 Issue 标签 + +```bash +# 审查通过:移除 status:in-review,添加 review:pass +node .claude/skills/gitea-collab/gitea.js --role reviewer issue labels --set <保留的非status标签ID>, + +# 审查未通过:移除 status:in-review,改为 status:blocked,添加 review:fail +node .claude/skills/gitea-collab/gitea.js --role reviewer issue labels --set ,<保留标签ID>, +``` + +--- + +## 6. 审查评论格式 + +### 通过 + +```markdown +## 审查通过 ✅ + +**审查类型**:[执行案/程序设计/美术资产/程序实现] +**审查日期**:YYYY-MM-DD +**审查记录**:docs/plans/reviews/-审查记录.md +**审查者**:审查 Agent (ai-reviewer) + +### 结论 +[一句话总结] + +### 检查通过项 +- ✅ [检查项1] +- ✅ [检查项2] +``` + +### 未通过 + +```markdown +## 审查未通过 ❌ + +**审查类型**:[执行案/程序设计/美术资产/程序实现] +**审查日期**:YYYY-MM-DD +**审查记录**:docs/plans/reviews/-审查记录.md +**审查者**:审查 Agent (ai-reviewer) + +### 结论 +[一句话总结] + +### 问题清单 +1. **[检查维度]**:[具体问题描述,附文档位置] — [修改建议] +``` + +--- + +## 7. 汇报模板 + +### 策划汇报 + +```markdown +## 策划汇报 — YYYY-MM-DD + +**任务ID**: D-XXX +**状态**: 进行中 / 已完成 +**产出**: path/to/output + +### 完成内容 +- ... + +### 待确认 +- ... +``` + +### 程序汇报 + +```markdown +## 程序汇报 — YYYY-MM-DD + +**任务ID**: P-XXX +**状态**: 进行中 / 已完成 +**产出**: path/to/output + +### 完成内容 +- ... + +### 偏差/问题 +- ... +``` + +### 审查退回后修改汇报 + +```markdown +## 修改汇报 — YYYY-MM-DD + +**任务ID**: +**修改原因**: 质量门审查退回 + +### 修改内容 +- [对应审查意见第X条]:[具体修改内容] + +### 修改后产出 +- path/to/output +``` diff --git a/scripts/gitea-api.js b/.claude/skills/gitea-collab/gitea-api.js similarity index 90% rename from scripts/gitea-api.js rename to .claude/skills/gitea-collab/gitea-api.js index 6909b7c..c5b2e8c 100644 --- a/scripts/gitea-api.js +++ b/.claude/skills/gitea-collab/gitea-api.js @@ -1,29 +1,29 @@ /** - * Gitea API Client — ChildHood 项目进度管理 + * Gitea API Client — 通用项目管理工具 * * 封装所有 Gitea REST API 调用,处理 UTF-8 编码和错误重试。 - * 供 gitea.js CLI 使用。 + * 供 gitea.js CLI 使用。连接信息从 gitea-config.json 读取。 */ const http = require('http'); -const DEFAULT_CONFIG = { - host: 'micecola.top', - port: 8418, - apiBase: '/api/v1', - repoPath: '/repos/starmen1265/ChildHood', - timeout: 15000, -}; - class GiteaAPI { /** * @param {string} token - Gitea API Token - * @param {object} [config] - 覆盖默认配置 + * @param {object} config - 必须包含 host, port, repoOwner, repoName */ constructor(token, config = {}) { if (!token) throw new Error('GITEA_TOKEN 未设置。请设置环境变量 GITEA_TOKEN。'); this.token = token; - this.config = { ...DEFAULT_CONFIG, ...config }; + this.config = { + apiBase: '/api/v1', + timeout: 15000, + ...config, + }; + // 从 config 构建 repoPath + if (!this.config.repoPath && this.config.repoOwner && this.config.repoName) { + this.config.repoPath = `/repos/${this.config.repoOwner}/${this.config.repoName}`; + } } // ====== 底层 HTTP 请求 ====== @@ -206,11 +206,10 @@ class GiteaAPI { /** 设置依赖:number 阻塞 blockedNumber */ addBlock(number, blockedNumber) { - // Gitea 要求 body 为 IssueMeta 格式(含 owner + repo) return this.request('POST', `/issues/${number}/blocks`, { index: blockedNumber, - owner: this.config.repoPath.split('/')[2], // /repos/{owner}/{repo} - repo: this.config.repoPath.split('/')[3], + owner: this.config.repoOwner, + repo: this.config.repoName, }); } @@ -218,8 +217,8 @@ class GiteaAPI { removeBlock(number, blockedNumber) { return this.request('DELETE', `/issues/${number}/blocks`, { index: blockedNumber, - owner: this.config.repoPath.split('/')[2], - repo: this.config.repoPath.split('/')[3], + owner: this.config.repoOwner, + repo: this.config.repoName, }); } @@ -251,4 +250,4 @@ class GiteaAPI { } } -module.exports = { GiteaAPI, DEFAULT_CONFIG }; +module.exports = { GiteaAPI }; diff --git a/scripts/gitea.js b/.claude/skills/gitea-collab/gitea.js similarity index 70% rename from scripts/gitea.js rename to .claude/skills/gitea-collab/gitea.js index f718d39..fd4e1f0 100644 --- a/scripts/gitea.js +++ b/.claude/skills/gitea-collab/gitea.js @@ -1,42 +1,28 @@ #!/usr/bin/env node /** - * ChildHood Gitea CLI — 项目进度管理工具 + * Gitea CLI — 项目管理工具 * - * 用法: node scripts/gitea.js [subcommand] [options] - * 环境变量: GITEA_TOKEN(必须) + * 用法: node gitea.js --role <角色> [subcommand] [options] + * 所有配置(连接信息、Token、角色映射、标签定义)从 gitea-config.json 读取。 */ +const fs = require('fs'); +const path = require('path'); const { GiteaAPI } = require('./gitea-api'); // ═══════════════════════════════════════════════════ -// 标准标签定义 +// 加载配置 // ═══════════════════════════════════════════════════ -const STANDARD_LABELS = [ - // Status (6) - { name: 'status:backlog', color: '#666666', description: '已定义但未排入当前工作' }, - { name: 'status:ready', color: '#007fff', description: '依赖已满足,可以开始' }, - { name: 'status:in-progress', color: '#e8b50a', description: '正在进行中' }, - { name: 'status:blocked', color: '#ee0701', description: '被阻塞(质量门未通过/依赖未完成)' }, - { name: 'status:in-review', color: '#8957e5', description: '等待质量门审查' }, - { name: 'status:done', color: '#0e8a16', description: '已完成' }, - // Gate (8) - { name: 'gate:execution-pass', color: '#28a745', description: '执行案质量门 ✅' }, - { name: 'gate:execution-fail', color: '#d73a49', description: '执行案质量门 ❌' }, - { name: 'gate:tech-pass', color: '#28a745', description: '程序设计质量门 ✅' }, - { name: 'gate:tech-fail', color: '#d73a49', description: '程序设计质量门 ❌' }, - { name: 'gate:art-pass', color: '#28a745', description: '美术资产验收门 ✅' }, - { name: 'gate:art-fail', color: '#d73a49', description: '美术资产验收门 ❌' }, - { name: 'gate:impl-pass', color: '#28a745', description: '程序实现验收门 ✅' }, - { name: 'gate:impl-fail', color: '#d73a49', description: '程序实现验收门 ❌' }, - // Review (2) - { name: 'review:pass', color: '#28a745', description: '审查已通过 ✅' }, - { name: 'review:fail', color: '#d73a49', description: '审查未通过 ❌' }, - // Stage (3) - { name: 'stage:sketch', color: '#bfd4f2', description: '草图阶段' }, - { name: 'stage:roadmap', color: '#d4c5f9', description: '总路线阶段' }, - { name: 'stage:iteration', color: '#fce6c0', description: '迭代阶段' }, -]; +const CONFIG_PATH = path.join(__dirname, 'gitea-config.json'); +let config; +try { + config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); +} catch (e) { + console.error(`❌ 无法读取配置文件: ${CONFIG_PATH}`); + console.error(` ${e.message}`); + process.exit(1); +} // ═══════════════════════════════════════════════════ // 命令行参数解析 @@ -79,38 +65,80 @@ function outText(text) { if (!globalArgs.options.json) console.log(text); } -function outJson(data) { - console.log(JSON.stringify(data, null, 2)); -} - -/** 简易表格输出 */ -function table(headers, rows) { - if (!rows.length) { outText(' (空)'); return; } - const widths = headers.map((h, i) => - Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length)) - ); - const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' '); - outText(' ' + fmt(headers)); - outText(' ' + fmt(headers.map(() => '─'.repeat(2)))); - for (const r of rows) outText(' ' + fmt(r)); -} - function ok(msg) { outText(`✅ ${msg}`); } function fail(msg) { outText(`❌ ${msg}`); } function info(msg) { outText(`ℹ️ ${msg}`); } -/** 截断长文本 */ function truncate(s, max = 60) { if (!s) return ''; const oneLine = s.replace(/\n/g, ' ').trim(); return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine; } -/** 从 Issue 提取 label 名称列表 */ function labelNames(issue) { return (issue.labels || []).map(l => l.name); } +/** 简易表格输出 */ +function table(headers, rows) { + if (!rows.length) { outText(' (空)'); return; } + const widths = headers.map((h, i) => + Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length))) + ; + const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' '); + outText(' ' + fmt(headers)); + outText(' ' + fmt(headers.map(() => '─'.repeat(2)))); + for (const r of rows) outText(' ' + fmt(r)); +} + +// ═══════════════════════════════════════════════════ +// 角色解析 +// ═══════════════════════════════════════════════════ + +/** + * 获取 Token(优先级:环境变量 > config.roles[role].token) + */ +function resolveToken(options) { + if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN; + + const role = options.role; + if (role) { + const validRoles = Object.keys(config.roles); + if (!validRoles.includes(role)) { + console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`); + process.exit(1); + } + const token = config.roles[role]?.token; + if (!token) { + console.error(`❌ 角色 "${role}" 未配置 token。请在 gitea-config.json 中设置。`); + process.exit(1); + } + return token; + } + + console.error('❌ 缺少认证信息。请使用以下方式之一:'); + console.error(' --role <角色名> 从 gitea-config.json 读取 Token'); + console.error(' GITEA_TOKEN=xxx 通过环境变量传入'); + process.exit(1); +} + +/** + * 解析 assignee:支持角色名(自动映射)或直接用户名 + */ +function resolveAssignee(value) { + if (!value) return null; + return config.roles[value]?.assignee || value; +} + +/** + * 自动从 --role 解析 assignee(用于 issue create) + */ +function autoAssignee(role) { + if (!role) return []; + const assignee = config.roles[role]?.assignee; + return assignee ? [assignee] : []; +} + // ═══════════════════════════════════════════════════ // 命令实现 // ═══════════════════════════════════════════════════ @@ -124,7 +152,6 @@ cmd.issue = { const params = {}; if (opts.options.state) params.state = opts.options.state; if (opts.options.milestone) { - // 支持数字 ID 或标题 const ms = isNaN(opts.options.milestone) ? await api.findMilestoneByTitle(opts.options.milestone) : { id: Number(opts.options.milestone) }; @@ -132,7 +159,10 @@ cmd.issue = { else { fail(`未找到里程碑: ${opts.options.milestone}`); return; } } if (opts.options.labels) params.labels = opts.options.labels; - if (opts.options.assigned) params.assigned_by = opts.options.assigned; + if (opts.options.assigned) { + // 支持角色名或直接用户名 + params.assigned_by = resolveAssignee(opts.options.assigned); + } const issues = await api.listIssues(params); out(issues); @@ -186,8 +216,11 @@ cmd.issue = { if (opts.options.labels) { data.labels = opts.options.labels.split(',').map(Number); } + // assignee: 显式指定 > 自动从 --role 映射 if (opts.options.assignees) { - data.assignees = opts.options.assignees.split(','); + data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); + } else { + data.assignees = autoAssignee(opts.options.role); } const issue = await api.createIssue(data); @@ -212,7 +245,9 @@ cmd.issue = { : { id: Number(opts.options.milestone) }; if (ms) data.milestone = ms.id; } - if (opts.options.assignees) data.assignees = opts.options.assignees.split(','); + if (opts.options.assignees) { + data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); + } const issue = await api.updateIssue(num, data); out(issue); @@ -273,7 +308,6 @@ cmd.issue = { await api.replaceIssueLabels(num, ids); if (!opts.options.json) ok(`Issue #${num} 的标签已替换为: ${ids.join(', ')}`); } else { - // 查询当前标签 const labels = await api.getIssueLabels(num); out(labels); if (!opts.options.json) { @@ -392,11 +426,13 @@ cmd.label = { async init(api, opts) { outText('\n🏷️ 初始化标准标签体系…\n'); + // 从 config.labels 合并所有标签定义 + const allLabels = Object.values(config.labels).flat(); const existing = await api.listLabels(); const existingNames = new Set(existing.map(l => l.name)); let created = 0, skipped = 0; - for (const def of STANDARD_LABELS) { + for (const def of allLabels) { if (existingNames.has(def.name)) { info(`跳过 (已存在): ${def.name}`); skipped++; @@ -414,31 +450,21 @@ cmd.label = { cmd.pipeline = { async init(api, opts) { - const title = opts.options.title || 'Pipeline Tracker — v0.1'; + const title = opts.options.title || 'Pipeline Tracker'; const milestone = opts.options.milestone; const body = `# 流水线进度追踪 -## v0.1 流水线 +## 当前阶段 +草图阶段 -| 步骤 | 状态 | 说明 | -|------|------|------| -| ① 设计案 | ✅ 已完成 | \`docs/design/prototype/\` 下 10 份设计文档 | -| ② 阶段路线图 | ✅ 已完成 | \`docs/plans/prototype-roadmap.md\` | -| ③ 执行案 | 🔴 质量门未通过 | 需创建独立执行案并修正 | -| ④ 程序设计 | ⬜ 未开始 | 依赖 ③ | -| ⑤ 美术产出 | ⬜ 未开始 | 白盒场景 + 占位资产 | -| ⑥ 程序实现 | ⬜ 未开始 | 原型允许与 ⑤ 交替 | -| ⑦ 版本复盘 | ⬜ 未开始 | — | +## 当前迭代 +(非迭代阶段,留空) -## 当前阻塞 - -③ 执行案质量门未通过,需修正后重新提交 - -## 相关任务 - -> PM 在此维护 Issue 索引 -`; +## 阶段历史 +| 阶段 | 里程碑 | 进入日期 | 完成日期 | +|------|--------|----------|----------| +| 草图阶段 | 草图阶段 | ${new Date().toISOString().slice(0, 10)} | — |`; const data = { title, body }; if (milestone) { @@ -457,10 +483,8 @@ cmd.pipeline = { }, async status(api, opts) { - // 查找 Pipeline Tracker Issue let tracker = await api.findIssueByTitle('Pipeline Tracker'); if (!tracker) { fail('未找到 Pipeline Tracker Issue。请先运行: pipeline init'); return; } - // 获取完整内容 tracker = await api.getIssue(tracker.number); out({ number: tracker.number, title: tracker.title, body: tracker.body }); if (!opts.options.json) { @@ -471,30 +495,28 @@ cmd.pipeline = { async update(api, opts) { const num = opts.rest[0]; - const step = opts.options.step; - const status = opts.options.status; - if (!num || !step || !status) { - fail('用法: pipeline update --step <1-7> --status <状态描述>'); + const field = opts.options.field; + const value = opts.options.value; + if (!num || !field || !value) { + fail('用法: pipeline update --field <阶段|当前迭代> --value <值>'); return; } const issue = await api.getIssue(num); let body = issue.body || ''; - const stepLabels = ['', '① 设计案', '② 阶段路线图', '③ 执行案', '④ 程序设计', '⑤ 美术产出', '⑥ 程序实现', '⑦ 版本复盘']; - const stepLabel = stepLabels[Number(step)]; - if (!stepLabel) { fail(`无效步骤: ${step},应为 1-7`); return; } - - // 替换对应步骤的状态 - const regex = new RegExp(`(\\| ${stepLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\| )(.+?)( \\|)`); - if (regex.test(body)) { - body = body.replace(regex, `$1${status}$3`); - await api.updateIssue(num, { body }); - out({ number: Number(num), step, status, updated: true }); - if (!opts.options.json) ok(`步骤 ${stepLabel} 已更新为: ${status}`); + if (field === '阶段') { + body = body.replace(/## 当前阶段\n.*/, `## 当前阶段\n${value}`); + } else if (field === '当前迭代') { + body = body.replace(/## 当前迭代\n.*/, `## 当前迭代\n${value}`); } else { - fail(`未在 Issue #${num} 的 body 中找到步骤: ${stepLabel}`); + fail(`无效字段: ${field},可选: 阶段, 当前迭代`); + return; } + + await api.updateIssue(num, { body }); + out({ number: Number(num), field, value, updated: true }); + if (!opts.options.json) ok(`Pipeline Tracker 字段 "${field}" 已更新为: ${value}`); }, }; @@ -525,75 +547,77 @@ cmd.test = { await test('GET /milestones', () => api.listMilestones()); await test('GET /issues', () => api.listIssues({ limit: 5 })); await test('GET /labels', () => api.listLabels()); - await test('GET /issues/1', () => api.getIssue(1)); - await test('GET /issues/1/comments', () => api.listComments(1)); - await test('GET /issues/1/dependencies', () => api.listDependencies(1)); - await test('GET /issues/1/blocks', () => api.listBlocks(1)); + + // 查找第一个存在的 Issue 用于后续测试 + const issues = await api.listIssues({ limit: 5, state: 'all' }); + const testIssue = issues.find(i => i.state === 'open') || issues[0]; + if (testIssue) { + await test(`GET /issues/${testIssue.number}`, () => api.getIssue(testIssue.number)); + await test(`GET /issues/${testIssue.number}/comments`, () => api.listComments(testIssue.number)); + await test(`GET /issues/${testIssue.number}/dependencies`, () => api.listDependencies(testIssue.number)); + await test(`GET /issues/${testIssue.number}/blocks`, () => api.listBlocks(testIssue.number)); + } await test('POST 创建含中文 Issue', async () => { const i = await api.createIssue({ title: '测试 Issue — UTF-8 验证 🎮', body: '这是中文内容测试。\n\n- 列表项\n- 特殊字符:✅❌🔴⬜\n\n创建时间: ' + new Date().toISOString(), }); - // 立即关闭测试 Issue await api.closeIssue(i.number); return `#${i.number}: ${i.title}`; }); - await test('POST 创建含中文评论', async () => { - const c = await api.createComment(1, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19)); - return `comment_id: ${c.id}`; - }); + if (testIssue) { + await test('POST 创建含中文评论', async () => { + const c = await api.createComment(testIssue.number, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19)); + return `comment_id: ${c.id}`; + }); - await test('PATCH 更新 Issue', async () => { - // 读取 Issue #2 的当前标题,更新后再还原 - const orig = await api.getIssue(2); - const newTitle = orig.title + ' [测试更新]'; - await api.updateIssue(2, { title: newTitle }); - await api.updateIssue(2, { title: orig.title }); // 还原 - return 'Issue #2 标题更新并还原'; - }); + await test('PATCH 更新 Issue', async () => { + const orig = await api.getIssue(testIssue.number); + const newTitle = orig.title + ' [测试更新]'; + await api.updateIssue(testIssue.number, { title: newTitle }); + await api.updateIssue(testIssue.number, { title: orig.title }); + return `Issue #${testIssue.number} 标题更新并还原`; + }); + } await test('PATCH 关闭/重开 Issue', async () => { - // 创建临时 Issue 测试 const tmp = await api.createIssue({ title: '临时测试 — 开关', body: '将被关闭再打开' }); await api.closeIssue(tmp.number); await api.reopenIssue(tmp.number); - await api.closeIssue(tmp.number); // 最终关闭 + await api.closeIssue(tmp.number); return `#${tmp.number} 关闭→重开→关闭`; }); await test('POST 创建 Label', async () => { const l = await api.createLabel({ name: '_test-label', color: 'ededed' }); - // 清理 try { await api.deleteLabel(l.id); } catch { /* ignore */ } return `label: ${l.name} (ID: ${l.id}) 已创建并清理`; }); - await test('POST Issue 添加标签', async () => { - // 获取已有 labels,添加到 Issue #1 再移除 - const labels = await api.listLabels(); - if (!labels.length) return '无可用标签,跳过'; - await api.addIssueLabels(1, [labels[0].id]); - return `添加标签 ${labels[0].name} 到 Issue #1`; - }); + if (testIssue) { + await test('POST Issue 添加标签', async () => { + const labels = await api.listLabels(); + if (!labels.length) return '无可用标签,跳过'; + await api.addIssueLabels(testIssue.number, [labels[0].id]); + return `添加标签 ${labels[0].name} 到 Issue #${testIssue.number}`; + }); + } // P1: 核心功能 outText('\n── P1: 核心功能验证 ──\n'); - await test('GET /issues?milestones=X 过滤', async () => { - const ms = await api.findMilestoneByTitle('v0.1-基础交互'); - if (!ms) return '未找到 v0.1 里程碑'; - const issues = await api.listIssues({ milestones: ms.id }); - return `里程碑 ${ms.title} 下有 ${issues.length} 个 Issue`; - }); - - await test('POST/DELETE Issue 依赖', async () => { - // Issue #1 block #2,然后移除 - await api.addBlock(1, 2); - await api.removeBlock(1, 2); - return '依赖关系添加→移除'; - }); + if (testIssue && issues.length >= 2) { + const other = issues.find(i => i.number !== testIssue.number); + if (other) { + await test('POST/DELETE Issue 依赖', async () => { + await api.addBlock(testIssue.number, other.number); + await api.removeBlock(testIssue.number, other.number); + return `依赖关系 ${testIssue.number}→${other.number} 添加→移除`; + }); + } + } await test('POST/PATCH Milestone', async () => { const ms = await api.createMilestone({ title: '_test-milestone', state: 'open' }); @@ -616,32 +640,34 @@ cmd.test = { // ═══════════════════════════════════════════════════ function showHelp() { + const roles = Object.keys(config.roles).join('|'); console.log(` -ChildHood Gitea CLI — 项目进度管理工具 +Gitea CLI — 项目管理工具 -用法: node scripts/gitea.js [subcommand] [options] +用法: node gitea.js --role <${roles}> [subcommand] [options] 全局选项: --help 显示帮助信息 - --json 以 JSON 格式输出(供 Agent 脚本调用) - --role 使用对应角色的 Token(从 scripts/.gitea-tokens.json 读取) + --json 以 JSON 格式输出 + --role <角色> 使用对应角色的身份(自动解析 Token 和 Assignee) 命令列表: issue list 列出 Issues - [--state open|closed|all] [--milestone ] [--labels ] [--assigned <用户名>] + [--state open|closed|all] [--milestone ] + [--labels ] [--assigned <角色名|用户名>] issue get <#> 查看 Issue 详情 - issue create 创建 Issue - --title "标题" [--body "内容"] [--milestone ] [--labels ] [--assignees <用户名,...>] - issue update <#> 更新 Issue - [--title "…"] [--body "…"] [--state open|closed] [--milestone ] + issue create 创建 Issue(assignee 自动从 --role 映射) + --title "标题" [--body "内容"] [--milestone ] + [--labels ] [--assignees <角色名,...>] + issue update <#> 更新 Issue [--title "…"] [--body "…"] [--state open|closed] issue close <#> 关闭 Issue [--body "完成评论"] issue reopen <#> 重新打开 Issue issue comment <#> 添加评论 --body "内容" issue comments <#> 查看评论列表 issue labels <#> 查看/管理标签 [--add ] [--set ] issue deps <#> 查看依赖关系 - issue block <#> <#> 设置依赖关系(前者阻塞后者) + issue block <#> <#> 设置依赖关系 issue unblock <#> <#> 移除依赖关系 milestone list 列出里程碑 [--state open|closed|all] @@ -650,24 +676,20 @@ ChildHood Gitea CLI — 项目进度管理工具 label list 列出标签 label create 创建标签 --name "名称" [--color "#RRGGBB"] - label init 初始化标准标签体系(status/gate 共 14 个) + label init 初始化标准标签体系(从 gitea-config.json 读取定义) - pipeline init 创建 Pipeline Tracker Meta Issue [--title "…"] [--milestone ] + pipeline init 创建 Pipeline Tracker Meta Issue pipeline status 查看当前流水线进度 - pipeline update <#> 更新流水线步骤 --step <1-7> --status "状态描述" + pipeline update <#> 更新 Pipeline 字段 --field <阶段|当前迭代> --value <值> - test 验证所有 API 接口(P0 阻塞性 + P1 核心功能) + test 验证所有 API 接口 -Token 管理: - --role 从 scripts/.gitea-tokens.json 自动读取对应角色的 Token - GITEA_TOKEN 环境变量 手动覆盖(优先级最高) - 两者都不提供时报错 +角色自动映射: + --role <角色名> 自动解析为 Token + Assignee + --assigned <角色名> 自动解析为 Gitea 用户名 + --assignees <角色名,...> 同上 -示例: - node scripts/gitea.js --role pm issue list --milestone v0.1-基础交互 - node scripts/gitea.js --role designer issue comment 2 --body "汇报内容" - node scripts/gitea.js --role dev pipeline status - GITEA_TOKEN=xxx node scripts/gitea.js test +配置文件: gitea-config.json(与 gitea.js 同目录) `); } @@ -677,60 +699,6 @@ Token 管理: let globalArgs; -// ====== Token 文件读取 ====== - -const path = require('path'); - -/** - * 从 scripts/.gitea-tokens.json 读取指定角色的 Token - * @param {string} role - 角色名(pm / designer / dev) - * @returns {string|null} Token 或 null - */ -function loadTokenFromFile(role) { - const tokenFile = path.join(__dirname, '.gitea-tokens.json'); - try { - const fs = require('fs'); - const data = JSON.parse(fs.readFileSync(tokenFile, 'utf-8')); - return data[role] || null; - } catch { - return null; - } -} - -/** - * 获取 Token(优先级:环境变量 > --role 文件 > 报错) - * @param {object} options - 解析后的 CLI options - * @returns {string} Token - */ -function resolveToken(options) { - // 优先级 1:环境变量 - if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN; - - // 优先级 2:--role 参数 - const role = options.role; - if (role) { - const validRoles = ['pm', 'designer', 'dev', 'artist', 'reviewer']; - if (!validRoles.includes(role)) { - console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`); - process.exit(1); - } - const token = loadTokenFromFile(role); - if (!token) { - console.error(`❌ 未在 scripts/.gitea-tokens.json 中找到角色 "${role}" 的 Token`); - console.error(' 请创建 scripts/.gitea-tokens.json,格式:'); - console.error(' { "pm": "token1", "designer": "token2", "dev": "token3" }'); - process.exit(1); - } - return token; - } - - // 都没有 - console.error('❌ 缺少认证信息。请使用以下方式之一:'); - console.error(' --role pm 从 scripts/.gitea-tokens.json 读取 Token'); - console.error(' GITEA_TOKEN=xxx 通过环境变量传入'); - process.exit(1); -} - async function main() { const args = parseArgs(process.argv.slice(2)); globalArgs = args; @@ -743,7 +711,13 @@ async function main() { // Token 解析 const token = resolveToken(args.options); - const api = new GiteaAPI(token); + // 构建 API 实例(从 config 读取连接信息) + const api = new GiteaAPI(token, { + host: config.host, + port: config.port, + repoOwner: config.repoOwner, + repoName: config.repoName, + }); // 路由 const commandGroup = cmd[args.command]; @@ -753,7 +727,6 @@ async function main() { process.exit(1); } - // 特殊: test 是单级命令 if (args.command === 'test') { await cmd.test.run(api, args); return; diff --git a/.claude/skills/gitea-pm/SKILL.md b/.claude/skills/gitea-pm/SKILL.md new file mode 100644 index 0000000..c1bdf56 --- /dev/null +++ b/.claude/skills/gitea-pm/SKILL.md @@ -0,0 +1,233 @@ +--- +name: "gitea-pm" +description: "PM 专属项目管理技能 — 任务创建/关闭/里程碑管理/Pipeline Tracker、会话启动、角色汇报模板。" +--- + +# Gitea PM 技能 + +> PM Agent 的专属技能,封装所有项目管理操作。 +> 所有配置(连接信息、Token、角色映射、标签定义)从 `gitea-config.json` 读取。 +> 本文档不含项目特定信息。 + +--- + +## 1. CLI 工具 + +```bash +node .claude/skills/gitea-pm/gitea.js --role pm [subcommand] [options] +``` + +**`--json` 标志**:所有命令支持 `--json` 输出纯 JSON,供脚本解析。 + +--- + +## 2. 认证与角色映射 + +- `--role <角色名>` 自动解析 Token 和 Assignee(从 `gitea-config.json` 的 `roles` 配置) +- `--assigned <角色名>` 自动解析为 Gitea 用户名 +- `--assignees <角色名,...>` 同上 +- 环境变量 `GITEA_TOKEN` 优先级最高 + +--- + +## 3. PM 会话启动 + +PM 每次启动时: + +1. **读取当前阶段**:`pipeline status` +2. **列出激活里程碑**:`milestone list --state open` +3. **列出所有活跃任务**:`issue list --state open`,展示摘要 +4. **展开需要关注的任务**:仅对 `status:in-progress`、`status:blocked`、`review:pass` 的任务执行 `issue get` +5. **按阶段调用技能**:无进度 → project-initializer / 草图 → stage-sketch / 总路线 → stage-roadmap / 迭代 → stage-iteration + +--- + +## 4. 命令速查 + +### 🔒 PM-only 命令 + +#### 任务创建 + +```bash +# 创建任务(assignee 自动从 --role 映射;如需指定其他角色,用 --assignees) +node .claude/skills/gitea-pm/gitea.js --role pm issue create \ + --title "D-001 概念设计" \ + --body "任务正文" \ + --milestone \ + --labels + +# 创建任务并指派给策划(显式指定) +node .claude/skills/gitea-pm/gitea.js --role pm issue create \ + --title "D-001 概念设计" \ + --assignees designer \ + --milestone + +# 设置标签 +node .claude/skills/gitea-pm/gitea.js --role pm issue labels --set + +# 设置依赖 +node .claude/skills/gitea-pm/gitea.js --role pm issue block +node .claude/skills/gitea-pm/gitea.js --role pm issue unblock +``` + +#### 任务关闭(用户确认后) + +```bash +node .claude/skills/gitea-pm/gitea.js --role pm issue close --body "关闭说明" +``` + +#### 任务重开 + +```bash +node .claude/skills/gitea-pm/gitea.js --role pm issue reopen +``` + +#### 里程碑管理 + +```bash +node .claude/skills/gitea-pm/gitea.js --role pm milestone create \ + --title "标题" --description "描述" + +node .claude/skills/gitea-pm/gitea.js --role pm milestone update \ + --title "..." --state closed +``` + +#### Pipeline Tracker + +```bash +# 初始化 +node .claude/skills/gitea-pm/gitea.js --role pm pipeline init + +# 更新阶段 +node .claude/skills/gitea-pm/gitea.js --role pm pipeline update --field 阶段 --value "迭代阶段" + +# 更新当前迭代 +node .claude/skills/gitea-pm/gitea.js --role pm pipeline update --field 当前迭代 --value "v1.4" +``` + +#### 标签初始化 + +```bash +node .claude/skills/gitea-pm/gitea.js --role pm label init +node .claude/skills/gitea-pm/gitea.js --role pm label create --name "名称" --color "#RRGGBB" +``` + +### 通用命令(PM 也用) + +```bash +# 查询 +node .claude/skills/gitea-pm/gitea.js --role pm issue list --state open +node .claude/skills/gitea-pm/gitea.js --role pm issue get +node .claude/skills/gitea-pm/gitea.js --role pm issue comments +node .claude/skills/gitea-pm/gitea.js --role pm issue deps +node .claude/skills/gitea-pm/gitea.js --role pm milestone list --state open +node .claude/skills/gitea-pm/gitea.js --role pm label list +node .claude/skills/gitea-pm/gitea.js --role pm pipeline status + +# 测试连通 +node .claude/skills/gitea-pm/gitea.js --role pm test +``` + +--- + +## 5. 任务 ID 规范 + +格式:`[角色前缀]-[三位数字]` + +| 前缀 | 角色 | `--assignees` 值(角色名) | +|------|------|--------------------------| +| `D` | 策划 | `designer` | +| `P` | 程序 | `dev` | +| `A` | 美术 | `artist` | + +每个前缀从 001 独立编号。 + +--- + +## 6. Pipeline Tracker + +Pipeline Tracker 是一个 Meta Issue,仅记录**无法从 Issue/Milestone 推导**的信息: + +- **当前阶段**:草图阶段 / 总路线阶段 / 迭代阶段 +- **当前迭代**:版本号(迭代阶段时填写) +- **阶段历史**:阶段转换记录表 + +**不记录**:里程碑详情、Issues 计数、流水线步骤状态、任务索引(这些从查询获取)。 + +--- + +## 7. 角色汇报模板 + +各角色完成工作后在对应 Issue 下评论。 + +### 策划汇报 + +``` +## 策划汇报 — YYYY-MM-DD + +**任务ID**: D-XXX +**状态**: 进行中 / 已完成 +**产出**: path/to/output + +### 完成内容 +- ... + +### 待确认 +- ... +``` + +### 程序汇报 + +``` +## 程序汇报 — YYYY-MM-DD + +**任务ID**: P-XXX +**状态**: 进行中 / 已完成 +**产出**: path/to/output + +### 完成内容 +- ... + +### 偏差/问题 +- ... +``` + +### 审查修改后汇报 + +``` +## 修改汇报 — YYYY-MM-DD + +**任务ID**: +**修改原因**: 质量门审查退回 + +### 修改内容 +- [对应审查意见第X条]:[具体修改内容] + +### 修改后产出 +- path/to/output +``` + +### PM 确认关闭任务 + +``` +## 任务完成 ✔ + +**完成日期**:YYYY-MM-DD +**产出**:path/to/output +**审查记录**:docs/plans/reviews/-审查记录.md +``` + +--- + +## 8. 标签体系 + +从 `gitea-config.json` 的 `labels` 字段定义。`label init` 命令从此配置创建标签。 + +### 标签类别 + +| 类别 | 前缀 | 用途 | +|------|------|------| +| status | `status:` | 任务状态(backlog/ready/in-progress/blocked/in-review/done) | +| review | `review:` | 审查结果(pass/fail) | +| stage | `stage:` | 开发阶段(sketch/roadmap/iteration) | +| gate | `gate:` | 旧标签,保留用于历史 Issue | diff --git a/.claude/skills/gitea-pm/gitea-api.js b/.claude/skills/gitea-pm/gitea-api.js new file mode 100644 index 0000000..c5b2e8c --- /dev/null +++ b/.claude/skills/gitea-pm/gitea-api.js @@ -0,0 +1,253 @@ +/** + * Gitea API Client — 通用项目管理工具 + * + * 封装所有 Gitea REST API 调用,处理 UTF-8 编码和错误重试。 + * 供 gitea.js CLI 使用。连接信息从 gitea-config.json 读取。 + */ + +const http = require('http'); + +class GiteaAPI { + /** + * @param {string} token - Gitea API Token + * @param {object} config - 必须包含 host, port, repoOwner, repoName + */ + constructor(token, config = {}) { + if (!token) throw new Error('GITEA_TOKEN 未设置。请设置环境变量 GITEA_TOKEN。'); + this.token = token; + this.config = { + apiBase: '/api/v1', + timeout: 15000, + ...config, + }; + // 从 config 构建 repoPath + if (!this.config.repoPath && this.config.repoOwner && this.config.repoName) { + this.config.repoPath = `/repos/${this.config.repoOwner}/${this.config.repoName}`; + } + } + + // ====== 底层 HTTP 请求 ====== + + /** + * 发送 API 请求 + * @param {string} method - GET / POST / PATCH / PUT / DELETE + * @param {string} path - API 路径(相对于 repo root),如 /issues、/milestones/2 + * @param {object|null} data - 请求体(自动 JSON 序列化) + * @param {object} [params] - URL 查询参数 + * @returns {Promise} 解析后的 JSON 响应 + */ + async request(method, path, data = null, params = {}) { + const basePath = this.config.apiBase + this.config.repoPath + path; + const url = new URL(`http://${this.config.host}:${this.config.port}${basePath}`); + + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== '') { + url.searchParams.set(k, String(v)); + } + } + + const body = data ? JSON.stringify(data) : null; + + const headers = { + 'Authorization': `token ${this.token}`, + 'Accept': 'application/json', + }; + if (body) { + headers['Content-Type'] = 'application/json; charset=utf-8'; + headers['Content-Length'] = Buffer.byteLength(body, 'utf-8'); + } + + return new Promise((resolve, reject) => { + const options = { + hostname: url.hostname, + port: url.port || 80, + path: url.pathname + url.search, + method: method.toUpperCase(), + headers, + }; + + const req = http.request(options, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf-8'); + if (res.statusCode >= 400) { + const err = new Error(`HTTP ${res.statusCode}: ${raw.substring(0, 300)}`); + err.statusCode = res.statusCode; + err.body = raw; + reject(err); + return; + } + if (!raw.trim()) { resolve(null); return; } + try { resolve(JSON.parse(raw)); } + catch { resolve(raw); } + }); + }); + + req.on('error', reject); + req.setTimeout(this.config.timeout, () => { + req.destroy(); + reject(new Error(`请求超时 (${this.config.timeout}ms): ${method} ${path}`)); + }); + + if (body) req.write(body, 'utf-8'); + req.end(); + }); + } + + // ====== Issues ====== + + listIssues(params = {}) { + return this.request('GET', '/issues', null, { state: 'open', limit: 50, ...params }); + } + + getIssue(number) { + return this.request('GET', `/issues/${number}`); + } + + createIssue(data) { + return this.request('POST', '/issues', data); + } + + updateIssue(number, data) { + return this.request('PATCH', `/issues/${number}`, data); + } + + closeIssue(number, comment) { + const p = this.updateIssue(number, { state: 'closed' }); + if (comment) return p.then(() => this.createComment(number, comment)); + return p; + } + + reopenIssue(number) { + return this.updateIssue(number, { state: 'open' }); + } + + // ====== Comments ====== + + listComments(number) { + return this.request('GET', `/issues/${number}/comments`); + } + + createComment(number, body) { + return this.request('POST', `/issues/${number}/comments`, { body }); + } + + // ====== Labels ====== + + listLabels(params = {}) { + return this.request('GET', '/labels', null, { page: 1, limit: 100, ...params }); + } + + createLabel(data) { + return this.request('POST', '/labels', data); + } + + getLabel(id) { + return this.request('GET', `/labels/${id}`); + } + + deleteLabel(id) { + return this.request('DELETE', `/labels/${id}`); + } + + getIssueLabels(number) { + return this.request('GET', `/issues/${number}/labels`); + } + + /** 添加标签到 Issue(不清除已有标签) */ + addIssueLabels(number, labelIds) { + return this.request('POST', `/issues/${number}/labels`, { labels: labelIds }); + } + + /** 替换 Issue 的全部标签 */ + replaceIssueLabels(number, labelIds) { + return this.request('PUT', `/issues/${number}/labels`, { labels: labelIds }); + } + + /** 移除 Issue 的单个标签 */ + removeIssueLabel(number, labelId) { + return this.request('DELETE', `/issues/${number}/labels/${labelId}`); + } + + // ====== Milestones ====== + + listMilestones(params = {}) { + return this.request('GET', '/milestones', null, { limit: 50, ...params }); + } + + getMilestone(id) { + return this.request('GET', `/milestones/${id}`); + } + + createMilestone(data) { + return this.request('POST', '/milestones', data); + } + + updateMilestone(id, data) { + return this.request('PATCH', `/milestones/${id}`, data); + } + + closeMilestone(id) { + return this.updateMilestone(id, { state: 'closed' }); + } + + // ====== Dependencies ====== + + /** 获取阻塞当前 Issue 的前置任务列表 */ + listDependencies(number) { + return this.request('GET', `/issues/${number}/dependencies`); + } + + /** 获取当前 Issue 阻塞的任务列表 */ + listBlocks(number) { + return this.request('GET', `/issues/${number}/blocks`); + } + + /** 设置依赖:number 阻塞 blockedNumber */ + addBlock(number, blockedNumber) { + return this.request('POST', `/issues/${number}/blocks`, { + index: blockedNumber, + owner: this.config.repoOwner, + repo: this.config.repoName, + }); + } + + /** 移除依赖 */ + removeBlock(number, blockedNumber) { + return this.request('DELETE', `/issues/${number}/blocks`, { + index: blockedNumber, + owner: this.config.repoOwner, + repo: this.config.repoName, + }); + } + + // ====== 辅助方法 ====== + + /** 通过名称查找标签,返回 ID(不存在则返回 null) */ + async findLabelByName(name) { + const labels = await this.listLabels(); + return labels.find(l => l.name === name) || null; + } + + /** 通过名称查找标签,不存在则创建,返回标签对象 */ + async ensureLabel(name, color) { + const existing = await this.findLabelByName(name); + if (existing) return existing; + return this.createLabel({ name, color }); + } + + /** 通过标题查找 Milestone,返回 ID */ + async findMilestoneByTitle(title) { + const milestones = await this.listMilestones(); + return milestones.find(m => m.title === title) || null; + } + + /** 通过标题查找 Issue(模糊匹配) */ + async findIssueByTitle(title) { + const issues = await this.listIssues({ state: 'all' }); + return issues.find(i => i.title.includes(title)) || null; + } +} + +module.exports = { GiteaAPI }; diff --git a/.claude/skills/gitea-pm/gitea.js b/.claude/skills/gitea-pm/gitea.js new file mode 100644 index 0000000..fd4e1f0 --- /dev/null +++ b/.claude/skills/gitea-pm/gitea.js @@ -0,0 +1,757 @@ +#!/usr/bin/env node +/** + * Gitea CLI — 项目管理工具 + * + * 用法: node gitea.js --role <角色> [subcommand] [options] + * 所有配置(连接信息、Token、角色映射、标签定义)从 gitea-config.json 读取。 + */ + +const fs = require('fs'); +const path = require('path'); +const { GiteaAPI } = require('./gitea-api'); + +// ═══════════════════════════════════════════════════ +// 加载配置 +// ═══════════════════════════════════════════════════ + +const CONFIG_PATH = path.join(__dirname, 'gitea-config.json'); +let config; +try { + config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); +} catch (e) { + console.error(`❌ 无法读取配置文件: ${CONFIG_PATH}`); + console.error(` ${e.message}`); + process.exit(1); +} + +// ═══════════════════════════════════════════════════ +// 命令行参数解析 +// ═══════════════════════════════════════════════════ + +function parseArgs(argv) { + const positional = []; + const options = {}; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--') { positional.push(...argv.slice(i + 1)); break; } + if (arg.startsWith('--')) { + const key = arg.slice(2); + const next = argv[i + 1]; + if (next && !next.startsWith('--')) { + options[key] = next; + i++; + } else { + options[key] = true; + } + } else { + positional.push(arg); + } + } + return { command: positional[0] || '', sub: positional[1] || '', rest: positional.slice(2), options }; +} + +// ═══════════════════════════════════════════════════ +// 输出格式化 +// ═══════════════════════════════════════════════════ + +function out(data) { + if (globalArgs.options.json) { + console.log(JSON.stringify(data, null, 2)); + } +} + +function outText(text) { + if (!globalArgs.options.json) console.log(text); +} + +function ok(msg) { outText(`✅ ${msg}`); } +function fail(msg) { outText(`❌ ${msg}`); } +function info(msg) { outText(`ℹ️ ${msg}`); } + +function truncate(s, max = 60) { + if (!s) return ''; + const oneLine = s.replace(/\n/g, ' ').trim(); + return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine; +} + +function labelNames(issue) { + return (issue.labels || []).map(l => l.name); +} + +/** 简易表格输出 */ +function table(headers, rows) { + if (!rows.length) { outText(' (空)'); return; } + const widths = headers.map((h, i) => + Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length))) + ; + const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' '); + outText(' ' + fmt(headers)); + outText(' ' + fmt(headers.map(() => '─'.repeat(2)))); + for (const r of rows) outText(' ' + fmt(r)); +} + +// ═══════════════════════════════════════════════════ +// 角色解析 +// ═══════════════════════════════════════════════════ + +/** + * 获取 Token(优先级:环境变量 > config.roles[role].token) + */ +function resolveToken(options) { + if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN; + + const role = options.role; + if (role) { + const validRoles = Object.keys(config.roles); + if (!validRoles.includes(role)) { + console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`); + process.exit(1); + } + const token = config.roles[role]?.token; + if (!token) { + console.error(`❌ 角色 "${role}" 未配置 token。请在 gitea-config.json 中设置。`); + process.exit(1); + } + return token; + } + + console.error('❌ 缺少认证信息。请使用以下方式之一:'); + console.error(' --role <角色名> 从 gitea-config.json 读取 Token'); + console.error(' GITEA_TOKEN=xxx 通过环境变量传入'); + process.exit(1); +} + +/** + * 解析 assignee:支持角色名(自动映射)或直接用户名 + */ +function resolveAssignee(value) { + if (!value) return null; + return config.roles[value]?.assignee || value; +} + +/** + * 自动从 --role 解析 assignee(用于 issue create) + */ +function autoAssignee(role) { + if (!role) return []; + const assignee = config.roles[role]?.assignee; + return assignee ? [assignee] : []; +} + +// ═══════════════════════════════════════════════════ +// 命令实现 +// ═══════════════════════════════════════════════════ + +const cmd = {}; + +// ──────── issue ──────── + +cmd.issue = { + async list(api, opts) { + const params = {}; + if (opts.options.state) params.state = opts.options.state; + if (opts.options.milestone) { + const ms = isNaN(opts.options.milestone) + ? await api.findMilestoneByTitle(opts.options.milestone) + : { id: Number(opts.options.milestone) }; + if (ms) params.milestones = ms.id; + else { fail(`未找到里程碑: ${opts.options.milestone}`); return; } + } + if (opts.options.labels) params.labels = opts.options.labels; + if (opts.options.assigned) { + // 支持角色名或直接用户名 + params.assigned_by = resolveAssignee(opts.options.assigned); + } + + const issues = await api.listIssues(params); + out(issues); + if (!opts.options.json) { + outText(`\n📋 Issues (${issues.length || 0})\n`); + if (!issues || !issues.length) { outText(' (无)'); return; } + table( + ['#', '标题', '状态', '里程碑', '标签'], + issues.map(i => [ + i.number, + truncate(i.title, 40), + i.state, + i.milestone?.title || '—', + labelNames(i).join(', ') || '—', + ]) + ); + } + }, + + async get(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue get '); return; } + const issue = await api.getIssue(num); + out(issue); + if (!opts.options.json) { + outText(`\n📌 Issue #${issue.number}: ${issue.title}`); + outText(` 状态: ${issue.state}`); + outText(` 里程碑: ${issue.milestone?.title || '—'}`); + outText(` 指派: ${(issue.assignees || []).map(a => a.login).join(', ') || '—'}`); + outText(` 标签: ${labelNames(issue).join(', ') || '—'}`); + outText(` 创建: ${issue.created_at?.slice(0, 10)}`); + outText(` 更新: ${issue.updated_at?.slice(0, 10)}`); + if (issue.body) { + outText(`\n--- Body ---\n${issue.body.slice(0, 500)}${issue.body.length > 500 ? '…' : ''}`); + } + } + }, + + async create(api, opts) { + const title = opts.options.title; + const body = opts.options.body || ''; + if (!title) { fail('用法: issue create --title "标题" [--body "内容"] [--milestone ] [--labels ]'); return; } + + const data = { title, body }; + if (opts.options.milestone) { + const ms = isNaN(opts.options.milestone) + ? await api.findMilestoneByTitle(opts.options.milestone) + : { id: Number(opts.options.milestone) }; + if (ms) data.milestone = ms.id; + } + if (opts.options.labels) { + data.labels = opts.options.labels.split(',').map(Number); + } + // assignee: 显式指定 > 自动从 --role 映射 + if (opts.options.assignees) { + data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); + } else { + data.assignees = autoAssignee(opts.options.role); + } + + const issue = await api.createIssue(data); + out(issue); + if (!opts.options.json) { + ok(`Issue #${issue.number} 已创建: ${issue.title}`); + outText(` ${issue.html_url}`); + } + }, + + async update(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue update [--title "…"] [--body "…"] [--state open|closed]'); return; } + + const data = {}; + if (opts.options.title !== undefined) data.title = opts.options.title; + if (opts.options.body !== undefined) data.body = opts.options.body; + if (opts.options.state) data.state = opts.options.state; + if (opts.options.milestone) { + const ms = isNaN(opts.options.milestone) + ? await api.findMilestoneByTitle(opts.options.milestone) + : { id: Number(opts.options.milestone) }; + if (ms) data.milestone = ms.id; + } + if (opts.options.assignees) { + data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); + } + + const issue = await api.updateIssue(num, data); + out(issue); + if (!opts.options.json) ok(`Issue #${num} 已更新`); + }, + + async close(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue close '); return; } + const comment = opts.options.body || opts.options.comment; + await api.closeIssue(num, comment); + out({ closed: true, number: Number(num) }); + if (!opts.options.json) ok(`Issue #${num} 已关闭`); + }, + + async reopen(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue reopen '); return; } + await api.reopenIssue(num); + out({ reopened: true, number: Number(num) }); + if (!opts.options.json) ok(`Issue #${num} 已重新打开`); + }, + + async comment(api, opts) { + const num = opts.rest[0]; + const body = opts.options.body; + if (!num || !body) { fail('用法: issue comment --body "内容"'); return; } + const c = await api.createComment(num, body); + out(c); + if (!opts.options.json) ok(`评论已添加到 Issue #${num}`); + }, + + async comments(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue comments '); return; } + const comments = await api.listComments(num); + out(comments); + if (!opts.options.json) { + outText(`\n💬 Issue #${num} 的评论 (${comments.length})\n`); + if (!comments.length) { outText(' (无评论)'); return; } + for (const c of comments) { + outText(` @${c.user?.login || '?'} (${c.created_at?.slice(0, 10)}):`); + outText(` ${truncate(c.body, 80)}`); + } + } + }, + + async labels(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue labels [--add ] [--set ]'); return; } + + if (opts.options.add) { + const ids = opts.options.add.split(',').map(Number); + await api.addIssueLabels(num, ids); + if (!opts.options.json) ok(`标签已添加到 Issue #${num}: ${ids.join(', ')}`); + } else if (opts.options.set) { + const ids = opts.options.set.split(',').map(Number); + await api.replaceIssueLabels(num, ids); + if (!opts.options.json) ok(`Issue #${num} 的标签已替换为: ${ids.join(', ')}`); + } else { + const labels = await api.getIssueLabels(num); + out(labels); + if (!opts.options.json) { + outText(`\n🏷️ Issue #${num} 的标签 (${labels.length})`); + for (const l of labels) { + outText(` ${l.name} (${l.id}) #${l.color}`); + } + } + } + }, + + async deps(api, opts) { + const num = opts.rest[0]; + if (!num) { fail('用法: issue deps '); return; } + const [blockedBy, blocking] = await Promise.all([ + api.listDependencies(num).catch(() => []), + api.listBlocks(num).catch(() => []), + ]); + out({ issue: Number(num), blockedBy, blocking }); + if (!opts.options.json) { + outText(`\n🔗 Issue #${num} 的依赖关系`); + outText(` 被阻塞于 (${blockedBy.length}): ${blockedBy.map(i => '#' + i.number).join(', ') || '无'}`); + outText(` 阻塞了 (${blocking.length}): ${blocking.map(i => '#' + i.number).join(', ') || '无'}`); + } + }, + + async block(api, opts) { + const [num, blocked] = opts.rest; + if (!num || !blocked) { fail('用法: issue block '); return; } + await api.addBlock(Number(num), Number(blocked)); + out({ blocker: Number(num), blocked: Number(blocked) }); + if (!opts.options.json) ok(`Issue #${blocked} 现在依赖于 Issue #${num}`); + }, + + async unblock(api, opts) { + const [num, blocked] = opts.rest; + if (!num || !blocked) { fail('用法: issue unblock '); return; } + await api.removeBlock(Number(num), Number(blocked)); + out({ blocker: Number(num), unblocked: Number(blocked) }); + if (!opts.options.json) ok(`Issue #${blocked} 不再依赖于 Issue #${num}`); + }, +}; + +// ──────── milestone ──────── + +cmd.milestone = { + async list(api, opts) { + const milestones = await api.listMilestones({ state: opts.options.state || 'all' }); + out(milestones); + if (!opts.options.json) { + outText(`\n🏁 里程碑 (${milestones.length})\n`); + if (!milestones.length) { outText(' (无)'); return; } + table( + ['ID', '标题', '状态', 'Open', 'Closed', '截止日期'], + milestones.map(m => [ + m.id, + m.title, + m.state, + m.open_issues, + m.closed_issues, + m.due_on?.slice(0, 10) || '—', + ]) + ); + } + }, + + async create(api, opts) { + const title = opts.options.title; + if (!title) { fail('用法: milestone create --title "标题" [--description "描述"] [--due YYYY-MM-DD]'); return; } + const data = { title, state: 'open' }; + if (opts.options.description) data.description = opts.options.description; + if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z'; + const ms = await api.createMilestone(data); + out(ms); + if (!opts.options.json) ok(`里程碑已创建: ${ms.title} (ID: ${ms.id})`); + }, + + async update(api, opts) { + const id = opts.rest[0]; + if (!id) { fail('用法: milestone update [--title "…"] [--description "…"] [--state open|closed]'); return; } + const data = {}; + if (opts.options.title) data.title = opts.options.title; + if (opts.options.description !== undefined) data.description = opts.options.description; + if (opts.options.state) data.state = opts.options.state; + if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z'; + const ms = await api.updateMilestone(id, data); + out(ms); + if (!opts.options.json) ok(`里程碑 ${id} 已更新`); + }, +}; + +// ──────── label ──────── + +cmd.label = { + async list(api, opts) { + const labels = await api.listLabels(); + out(labels); + if (!opts.options.json) { + outText(`\n🏷️ 标签 (${labels.length})\n`); + if (!labels.length) { outText(' (无)'); return; } + table( + ['ID', '名称', '颜色'], + labels.map(l => [l.id, l.name, '#' + l.color]) + ); + } + }, + + async create(api, opts) { + const name = opts.options.name; + const color = (opts.options.color || '#ededed').replace('#', ''); + if (!name) { fail('用法: label create --name "名称" [--color "#RRGGBB"]'); return; } + const label = await api.createLabel({ name, color }); + out(label); + if (!opts.options.json) ok(`标签已创建: ${name} (ID: ${label.id})`); + }, + + async init(api, opts) { + outText('\n🏷️ 初始化标准标签体系…\n'); + // 从 config.labels 合并所有标签定义 + const allLabels = Object.values(config.labels).flat(); + const existing = await api.listLabels(); + const existingNames = new Set(existing.map(l => l.name)); + let created = 0, skipped = 0; + + for (const def of allLabels) { + if (existingNames.has(def.name)) { + info(`跳过 (已存在): ${def.name}`); + skipped++; + } else { + await api.createLabel({ name: def.name, color: def.color.replace('#', ''), description: def.description }); + ok(`已创建: ${def.name}`); + created++; + } + } + outText(`\n完成: 新建 ${created} 个, 跳过 ${skipped} 个`); + }, +}; + +// ──────── pipeline ──────── + +cmd.pipeline = { + async init(api, opts) { + const title = opts.options.title || 'Pipeline Tracker'; + const milestone = opts.options.milestone; + + const body = `# 流水线进度追踪 + +## 当前阶段 +草图阶段 + +## 当前迭代 +(非迭代阶段,留空) + +## 阶段历史 +| 阶段 | 里程碑 | 进入日期 | 完成日期 | +|------|--------|----------|----------| +| 草图阶段 | 草图阶段 | ${new Date().toISOString().slice(0, 10)} | — |`; + + const data = { title, body }; + if (milestone) { + const ms = isNaN(milestone) + ? await api.findMilestoneByTitle(milestone) + : { id: Number(milestone) }; + if (ms) data.milestone = ms.id; + } + + const issue = await api.createIssue(data); + out(issue); + if (!opts.options.json) { + ok(`Pipeline Tracker 已创建: Issue #${issue.number}`); + outText(` ${issue.html_url}`); + } + }, + + async status(api, opts) { + let tracker = await api.findIssueByTitle('Pipeline Tracker'); + if (!tracker) { fail('未找到 Pipeline Tracker Issue。请先运行: pipeline init'); return; } + tracker = await api.getIssue(tracker.number); + out({ number: tracker.number, title: tracker.title, body: tracker.body }); + if (!opts.options.json) { + outText(`\n📊 Pipeline Tracker #${tracker.number}\n`); + outText(tracker.body); + } + }, + + async update(api, opts) { + const num = opts.rest[0]; + const field = opts.options.field; + const value = opts.options.value; + if (!num || !field || !value) { + fail('用法: pipeline update --field <阶段|当前迭代> --value <值>'); + return; + } + + const issue = await api.getIssue(num); + let body = issue.body || ''; + + if (field === '阶段') { + body = body.replace(/## 当前阶段\n.*/, `## 当前阶段\n${value}`); + } else if (field === '当前迭代') { + body = body.replace(/## 当前迭代\n.*/, `## 当前迭代\n${value}`); + } else { + fail(`无效字段: ${field},可选: 阶段, 当前迭代`); + return; + } + + await api.updateIssue(num, { body }); + out({ number: Number(num), field, value, updated: true }); + if (!opts.options.json) ok(`Pipeline Tracker 字段 "${field}" 已更新为: ${value}`); + }, +}; + +// ──────── test ──────── + +cmd.test = { + async run(api, opts) { + outText('\n🧪 Gitea API 接口验证\n'); + const results = []; + let passed = 0, failed = 0; + + async function test(name, fn) { + try { + const result = await fn(); + ok(name); + results.push({ name, status: 'pass', detail: result }); + passed++; + } catch (e) { + fail(`${name} — ${e.message}`); + results.push({ name, status: 'fail', error: e.message }); + failed++; + } + } + + // P0: 阻塞性验证 + outText('── P0: 阻塞性验证 ──\n'); + + await test('GET /milestones', () => api.listMilestones()); + await test('GET /issues', () => api.listIssues({ limit: 5 })); + await test('GET /labels', () => api.listLabels()); + + // 查找第一个存在的 Issue 用于后续测试 + const issues = await api.listIssues({ limit: 5, state: 'all' }); + const testIssue = issues.find(i => i.state === 'open') || issues[0]; + if (testIssue) { + await test(`GET /issues/${testIssue.number}`, () => api.getIssue(testIssue.number)); + await test(`GET /issues/${testIssue.number}/comments`, () => api.listComments(testIssue.number)); + await test(`GET /issues/${testIssue.number}/dependencies`, () => api.listDependencies(testIssue.number)); + await test(`GET /issues/${testIssue.number}/blocks`, () => api.listBlocks(testIssue.number)); + } + + await test('POST 创建含中文 Issue', async () => { + const i = await api.createIssue({ + title: '测试 Issue — UTF-8 验证 🎮', + body: '这是中文内容测试。\n\n- 列表项\n- 特殊字符:✅❌🔴⬜\n\n创建时间: ' + new Date().toISOString(), + }); + await api.closeIssue(i.number); + return `#${i.number}: ${i.title}`; + }); + + if (testIssue) { + await test('POST 创建含中文评论', async () => { + const c = await api.createComment(testIssue.number, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19)); + return `comment_id: ${c.id}`; + }); + + await test('PATCH 更新 Issue', async () => { + const orig = await api.getIssue(testIssue.number); + const newTitle = orig.title + ' [测试更新]'; + await api.updateIssue(testIssue.number, { title: newTitle }); + await api.updateIssue(testIssue.number, { title: orig.title }); + return `Issue #${testIssue.number} 标题更新并还原`; + }); + } + + await test('PATCH 关闭/重开 Issue', async () => { + const tmp = await api.createIssue({ title: '临时测试 — 开关', body: '将被关闭再打开' }); + await api.closeIssue(tmp.number); + await api.reopenIssue(tmp.number); + await api.closeIssue(tmp.number); + return `#${tmp.number} 关闭→重开→关闭`; + }); + + await test('POST 创建 Label', async () => { + const l = await api.createLabel({ name: '_test-label', color: 'ededed' }); + try { await api.deleteLabel(l.id); } catch { /* ignore */ } + return `label: ${l.name} (ID: ${l.id}) 已创建并清理`; + }); + + if (testIssue) { + await test('POST Issue 添加标签', async () => { + const labels = await api.listLabels(); + if (!labels.length) return '无可用标签,跳过'; + await api.addIssueLabels(testIssue.number, [labels[0].id]); + return `添加标签 ${labels[0].name} 到 Issue #${testIssue.number}`; + }); + } + + // P1: 核心功能 + outText('\n── P1: 核心功能验证 ──\n'); + + if (testIssue && issues.length >= 2) { + const other = issues.find(i => i.number !== testIssue.number); + if (other) { + await test('POST/DELETE Issue 依赖', async () => { + await api.addBlock(testIssue.number, other.number); + await api.removeBlock(testIssue.number, other.number); + return `依赖关系 ${testIssue.number}→${other.number} 添加→移除`; + }); + } + } + + await test('POST/PATCH Milestone', async () => { + const ms = await api.createMilestone({ title: '_test-milestone', state: 'open' }); + await api.updateMilestone(ms.id, { description: '测试用' }); + await api.updateMilestone(ms.id, { state: 'closed' }); + return `milestone ${ms.id} 创建→更新→关闭`; + }); + + // 汇总 + outText(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`); + outText(`总计: ${passed + failed} 项, ✅ ${passed} 通过, ❌ ${failed} 失败`); + out(results); + + if (failed > 0) process.exit(1); + }, +}; + +// ═══════════════════════════════════════════════════ +// 帮助文本 +// ═══════════════════════════════════════════════════ + +function showHelp() { + const roles = Object.keys(config.roles).join('|'); + console.log(` +Gitea CLI — 项目管理工具 + +用法: node gitea.js --role <${roles}> [subcommand] [options] + +全局选项: + --help 显示帮助信息 + --json 以 JSON 格式输出 + --role <角色> 使用对应角色的身份(自动解析 Token 和 Assignee) + +命令列表: + + issue list 列出 Issues + [--state open|closed|all] [--milestone ] + [--labels ] [--assigned <角色名|用户名>] + issue get <#> 查看 Issue 详情 + issue create 创建 Issue(assignee 自动从 --role 映射) + --title "标题" [--body "内容"] [--milestone ] + [--labels ] [--assignees <角色名,...>] + issue update <#> 更新 Issue [--title "…"] [--body "…"] [--state open|closed] + issue close <#> 关闭 Issue [--body "完成评论"] + issue reopen <#> 重新打开 Issue + issue comment <#> 添加评论 --body "内容" + issue comments <#> 查看评论列表 + issue labels <#> 查看/管理标签 [--add ] [--set ] + issue deps <#> 查看依赖关系 + issue block <#> <#> 设置依赖关系 + issue unblock <#> <#> 移除依赖关系 + + milestone list 列出里程碑 [--state open|closed|all] + milestone create 创建里程碑 --title "标题" [--description "…"] [--due YYYY-MM-DD] + milestone update 更新里程碑 [--title "…"] [--state open|closed] + + label list 列出标签 + label create 创建标签 --name "名称" [--color "#RRGGBB"] + label init 初始化标准标签体系(从 gitea-config.json 读取定义) + + pipeline init 创建 Pipeline Tracker Meta Issue + pipeline status 查看当前流水线进度 + pipeline update <#> 更新 Pipeline 字段 --field <阶段|当前迭代> --value <值> + + test 验证所有 API 接口 + +角色自动映射: + --role <角色名> 自动解析为 Token + Assignee + --assigned <角色名> 自动解析为 Gitea 用户名 + --assignees <角色名,...> 同上 + +配置文件: gitea-config.json(与 gitea.js 同目录) +`); +} + +// ═══════════════════════════════════════════════════ +// 路由与入口 +// ═══════════════════════════════════════════════════ + +let globalArgs; + +async function main() { + const args = parseArgs(process.argv.slice(2)); + globalArgs = args; + + if (args.options.help || (!args.command && !args.sub)) { + showHelp(); + return; + } + + // Token 解析 + const token = resolveToken(args.options); + + // 构建 API 实例(从 config 读取连接信息) + const api = new GiteaAPI(token, { + host: config.host, + port: config.port, + repoOwner: config.repoOwner, + repoName: config.repoName, + }); + + // 路由 + const commandGroup = cmd[args.command]; + if (!commandGroup) { + console.error(`❌ 未知命令: ${args.command}`); + console.error(' 运行 --help 查看可用命令'); + process.exit(1); + } + + if (args.command === 'test') { + await cmd.test.run(api, args); + return; + } + + const handler = commandGroup[args.sub]; + if (!handler) { + console.error(`❌ 未知子命令: ${args.command} ${args.sub}`); + console.error(` 运行 --help 查看 ${args.command} 的可用子命令`); + process.exit(1); + } + + try { + await handler(api, args); + } catch (e) { + if (args.options.json) { + console.error(JSON.stringify({ error: e.message, statusCode: e.statusCode })); + } else { + console.error(`\n❌ 错误: ${e.message}`); + } + process.exit(1); + } +} + +main().catch(e => { + console.error('致命错误:', e); + process.exit(1); +}); diff --git a/.claude/skills/project-initializer.md b/.claude/skills/project-initializer.md index cb135ed..0c0d0e1 100644 --- a/.claude/skills/project-initializer.md +++ b/.claude/skills/project-initializer.md @@ -1,6 +1,6 @@ --- name: "project-initializer" -description: "项目初始化技能 — 检查版本管理/项目管理配置、初始化 Gitea 标签/里程碑/任务。仅在项目无进度时由 PM 调用。" +description: "项目初始化技能 — 检查版本管理/项目管理配置、初始化 Gitea 标签/里程碑。仅在项目无进度时由 PM 调用。" --- # 项目初始化 @@ -17,24 +17,27 @@ description: "项目初始化技能 — 检查版本管理/项目管理配置、 逐项确认以下内容: 1. **SSH 仓库地址**:确认 `git remote -v` 中的仓库地址正确 -2. **角色 Gitea 账号**:确认以下账号存在且可访问 - - `starmen1265`(用户) +2. **角色 Gitea 账号**:确认以下 AI 角色账号存在且可访问 - `ai-pm` - `ai-designer` - `ai-programmer` - `ai-artist` - `ai-reviewer` -3. **SSH key 配置**:运行 `ssh -T git@micecola.top -p 2223` 测试连接 + + 如不确定账号是否存在,通过 Token 文件或 API 验证。 + +3. **SSH key 配置**:测试 SSH 连接(命令参数中的主机和端口需向用户确认) - 失败 → 引导用户生成并配置 SSH key ### 步骤 2:项目管理配置 确认以下配置就绪: -1. **Token 文件**:`scripts/.gitea-tokens.json` 存在且包含所有角色 token - - `pm`、`designer`、`dev`、`artist`、`reviewer` -2. **Token 文件已忽略**:`.gitignore` 中包含 `scripts/.gitea-tokens.json` -3. **Gitea CLI 可用**:`node scripts/gitea.js --role pm test` 运行通过 +1. **配置文件**:`.claude/skills/gitea-pm/gitea-config.json` 存在且包含: + - `host`、`port`、`repoOwner`、`repoName` + - `roles` 中所有角色的 `assignee` 和 `token` +2. **配置文件已忽略**:`.gitignore` 中包含 `gitea-config.json` +3. **Gitea CLI 可用**:`node .claude/skills/gitea-pm/gitea.js --role pm test` 运行通过 缺失时引导用户配置。 @@ -50,74 +53,27 @@ description: "项目初始化技能 — 检查版本管理/项目管理配置、 - 将迁移方案提交用户确认后执行 **情况 B:无现有代码(全新项目)** -- 建议用户手动准备开发环境: - 1. 安装 UE 5.7.4 - 2. 创建第三人称/第一人称/顶视角模板项目 - 3. 添加插件:BlockoutStarterPack、LowpolyCharacter - 4. 启用 PythonEditorPlugins 插件 -- 等待用户确认环境就绪 +- 建议用户准备开发环境: + 1. 安装对应版本的 UE + 2. 创建模板项目 + 3. 添加所需插件 +- **不等待就绪**,继续往下执行。只要在迭代阶段开始前环境准备就绪即可。 ### 步骤 4:Gitea 初始化 依次执行(所有操作需用户确认后再执行): ```bash -# 1. 创建标准标签 -node scripts/gitea.js --role pm label init +# 1. 创建标准标签(从 gitea-config.json 读取定义) +node .claude/skills/gitea-pm/gitea.js --role pm label init # 2. 创建 Pipeline Tracker Issue -node scripts/gitea.js --role pm issue create \ - --title "Pipeline Tracker" \ - --body "# 流水线进度追踪 - -## 当前阶段 -草图阶段 - -## 当前里程碑 -- 名称: 草图阶段 -- 状态: open -- Open Issues: 1 -- Closed Issues: 0 - -## 阶段历史 -| 阶段 | 里程碑 | 进入日期 | 完成日期 | -|------|--------|----------|----------| -| 草图阶段 | 草图阶段 | $(date +%Y-%m-%d) | — |" \ - --labels "stage:sketch,role:pm,status:in-progress" +node .claude/skills/gitea-pm/gitea.js --role pm pipeline init # 3. 创建草图阶段里程碑 -node scripts/gitea.js --role pm milestone create \ +node .claude/skills/gitea-pm/gitea.js --role pm milestone create \ --title "草图阶段" \ --description "概念设计与技术可行性验证" - -# 4. 创建第一个任务:概念设计 -node scripts/gitea.js --role pm issue create \ - --title "D-001 概念设计" \ - --body "## 任务信息 - -- **任务ID**: D-001 -- **所属阶段**: 草图阶段 -- **角色**: 策划 - -## 预期产出 - -产出概念草案 \`docs/design/概念草案.md\`,包含以下 7 个必填字段: -1. 一句话概述(这是什么游戏) -2. 核心玩法(玩家做什么) -3. 美术风格 -4. 目标平台 -5. 灵感来源 -6. 核心假设(为什么好玩) -7. 范围约束(MVP 包含什么) - -## 质量标准 - -- 每个字段有明确的、不含糊的描述 -- 美术风格有参考图或参考作品 -- 核心假设可验证(能通过原型验证)" \ - --milestone "草图阶段" \ - --labels "stage:sketch,role:design,status:ready" \ - --assignees "ai-designer" ``` ### 步骤 5:确认完成 @@ -125,8 +81,8 @@ node scripts/gitea.js --role pm issue create \ 初始化完成后向用户报告: - ✅ 版本管理配置状态 - ✅ 项目管理配置状态 -- ✅ Gitea 初始化结果(标签数、Pipeline Tracker Issue 编号、里程碑、首个任务编号) -- 📋 建议下一步:启动策划角色开始概念设计 +- ✅ Gitea 初始化结果(标签数、Pipeline Tracker Issue 编号、里程碑) +- 📋 建议下一步:进入草图阶段,由 stage-sketch 技能接管 --- @@ -135,3 +91,4 @@ node scripts/gitea.js --role pm issue create \ - 每一步都需要用户确认后才执行,不自动推进 - 如果中途失败(如 Gitea 连接失败),报告具体错误并建议修复方案 - 初始化完成后,PM 进入草图阶段,后续由 stage-sketch 技能接管 +- **不创建首个任务** — 任务创建由 PM 根据阶段技能的逻辑来执行 diff --git a/.claude/skills/quality-gate-review.md b/.claude/skills/quality-gate-review.md index e5a3ff9..83f2276 100644 --- a/.claude/skills/quality-gate-review.md +++ b/.claude/skills/quality-gate-review.md @@ -7,7 +7,7 @@ description: "审查标准技能 — 4 道质量门标准、阶段对应标准 > 本技能定义 4 道质量门的审查标准、阶段对应标准和审查后强制操作。 > 供 **game-reviewer 子代理**使用——角色完成工作后启动 reviewer 子代理,reviewer 按此标准审查。 -> 项目管理工具操作细节参见 `.claude/skills/gitea-collab.md`。 +> 项目管理工具操作细节参见 `gitea-collab` 技能。 --- diff --git a/.claude/skills/stage-iteration.md b/.claude/skills/stage-iteration.md index 4052459..f47f539 100644 --- a/.claude/skills/stage-iteration.md +++ b/.claude/skills/stage-iteration.md @@ -285,10 +285,10 @@ docs/technical/<系统名>-tech.md" ```bash # 1. 关闭当前迭代里程碑 -node scripts/gitea.js --role pm milestone update <当前里程碑ID> --state closed +node .claude/skills/gitea-pm/gitea.js --role pm milestone update <当前里程碑ID> --state closed # 2. 创建下一个迭代里程碑 -node scripts/gitea.js --role pm milestone create \ +node .claude/skills/gitea-pm/gitea.js --role pm milestone create \ --title "迭代-vX.X" \ --description "第 N 个迭代版本:<简要描述>" diff --git a/.claude/skills/stage-roadmap.md b/.claude/skills/stage-roadmap.md index 8f81758..fec944f 100644 --- a/.claude/skills/stage-roadmap.md +++ b/.claude/skills/stage-roadmap.md @@ -164,10 +164,10 @@ description: "总路线阶段技能 — 全面设计文档产出与版本路径 ```bash # 1. 关闭总路线阶段里程碑 -node scripts/gitea.js --role pm milestone update <总路线里程碑ID> --state closed +node .claude/skills/gitea-pm/gitea.js --role pm milestone update <总路线里程碑ID> --state closed # 2. 创建第一个迭代版本里程碑 -node scripts/gitea.js --role pm milestone create \ +node .claude/skills/gitea-pm/gitea.js --role pm milestone create \ --title "迭代-v0.1" \ --description "第一个迭代版本" diff --git a/.claude/skills/stage-sketch.md b/.claude/skills/stage-sketch.md index 2b72bd0..f0ef059 100644 --- a/.claude/skills/stage-sketch.md +++ b/.claude/skills/stage-sketch.md @@ -32,7 +32,7 @@ description: "草图阶段技能 — 概念设计与技术可行性验证。管 **任务 1:概念设计** (D-XXX) ```bash -node scripts/gitea.js --role pm issue create \ +node .claude/skills/gitea-pm/gitea.js --role pm issue create \ --title "D-XXX 概念设计" \ --body "## 任务信息 - **任务ID**: D-XXX @@ -59,7 +59,7 @@ node scripts/gitea.js --role pm issue create \ **任务 2:技术可行性报告** (P-XXX) ```bash -node scripts/gitea.js --role pm issue create \ +node .claude/skills/gitea-pm/gitea.js --role pm issue create \ --title "P-XXX 技术可行性评估" \ --body "## 任务信息 - **任务ID**: P-XXX @@ -105,10 +105,10 @@ node scripts/gitea.js --role pm issue create \ ```bash # 1. 关闭草图阶段里程碑 -node scripts/gitea.js --role pm milestone update <草图里程碑ID> --state closed +node .claude/skills/gitea-pm/gitea.js --role pm milestone update <草图里程碑ID> --state closed # 2. 创建总路线阶段里程碑 -node scripts/gitea.js --role pm milestone create \ +node .claude/skills/gitea-pm/gitea.js --role pm milestone create \ --title "总路线阶段" \ --description "全面设计文档产出:世界观、剧情、角色、系统、关卡、UI、音频、版本路径" diff --git a/.gitignore b/.gitignore index ceed3fc..8d75496 100644 --- a/.gitignore +++ b/.gitignore @@ -98,9 +98,11 @@ Desktop.ini .worktrees/ # ============================== -# Gitea API Tokens(每台机器独立配置) +# Gitea 配置(含 Token,每台机器独立配置) # ============================== scripts/.gitea-tokens.json +.claude/skills/gitea-pm/gitea-config.json +.claude/skills/gitea-collab/gitea-config.json # ============================== # Python diff --git a/CLAUDE.md b/CLAUDE.md index 800dd88..b1b11f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,10 @@ ## 项目管理 - **工具**: Gitea(Issues + Milestones) -- **协作技能**: `gitea-collab`(封装所有项目管理操作的实现细节) +- **PM 技能**: `gitea-pm`(任务创建/关闭、里程碑管理、Pipeline Tracker) +- **通用技能**: `gitea-collab`(任务查询、进度汇报、审查操作) - **进度查询**: Pipeline Tracker(Meta Issue,仅记录当前阶段和阶段历史) -- **角色操作**: 各角色通过 `gitea-collab` 技能与 Gitea 交互,不在角色文档中硬编码具体命令 +- **配置文件**: `.claude/skills/gitea-pm/gitea-config.json`(含连接信息、Token、角色映射、标签定义) ---