mcpField
I
issue-tracker

issue-tracker

github.com/clhbid开发0488 次安装3 天前更新
代码仓库

关于 issue-tracker

issue-tracker 是面向 CLHbid 仓库的 GitHub 问题与项目看板操作指南。它统一问题类型、状态、引用、分流和交付规范,并通过 gh 命令行执行操作。

主要功能

  1. 01定义问题类别与生命周期术语
  2. 02统一完整问题引用格式
  3. 03映射项目状态与分流角色
  4. 04说明 gh 命令行问题操作
  5. 05解释看板查询与交付结构

适用场景

  1. 01创建或更新仓库问题
  2. 02按分流状态处理问题
  3. 03跨 CLHbid 仓库跟踪工作
  4. 04操作前解析问题引用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/clhbid/agent-context --skill issue-tracker

安装后在 Claude Code 中输入/issue-tracker确认已加载。

附带文件board.graphqlsnapshot.graphql

详细介绍

问题跟踪:GitHub

此仓库的问题和规格都记录在 GitHub 问题中。工作状态记录在组织项目 CLHbid Delivery 中,该项目涵盖所有 clhbid 仓库。所有操作都使用 gh CLI。

术语

技能规划和汇报所用的术语。以下每条规则均以这些术语表述。

周期:大约两周一个的规划期,工作承诺在此期间完成。将问题纳入一个周期,就表示承诺在该周期内完成。避免使用:sprint、milestone。

类别:问题的类型,以问题类型的形式记录——每个问题恰好属于 Bug、Enhancement 或 Epic 其中之一。避免使用:label、kind。

Epic:顶层问题,问题类型为 Epic,规模太大,无法在一个周期内完成,通过其子问题交付。Epic 永远不会纳入周期;每个子问题都适合在一个周期内完成,并单独纳入周期。避免使用:initiative、program、tracker issue、parent(每个 epic 都是 parent,但只有少数 parent 是 epic)。

已承诺单元:设置了 Cycle 的问题——顶层问题,或 epic 的子问题。其后代会继承周期,永远不会直接设置周期。避免使用:planned issue、scheduled issue。

延期:已纳入某个周期,但该周期结束时仍未关闭。始终表示未履行承诺,绝不能称为“进展符合预期”。避免使用:in flight、carried over。

进度:epic 已完成子问题数占其子问题总数的比例。避免使用:percent complete、velocity、burndown。

业务工作:业务所规划和阅读的内容——顶层问题,或 epic 的子问题。避免使用:ticket、story。

约定

  • 创建问题:gh issue create --title "..." --body "..."。多行正文请使用 heredoc。此命令既不会应用问题表单,也不会设置类型——请遵循从组织表单创建问题。

  • 读取、列出、评论:gh issue view <number> --comments、gh issue list、gh issue comment <number> --body "..."。--jq 需要配合 --json 使用,因此筛选读取结果时要省略 --comments 并指定字段:gh issue view <number> --json number,title,labels --jq ...

  • 将问题设为子问题:创建后执行 gh issue edit <child> --parent <parent-url>——从组织表单创建问题中的 REST 创建请求不接受 parent。URL 形式可指向其他仓库中的 parent,而 epic 的子问题通常就位于其他仓库。

  • 引用问题:使用完整引用格式——见引用。

  • 设置状态:使用项目字段,而非标签——见状态。

  • 关闭:问题以 completed、not planned 或 duplicate 为关闭原因。原因会记录在案,因此请选用匹配的原因,并在关闭评论中说明理由。

    # Completed — the work is done
    gh issue close <number> --reason completed --comment "Shipped in #<pr>."
    
    # Not planned — we are not delivering it, abandoned and deferred alike
    gh issue close <number> --reason "not planned" --comment "Deferred to <date>; <owner> holds it."
    
    # Duplicate — another issue carries the work
    gh issue close <number> --duplicate-of <other-number> --comment "Tracked under #<other-number>."
    
    • 省略 --reason 时,默认使用 completed。
    • not planned 是两个词——not-planned 会被拒绝。它本身就是延期或放弃的完整记录,不需要其他内容。
    • --duplicate-of 会将原因设为 duplicate,并关联两个问题,因此保留问题的讨论串会成为历史记录。请使用仍保持打开状态的问题编号。
  • 修正关闭原因:gh issue edit 无法设置关闭原因,而 gh issue close 对已关闭的问题不会执行任何操作。对于 completed 和 not planned,请执行 PATCH: gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f state=closed -f state_reason=not_planned 若要事后记录重复问题,请先重新打开,再关闭: gh issue reopen <number> && gh issue close <number> --duplicate-of <other-number>。

