项目看板强约束

作者:鹿Sir开发工具v1

把 GitHub Projects 看板作为项目唯一事实源,提供基于缓存的看板校验门禁、状态流转规则与状态查询函数,未入板、未设状态或非法流转的工作一律拦截。用 GitHub Projects 管理任务、执行议题驱动开发、强制团队遵循看板状态机、优化 gh CLI API 调用成本时使用。当用户提到项目看板、issue 状态流转、GitHub Projects、看板门禁、任务状态管理时触发。触发词:项目看板、GitHub Projects、状态流转、门禁校验、issue 管理。

下载量
400
点赞
98
价格
免费

技能文档

---
name: majiayu000-project-board-enforcement-troykelly-claude-skills
title: 项目看板强约束
category: 开发工具
description: 把 GitHub Projects 看板作为项目唯一事实源,提供基于缓存的看板校验门禁、状态流转规则与状态查询函数,未入板、未设状态或非法流转的工作一律拦截。用 GitHub Projects 管理任务、执行议题驱动开发、强制团队遵循看板状态机、优化 gh CLI API 调用成本时使用。当用户提到项目看板、issue 状态流转、GitHub Projects、看板门禁、任务状态管理时触发。触发词:项目看板、GitHub Projects、状态流转、门禁校验、issue 管理。
---

# 项目看板强约束

GitHub Projects 看板是所有工作状态的唯一事实源。不是标签,不是评论,不是记忆——是看板。

**核心原则**:没有以正确字段进入项目看板的工作,即视为不存在。

## 技能工作流

### 步骤1:初始化环境与缓存

先确认项目环境变量已配置:

```bash
echo $GITHUB_PROJECT      # 完整 URL: https://github.com/users/USER/projects/N
echo $GITHUB_PROJECT_NUM  # 仅编号: N
echo $GH_PROJECT_OWNER    # 属主: @me 或组织名
```

任一缺失时先补齐,否则停止。随后初始化读缓存(后续所有读操作 0 API 调用):

```bash
init_project_cache() {
  export GH_CACHE_ITEMS=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --format json)
  export GH_CACHE_FIELDS=$(gh project field-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --format json)
  export GH_PROJECT_ID=$(echo "$GH_CACHE_FIELDS" | jq -r '.project.id // empty')
  export GH_STATUS_FIELD_ID=$(echo "$GH_CACHE_FIELDS" | jq -r '.fields[] | select(.name == "Status") | .id')
  # 缓存 Status 各选项 ID
  for opt in Backlog Ready "In Progress" "In Review" Done Blocked; do
    local id=$(echo "$GH_CACHE_FIELDS" | jq -r ".fields[] | select(.name == \"Status\") | .options[] | select(.name == \"$opt\") | .id")
    case "$opt" in
      "Backlog")     export GH_STATUS_BACKLOG_ID="$id" ;;
      "Ready")       export GH_STATUS_READY_ID="$id" ;;
      "In Progress") export GH_STATUS_IN_PROGRESS_ID="$id" ;;
      "In Review")   export GH_STATUS_IN_REVIEW_ID="$id" ;;
      "Done")        export GH_STATUS_DONE_ID="$id" ;;
      "Blocked")     export GH_STATUS_BLOCKED_ID="$id" ;;
    esac
  done
}
```

### 步骤2:检查项目字段配置

每个项目必须配置以下字段,缺失时先建字段再开工:

| 字段 | 类型 | 必选值 |
|-------|------|-----------------|
| Status | 单选 | Backlog, Ready, In Progress, In Review, Done, Blocked |
| Type | 单选 | Feature, Bug, Chore, Research, Spike, Epic, Initiative |
| Priority | 单选 | Critical, High, Medium, Low |

推荐字段:

| 字段 | 类型 | 用途 |
|-------|------|--------|
| Verification | 单选 | Not Verified, Failing, Partial, Passing |
| Criteria Met | 数字 | 已完成的验收标准数 |
| Criteria Total | 数字 | 验收标准总数 |
| Last Verified | 日期 | 最近一次验证时间 |
| Epic | 文本 | 父 Epic 议题编号 |
| Initiative | 文本 | 父 Initiative 议题编号 |

### 步骤3:开工前门禁校验

**规则:每个 issue、epic、initiative 必须先入项目看板,才允许开工。这不是建议,是硬性门禁。**

校验议题已入板(读缓存,0 API 调用):

