issue-tracker
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIssue 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 CLI for all operations.
gh本仓库的Issue和需求规格均以GitHub Issue的形式存在。工作状态保存在CLHbid Delivery组织项目看板中,该看板覆盖所有clhbid仓库。所有操作均使用 CLI执行。
ghLanguage
术语约定
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 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).
EpicCommitted unit: the issue whose 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.
CycleSlipped: 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: 带有 Issue类型的顶层Issue,规模过大无法在单个Cycle内完成,通过其子Issue交付。Epic永远不会被提交到某个Cycle;每个子Issue都适合在一个Cycle内完成,并单独提交。避免使用:initiative、program、tracker issue、parent(每个Epic都是父Issue,但很少有父Issue是Epic)。
EpicCommitted unit: 设置了的Issue——可以是顶层Issue,也可以是Epic的子Issue。其所有后代Issue会继承该Cycle,且不会直接设置Cycle值。避免使用:planned issue、scheduled issue。
CycleSlipped: 已承诺加入某个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:. Use a heredoc for multi-line bodies. This does not apply an issue form — follow Creating an issue from the org form.
gh issue create --title "..." --body "..." -
Read, list, comment:,
gh issue view <number> --comments,gh issue list.gh issue comment <number> --body "..."requires--jq, so filtering a read means dropping--jsonand naming the fields:--commentsgh issue view <number> --json number,title,labels --jq ... -
Refer to an issue as, in prose and in tables alike. GitHub renders that form as a link with a hovercard and shortens it to
<org>/<repo>#<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#<number>, 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 withclhbid/clhbid.com, falling back togh pr view <n>.gh issue view <n> -
Set state: a project field, not a label — see Status.
-
Close: an issue closes as,
completedornot planned. The reason is the record, so pick the one that matches and say why in a closing comment.duplicatebash# 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>."- is the default when
completedis omitted.--reason - is two words —
not plannedis rejected. It is the whole record for a deferral or an abandonment; nothing else is needed.not-planned - sets the reason to
--duplicate-ofand links the two issues, so the survivor's thread becomes the history. Take the number of the issue that stays open.duplicate
-
Fix a close reason:cannot set one, and
gh issue editno-ops on an already-closed issue. Forgh issue closeandcompleted, PATCH it:not plannedTo record a duplicate after the fact, reopen and re-close: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>
-
创建Issue:。多行正文使用heredoc。此命令不会应用Issue表单——请遵循从组织表单创建Issue。
gh issue create --title "..." --body "..." -
查看、列出、评论:、
gh issue view <number> --comments、gh issue list。gh issue comment <number> --body "..."参数需要配合--jq使用,因此过滤查询结果时需要去掉--json并指定字段:--commentsgh issue view <number> --json number,title,labels --jq ... -
引用Issue时使用格式,无论是在正文还是表格中。GitHub会将这种格式渲染为带悬停卡片的链接,并且在同一仓库内会自动简写为
<org>/<repo>#<number>,因此使用完整格式不会增加阅读成本。裸写的#<number>会根据其所在文本的仓库来解析——在组织讨论中,它会解析为#<number>仓库的Issue,而不是你想指的那个仓库——并且会静默链接到错误的Issue。此外,GitHub的Issue和Pull Request共用同一编号空间,因此解析未限定的引用时,可以先用clhbid/clhbid.com,失败再用gh pr view <n>。gh issue view <n> -
设置状态: 使用项目字段,而非标签——参见Status。
-
关闭: Issue关闭时的状态为、
completed或not planned。关闭原因是正式记录,因此请选择匹配的原因,并在关闭评论中说明理由。duplicatebash# 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并关联两个Issue,因此保留的那个Issue的线程就成为历史记录。请使用保持打开状态的Issue的编号。duplicate
- 省略
-
修改关闭原因:无法设置关闭原因,而
gh issue edit对已关闭的Issue无效。对于gh issue close和completed,可以用PATCH请求修改:not planned若要事后标记为重复项,请先重新打开再关闭: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| Status | Meaning |
|---|---|
| Backlog | The inbox — everything not yet routed. New issues land here |
| Waiting on input | We asked the reporter or the business something and cannot proceed until they answer |
| Ready for Agent | Fully specified, sliced to one pull request, and carrying an agent brief |
| Ready for Human | Needs a person — judgement, external systems, manual verification, or a pull request to review |
| In progress | Claimed and being worked |
| Done | Closed, for any reason ( |
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 . Every issue arrives there and leaves by being routed to
, or , or by being closed. Work considered and
deliberately deferred goes back to and is reconsidered next time — so never
means "already dealt with", and a full inbox is the normal state rather than a backlog of triage
debt.
BacklogReady for AgentReady for HumanWaiting on inputBacklogBacklogAn empty is a healthy state, not a query to debug: nothing is blocked on the
business.
Waiting on inputPrefer , which needs both halves of its row — fully specified and already
sliced. Work that cannot be specified until someone decides something is : a
question to answer, not work to schedule. is for what no agent can finish. For
what counts as an agent brief, see § The agent brief.
Ready for AgentWaiting on inputReady for Humanafk-loopClaiming is an assignee write. is the atomic first
write that stops two agents taking the same issue; setting to follows it.
gh issue edit <n> --add-assignee @meStatusIn progressAn 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 unset. Any group-by- view therefore carries a
large No Cycle bucket: descendants whose committed ancestor is planned, and epics, which never
carry one.
CycleCycleCLHbid Delivery项目中的字段是Issue所处状态的唯一可信来源。
Status| Status | 含义 |
|---|---|
| Backlog | 收件箱——所有尚未分派的工作。新Issue会进入此处 |
| Waiting on input | 我们已向报告人或业务方提出问题,在得到答复前无法推进 |
| Ready for Agent | 已完成完整规格说明,拆分为单个Pull Request,并附有Agent任务说明 |
| Ready for Human | 需要人工处理——涉及判断、外部系统、手动验证,或需要审核Pull Request |
| In progress | 已被认领,正在处理中 |
| Done | 已关闭,无论原因是什么( |
其他所有信息均保持GitHub原生状态,不会在看板上重复存储:assignee(负责人)、linked pull requests(审核中的Pull Request)、sub-issues(子Issue,工作分解)、issue dependencies(Issue依赖,阻塞关系)、closed(已完成)。
分诊工作的目标是清空。 所有Issue都会进入此处,然后被分派到、或,或者被关闭。经过评估后决定延期的工作会放回,下次再重新评估——因此绝不代表“已处理”,收件箱饱满是正常状态,而非分诊债务积压。
BacklogReady for AgentReady for HumanWaiting on inputBacklogBacklogWaiting on input优先使用,该状态需要同时满足两个条件:完整的规格说明_且_已完成拆分。如果某项工作需要等人做出决策才能明确规格,则属于:这是一个待回答的问题,而非待排期的工作。用于处理任何Agent都无法完成的工作。关于Agent简报的定义,请参见 § The agent brief。
Ready for AgentWaiting on inputReady for Humanafk-loop认领操作就是写入assignee字段。 是原子性的第一步写入操作,可以避免两个Agent认领同一个Issue;之后再将设置为。
gh issue edit <n> --add-assignee @meStatusIn progress不在看板上的Issue没有状态,并且对下面所有查询都不可见。自动添加工作流会将新打开的Issue添加到看板上;创建Issue后,请等待工作流执行片刻,然后确认它已成功加入看板,手动添加遗漏的Issue。
添加一个Issue会自动添加其整个层级树。 添加父Issue会拉入所有后代Issue,跨仓库边界,甚至包括不在范围内的仓库。因此,在承诺单元上设置规划并不会让看板变得浅显——看板上大部分是后代Issue,它们加入时字段未设置。因此,任何按分组的视图都会有一个很大的No Cycle分组:包括已规划的承诺单元的后代Issue,以及永远不会设置Cycle的Epic。
CycleCycleSetting Status
设置Status
gh project item-editbash
undefinedgh project item-editbash
undefinedproject 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'
-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'
-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 value, not
a label — Status has what each one means.
Status| Role in mattpocock/skills | Status here |
|---|---|
| Backlog |
| Waiting on input |
| Ready for Agent |
| Ready for Human |
| close with |
When a skill says "apply the AFK-ready triage label", set to the value in this table.
and have no counterpart in that vocabulary.
StatusIn progressDoneSkill使用五种标准分诊角色。这里的角色是一个**值**,而非标签——每个角色的含义参见Status。
Status| mattpocock/skills中的角色 | 对应的Status |
|---|---|
| Backlog |
| Waiting on input |
| Ready for Agent |
| Ready for Human |
| 使用 |
当某个Skill提到“应用AFK-ready分诊标签”时,将设置为本表中的对应值。和在该词汇表中没有对应项。
StatusIn progressDoneBusiness 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 , and 🗺️ Epics is the epics with their progress.
The same split governs anything read outside the board:
no:parent-issue -type:Epic- 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 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.
business - 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的过滤条件是,🗺️ Epics展示所有Epic及其进度。同样的划分也适用于看板之外的任何查询:
no:parent-issue -type:Epic- 向业务方汇报——周期报告以及业务方查看的其他内容——仅包含业务工作,包括统计数据:顶层Issue和Epic的子Issue,Epic本身会在单独的部分汇报,而非出现在任何Cycle表格中。如需获取业务视图,可在任何查询方案中添加下面的过滤器,就像Planning view方案那样。看板视图无法表达这种过滤条件——项目过滤器不支持跨条件的OR操作——这就是为什么💼 Business仅通过Cycle视图展示Epic子Issue。
business - 分派和执行工作时读取叶子节点,因为分支和Pull Request都是关联在叶子Issue上的。Agent frontier方案就是例子:它排除了所有有子Issue的项。
Querying the board
查询看板
Issue search cannot read project fields: is not a qualifier and matches
nothing. Every state query therefore runs against the board with
— one query, filtered per use with .
status:"In progress"board.graphql--jqFind 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:
board.graphqlbash
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
doneIf none of them exists, say so rather than guessing — every recipe below needs it.
bash
undefinedIssue搜索无法读取项目字段:不是合法的限定符,匹配不到任何内容。因此,所有状态查询都使用针对看板执行——一个查询,通过按需过滤。
status:"In progress"board.graphql--jq首先找到文件。 它与本文件位于同一目录,但具体位置取决于Skill的安装方式。每个会话只需解析一次,之后可重复使用:
board.graphqlbash
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
undefinedAgent 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
.datathe jq program, before .data
, in any recipe that wants its business view
.datadef 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)):
```bashand (.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)):
```bashMerged 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
--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
--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 field. An iteration ends the day before the meeting that
closes it.
CycleRead them from the field's . holds the closed ones; the
running cycle is the entry in whose is on or before today and whose
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.
configurationcompletedIterationsiterationsstartDatestartDate + durationCycleCycleCycleThree 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
(). Dates live in the iteration's own and . Titles must be
unique: recovering from a configuration write resolves iterations by title, and two cycles sharing
one makes that ambiguous.
Sep 29 - Oct 12startDatedurationCycle是字段的迭代项。迭代在关闭会议的前一天结束。
Cycle从字段的中读取Cycle列表。包含已结束的Cycle;当前正在进行的Cycle是中早于或等于今天,且晚于今天的项;未来的Cycle是开始时间在今天之后的项。请按日期选择,而非按索引——数组的顺序没有保证。
configurationcompletedIterationsiterationsstartDatestartDate + durationCycleCycleCycle始终有三个活跃的迭代——当前Cycle和两个未来的Cycle——因此,延期两次会议的工作总能找到放置的位置。
标题承载主题,在达成一致之前,使用日期范围作为占位符(例如)。日期存储在迭代的和字段中。标题必须唯一:从配置写入中恢复时,会通过标题解析迭代,如果两个Cycle共享同一个标题,就会产生歧义。
Sep 29 - Oct 12startDatedurationWriting the configuration clears the whole board
写入配置会清空整个看板
updateProjectV2FielditerationConfigurationCyclecompletedIterationsiterationsMake one configuration write per cycle, folding every pending change into it — the new
iteration, the agreed end date, any theme titles — and wrap it:
- Snapshot every item's , keyed by
Cycle. Archived items carry values too, so this is its own query passing<org>/<repo>#<number>—archivedStates: [ARCHIVED, NOT_ARCHIVED]is live-only and would silently skip them.board.graphql - Write once, passing all iterations, completed ones past-dated so GitHub re-sorts them
back into . The configuration's own
completedIterationscannot be read back — onlystartDateis exposed — so pass the earliest iteration'sstartDay.startDate - Restore every snapshot value, resolving iterations by title, because every id changed.
- Read back and the restored count. An empty
completedIterationsis the failure signature; a count short of the snapshot means items were missed.completedIterations
updateProjectV2FielditerationConfigurationCyclecompletedIterationsiterations每个Cycle只进行一次配置写入,将所有待处理的变更都合并进去——新迭代、商定的结束日期、任何主题标题——并按以下步骤执行:
- 快照所有项的值,以
Cycle为键。已归档的项也有Cycle值,因此需要单独的查询,传入<org>/<repo>#<number>——archivedStates: [ARCHIVED, NOT_ARCHIVED]仅查询活跃项,会静默跳过已归档的项。board.graphql - 一次性写入,传入所有迭代,已完成的迭代要设置为过去的日期,这样GitHub会将它们重新排序到中。配置本身的
completedIterations无法回读——只暴露了startDate——因此请传入最早迭代的startDay。startDate - 恢复所有快照值,通过标题解析迭代,因为所有ID都变了。
- 回读和恢复的数量。如果
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 issue type is the marker, and the only issue type the skills key off. Set it with
Epic;gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=Epiccannot, so create the issue and PATCH it. Find them withgh issue create, or the Epics recipe. If the org has nogh issue list --search type:Epictype, creating one isEpicon the org node, and it needs thecreateIssueTypescope that the defaultadmin:orglogin lacks.gh - Status walks the ordinary ladder, and so does each child, triaged like anything else.
while the epic still needs decomposing,
Backlogonce its children exist,Ready for Humanfrom the first child committed to a cycle,In progresswhen a person closes it.Done - 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.
Cycle - 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。
- Issue类型是标记,也是Skill唯一依赖的Issue类型。使用
Epic设置;gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=Epic无法设置类型,因此先创建Issue,再用PATCH修改。可以通过gh issue create或Epics查询方案找到它们。如果组织没有gh issue list --search type:Epic类型,需要在组织节点上调用Epic创建,这需要createIssueType权限,而默认的admin:org登录没有该权限。gh - Status遵循普通的流转流程,每个子Issue也是如此,和其他Issue一样进行分诊。Epic仍需分解时处于,子Issue创建完成后变为
Backlog,第一个子Issue承诺到Cycle后变为Ready for Human,人工关闭后变为In progress。Done - 永远不会设置在Epic上。 每个子项都是一个承诺单元,有自己的Cycle——因此如果某个子项在Cycle结束时仍处于打开状态,就是Slipped,而Epic既不算完成也不算延期,只是仍在推进中。
Cycle - 在第一个子项承诺到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 to one of the two ahead. The issue stays open and the board carries it.
Cycle - A later date — comment with the decision, the owner, and when it returns, then close with
. The owner sets the calendar reminder; nothing in GitHub will raise it for them.
--reason "not planned" - Unknown — leave it in for triage to reconsider.
Backlog
Reopen rather than refile when it comes back, so the thread stays the history.
延期是需要记录的决策。延期的形式取决于工作恢复的时间:
- 未来某个Cycle——将设置为未来两个中的一个。Issue保持打开状态,保留在看板上。
Cycle - 之后某个日期——添加评论说明决策、负责人以及恢复时间,然后使用关闭。负责人需要自己设置日历提醒;GitHub不会自动提醒。
--reason "not planned" - 时间未定——留在中,下次分诊时再评估。
Backlog
工作恢复时,重新打开Issue,而非新建,这样线程可以保留历史记录。
Labels
标签
Labels never carry state. Two matter:
- — something is broken.
bug - — everything else: new features, and the tooling, config and refactor work we used to call chores.
enhancement
Issue templates apply exactly one of them, and adds its own — see
Wayfinding operations.
/wayfinderAny 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 are the source of truth for what an issue
body contains.
clhbid/.github/.github/ISSUE_TEMPLATE/Before , fetch the form you need:
gh issue createbash
gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
--jq .content | base64 -dUse each field's as a heading in the issue body, in the same order as the form. Fill
every required field, written with . Omit optional fields when they do not apply.
This rule applies to any agent-authored body, including later edits.
label### writing-leanAnything 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 § The agent brief.
afk-loopCreating this way still means one category label ( or ), no state label, and then
confirming the issue landed on — see Status.
bugenhancementBacklogclhbid/.github/.github/ISSUE_TEMPLATE/在执行之前,先获取你需要的表单:
gh issue createbash
gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
--jq .content | base64 -d将每个字段的作为Issue正文中的标题,顺序与表单一致。填写所有必填字段,使用风格撰写。不适用的可选字段请省略。此规则适用于所有Agent撰写的正文,包括后续的编辑。
label### writing-lean表单字段之外的任何内容——验收标准、接口说明、验证步骤或表单本身之外的范围边界——都应该放在Issue的agent-brief评论(或分诊笔记评论)中,而不是作为额外的正文标题。参见 § The agent brief。
afk-loopTemplates apply a category, never a state
模板仅应用分类,绝不应用状态
Issue forms come from the org defaults in under . A
person filing an issue should use one — they collect fields a triager otherwise has to ask for.
clhbid/.github.github/ISSUE_TEMPLATE/A form applies exactly one category label ( or ) and no state. Category
says what kind of thing an issue is; 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.
bugenhancementStatusIssue表单来自中的组织默认模板,位于目录下。用户提交Issue时应该使用表单——表单会收集分诊人员原本需要另行询问的字段。
clhbid/.github.github/ISSUE_TEMPLATE/表单只会应用一个分类标签(或),不会设置状态。分类说明Issue是什么类型;说明Issue进展到什么程度——分类和状态之间没有任何隐含关系。
当Agent不通过浏览器表单创建Issue时,请遵循从组织表单创建Issue。
bugenhancementStatusCommit convention
提交规范
Commit subjects carry the issue number as a bare prefix — . A trailing
is the pull request, added by squash-merge, and is never the issue. When resolving the spec for
a diff, read the leading and ignore the trailing .
2217: <subject>(#NNNN)NNNN:(#NNNN)提交信息的主题以Issue编号作为前缀——格式为。末尾的是Pull Request编号,由squash-merge添加,绝不是Issue编号。解析某个diff对应的需求时,读取开头的,忽略末尾的。
2217: <主题>(#NNNN)NNNN:(#NNNN)Decomposing work before Ready for Agent
在Ready for Agent之前分解工作
Keep . If work is too large, split the issue, not the pull request.
Size is a precondition of . Work too large for a cycle is an epic,
split one level higher.
1 issue = 1 branch = 1 PRReady for AgentSmaller 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 deletion test before
anything else, then split. There is no CI gate for this.
writing-lean- 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 , 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.
main - If an agent finds mid-flight that work is too large, it simplifies, creates sub-issues, links
them as children, sets the leaves to , 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.
Ready for Agent
保持的原则。如果工作量过大,拆分Issue,而不是Pull Request。工作量大小是的前置条件。如果工作量大到一个Cycle都完不成,那就是Epic,需要往上拆一层。
1 issue = 1 branch = 1 PRReady for Agent越小越好。 变更行数的上限约为1000行——不包括lockfile、快照和生成的文件——但这是上限,不是目标:可以清晰拆分的变更集应该拆得远小于这个上限,因为审核者会仔细阅读小的diff,而对大的diff只是略读。如果计划的变更超过上限,先简化,首先删掉通不过删除测试的文档,然后再拆分。这一点没有CI门禁检查。
writing-lean- 有效的拆分应该是可独立合并且通过测试的。不影响行为的拆分(重命名、提取、重构)只要能独立存在就算有效。
- 当拆分的部分相互依赖时,使用堆叠的Pull Request。 Pull Request可以将目标分支设为非的其他分支,当基分支合并后,GitHub会自动调整目标分支——因此依赖其他部分的拆分可以立即创建并审核,而无需等待父部分合并。
main - 如果Agent在工作过程中发现工作量过大,它会先简化,创建子Issue,将它们作为子项关联,将叶子节点设置为,为已完成的工作创建Pull Request——如果有依赖,就堆叠在前一个拆分上——然后在原始Issue上评论,说明拆分情况。
Ready for Agent
Pull requests as a triage surface
作为分诊入口的Pull Request
PRs as a request surface: no. (Set to if this repo treats external PRs as feature requests; reads this flag.) While it is , external PRs are not triaged and no state handling applies.
yes/triagenogh prPRs as a request surface: no. (如果本仓库将外部PR视为功能需求,请设置为;会读取此标志。) 当设置为时,不会对外部PR进行分诊,也不适用任何状态处理逻辑。
yes/triagenogh prWhen a skill says…
当Skill提到…时
- "publish to the issue tracker" — create a GitHub issue via Creating an issue from the org form.
- "fetch the relevant ticket" — .
gh issue view <number> --comments
- "publish to the issue tracker"——通过从组织表单创建Issue创建GitHub Issue。
- "fetch the relevant ticket"——执行。
gh issue view <number> --comments
Wayfinding operations
寻路操作
Used by . The map is a single issue with child issues as tickets.
/wayfinder- Map: a single issue labelled , holding the Notes / Decisions-so-far / Fog body.
wayfinder:map.gh issue create --label wayfinder:map - Child ticket: an issue linked to the map as a GitHub sub-issue (on the sub-issues endpoint). Labels:
gh api(wayfinder:<type>/research/prototype/grilling). Once claimed, the ticket is assigned to the driving dev.task - Blocking: GitHub's native issue dependencies. Add an edge with , where
gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>is the blocker's numeric database id (<blocker-db-id>, not thegh api repos/<owner>/<repo>/issues/<n> --jq .idor#number). GitHub reportsnode_id(open blockers only — the live gate). A ticket is unblocked when every blocker is closed.issue_dependencies_summary.blocked_by - Frontier query: list the map's open children, drop any with an open blocker or an assignee; first in map order wins.
- Claim: — the session's first write.
gh issue edit <n> --add-assignee @me - Resolve: , then
gh issue comment <n> --body "<answer>", then append a context pointer (gist + link) to the map's Decisions-so-far.gh issue close <n>
由使用。**map(地图)是一个单独的Issue,其child issue(子Issue)**作为工单。
/wayfinder- Map: 带有标签的单个Issue,正文包含Notes / Decisions-so-far / Fog部分。使用
wayfinder:map创建。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>是阻塞Issue的数字database id(通过<blocker-db-id>获取,不是gh api repos/<owner>/<repo>/issues/<n> --jq .id或#number)。GitHub会报告node_id(仅统计未关闭的阻塞项——即当前的门控)。当所有阻塞项都关闭时,工单才会解除阻塞。issue_dependencies_summary.blocked_by - Frontier query: 列出地图中所有打开的子Issue,排除有未关闭阻塞项或已分配负责人的项;按地图顺序排在第一个的就是结果。
- Claim: 执行——这是会话的第一次写入操作。
gh issue edit <n> --add-assignee @me - Resolve: 执行,然后
gh issue comment <n> --body "<answer>",然后向地图的Decisions-so-far部分追加一个上下文指针(gist + 链接)。gh issue close <n>
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 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.
status:"In progress"REST lies about parentage. errors outright, and
returns for a child exactly as
for a top-level issue. GraphQL's answers truthfully, which is what
selects. Checking issue by issue costs a request each and gets
skipped under pressure, so put (or ) in the query
itself.
gh issue view --json parentgh api repos/{owner}/{repo}/issues/{n} --jq .parent.numbernullIssue.parentboard.graphqlno:parent-issue.content.parent == nullAdding an item unarchives it. 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.
addProjectV2ItemByIdA 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.
lags the same way — it read immediately after a blocker was
linked and shortly after — so the Agent frontier recipe run straight after a write can hand
out an issue that is already blocked.
issueDependenciesSummary.blockedBy01错误的过滤器看起来可能是正确的。 GitHub会忽略未知的限定符,而不是报错。它可能在两个方向上出错——拼写错误可能返回所有结果,而Issue搜索中的可能返回空结果——因此结果数量本身不能证明什么。请用你已知答案的查询来验证新的过滤器。
status:"In progress"REST接口在父级关系上会返回错误信息。 会直接报错,而对子Issue返回的和顶层Issue一样。GraphQL的会返回正确的结果,这也是所选择的。逐个检查Issue会消耗大量请求,在压力下容易被跳过,因此请将(或)直接放在查询中。
gh issue view --json parentgh api repos/{owner}/{repo}/issues/{n} --jq .parent.numbernullIssue.parentboard.graphqlno:parent-issue.content.parent == null添加项会自动取消归档。 对已归档的项调用会静默地将其恢复到活跃看板。仅查询活跃项的读取操作看不到已归档的项,因此重新打开的Issue看起来像是不在看板上,会被重新添加,而副作用就是它会从归档中恢复。
addProjectV2ItemById回读结果可能是陈旧的。 项目API是最终一致性的。每次写入后都要通过回读验证,但写入后立即出现的不一致应视为未确认,而非失败——请延迟一段时间后重新检查,绝不要因为一次不一致的读操作就重新执行写入。也有同样的延迟——刚链接阻塞项后读取会返回,过一会儿才会返回——因此写入后立即运行Agent frontier方案可能会分配一个已经被阻塞的Issue。
issueDependenciesSummary.blockedBy01