参考

所有参考都必须写全,格式为 <org>/<repo>#<number>,无论是在正文还是表格中。GitHub 会将其渲染为带悬浮卡片的链接,并在同一仓库时缩短为 #<number>,因此阅读起来毫不费力。单独的 #<number> 会指向文本所在的仓库——在组织讨论中,这个仓库是 clhbid/clhbid.com,而不是你想引用的仓库——并且会在不提示的情况下链接到错误的问题。

阅读简写引用;写入完整引用。 人们会使用单独的 #<number> 和下方别名,匹配时不区分大小写。采取行动前,先解析引用,并说明你解析到了哪个问题。编辑包含简写引用的文档或笔记时,建议将其替换为完整引用。若别名未知或可能指代两个问题,请先询问;若某个仓库尚未列入表格,请为其建议一个简短别名。

别名仓库
LAclhbid/CLHbid-LiveAuction
comclhbid/clhbid.com
ACclhbid/agent-context
infraclhbid/infrastructure
TZ, hotpatchclhbid/canadian-time-zone-hotpatch
DFclhbid/devcontainer-features
gh, .githubclhbid/.github
scriptsclhbid/clhbid-scripts
E2Iclhbid/email-to-issue
SFETclhbid/salesforce-email-templates
templateclhbid/repository-template

问题和拉取请求共用同一编号空间,因此请先用 gh pr view <n> 解析编号;若找不到,再用 gh issue view <n>。

状态

CLHbid Delivery 项目中的 Status 是唯一可信来源,用于说明一个 issue 处于什么状态。

状态含义
Backlog收件箱——所有尚未分流的事项。新 issue 会进入此处
Waiting on input我们向报告者或业务方询问了某些事项,在收到回复前无法继续
Ready for Agent需求已完整明确,已拆分为一个拉取请求,并附有 Agent 简报
Ready for Human需要由人工处理——判断、外部系统、手动验证,或审核拉取请求
In progress已认领,正在处理
Done已关闭,原因不限(completed、not planned 和 duplicate)

其他事项都保留原生状态,绝不重复添加到看板上:assignee(由谁负责)、关联的拉取请求(正在审核的内容)、子 issue(任务拆分)、issue 依赖项(阻塞关系)、closed(已完成)。

分流会清空 Backlog。 每个 issue 都会进入这里,然后被分流至 Ready for Agent、Ready for Human 或 Waiting on input,也可能被关闭。经过评估后有意推迟的工作会回到 Backlog,并在下次重新考虑——因此,Backlog 绝不表示“已经处理过”,收件箱已满是正常状态,并不意味着积压了待分流的工作。

Waiting on input 为空是健康状态,而不是需要排查的问题:目前没有任何事项在等待业务方回复。

优先选择 Ready for Agent,但必须同时满足该状态的两个条件——需求已完整明确,并且 已经拆分。若某项工作要等到有人作出决定才能明确需求,则应设为 Waiting on input:这是一个待回答的问题,而不是待安排的工作。Ready for Human 用于无法由任何 Agent 完成的工作。关于 Agent 简报的要求,请参阅 afk-loop § The agent brief。

通过电子邮件向业务方征求意见。 一旦需要作出业务决策,就应立即通过电子邮件询问,而不是留到周期评审时再处理;周期评审应留给确实需要深入讨论的事项。邮件应使用通俗语言说明当前情况、需要作出的决定、各个选项及其后果、推荐方案及理由,并附上 issue 链接。

认领操作就是写入 assignee。 gh issue edit <n> --add-assignee @me 是原子性的首次写入,可避免两个 Agent 认领同一个 issue;之后再将 Status 设为 In progress。

未添加到看板的 issue 没有状态,并且对以下所有查询都不可见。自动添加工作流会将新建的 issue 添加到看板;创建 issue 后,稍等片刻,确认它已出现在看板上,并补上任何遗漏项。

