issue-tracker

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Issue tracker: GitHub

Issue跟踪:GitHub

Issues and specs for this repo live as GitHub issues. Work state lives on the CLHbid Delivery org project, which spans every clhbid repo. Use the
gh
CLI for all operations.
本仓库的Issue和需求规格均以GitHub Issue的形式存在。工作状态保存在CLHbid Delivery组织项目看板中,该看板覆盖所有clhbid仓库。所有操作均使用
gh
CLI执行。

Language

术语约定

The words the skills plan and report in. Every rule below is written in these terms.
Cycle: a fortnight-ish planning period that work is committed to. Committing an issue to a cycle is a promise to finish it in that cycle. Avoid: sprint, milestone.
Epic: a top-level issue carrying the
Epic
issue type, too large for one cycle and delivered through its children. An epic is never committed to a cycle; each child fits in one cycle and is committed on its own. Avoid: initiative, program, tracker issue, parent (every epic is a parent; few parents are epics).
Committed unit: the issue whose
Cycle
is set — a top-level issue, or a child of an epic. Its descendants inherit the cycle and are never set directly. Avoid: planned issue, scheduled issue.
Slipped: committed to a cycle and still open when it closed. Always a missed commitment, never "progressing as expected". Avoid: in flight, carried over.
Progress: an epic's completed children over its total children. Avoid: percent complete, velocity, burndown.
Business work: what the business plans and reads about — a top-level issue, or a child of an epic. Avoid: ticket, story.
Skill进行规划和报告时使用的词汇。以下所有规则均采用这些术语表述。
Cycle: 约两周的规划周期,工作会被承诺在该周期内完成。将Issue提交到某个Cycle意味着承诺在该周期内完成。避免使用:sprint、milestone。
Epic: 带有
Epic
Issue类型的顶层Issue,规模过大无法在单个Cycle内完成,通过其子Issue交付。Epic永远不会被提交到某个Cycle;每个子Issue都适合在一个Cycle内完成,并单独提交。避免使用:initiative、program、tracker issue、parent(每个Epic都是父Issue,但很少有父Issue是Epic)。
Committed unit: 设置了
Cycle
的Issue——可以是顶层Issue,也可以是Epic的子Issue。其所有后代Issue会继承该Cycle,且不会直接设置Cycle值。避免使用:planned issue、scheduled issue。
Slipped: 已承诺加入某个Cycle,但在Cycle结束时仍处于打开状态。始终表示未兑现的承诺,绝非“按预期推进中”。避免使用:in flight、carried over。
Progress: 一个Epic已完成的子Issue占总子Issue的比例。避免使用:percent complete、velocity、burndown。
Business work: 业务方规划和查看的工作内容——顶层Issue,或Epic的子Issue。避免使用:ticket、story。

Conventions

操作规范

  • Create an issue:
    gh issue create --title "..." --body "..."
    . Use a heredoc for multi-line bodies. This does not apply an issue form — follow Creating an issue from the org form.
  • Read, list, comment:
    gh issue view <number> --comments
    ,
    gh issue list
    ,
    gh issue comment <number> --body "..."
    .
    --jq
    requires
    --json
    , so filtering a read means dropping
    --comments
    and naming the fields:
    gh issue view <number> --json number,title,labels --jq ...
  • Refer to an issue as
    <org>/<repo>#<number>
    , in prose and in tables alike. GitHub renders that form as a link with a hovercard and shortens it to
    #<number>
    when it is same-repo, so the qualified form costs nothing to read. A bare
    #<number>
    resolves against whichever repo hosts the text it sits in
    — in an org discussion that is
    clhbid/clhbid.com
    , not the repo you meant — and links silently to the wrong issue. GitHub also shares one number space across issues and pull requests, so resolve an unqualified reference with
    gh pr view <n>
    , falling back to
    gh issue view <n>
    .
  • Set state: a project field, not a label — see Status.
  • Close: an issue closes as
    completed
    ,
    not planned
    or
    duplicate
    . The reason is the record, so pick the one that matches and say why in a closing comment.
    bash
    # 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>."
    • completed
      is the default when
      --reason
      is omitted.
    • not planned
      is two words —
      not-planned
      is rejected. It is the whole record for a deferral or an abandonment; nothing else is needed.
    • --duplicate-of
      sets the reason to
      duplicate
      and links the two issues, so the survivor's thread becomes the history. Take the number of the issue that stays open.
  • Fix a close reason:
    gh issue edit
    cannot set one, and
    gh issue close
    no-ops on an already-closed issue. For
    completed
    and
    not planned
    , PATCH it:
    gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f state=closed -f state_reason=not_planned
    To record a duplicate after the fact, reopen and re-close:
    gh issue reopen <number> && gh issue close <number> --duplicate-of <other-number>
    .
  • 创建Issue:
    gh issue create --title "..." --body "..."
    。多行正文使用heredoc。此命令不会应用Issue表单——请遵循从组织表单创建Issue。
  • 查看、列出、评论:
    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 ...
  • 引用Issue时使用
    <org>/<repo>#<number>
    格式
    ,无论是在正文还是表格中。GitHub会将这种格式渲染为带悬停卡片的链接,并且在同一仓库内会自动简写为
    #<number>
    ,因此使用完整格式不会增加阅读成本。裸写的
    #<number>
    会根据其所在文本的仓库来解析
    ——在组织讨论中,它会解析为
    clhbid/clhbid.com
    仓库的Issue,而不是你想指的那个仓库——并且会静默链接到错误的Issue。此外,GitHub的Issue和Pull Request共用同一编号空间,因此解析未限定的引用时,可以先用
    gh pr view <n>
    ,失败再用
    gh issue view <n>
    。
  • 设置状态: 使用项目字段,而非标签——参见Status。
  • 关闭: Issue关闭时的状态为
    completed
    、
    not planned
    或
    duplicate
    。关闭原因是正式记录,因此请选择匹配的原因,并在关闭评论中说明理由。
    bash
    # 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
      并关联两个Issue,因此保留的那个Issue的线程就成为历史记录。请使用保持打开状态的Issue的编号。
  • 修改关闭原因:
    gh issue edit
    无法设置关闭原因,而
    gh issue close
    对已关闭的Issue无效。对于
    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>
    。

