关于 issue-tracker
issue-tracker 是面向 CLHbid 仓库的 GitHub 问题与项目看板操作指南。它统一问题类型、状态、引用、分流和交付规范,并通过 gh 命令行执行操作。
主要功能
- 01定义问题类别与生命周期术语
- 02统一完整问题引用格式
- 03映射项目状态与分流角色
- 04说明 gh 命令行问题操作
- 05解释看板查询与交付结构
适用场景
- 01创建或更新仓库问题
- 02按分流状态处理问题
- 03跨 CLHbid 仓库跟踪工作
- 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> 和下方别名,匹配时不区分大小写。采取行动前,先解析引用,并说明你解析到了哪个问题。编辑包含简写引用的文档或笔记时,建议将其替换为完整引用。若别名未知或可能指代两个问题,请先询问;若某个仓库尚未列入表格,请为其建议一个简短别名。
| 别名 | 仓库 |
|---|---|
LA | clhbid/CLHbid-LiveAuction |
com | clhbid/clhbid.com |
AC | clhbid/agent-context |
infra | clhbid/infrastructure |
TZ, hotpatch | clhbid/canadian-time-zone-hotpatch |
DF | clhbid/devcontainer-features |
gh, .github | clhbid/.github |
scripts | clhbid/clhbid-scripts |
E2I | clhbid/email-to-issue |
SFET | clhbid/salesforce-email-templates |
template | clhbid/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-triage | Backlog |
needs-info | Waiting on input |
ready-for-agent | Ready for Agent |
ready-for-human | Ready 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 的已完成周期都会被直接删除。
每个周期只写入一次配置,将所有待处理的更改合并其中——新迭代、确定后的结束日期、任何主题标题——并按以下步骤操作:
-
快照每个条目的
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 -
仅写入一次,传入所有迭代,并将已完成的迭代日期设为过去日期,以便 GitHub 将其重新排序到
completedIterations中。配置自身的startDate无法读取回来——只能获取startDay——因此请传入最早迭代的startDate。 -
恢复快照中的每个值,按
startDate查找迭代——所有 ID 都已更改,而且标题可能已移至另一个日期。归档条目无法编辑,因此需先取消归档(unarchiveProjectV2Item),设置其Cycle,再重新归档(archiveProjectV2Item)。 -
读回
completedIterations和恢复的条目数。completedIterations为空表示失败;条目数少于快照数量则表示有条目遗漏。
史诗
史诗是一个周期内无法完成的大型工作——参见语言。层级结构为 史诗 → issue → 子 issue,首尾两层均可选,且史诗层级只有一层:史诗不属于任何上级,其子项是已承诺的工作单元。若工作最终跨越多个周期,就将其拆分为各自能在一个周期内完成的子项,并据此将其设为史诗。
Epicissue 类型是标记。 组织有三种类型——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 前沿流程可能会分派一个实际上已被阻塞的问题。