添加一个 issue 会添加其整棵树。 添加父级 issue 会拉入所有后代,包括跨仓库边界的后代,以及来自不在当前范围内的仓库的后代。因此,即使规划仅针对已承诺的工作单元,看板也不会因此变得层级很浅——其中大部分内容都是后代 issue,而且它们的 Cycle 未设置。因此,所有按 Cycle 分组的视图都会有一个很大的 No Cycle 分组:其中包括已规划的已承诺祖先的后代,以及永远不会设置 Cycle 的 epic。

设置状态

在 Copilot 会话中,运行看板命令,以及查询看板中的命令,并设置 GH_TOKEN="$COPILOT_PROJECT_TOKEN"。会话自身的令牌只能访问一个仓库;此令牌用于写入看板和读取议题。其所有者会定期轮换该令牌,详见 clhbid/clhbid.com 的 docs/environment-variables.md。

gh project item-edit 需要三个 ID。每个会话查找一次:

# project id, Status field id, and the option id for each status
gh api graphql -f query='
  query { organization(login: "clhbid") { projectV2(number: 4) {
    id field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } } } } }'

# the item id for an issue (an issue may sit on several projects — take project 4)
gh api graphql -f query='
  query($owner: String!, $repo: String!, $num: Int!) {
    repository(owner: $owner, name: $repo) { issue(number: $num) {
      projectItems(first: 10) { nodes { id project { number } } } } } }' \
  -f owner="$(gh repo view --json owner --jq .owner.login)" \
  -f repo="$(gh repo view --json name --jq .name)" \
  -F num=<number> \
  --jq '.data.repository.issue.projectItems.nodes[] | select(.project.number == 4) | .id'

从检出内容中获取仓库,绝不要写死仓库名。 看板涵盖 clhbid 的所有仓库,而议题编号按仓库分别计算,因此固定的 repo= 会在其他地方解析到错误的议题——通常会返回 NOT_FOUND;但如果两个仓库中都存在该编号,就会返回错误议题对应的真实项目项 ID,而 item-edit 会接受它。

然后:

gh project item-edit --project-id <project-id> --id <item-id> \
  --field-id <status-field-id> --single-select-option-id <option-id>

之后读回该值——错误的选项 ID 会被静默接受。

分类角色

这些技能使用七种规范的分类角色:两个类别角色和五个状态角色。此处没有角色对应标签——类别角色是议题类型(见史诗),状态角色是 Status 值(状态中说明了各值的含义)。

mattpocock/skills 中的角色此处
bug类型为 Bug
enhancement类型为 Enhancement
needs-triageBacklog
needs-infoWaiting on input
ready-for-agentReady for Agent
ready-for-humanReady for Human
wontfix使用 --reason "not planned" 关闭

当技能指示“应用类别”时,设置类型;当技能指示“应用 AFK-ready 分类标签”时,将 Status 设置为此表中的值。 In progress 和 Done 在该词汇中没有对应项。

业务与交付

拆分工作是交付层面的考量。 工作会拆分为子议题,以便跟踪、切分,并按每个拉取请求逐一审查——见在 Ready for Agent 前拆分工作。业务不会以这种粒度跟踪工作:顶级议题是业务工作的单位,其子议题则是完成这项工作的方式。唯一的例外是史诗:其子议题各自都是独立的业务工作,因为每项工作都会单独纳入一个周期。

看板通过视图服务于这两类受众——📋 Delivery board 包含除史诗以外的所有内容,💼 Business 是 no:parent-issue -type:Epic,🗺️ Epics 则是史诗及其进度。

查询看板

议题搜索无法读取项目字段:status:"In progress" 不是有效筛选条件,不会匹配任何内容。因此,所有状态查询都通过 board.graphql 在看板上执行——使用一个查询,并通过 --jq 按用途筛选。

先找到 board.graphql。 它与 snapshot.graphql 一起位于此文件旁边,但其位置取决于技能的安装方式。每个会话解析一次,然后重复使用:

for d in ~/.claude/skills/issue-tracker \
         ~/.agents/skills/issue-tracker \
         .claude/skills/issue-tracker; do
  [ -f "$d/board.graphql" ] && BOARD_QUERY="$d/board.graphql" \
    && SNAPSHOT_QUERY="$d/snapshot.graphql" && break
done

如果这些位置都不存在,请如实说明,不要猜测——下面的每条操作都需要它。