Status

Status

Status
on the CLHbid Delivery project is the single source of truth for what state an issue is in.
StatusMeaning
BacklogThe inbox — everything not yet routed. New issues land here
Waiting on inputWe asked the reporter or the business something and cannot proceed until they answer
Ready for AgentFully specified, sliced to one pull request, and carrying an agent brief
Ready for HumanNeeds a person — judgement, external systems, manual verification, or a pull request to review
In progressClaimed and being worked
DoneClosed, for any reason (
completed
,
not planned
and
duplicate
)
Everything else stays native and is never duplicated onto the board: assignee (who holds it), linked pull requests (what is in review), sub-issues (decomposition), issue dependencies (blocking), closed (finished).
Triage empties
Backlog
.
Every issue arrives there and leaves by being routed to
Ready for Agent
,
Ready for Human
or
Waiting on input
, or by being closed. Work considered and deliberately deferred goes back to
Backlog
and is reconsidered next time — so
Backlog
never means "already dealt with", and a full inbox is the normal state rather than a backlog of triage debt.
An empty
Waiting on input
is a healthy state
, not a query to debug: nothing is blocked on the business.
Prefer
Ready for Agent
, which needs both halves of its row — fully specified and already sliced. Work that cannot be specified until someone decides something is
Waiting on input
: a question to answer, not work to schedule.
Ready for Human
is for what no agent can finish. For what counts as an agent brief, see
afk-loop
§ The agent brief
.
Claiming is an assignee write.
gh issue edit <n> --add-assignee @me
is the atomic first write that stops two agents taking the same issue; setting
Status
to
In progress
follows it.
An issue that is not on the board has no state and is invisible to every query below. Auto-add workflows put newly-opened issues on the board; after creating one, give the workflow a moment and then confirm it landed, adding anything that was missed.
Adding one issue adds its whole tree. Adding a parent pulls in every descendant, across repository boundaries and including repos nobody had in scope. So planning being set on committed units does not make the board that shallow — most of it is descendants, and they arrive with
Cycle
unset. Any group-by-
Cycle
view therefore carries a large No Cycle bucket: descendants whose committed ancestor is planned, and epics, which never carry one.
CLHbid Delivery项目中的
Status
字段是Issue所处状态的唯一可信来源。
Status含义
Backlog收件箱——所有尚未分派的工作。新Issue会进入此处
Waiting on input我们已向报告人或业务方提出问题,在得到答复前无法推进
Ready for Agent已完成完整规格说明,拆分为单个Pull Request,并附有Agent任务说明
Ready for Human需要人工处理——涉及判断、外部系统、手动验证,或需要审核Pull Request
In progress已被认领,正在处理中
Done已关闭,无论原因是什么(
completed
、
not planned
和
duplicate
)
其他所有信息均保持GitHub原生状态,不会在看板上重复存储:assignee(负责人)、linked pull requests(审核中的Pull Request)、sub-issues(子Issue,工作分解)、issue dependencies(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
。
认领操作就是写入assignee字段。
gh issue edit <n> --add-assignee @me
是原子性的第一步写入操作,可以避免两个Agent认领同一个Issue;之后再将
Status
设置为
In progress
。
不在看板上的Issue没有状态,并且对下面所有查询都不可见。自动添加工作流会将新打开的Issue添加到看板上;创建Issue后,请等待工作流执行片刻,然后确认它已成功加入看板,手动添加遗漏的Issue。
添加一个Issue会自动添加其整个层级树。 添加父Issue会拉入所有后代Issue,跨仓库边界,甚至包括不在范围内的仓库。因此,在承诺单元上设置规划并不会让看板变得浅显——看板上大部分是后代Issue,它们加入时
Cycle
字段未设置。因此,任何按
Cycle
分组的视图都会有一个很大的No Cycle分组:包括已规划的承诺单元的后代Issue,以及永远不会设置Cycle的Epic。

Setting Status

设置Status

gh project item-edit
needs three IDs. Look them up once per session:
bash
undefined
gh project item-edit
需要三个ID。每个会话只需查询一次:
bash
undefined

project id, Status field id, and the option id for each status

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 } } } } } }'
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)

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'

**Take the repo from the checkout, never a literal.** The board spans every clhbid repo and issue
numbers are per-repo, so a pinned `repo=` resolves the wrong issue somewhere else — usually a
`NOT_FOUND`, but a number that exists in both repos returns a real item id for the wrong issue, and
`item-edit` accepts it.

Then:

```bash
gh project item-edit --project-id <project-id> --id <item-id> \
  --field-id <status-field-id> --single-select-option-id <option-id>
Read the value back afterwards — a wrong option id is accepted silently.
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仓库,且Issue编号是每个仓库独立的,因此写死的`repo=`会解析到其他仓库的错误Issue——通常会返回`NOT_FOUND`,但如果两个仓库有相同的编号,就会返回错误Issue的有效item id,而`item-edit`会接受这个ID。

然后执行:

```bash
gh project item-edit --project-id <project-id> --id <item-id> \
  --field-id <status-field-id> --single-select-option-id <option-id>
操作完成后请回读验证——错误的option id会被静默接受,不会报错。

Triage roles

分诊角色

The skills speak in terms of five canonical triage roles. Here a role is a
Status
value
, not a label — Status has what each one means.
Role in mattpocock/skillsStatus here
needs-triage
Backlog
needs-info
Waiting on input
ready-for-agent
Ready for Agent
ready-for-human
Ready for Human
wontfix
close with
--reason "not planned"
When a skill says "apply the AFK-ready triage label", set
Status
to the value in this table.
In progress
and
Done
have no counterpart in that vocabulary.
Skill使用五种标准分诊角色。这里的角色是一个**
Status
值**,而非标签——每个角色的含义参见Status。
mattpocock/skills中的角色对应的Status
needs-triage
Backlog
needs-info
Waiting on input
ready-for-agent
Ready for Agent
ready-for-human
Ready for Human
wontfix
使用
--reason "not planned"
关闭
当某个Skill提到“应用AFK-ready分诊标签”时,将
Status
设置为本表中的对应值。
In progress
和
Done
在该词汇表中没有对应项。