```bash
verify_issue_in_project() {
  local issue=$1

  ITEM_ID=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue) | .id")

  if [ -z "$ITEM_ID" ] || [ "$ITEM_ID" = "null" ]; then
    echo "BLOCKED: Issue #$issue is not in the project board."
    echo ""
    echo "Add it with:"
    echo "  gh project item-add $GITHUB_PROJECT_NUM --owner $GH_PROJECT_OWNER --url \$(gh issue view $issue --json url -q .url)"
    return 1
  fi

  echo "$ITEM_ID"
  return 0
}
```

校验状态字段已设置(读缓存,0 API 调用):

```bash
verify_status_set() {
  local issue=$1
  local item_id=$2

  STATUS=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.id == \"$item_id\") | .status.name")

  if [ -z "$STATUS" ] || [ "$STATUS" = "null" ]; then
    echo "BLOCKED: Issue #$issue has no Status set in project board."
    return 1
  fi

  echo "$STATUS"
  return 0
}
```

议题创建后加入看板(1 次写调用 + 刷新缓存):

```bash
add_issue_to_project() {
  local issue_url=$1

  gh project item-add "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --url "$issue_url"
  if [ $? -ne 0 ]; then
    echo "ERROR: Failed to add issue to project."
    return 1
  fi

  export GH_CACHE_ITEMS=$(gh project item-list "$GITHUB_PROJECT_NUM" --owner "$GH_PROJECT_OWNER" --format json)

  local issue_num=$(echo "$issue_url" | grep -oE '[0-9]+$')
  ITEM_ID=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue_num) | .id")

  echo "$ITEM_ID"
  return 0
}
```

### 步骤4:状态流转

合法流转路径:

```text
Backlog → Ready → In Progress → In Review → Done
   ↓        ↓          ↓            ↓
   └────────┴──────────┴────────────┴──→ Blocked
                                            ↓
                                    (回到前一状态)
```

流转合法性校验:

```bash
validate_transition() {
  local current=$1
  local target=$2

  case "$current→$target" in
    "Backlog→Ready"|"Ready→In Progress"|"In Progress→In Review"|"In Review→Done")
      return 0 ;;
    *"→Blocked")
      return 0 ;;
    "Blocked→Backlog"|"Blocked→Ready"|"Blocked→In Progress")
      return 0 ;;
    *)
      echo "INVALID_TRANSITION: $current → $target"
      return 1 ;;
  esac
}
```

更新状态(用缓存 ID,1 次写调用):

```bash
set_project_status() {
  local item_id=$1
  local new_status=$2

  local option_id
  case "$new_status" in
    "Backlog")     option_id="$GH_STATUS_BACKLOG_ID" ;;
    "Ready")       option_id="$GH_STATUS_READY_ID" ;;
    "In Progress") option_id="$GH_STATUS_IN_PROGRESS_ID" ;;
    "In Review")   option_id="$GH_STATUS_IN_REVIEW_ID" ;;
    "Done")        option_id="$GH_STATUS_DONE_ID" ;;
    "Blocked")     option_id="$GH_STATUS_BLOCKED_ID" ;;
    *)
      option_id=$(echo "$GH_CACHE_FIELDS" | jq -r ".fields[] | select(.name == \"Status\") | .options[] | select(.name == \"$new_status\") | .id")
      ;;
  esac

  if [ -z "$option_id" ] || [ "$option_id" = "null" ]; then
    echo "ERROR: Status '$new_status' not found in project."
    return 1
  fi

  gh project item-edit --project-id "$GH_PROJECT_ID" --id "$item_id" \
    --field-id "$GH_STATUS_FIELD_ID" --single-select-option-id "$option_id"

  return $?
}
```

设置 Type 字段(创建议题时调用,1 次写调用):

```bash
set_project_type() {
  local item_id=$1
  local type=$2

  local type_field_id=$(echo "$GH_CACHE_FIELDS" | jq -r '.fields[] | select(.name == "Type") | .id')
  local option_id=$(echo "$GH_CACHE_FIELDS" | jq -r ".fields[] | select(.name == \"Type\") | .options[] | select(.name == \"$type\") | .id")

  if [ -z "$option_id" ] || [ "$option_id" = "null" ]; then
    echo "ERROR: Type '$type' not found in project."
    return 1
  fi

  gh project item-edit --project-id "$GH_PROJECT_ID" --id "$item_id" \
    --field-id "$type_field_id" --single-select-option-id "$option_id"
}
```