# Agent frontier — ready, leaf, unblocked, unclaimed
gh api graphql --paginate -F query=@"$BOARD_QUERY" --jq '
  .data.organization.projectV2.items.nodes[]
  | select(.content.state == "OPEN" and .status.name == "Ready for Agent"
           and .content.assignees.totalCount == 0
           and .content.subIssues.totalCount == 0
           and .content.issueDependenciesSummary.blockedBy == 0)
  | "\(.content.repository.name)#\(.content.number)  \(.content.title)"'

# Needs triage — the inbox
... | select(.content.state == "OPEN" and .status.name == "Backlog")

# Needs business input
... | select(.content.state == "OPEN" and .status.name == "Waiting on input")

# In flight
... | select(.content.state == "OPEN" and .content.assignees.totalCount > 0)

# Security — every open issue labelled security
... | select(.content.state == "OPEN" and any(.content.labels.nodes[].name; . == "security"))

# Business filter — top-level issues and epic children, never an epic itself. Goes at the head of
# the jq program, before `.data`, in any recipe that wants its business view
def business: .content.issueType.name != "Epic"
              and (.content.parent == null or .content.parent.issueType.name == "Epic");

# Planning view — open business work
... | select(.content.state == "OPEN" and business)

# Epics — every open epic with its progress
... | select(.content.state == "OPEN" and .content.issueType.name == "Epic")
    | "\(.content.repository.name)#\(.content.number)  \(.content.subIssuesSummary.completed) of \(.content.subIssuesSummary.total)  \(.content.title)"

# Epics ready to close — open, with every child closed. A person closes them
... | select(.content.state == "OPEN" and .content.issueType.name == "Epic"
             and .content.subIssuesSummary.total > 0
             and .content.subIssuesSummary.completed == .content.subIssuesSummary.total)

# Epic candidates — committed parents that are not epics. Read each one; the recipe only narrows
... | select(.content.state == "OPEN" and .content.issueType.name != "Epic"
             and .content.parent == null and .content.subIssuesSummary.total > 0
             and .cycle != null)
    | "\(.content.repository.name)#\(.content.number)  \(.content.subIssuesSummary.completed) of \(.content.subIssuesSummary.total)  \(.content.title)"

# Status/state mismatch — the Item closed workflow missing one. Healthy result is empty
... | select((.content.state == "CLOSED" and .status.name != "Done")
          or (.content.state == "OPEN"   and .status.name == "Done"))

# Worked off-cycle — work that never got credited to the cycle it happened in
... | select((.content.state == "CLOSED"
              and .content.closedAt >= env.CYCLE_START and .content.closedAt < env.CYCLE_END
              and (.cycle.title // "") != env.CYCLE_TITLE)
          or (.content.state == "OPEN" and .status.name == "In progress" and .cycle == null
              and .content.updatedAt >= env.CYCLE_START))

# ...of which dormant — swap that last clause for uncycled In progress work nobody touched
              and .content.updatedAt < env.CYCLE_START))

# Stale closed items — closed but never archived. Healthy result is empty
... | select(.content.state == "CLOSED"
             and (.content.closedAt | fromdateiso8601) < (now - 4838400))

# Stale issues — business work untouched for eight weeks
... | select(.content.state == "OPEN" and business
             and (.status.name | IN("Backlog", "Ready for Agent", "Ready for Human"))
             and (.content.updatedAt | fromdateiso8601) < (now - 4838400))

# ...of which newly stale — add this clause to keep the ones not yet stale when the cycle began
             and (.content.updatedAt | fromdateiso8601)
                 >= ((env.CYCLE_START | strptime("%Y-%m-%d") | mktime) - 4838400)

... 代表上面的完整命令。--paginate 会对每一页应用 --jq,因此筛选和列出内容的操作可以照常运行;计数则需要通过管道执行(| wc -l)。

此处看不到已归档的项目项。 ProjectV2.items 默认使用 archivedStates: [NOT_ARCHIVED],因此每条操作都只读取当前看板。这使得“已关闭但过时”的操作可以重复运行——它看不到刚刚归档的内容——也正因如此,snapshot.graphql 会显式传入 archivedStates: [ARCHIVED, NOT_ARCHIVED]。 八周是 4838400 秒,这就是 stale 的定义——条目是否过期由看板决定,而非某个人的判断;过期情况按业务工作衡量。周期方案从环境变量读取 CYCLE_TITLE、CYCLE_START 和 CYCLE_END;根据迭代的 title、startDate 和 startDate + duration 设置它们。CYCLE_END 是不包含在内的——迭代在结束会议前一天结束,因此标题为 14 Aug → 28 Aug 的周期,其 CYCLE_START=2026-08-14,且 CYCLE_END=2026-08-28。