Business and delivery

业务与交付

Decomposition is a delivery concern. Work is broken into sub-issues so it can be tracked, sliced and reviewed one pull request at a time — see Decomposing work before Ready for Agent. The business does not track work at that grain: a top-level issue is the unit of business work, and its children are how that work gets done. The one exception is an epic, whose children are each business work in their own right because each is committed to a cycle on its own.
The board carries both audiences as views — 📋 Delivery board is everything but epics, 💼 Business is
no:parent-issue -type:Epic
, and 🗺️ Epics is the epics with their progress. The same split governs anything read outside the board:
  • Reporting to the business — cycle notes, and anything else the business reads — is business work only, counts included: top-level issues and the children of epics, with epics themselves reported in their own section rather than in any cycle table. Add the
    business
    filter below to any recipe to get its business view, as the Planning view recipe does. A board view cannot express it — project filters have no OR across qualifiers — which is why 💼 Business shows epic children only through the cycle views.
  • Dispatching and doing the work reads the leaves, because that is where a branch and a pull request attach. The Agent frontier recipe is the example: it excludes anything with children.
工作分解是交付层面的事务。 工作会被拆分为多个子Issue,以便逐个Pull Request进行跟踪、拆分和审核——参见在Ready for Agent之前分解工作。业务方不会在这么细的粒度上跟踪工作:顶层Issue是业务工作的单位,其子Issue只是完成工作的方式。唯一的例外是Epic,Epic的每个子Issue本身都是业务工作,因为每个子Issue都会单独承诺到一个Cycle中。
看板通过不同视图满足两类受众的需求——📋 Delivery board包含除Epic之外的所有内容,💼 Business的过滤条件是
no:parent-issue -type:Epic
,🗺️ Epics展示所有Epic及其进度。同样的划分也适用于看板之外的任何查询:
  • 向业务方汇报——周期报告以及业务方查看的其他内容——仅包含业务工作,包括统计数据:顶层Issue和Epic的子Issue,Epic本身会在单独的部分汇报,而非出现在任何Cycle表格中。如需获取业务视图,可在任何查询方案中添加下面的
    business
    过滤器,就像Planning view方案那样。看板视图无法表达这种过滤条件——项目过滤器不支持跨条件的OR操作——这就是为什么💼 Business仅通过Cycle视图展示Epic子Issue。
  • 分派和执行工作时读取叶子节点,因为分支和Pull Request都是关联在叶子Issue上的。Agent frontier方案就是例子:它排除了所有有子Issue的项。

Querying the board

查询看板

Issue search cannot read project fields:
status:"In progress"
is not a qualifier and matches nothing. Every state query therefore runs against the board with
board.graphql
— one query, filtered per use with
--jq
.
Find
board.graphql
first.
It sits beside this file, but where that is depends on how the skill was installed. Resolve it once per session and reuse it:
bash
for p in ~/.claude/skills/issue-tracker/board.graphql \
         ~/.agents/skills/issue-tracker/board.graphql \
         .claude/skills/issue-tracker/board.graphql; do
  [ -f "$p" ] && BOARD_QUERY="$p" && break
done
If none of them exists, say so rather than guessing — every recipe below needs it.
bash
undefined
Issue搜索无法读取项目字段:
status:"In progress"
不是合法的限定符,匹配不到任何内容。因此,所有状态查询都使用
board.graphql
针对看板执行——一个查询,通过
--jq
按需过滤。
首先找到
board.graphql
文件。
它与本文件位于同一目录,但具体位置取决于Skill的安装方式。每个会话只需解析一次,之后可重复使用:
bash
for p in ~/.claude/skills/issue-tracker/board.graphql \
         ~/.agents/skills/issue-tracker/board.graphql \
         .claude/skills/issue-tracker/board.graphql; do
  [ -f "$p" ] && BOARD_QUERY="$p" && break
done
如果这些路径都不存在,请明确说明,不要猜测——下面的所有方案都需要该文件。
bash
undefined

Agent frontier — ready, leaf, unblocked, unclaimed

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)"'
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

Needs triage — the inbox

... | select(.content.state == "OPEN" and .status.name == "Backlog")
... | select(.content.state == "OPEN" and .status.name == "Backlog")

Needs business input

Needs business input

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

In flight

In flight

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

Business filter — top-level issues and epic children, never an epic itself. Goes at the head of

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

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");
def business: .content.issueType.name != "Epic" and (.content.parent == null or .content.parent.issueType.name == "Epic");

Planning view — open business work

Planning view — open business work

... | select(.content.state == "OPEN" and business)
... | select(.content.state == "OPEN" and business)

Epics — every open epic with its progress

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)"
... | 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

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)
... | 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

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)"
... | 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

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"))
... | 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

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))
... | 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

...of which dormant — swap that last clause for uncycled In progress work nobody touched

and .content.updatedAt < env.CYCLE_START))
and .content.updatedAt < env.CYCLE_START))

Stale closed items — closed but never archived. Healthy result is empty

Stale closed items — closed but never archived. Healthy result is empty

... | select(.content.state == "CLOSED" and (.content.closedAt | fromdateiso8601) < (now - 4838400))
... | select(.content.state == "CLOSED" and (.content.closedAt | fromdateiso8601) < (now - 4838400))

Stale issues — business work untouched for eight weeks

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))
... | 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

...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)

`...` stands in for the full command above. `--paginate` applies `--jq` per page, so filtering and
listing work as written; counting needs a pipe (`| wc -l`).