### 步骤5:状态查询(全部读缓存,0 API 调用)

```bash
# 按状态取议题号
get_issues_by_status() {
  local status=$1
  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.status.name == \"$status\") | .content.number"
}

# 按类型取议题号
get_issues_by_type() {
  local type=$1
  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.type.name == \"$type\") | .content.number"
}

# 取 Epic 的子议题
get_epic_children() {
  local epic_num=$1
  echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.epic == \"#$epic_num\") | .content.number"
}

# 按状态计数
count_by_status() {
  local status=$1
  echo "$GH_CACHE_ITEMS" | jq "[.items[] | select(.status.name == \"$status\")] | length"
}
```

### 步骤6:分支与看板同步核验

对比 git 分支与看板状态发现漂移:有分支的议题应处于 In Progress 或 In Review;In Progress 的议题应有活跃分支。

```bash
status=$(echo "$GH_CACHE_ITEMS" | jq -r ".items[] | select(.content.number == $issue) | .status.name")
```

## 门禁点

以下工作流节点必须执行看板校验:

| 工作流节点 | 门禁动作 |
|----------------|------|
| 任何工作开始前 | 校验议题已入板 |
| 议题创建后 | 加入看板并设置字段 |
| 开工时 | Status → In Progress |
| 创建分支 | 校验看板成员资格 |
| PR 创建后 | Status → In Review |
| 工作完成 | Status → Done |
| 遇阻 | Status → Blocked |
| Epic 创建后 | 入板并设 Type=Epic |
| 子议题创建后 | 入板并关联父级 |

## 标签 vs 看板

- **错误**:用标签管状态(`status:in-progress`)
- **正确**:用看板 Status 字段管状态

标签只承载补充信息:`epic`、`epic-[name]`、`spawned-from:#N`、`review-finding`。

## 错误消息

所有看板错误都给出可操作的修复方式:

| 错误码 | 含义 | 修复 |
|------------|---------|-----|
| NOT_IN_PROJECT | 议题未入板 | `gh project item-add ...` |
| NO_STATUS | Status 未设置 | 更新 Status 字段 |
| INVALID_TRANSITION | 非法状态变更 | 使用合法流转路径 |
| PROJECT_NOT_FOUND | 项目不可访问 | 核对 GITHUB_PROJECT_NUM |

## 过关检查清单

通过任一门禁前逐项确认:

- [ ] GitHub API 缓存已初始化(GH_CACHE_ITEMS、GH_CACHE_FIELDS 已设置)
- [ ] 议题已入板(从缓存确认,不调 API)
- [ ] Status 字段已设置
- [ ] Type 字段已设置
- [ ] Priority 字段已设置(新议题)
- [ ] Epic 关联已设置(作为子议题时)
- [ ] 状态变更走合法流转

## API 成本对比

| 操作 | 无缓存 | 有缓存 |
|-----------|----------------|---------------|
| verify_issue_in_project | 1 次 | 0 次 |
| verify_status_set | 1 次 | 0 次 |
| add_issue_to_project | 2 次 | 2 次 |
| set_project_status | 4 次 | 1 次 |
| set_project_type | 3 次 | 1 次 |
| get_issues_by_status | 1 次 | 0 次 |
| count_by_status | 1 次 | 0 次 |
| verify_project_sync(10 分支) | 10 次 | 0 次 |

使用说明

# 项目看板强约束

把 GitHub Projects 看板作为唯一事实源:未入板不开工、状态流转强校验、读操作全走缓存零 API 成本。

## 使用

```text
帮我检查这个仓库的 issue 是否都已入板、状态字段是否完整
```

```text
把 #42 议题加入项目看板并流转到 In Progress
```

## 工作原理

先用 `init_project_cache` 一次性拉取看板条目与字段定义到环境变量缓存,此后所有读操作(入板校验、状态查询、计数、Epic 子项)0 API 调用;写操作(入板、状态/类型变更)各 1 次调用。内置 Backlog→Ready→In Progress→In Review→Done(旁路 Blocked)状态机校验与分支-看板漂移核验,任一门禁不通过即阻断并输出修复命令。

如何安装此技能?

访问技能市场,点击「安装」按钮,按提示将技能包放入 AI 编程助手的 skills 目录即可。

浏览技能市场

支持平台:Qoder · QoderWork · Claude · Codex 等 AI 编程助手