新近过期状态是推导出来的,绝不存储。 它会将一个 updatedAt 与两个阈值进行比较:相对于今天是否过期,以及相对于 CYCLE_START 是否尚未过期。周期结束时无需保存基线快照。若改为以 CYCLE_END 为基准,就等于询问哪些条目在今天到今天之间过期,结果将为空。

拉取请求不是看板条目,因此下面的方案会针对每个仓库运行 gh pr list,并从标题前缀 NNNN: 或分支前缀 NNNN- 中提取议题编号(参见提交约定):

# Merged pull request, open issue — a closing reference that never fired, or none. Healthy result is empty
for repo in $(gh repo list clhbid --no-archived --json name,hasIssuesEnabled --jq '.[] | select(.hasIssuesEnabled) | .name'); do
  open=$(gh issue list --repo "clhbid/$repo" --state open --limit 500 --json number --jq '.[].number')
  gh pr list --repo "clhbid/$repo" --state merged --limit 200 --json number,title,headRefName,closingIssuesReferences \
    --jq '.[] | ((.title | capture("^#?(?<n>[0-9]+)[: ]")?) // (.headRefName | capture("(^|/)(?<n>[0-9]+)-")?) // empty) as $m
          | "\($m.n)\t\(.number)\t\(.closingIssuesReferences | length)\t\(.title)"' \
  | while IFS=$'\t' read -r n pr refs title; do
      grep -qx "$n" <<<"$open" && echo "$repo#$n open — merged in $repo#$pr, $refs closing reference(s)  $title"
    done
done

只有合并到默认分支时,关闭引用才会生效。 有关闭引用但议题仍处于打开状态的行,表示拉取请求合并到了其他位置——例如,GitHub 重新指定目标分支之前,堆叠切片已合并到其父项;或者合并到了 scale-up 之类的分支。没有关闭引用的行,则表示没有 Closes 行,或是有意使用了 Refs/Part of。无论哪种情况,都要手动将议题关闭为 completed,并注明拉取请求,遵循约定——除非该拉取请求只是部分完成,且议题仍在处理中。

这些方案是权威标准,而且没有 API 可以返回视图内容——因此无法直接查询视图,只能镜像:

#视图筛选条件用途
1📋 交付看板-type:Epic除史诗之外的所有内容,按 Status 分组
2🎯 本周期cycle:@current当前进行中的周期
3⏭️ 下个周期cycle:@next接下来计划的内容
4💼 业务no:parent-issue -type:Epic顶层工作,包括已交付和未完成的工作
5🤖 前沿status:"Ready for Agent" no:assignee -is:blocked镜像前沿方案
6⏳ 等待status:"Waiting on input"镜像需要业务输入的方案
8📥 分类处理status:Backlog no:parent-issue收件箱,仅限顶层议题
9🗺️ 史诗type:Epic镜像史诗方案
13🔒 安全label:security is:open镜像安全方案

视图的编号固定不变,删除后也不会重复使用,因此表格中的编号会有间隔。只有筛选条件值得与此表核对:视图名称、列和标签页位置由使用者决定,因此这些方面的变更代表意图,而非偏差。唯一值得指明的列是 🗺️ 史诗中的 子议题进度——这是一个内置字段,也是该视图存在的原因:显示进度条。

筛选条件可以写入。 updateProjectV2View 接受 name、layout、filter 和 configuration,因此可以修复偏离预期的筛选条件,而不只是报告问题。分组和排序可以读取——ProjectV2View 暴露了 groupByFields、verticalGroupByFields 和 sortByFields——但不可写入:ProjectV2ViewConfigurationInput 只接受 visibleFieldIds。 两个看板视图都按 Cycle 分组为泳道,以 Status 作为列;没有条目的迭代不会呈现为泳道——因此,已完成的周期会在其条目归档后从看板中消失,无需使用筛选器隐藏。

周期

周期是 Cycle 字段的一次迭代。迭代在结束该周期的会议前一天结束。

从该字段的 configuration 中读取周期。completedIterations 保存已关闭的周期;当前周期是 iterations 中 startDate 早于或等于今天,且 startDate + duration 晚于今天的条目;未来周期则是开始日期晚于今天的条目。应按日期而非索引选择——数组顺序没有保证。