**Archived items are invisible here.** `ProjectV2.items` defaults to
`archivedStates: [NOT_ARCHIVED]`, so every recipe reads the live board only. That is what makes the
stale-closed recipe re-runnable — it cannot see what it just archived — and it is why the `Cycle`
snapshot in [Cycles](#cycles) passes `archivedStates: [ARCHIVED, NOT_ARCHIVED]` explicitly.

**Eight weeks is 4838400 seconds**, and it is the definition of _stale_ — an item is stale on the
board, not in someone's judgement, and staleness is measured on business work. The cycle recipes
read `CYCLE_TITLE`, `CYCLE_START` and
`CYCLE_END` from the environment; set them from the iteration's `title`, `startDate` and
`startDate + duration`. `CYCLE_END` is **exclusive** — an iteration ends the day before the meeting
that closes it, so a cycle titled `14 Aug → 28 Aug` has `CYCLE_START=2026-08-14` and
`CYCLE_END=2026-08-28`.

**Newly stale is derived, never stored.** It compares one `updatedAt` against two thresholds: stale
against today, and not-yet-stale against `CYCLE_START`. There is no baseline to snapshot at the end
of a cycle. Anchoring it to `CYCLE_END` instead would ask which items went stale between today and
today, and return nothing.

**Pull requests are not board items**, so the recipe below runs per repository against
`gh pr list` instead, taking the issue from the `NNNN:` title prefix or the `NNNN-` branch prefix
(see [Commit convention](#commit-convention)):

```bash
and (.content.updatedAt | fromdateiso8601) >= ((env.CYCLE_START | strptime("%Y-%m-%d") | mktime) - 4838400)

`...`代表上面的完整命令。`--paginate`会对每一页应用`--jq`,因此按上述写法可以正常过滤和列表;如果需要计数,需要加管道(`| wc -l`)。

**已归档的项在此处不可见。** `ProjectV2.items`默认使用`archivedStates: [NOT_ARCHIVED]`,因此所有方案都仅读取活跃看板。这就是为什么已关闭陈旧项方案可以重复运行——它看不到自己刚归档的内容——也是为什么[Cycles](#cycles)中的`Cycle`快照会显式传入`archivedStates: [ARCHIVED, NOT_ARCHIVED]`。

**八周等于4838400秒**,这是_stale(陈旧)_的定义——项是否陈旧是根据看板数据判断的,而非人为判断,且陈旧性仅针对业务工作衡量。Cycle相关方案会从环境变量中读取`CYCLE_TITLE`、`CYCLE_START`和`CYCLE_END`;请根据迭代的`title`、`startDate`和`startDate + duration`设置这些变量。`CYCLE_END`是**不包含在内的(exclusive)**——迭代在关闭会议的前一天结束,因此标题为`14 Aug → 28 Aug`的Cycle,其`CYCLE_START=2026-08-14`,`CYCLE_END=2026-08-28`。

**新增陈旧项是推导出来的,绝不存储。** 它将`updatedAt`与两个阈值进行比较:与今天比较判断是否陈旧,与`CYCLE_START`比较判断是否之前未陈旧。Cycle结束时不需要快照基线。如果将锚点改为`CYCLE_END`,就相当于查询“今天到今天之间有哪些项变陈旧”,结果会是空的。

**Pull Request不是看板项**,因此下面的方案会针对每个仓库运行`gh pr list`,从标题前缀`NNNN:`或分支前缀`NNNN-`中提取Issue编号(参见[提交规范](#commit-convention)):

```bash

Merged pull request, open issue — a closing reference that never fired, or none. Healthy result is empty

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

**A closing reference fires only on a merge into the default branch.** A row with a closing
reference and an open issue is a pull request that merged somewhere else — a stacked slice merged
into its parent before GitHub retargeted it, or anything merged into `scale-up` or `development`.
A row with none had no `Closes` line, or a deliberate `Refs`/`Part of`. Either way the issue closes
by hand — as `completed`, naming the pull request, per [Conventions](#conventions) — unless the pull
request was a partial and the issue is still live.

**These recipes are canonical**, and no API returns a view's contents — so a view can never be
queried directly, only mirrored:

| #   | View              | Filter                                             | Purpose                                   |
| --- | ----------------- | -------------------------------------------------- | ----------------------------------------- |
| 1   | 📋 Delivery board | `-type:Epic`                                       | everything but epics, grouped by `Status` |
| 2   | 🎯 This cycle     | `cycle:@current`                                   | the running cycle                         |
| 3   | ⏭️ Next cycle     | `cycle:@next`                                      | what is planned next                      |
| 4   | 💼 Business       | `no:parent-issue -type:Epic`                       | top-level work, delivered and outstanding |
| 5   | 🤖 Frontier       | `status:"Ready for Agent" no:assignee -is:blocked` | mirrors the frontier recipe               |
| 6   | ⏳ Waiting        | `status:"Waiting on input"`                        | mirrors the needs-business-input recipe   |
| 8   | 📥 Triage         | `status:Backlog no:parent-issue`                   | the inbox, top-level only                 |
| 9   | 🗺️ Epics          | `type:Epic`                                        | mirrors the epics recipe                  |

View **numbers** are stable and never reused after a delete, which is why 7 is missing. Only the
filter is worth asserting against this table: a view's name, columns and tab position belong to
whoever uses it, so a change there is intent rather than drift. The one column worth naming is
**Sub-issues progress** on 🗺️ Epics — a built-in field, and the progress bar the view exists for.

**A filter is writable.** `updateProjectV2View` takes `name`, `layout`, `filter` and
`configuration`, so a drifted filter can be **repaired**, not only reported. Grouping and sorting
are **readable** — `ProjectV2View` exposes `groupByFields`, `verticalGroupByFields` and
`sortByFields` — but not writable: `ProjectV2ViewConfigurationInput` takes only `visibleFieldIds`.

Both board views group by `Cycle` as swimlanes with `Status` as the columns, and **an iteration with
no items renders no swimlane** — so a completed cycle leaves the board once its items are archived,
and no filter is needed to hide one.
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

**关闭引用仅在合并到默认分支时触发。** 如果某一行有关闭引用但Issue仍处于打开状态,说明该Pull Request合并到了其他地方——比如在GitHub重新调整目标分支之前,堆叠的切片合并到了其父分支,或者合并到了`scale-up`或`development`分支。如果没有关闭引用,说明PR中没有`Closes`行,或者是故意使用`Refs`/`Part of`。无论哪种情况,都需要手动关闭Issue——按照[操作规范](#conventions),标记为`completed`并注明对应的Pull Request——除非该PR只是部分交付,Issue仍需继续处理。

**这些方案是标准规范**,没有API可以直接返回视图的内容——因此无法直接查询视图,只能镜像实现:

| 编号 | 视图名称              | 过滤条件                                             | 用途                                   |
| --- | ----------------- | -------------------------------------------------- | ----------------------------------------- |
| 1   | 📋 Delivery board | `-type:Epic`                                       | 除Epic外的所有内容,按`Status`分组 |
| 2   | 🎯 This cycle     | `cycle:@current`                                   | 当前正在进行的Cycle                         |
| 3   | ⏭️ Next cycle     | `cycle:@next`                                      | 下一个Cycle的规划内容                      |
| 4   | 💼 Business       | `no:parent-issue -type:Epic`                       | 顶层工作,包括已交付和未交付的 |
| 5   | 🤖 Frontier       | `status:"Ready for Agent" no:assignee -is:blocked` | 镜像Agent前沿方案               |
| 6   | ⏳ Waiting        | `status:"Waiting on input"`                        | 镜像待业务输入方案   |
| 8   | 📥 Triage         | `status:Backlog no:parent-issue`                   | 收件箱,仅顶层Issue                 |
| 9   | 🗺️ Epics          | `type:Epic`                                        | 镜像Epics方案                  |

视图的**编号**是稳定的,删除后不会复用,这就是为什么缺少7号。只有过滤条件值得与本表进行校验:视图的名称、列和标签页位置属于使用者,因此这些变化是有意为之,而非配置漂移。唯一值得一提的列是🗺️ Epics视图上的**Sub-issues progress**——这是一个内置字段,也是该视图存在的意义:展示进度条。

**过滤条件是可写的。** `updateProjectV2View`接受`name`、`layout`、`filter`和`configuration`参数,因此漂移的过滤条件可以被**修复**,而不仅仅是上报。分组和排序是**可读的**——`ProjectV2View`暴露了`groupByFields`、`verticalGroupByFields`和`sortByFields`——但不可写:`ProjectV2ViewConfigurationInput`仅接受`visibleFieldIds`。

两个看板视图都按`Cycle`分组作为泳道,`Status`作为列,**没有项的迭代不会渲染泳道**——因此,已完成的Cycle在其所有项被归档后会自动从看板上消失,无需额外过滤。

Cycles

Cycles

A cycle is an iteration of the
Cycle
field. An iteration ends the day before the meeting that closes it.
Read them from the field's
configuration
.
completedIterations
holds the closed ones; the running cycle is the entry in
iterations
whose
startDate
is on or before today and whose
startDate + duration
is after it, and the future cycles are the ones starting after it. Select by date rather than by index — nothing guarantees the array's order.
Cycle
is set on the committed unit — a top-level issue, or a child of an epic — and its descendants inherit it, so a sub-issue with no
Cycle
is planned, not missed. An epic's own
Cycle
is never set.
Three iterations are live at all times — the running cycle and two future ones — so planning always has somewhere to put work deferred two meetings out.
The title carries the theme, and until one is agreed it is the date range as a placeholder (
Sep 29 - Oct 12
). Dates live in the iteration's own
startDate
and
duration
. Titles must be unique: recovering from a configuration write resolves iterations by title, and two cycles sharing one makes that ambiguous.
Cycle是
Cycle
字段的迭代项。迭代在关闭会议的前一天结束。
从字段的
configuration
中读取Cycle列表。
completedIterations
包含已结束的Cycle;当前正在进行的Cycle是
iterations
中
startDate
早于或等于今天,且
startDate + duration
晚于今天的项;未来的Cycle是开始时间在今天之后的项。请按日期选择,而非按索引——数组的顺序没有保证。
Cycle
设置在**committed unit(承诺单元)**上——顶层Issue或Epic的子Issue——其后代会继承该值,因此没有
Cycle
的子Issue是已规划的,而非遗漏的。Epic本身永远不会设置
Cycle
。
始终有三个活跃的迭代——当前Cycle和两个未来的Cycle——因此,延期两次会议的工作总能找到放置的位置。
标题承载主题,在达成一致之前,使用日期范围作为占位符(例如
Sep 29 - Oct 12
)。日期存储在迭代的
startDate
和
duration
字段中。标题必须唯一:从配置写入中恢复时,会通过标题解析迭代,如果两个Cycle共享同一个标题,就会产生歧义。

Writing the configuration clears the whole board

写入配置会清空整个看板

updateProjectV2Field
is the only mutation that touches
iterationConfiguration
, and every write is a full replacement that regenerates every iteration id and clears every item's
Cycle
. Even an identical configuration written back does it, so appending an iteration costs exactly what renaming one does. The input also has no
completedIterations
, so any completed cycle not passed back inside
iterations
is deleted outright.
Make one configuration write per cycle, folding every pending change into it — the new iteration, the agreed end date, any theme titles — and wrap it:
  1. Snapshot every item's
    Cycle
    , keyed by
    <org>/<repo>#<number>
    . Archived items carry values too, so this is its own query passing
    archivedStates: [ARCHIVED, NOT_ARCHIVED]
    —
    board.graphql
    is live-only and would silently skip them.
  2. Write once, passing all iterations, completed ones past-dated so GitHub re-sorts them back into
    completedIterations
    . The configuration's own
    startDate
    cannot be read back — only
    startDay
    is exposed — so pass the earliest iteration's
    startDate
    .
  3. Restore every snapshot value, resolving iterations by title, because every id changed.
  4. Read back
    completedIterations
    and the restored count. An empty
    completedIterations
    is the failure signature; a count short of the snapshot means items were missed.
updateProjectV2Field
是唯一能修改
iterationConfiguration
的变更操作,每次写入都是完全替换,会重新生成所有迭代ID,并清空所有项的
Cycle
值
。即使写回完全相同的配置也会触发此操作,因此添加一个迭代的成本和重命名一个迭代的成本是一样的。此外,输入参数中没有
completedIterations
,因此任何未在
iterations
中传回的已完成Cycle都会被直接删除。
每个Cycle只进行一次配置写入,将所有待处理的变更都合并进去——新迭代、商定的结束日期、任何主题标题——并按以下步骤执行:
  1. 快照所有项的
    Cycle
    值,以
    <org>/<repo>#<number>
    为键。已归档的项也有Cycle值,因此需要单独的查询,传入
    archivedStates: [ARCHIVED, NOT_ARCHIVED]
    ——
    board.graphql
    仅查询活跃项,会静默跳过已归档的项。
  2. 一次性写入,传入所有迭代,已完成的迭代要设置为过去的日期,这样GitHub会将它们重新排序到
    completedIterations
    中。配置本身的
    startDate
    无法回读——只暴露了
    startDay
    ——因此请传入最早迭代的
    startDate
    。
  3. 恢复所有快照值,通过标题解析迭代,因为所有ID都变了。
  4. 回读
    completedIterations
    和恢复的数量。如果
    completedIterations
    为空,说明写入失败;如果数量少于快照,说明有项被遗漏。

Epics

Epics

An epic is work too large for one cycle — see Language. The hierarchy is epic → issue → sub-issues, the first and last optional, and the epic layer is one deep: an epic sits under nothing, and its children are the committed units. Work that turns out to span cycles becomes an epic by being split into children that each fit one.
  • The
    Epic
    issue type is the marker
    , and the only issue type the skills key off. Set it with
    gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=Epic
    ;
    gh issue create
    cannot, so create the issue and PATCH it. Find them with
    gh issue list --search type:Epic
    , or the Epics recipe. If the org has no
    Epic
    type, creating one is
    createIssueType
    on the org node, and it needs the
    admin:org
    scope that the default
    gh
    login lacks.
  • Status walks the ordinary ladder, and so does each child, triaged like anything else.
    Backlog
    while the epic still needs decomposing,
    Ready for Human
    once its children exist,
    In progress
    from the first child committed to a cycle,
    Done
    when a person closes it.
  • Cycle
    is never set on the epic.
    Each child is a committed unit and carries its own — so a child left open at cycle close slipped, and the epic is neither done nor slipped, only progressing.
  • Decompose before the first child is committed. Progress counts direct children only, so it reads truthfully only when every child is a one-cycle piece. A child that will not fit a cycle is split into two children of the epic, keeping the layer one deep.
  • Closing is a judgement, so a person does it. The Epics ready to close recipe finds candidates.
Epic是规模过大、无法在一个Cycle内完成的工作——参见术语约定。层级结构为epic → issue → sub-issues,第一层和最后一层是可选的,且Epic层只有一层:Epic没有父级,其子项就是承诺单元。如果某项工作最终需要跨多个Cycle,就将其拆分为多个每个都能在一个Cycle内完成的子项,从而成为一个Epic。
  • Epic
    Issue类型是标记
    ,也是Skill唯一依赖的Issue类型。使用
    gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=Epic
    设置;
    gh issue create
    无法设置类型,因此先创建Issue,再用PATCH修改。可以通过
    gh issue list --search type:Epic
    或Epics查询方案找到它们。如果组织没有
    Epic
    类型,需要在组织节点上调用
    createIssueType
    创建,这需要
    admin:org
    权限,而默认的
    gh
    登录没有该权限。
  • Status遵循普通的流转流程,每个子Issue也是如此,和其他Issue一样进行分诊。Epic仍需分解时处于
    Backlog
    ,子Issue创建完成后变为
    Ready for Human
    ,第一个子Issue承诺到Cycle后变为
    In progress
    ,人工关闭后变为
    Done
    。
  • Cycle
    永远不会设置在Epic上。
    每个子项都是一个承诺单元,有自己的Cycle——因此如果某个子项在Cycle结束时仍处于打开状态,就是Slipped,而Epic既不算完成也不算延期,只是仍在推进中。
  • 在第一个子项承诺到Cycle之前完成分解。 进度仅统计直接子项,因此只有当每个子项都是一个Cycle能完成的工作量时,进度才是准确的。如果某个子项无法在一个Cycle内完成,就将其拆分为两个Epic的子项,保持Epic层只有一层深度。
  • 关闭是人为判断,因此由人工执行。Epics ready to close查询方案可以找到候选项。

Deferring work

延期工作

Deferring is a decision to record. Its form follows when the work comes back:
  • A future cycle — set
    Cycle
    to one of the two ahead. The issue stays open and the board carries it.
  • A later date — comment with the decision, the owner, and when it returns, then close with
    --reason "not planned"
    . The owner sets the calendar reminder; nothing in GitHub will raise it for them.
  • Unknown — leave it in
    Backlog
    for triage to reconsider.
Reopen rather than refile when it comes back, so the thread stays the history.
延期是需要记录的决策。延期的形式取决于工作恢复的时间:
  • 未来某个Cycle——将
    Cycle
    设置为未来两个中的一个。Issue保持打开状态,保留在看板上。
  • 之后某个日期——添加评论说明决策、负责人以及恢复时间,然后使用
    --reason "not planned"
    关闭。负责人需要自己设置日历提醒;GitHub不会自动提醒。
  • 时间未定——留在
    Backlog
    中,下次分诊时再评估。
工作恢复时,重新打开Issue,而非新建,这样线程可以保留历史记录。

Labels

标签

Labels never carry state. Two matter:
  • bug
    — something is broken.
  • enhancement
    — everything else: new features, and the tooling, config and refactor work we used to call chores.
Issue templates apply exactly one of them, and
/wayfinder
adds its own — see Wayfinding operations.
Any other label is decoration — read it if you like, but nothing keys off it.
标签不承载状态信息。只有两个标签有实际意义:
  • bug
    — 有功能损坏。
  • enhancement
    — 其他所有内容:新功能,以及我们过去称为杂项的工具、配置和重构工作。
Issue模板会应用其中一个标签,
/wayfinder
会添加自己的标签——参见寻路操作。
其他任何标签都是装饰性的——你可以查看,但没有任何逻辑依赖它们。

Creating an issue from the org form

从组织表单创建Issue

Issue forms in
clhbid/.github/.github/ISSUE_TEMPLATE/
are the source of truth for what an issue body contains.
Before
gh issue create
, fetch the form you need:
bash
gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
  --jq .content | base64 -d
Use each field's
label
as a
### 
heading in the issue body, in the same order as the form. Fill every required field, written with
writing-lean
. Omit optional fields when they do not apply. This rule applies to any agent-authored body, including later edits.
Anything beyond the form's fields — acceptance criteria, interface notes, verification steps, or scope boundaries outside the form itself — belongs in the issue's agent-brief comment (or a triage-notes comment), not as extra body headings. See
afk-loop
§ The agent brief
.
Creating this way still means one category label (
bug
or
enhancement
), no state label, and then confirming the issue landed on
Backlog
— see Status.
clhbid/.github/.github/ISSUE_TEMPLATE/
中的Issue表单是Issue正文内容的唯一可信来源。
在执行
gh issue create
之前,先获取你需要的表单:
bash
gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
  --jq .content | base64 -d
将每个字段的
label
作为Issue正文中的
### 
标题,顺序与表单一致。填写所有必填字段,使用
writing-lean
风格撰写。不适用的可选字段请省略。此规则适用于所有Agent撰写的正文,包括后续的编辑。
表单字段之外的任何内容——验收标准、接口说明、验证步骤或表单本身之外的范围边界——都应该放在Issue的agent-brief评论(或分诊笔记评论)中,而不是作为额外的正文标题。参见
afk-loop
§ The agent brief
。
通过这种方式创建Issue时,仍然只会添加一个分类标签(
bug
或
enhancement
),没有状态标签,之后需要确认Issue已进入
Backlog
——参见Status。

Templates apply a category, never a state

模板仅应用分类,绝不应用状态

Issue forms come from the org defaults in
clhbid/.github
under
.github/ISSUE_TEMPLATE/
. A person filing an issue should use one — they collect fields a triager otherwise has to ask for.
A form applies exactly one category label (
bug
or
enhancement
) and no state. Category says what kind of thing an issue is;
Status
says where it has got to — nothing about the category implies a state. When an agent creates an issue without the browser form, use Creating an issue from the org form.
Issue表单来自
clhbid/.github
中的组织默认模板,位于
.github/ISSUE_TEMPLATE/
目录下。用户提交Issue时应该使用表单——表单会收集分诊人员原本需要另行询问的字段。
表单只会应用一个分类标签(
bug
或
enhancement
),不会设置状态。分类说明Issue是什么类型;
Status
说明Issue进展到什么程度——分类和状态之间没有任何隐含关系。 当Agent不通过浏览器表单创建Issue时,请遵循从组织表单创建Issue。

Commit convention

提交规范

Commit subjects carry the issue number as a bare prefix —
2217: <subject>
. A trailing
(#NNNN)
is the pull request, added by squash-merge, and is never the issue. When resolving the spec for a diff, read the leading
NNNN:
and ignore the trailing
(#NNNN)
.
提交信息的主题以Issue编号作为前缀——格式为
2217: <主题>
。末尾的
(#NNNN)
是Pull Request编号,由squash-merge添加,绝不是Issue编号。解析某个diff对应的需求时,读取开头的
NNNN:
,忽略末尾的
(#NNNN)
。

Decomposing work before Ready for Agent

在Ready for Agent之前分解工作

Keep
1 issue = 1 branch = 1 PR
. If work is too large, split the issue, not the pull request. Size is a precondition of
Ready for Agent
. Work too large for a cycle is an epic, split one level higher.
Smaller is better. ~1000 changed lines is the ceiling — excluding lockfiles, snapshots and generated files — but it is a limit, not a target: a changeset that splits cleanly should be split well below it, because a reviewer reads a small diff and skims a large one. If a plan crosses the ceiling, simplify first, cutting documentation that fails the
writing-lean
deletion test before anything else, then split. There is no CI gate for this.
  • A valid slice is independently mergeable and green. Behaviour-neutral slices (rename, extraction, refactor) count when they stand alone.
  • Stack the pull requests when slices depend on each other. A pull request can target a branch other than
    main
    , and GitHub retargets it automatically when its base merges — so a dependent slice can be opened and reviewed straight away instead of waiting for its parent to land.
  • If an agent finds mid-flight that work is too large, it simplifies, creates sub-issues, links them as children, sets the leaves to
    Ready for Agent
    , opens a pull request for the work it has finished — stacked on the previous slice where they depend on each other — and comments on the original issue explaining the cut.
保持
1 issue = 1 branch = 1 PR
的原则。如果工作量过大,拆分Issue,而不是Pull Request。工作量大小是
Ready for Agent
的前置条件。如果工作量大到一个Cycle都完不成,那就是Epic,需要往上拆一层。
越小越好。 变更行数的上限约为1000行——不包括lockfile、快照和生成的文件——但这是上限,不是目标:可以清晰拆分的变更集应该拆得远小于这个上限,因为审核者会仔细阅读小的diff,而对大的diff只是略读。如果计划的变更超过上限,先简化,首先删掉通不过
writing-lean
删除测试的文档,然后再拆分。这一点没有CI门禁检查。
  • 有效的拆分应该是可独立合并且通过测试的。不影响行为的拆分(重命名、提取、重构)只要能独立存在就算有效。
  • 当拆分的部分相互依赖时,使用堆叠的Pull Request。 Pull Request可以将目标分支设为非
    main
    的其他分支,当基分支合并后,GitHub会自动调整目标分支——因此依赖其他部分的拆分可以立即创建并审核,而无需等待父部分合并。
  • 如果Agent在工作过程中发现工作量过大,它会先简化,创建子Issue,将它们作为子项关联,将叶子节点设置为
    Ready for Agent
    ,为已完成的工作创建Pull Request——如果有依赖,就堆叠在前一个拆分上——然后在原始Issue上评论,说明拆分情况。

Pull requests as a triage surface

作为分诊入口的Pull Request

PRs as a request surface: no. (Set to
yes
if this repo treats external PRs as feature requests;
/triage
reads this flag.)
While it is
no
, external PRs are not triaged and no
gh pr
state handling applies.
PRs as a request surface: no. (如果本仓库将外部PR视为功能需求,请设置为
yes
;
/triage
会读取此标志。)
当设置为
no
时,不会对外部PR进行分诊,也不适用任何
gh pr
状态处理逻辑。

When a skill says…

当Skill提到…时

  • "publish to the issue tracker"——通过从组织表单创建Issue创建GitHub Issue。
  • "fetch the relevant ticket"——执行
    gh issue view <number> --comments
    。

Wayfinding operations

寻路操作

Used by
/wayfinder
. The map is a single issue with child issues as tickets.
  • Map: a single issue labelled
    wayfinder:map
    , holding the Notes / Decisions-so-far / Fog body.
    gh issue create --label wayfinder:map
    .
  • Child ticket: an issue linked to the map as a GitHub sub-issue (
    gh api
    on the sub-issues endpoint). Labels:
    wayfinder:<type>
    (
    research
    /
    prototype
    /
    grilling
    /
    task
    ). Once claimed, the ticket is assigned to the driving dev.
  • Blocking: GitHub's native issue dependencies. Add an edge with
    gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>
    , where
    <blocker-db-id>
    is the blocker's numeric database id (
    gh api repos/<owner>/<repo>/issues/<n> --jq .id
    , not the
    #number
    or
    node_id
    ). GitHub reports
    issue_dependencies_summary.blocked_by
    (open blockers only — the live gate). A ticket is unblocked when every blocker is closed.
  • Frontier query: list the map's open children, drop any with an open blocker or an assignee; first in map order wins.
  • Claim:
    gh issue edit <n> --add-assignee @me
    — the session's first write.
  • Resolve:
    gh issue comment <n> --body "<answer>"
    , then
    gh issue close <n>
    , then append a context pointer (gist + link) to the map's Decisions-so-far.
由
/wayfinder
使用。**map(地图)是一个单独的Issue,其child issue(子Issue)**作为工单。
  • Map: 带有
    wayfinder:map
    标签的单个Issue,正文包含Notes / Decisions-so-far / Fog部分。使用
    gh issue create --label wayfinder:map
    创建。
  • Child ticket: 作为GitHub子Issue关联到地图的Issue(通过子Issue端点调用
    gh api
    )。标签:
    wayfinder:<type>
    (
    research
    /
    prototype
    /
    grilling
    /
    task
    )。认领后,工单会分配给负责的开发人员。
  • Blocking: GitHub的原生Issue依赖。使用以下命令添加边:
    gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>
    ,其中
    <blocker-db-id>
    是阻塞Issue的数字database id(通过
    gh api repos/<owner>/<repo>/issues/<n> --jq .id
    获取,不是
    #number
    或
    node_id
    )。GitHub会报告
    issue_dependencies_summary.blocked_by
    (仅统计未关闭的阻塞项——即当前的门控)。当所有阻塞项都关闭时,工单才会解除阻塞。
  • Frontier query: 列出地图中所有打开的子Issue,排除有未关闭阻塞项或已分配负责人的项;按地图顺序排在第一个的就是结果。
  • Claim: 执行
    gh issue edit <n> --add-assignee @me
    ——这是会话的第一次写入操作。
  • Resolve: 执行
    gh issue comment <n> --body "<answer>"
    ,然后
    gh issue close <n>
    ,然后向地图的Decisions-so-far部分追加一个上下文指针(gist + 链接)。

Traps

常见陷阱

A wrong filter looks correct. GitHub ignores an unknown qualifier rather than erroring. It can fail in either direction — a typo may return everything, while
status:"In progress"
in issue search returns nothing — so a result count proves nothing on its own. Check a new filter against a query whose answer you already know.
REST lies about parentage.
gh issue view --json parent
errors outright, and
gh api repos/{owner}/{repo}/issues/{n} --jq .parent.number
returns
null
for a child exactly as for a top-level issue. GraphQL's
Issue.parent
answers truthfully, which is what
board.graphql
selects. Checking issue by issue costs a request each and gets skipped under pressure, so put
no:parent-issue
(or
.content.parent == null
) in the query itself
.
Adding an item unarchives it.
addProjectV2ItemById
on an already-archived item silently returns it to the live board. Archived items are invisible to a live-only read, so a reopened issue looks absent, gets re-added, and comes back out of the archive as a side effect.
A read-back can be stale. The project API is eventually consistent. Verify every write by read-back, but treat a mismatch straight after a write as unconfirmed rather than failed — re-check after a delay, and never re-issue the write on the strength of one disagreeing read.
issueDependenciesSummary.blockedBy
lags the same way — it read
0
immediately after a blocker was linked and
1
shortly after — so the Agent frontier recipe run straight after a write can hand out an issue that is already blocked.
错误的过滤器看起来可能是正确的。 GitHub会忽略未知的限定符,而不是报错。它可能在两个方向上出错——拼写错误可能返回所有结果,而Issue搜索中的
status:"In progress"
可能返回空结果——因此结果数量本身不能证明什么。请用你已知答案的查询来验证新的过滤器。
REST接口在父级关系上会返回错误信息。
gh issue view --json parent
会直接报错,而
gh api repos/{owner}/{repo}/issues/{n} --jq .parent.number
对子Issue返回的
null
和顶层Issue一样。GraphQL的
Issue.parent
会返回正确的结果,这也是
board.graphql
所选择的。逐个检查Issue会消耗大量请求,在压力下容易被跳过,因此请将
no:parent-issue
(或
.content.parent == null
)直接放在查询中
。
添加项会自动取消归档。 对已归档的项调用
addProjectV2ItemById
会静默地将其恢复到活跃看板。仅查询活跃项的读取操作看不到已归档的项,因此重新打开的Issue看起来像是不在看板上,会被重新添加,而副作用就是它会从归档中恢复。
回读结果可能是陈旧的。 项目API是最终一致性的。每次写入后都要通过回读验证,但写入后立即出现的不一致应视为未确认,而非失败——请延迟一段时间后重新检查,绝不要因为一次不一致的读操作就重新执行写入。
issueDependenciesSummary.blockedBy
也有同样的延迟——刚链接阻塞项后读取会返回
0
,过一会儿才会返回
1
——因此写入后立即运行Agent frontier方案可能会分配一个已经被阻塞的Issue。