Cycle 设置在已承诺的工作单元上——顶层议题,或史诗的子议题——其后代会继承该值,因此没有 Cycle 的子议题代表已规划,而非遗漏。史诗自身的 Cycle 始终不设置。

任何时候都会有三个有效迭代——当前周期和两个未来周期——因此规划时总有地方可以安排两次会议后再处理的工作。

标题用于承载主题;在主题确定之前,使用日期范围作为占位符(Sep 29 - Oct 12)。日期保存在迭代自身的 startDate 和 duration 中。标题必须唯一:周期操作步骤通过 CYCLE_TITLE 匹配,泳道也以标题标记,因此两个周期若共用同一标题,就会被显示为一个。

写入配置会清空整个看板

updateProjectV2Field 是唯一会修改 iterationConfiguration 的操作,且每次写入都是完整替换,会重新生成所有迭代 ID,并清除每个条目的 Cycle。即使写回完全相同的配置也会如此,因此追加一个迭代的成本与重命名一个迭代完全相同。输入中也没有 completedIterations,所以任何未放回 iterations 的已完成周期都会被直接删除。

每个周期只写入一次配置,将所有待处理的更改合并其中——新迭代、确定后的结束日期、任何主题标题——并按以下步骤操作:

  1. 快照每个条目的 Cycle,包括已归档条目,使用 snapshot.graphql——board.graphql 仅包含实时条目,会悄悄跳过已归档条目:

    gh api graphql --paginate -F query=@"$SNAPSHOT_QUERY" \
      --jq '.data.organization.projectV2.items.nodes[] | select(.cycle.startDate)' \
      > /tmp/cycle-snapshot.jsonl
    
  2. 仅写入一次,传入所有迭代,并将已完成的迭代日期设为过去日期,以便 GitHub 将其重新排序到 completedIterations 中。配置自身的 startDate 无法读取回来——只能获取 startDay——因此请传入最早迭代的 startDate。

  3. 恢复快照中的每个值,按 startDate 查找迭代——所有 ID 都已更改,而且标题可能已移至另一个日期。归档条目无法编辑,因此需先取消归档(unarchiveProjectV2Item),设置其 Cycle,再重新归档(archiveProjectV2Item)。

  4. 读回 completedIterations 和恢复的条目数。completedIterations 为空表示失败;条目数少于快照数量则表示有条目遗漏。

史诗

史诗是一个周期内无法完成的大型工作——参见语言。层级结构为 史诗 → issue → 子 issue,首尾两层均可选,且史诗层级只有一层:史诗不属于任何上级,其子项是已承诺的工作单元。若工作最终跨越多个周期,就将其拆分为各自能在一个周期内完成的子项,并据此将其设为史诗。

  • Epic issue 类型是标记。 组织有三种类型——Bug(出现故障)、Enhancement(其他所有事项:新功能,以及我们过去称为杂务的工具、配置和重构工作)和 Epic——每个 issue 只能有一种类型。可通过 gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=<Bug|Enhancement|Epic> 设置,或在 REST 创建调用中使用 -f type=;gh issue create 和 gh issue edit 没有类型标志。可在 GraphQL 中通过 issueType { name } 读取,或在 REST 中通过 .type.name 读取。使用 gh issue list --search type:Epic 或 Epics 操作指南查找。如果组织缺少某种类型,可在组织节点上调用 createIssueType 创建;此操作需要 admin:org 权限,而默认的 gh 登录不具备该权限。
  • 状态按常规阶梯推进,每个子项也一样,像其他 issue 一样进行分类。史诗仍需拆分时设为 Backlog;子项创建后设为 Ready for Human;从第一个子项纳入周期时设为 In progress;由人员关闭时设为 Done。
  • 永远不要为史诗设置 Cycle。 每个子项都是一个已承诺的工作单元,并各自携带周期——因此,周期结束时仍未关闭的子项属于延期,而史诗既不算完成,也不算延期,只是仍在推进。
  • 在第一个子项纳入周期前完成拆分。 进度只统计直接子项,因此只有在每个子项都是一个周期内可完成的工作时,进度才真实反映情况。无法在一个周期内完成的子项,应拆成史诗的两个子项,保持层级只有一层。
  • 关闭是一种判断,因此由人员执行。可关闭的史诗操作指南会查找候选项。

延后工作

延后是一项需要记录的决定。记录方式取决于工作何时恢复:

  • 未来某个周期——将 Cycle 设为两个未来周期之一。issue 保持未关闭状态,由看板跟踪。
  • 更晚的日期——评论中写明决定、负责人和恢复时间,然后使用 --reason "not planned" 关闭。负责人需设置日历提醒;GitHub 不会替他们提醒。
  • 时间未知——将其留在 Backlog,以便分流时重新考虑。

工作恢复时,重新打开而不是重新提交,以保留讨论串作为历史记录。

标签

标签不表示状态或类别——状态由 Status 表示,类别由issue 类型表示。两种标签有实际含义:

  • wayfinder:*——wayfinder 集合会应用此类标签;参见 Wayfinding operations。
  • security——用于任何涉及安全漏洞、安全加固或安全流程变更的 issue。创建或分类此类 issue 时,除了设置类型,也要应用此标签: gh issue edit <n> --add-label security,或在使用组织表单创建 issue的创建调用中使用 -f 'labels[]=security'。它是标签而非类型,因为一个 issue 只能有一种类型,而安全问题可能属于 Bug 或 Enhancement。

其他任何标签,包括 bug 和 enhancement,都只是装饰——你愿意的话可以查看,但没有任何操作会以此为依据。

标签词汇不会同步到各个仓库,因此仓库中可能缺少某个标签。应用前请检查(gh label list --repo <owner>/<repo> --search <name>);如果缺少,请根据 clhbid/agent-context 中 labels.yml 提供的名称、颜色和描述创建:

gh api repos/clhbid/agent-context/contents/labels.yml --jq .content | base64 -d
gh label create <name> --repo <owner>/<repo> --color <color> --description "<description>"

从组织表单创建 issue

clhbid/.github/.github/ISSUE_TEMPLATE/ 中的 issue 表单是 issue 正文内容的事实来源。

创建 issue 前,先获取所需的表单:

gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
  --jq .content | base64 -d

按表单中各字段的顺序,将每个字段的 label 用作 ### 标题。填写所有必填字段,并遵循 writing-lean。不适用时,省略可选字段。 这条规则适用于 Agent 编写的任何正文,包括后续编辑。

表单字段以外的内容——验收标准、界面说明、验证步骤或表单本身未涵盖的范围边界——应放在 issue 的 agent-brief 评论(或 triage-notes 评论)中,而不是作为额外的正文标题。参见 afk-loop § Agent brief。

通过 REST 端点创建 issue,以便在同一次调用中设置类型——gh issue create 没有 --type 标志。类型与表单匹配:bug.yml 对应 Bug,enhancement.yml 对应 Enhancement。

gh api repos/<owner>/<repo>/issues -f title="..." -f body="..." -f type=<Bug|Enhancement> \
  --jq .html_url

这样设置了一个类型,不设置状态,然后确认 issue 已进入 Backlog——参见状态。

模板只设置类别,不设置状态

Issue 表单来自 clhbid/.github 下 .github/ISSUE_TEMPLATE/ 中的组织默认模板。提交 issue 的人应使用其中一个表单——这些表单会收集分流人员原本需要询问的信息。

表单只设置一个类别类型(Bug 或 Enhancement),不设置状态。类别说明 issue 属于哪种事项;Status 说明事项进展到哪一步——类别本身不代表任何状态。 Agent 不通过浏览器表单创建 issue 时,请使用 从组织表单创建 issue。

提交约定

提交主题以裸前缀携带 issue 编号——2217: <subject>。末尾的 (#NNNN) 是 squash-merge 添加的拉取请求编号,绝不是 issue 编号。为 diff 查找规范时,读取开头的 NNNN:,忽略末尾的 (#NNNN)。

在 Ready for Agent 之前拆分工作

保持 1 issue = 1 branch = 1 PR。如果工作量太大,就拆分issue,而不是拉取请求;如果两个 issue 实际上属于同一项改动,就将其中一个以重复项为由关闭。工作规模是设置 Ready for Agent 的前提。超出一个周期的工作属于史诗,应在更高层级拆分。

越小越好。 改动约 1000 行是上限——不包括锁文件、快照和生成文件——但这是限制,不是目标:可以清晰拆分的改动集应拆得远低于此上限,因为审核者会仔细阅读小型 diff,而对大型 diff 只能快速浏览。如果计划超过上限,应先简化,优先删去未通过 writing-lean 删除测试的文档,然后再做其他处理。没有 CI 门禁来检查这一点。

  • 有效的切分应可独立合并且检查通过。行为不变的切分(重命名、提取、重构)只要可以独立存在,也符合要求。如果计划中的切分无法各自通过检查并合并,就将状态设为 Ready for Human,并评论说明原因。
  • 如果切分之间存在依赖,就将拉取请求堆叠起来。 拉取请求可以将默认分支(main 或 development)以外的分支作为目标分支;当基分支合并时,GitHub 会自动调整目标分支——因此依赖切分可以立即创建并审核,无需等待其父级合并。
  • 如果 Agent 在工作过程中发现工作量太大,就先简化,创建子 issue 并将其关联为子项,将叶子 issue 的状态设为 Ready for Agent,为已完成的工作创建拉取请求——存在依赖关系时,应堆叠在前一个切分之上——然后在原 issue 中评论说明拆分原因。

将拉取请求用作分流入口

将 PR 用作请求入口:否。 (如果此仓库将外部 PR 视为功能请求,请设为 yes;triage 会读取此标志。) 只要此项为 no,外部 PR 就不会进入分流流程,也不适用任何 gh pr 状态处理。

当技能说明……

  • “发布到问题跟踪器” — 通过 通过组织表单创建问题 创建 GitHub 问题。
  • “获取相关工单” — gh issue view <number> --comments。

Wayfinding 操作

供 wayfinder 使用。地图是单个问题,子问题则作为工单。

  • 地图:带有 wayfinder:map 标签的单个问题,其中包含 Notes / Decisions-so-far / Fog 正文。gh issue create --label wayfinder:map。
  • 子工单:作为 GitHub 子问题链接到地图的问题;请参阅约定。标签:wayfinder:<type>(research/prototype/grilling/task)。工单被认领后,会分配给负责推进的开发者。
  • 阻塞关系:GitHub 的原生问题依赖关系。使用 gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id> 添加一条依赖关系,其中 <blocker-db-id> 是阻塞方的数字数据库 ID(gh api repos/<owner>/<repo>/issues/<n> --jq .id,不是 #number 或 node_id)。GitHub 会报告 issue_dependencies_summary.blocked_by(仅统计未关闭的阻塞项——这是实时门槛)。所有阻塞项都关闭后,工单才解除阻塞。
  • 前沿查询:列出地图中所有未关闭的子问题,剔除存在未关闭阻塞项或已分配负责人的问题;按地图中的顺序,取第一个。
  • 认领:gh issue edit <n> --add-assignee @me — 会话中的首次写入操作。
  • 解决:gh issue comment <n> --body "<answer>",然后 gh issue close <n>,再将上下文指针(gist + 链接)追加到地图的 Decisions-so-far 中。

注意事项

错误的筛选条件看起来也像是正确的。 GitHub 遇到未知限定符时会忽略它,而不是报错。因此结果可能朝任一方向出错——拼写错误可能返回所有内容,而在问题搜索中使用 status:"In progress" 则可能什么也搜不到——所以仅凭结果数量无法证明筛选正确。请用一个答案已知的查询来验证新筛选条件。

REST 对父子关系的描述不可信。 gh issue view --json parent 会直接报错,而 gh api repos/{owner}/{repo}/issues/{n} --jq .parent.number 对子问题和顶层问题都会返回 null。GraphQL 的 Issue.parent 会如实返回,这也是 board.graphql 所选取的字段。逐个检查问题每次都要花费一次请求,压力大时往往会被跳过,因此应在查询本身加入 no:parent-issue(或 .content.parent == null)。

添加项目会取消归档。 对已归档的项目执行 addProjectV2ItemById,会在不提示的情况下将其恢复到实时看板。仅读取实时项目时看不到已归档项目,因此一个重新打开的问题看起来会像是不存在,随后被重新添加,并因此从归档中恢复。

回读结果可能过时。 项目 API 最终会保持一致。每次写入后都要通过回读验证,但如果写入后立即发现不匹配,应视为尚未确认,而不是失败——稍后再次检查,切勿仅凭一次不一致的读取就重新执行写入。issueDependenciesSummary.blockedBy 也会有同样的延迟——关联阻塞项后,它立即读取到 0,稍后才变为 1——因此,写入后立刻运行的 Agent 前沿流程可能会分派一个实际上已被阻塞的问题。

评论

支持 Markdown