work-map

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

work-map

work-map:处理决策地图

One session, one decision. The map — not this conversation — is the state carrier: everything that gets decided is written onto a ticket before the session ends, so the next session can start cold from the map and lose nothing.
Emit this diagram once, at the start, so the user can see the whole run:
WORK A DECISION MAP — one decision, then stop
─────────────────────────────────────────────

  ① PREFLIGHT
  │   which backend holds THIS map?
  │   local docs/decision-map/<slug>/
  │   or GitHub issues (needs --repo)
  ▼
  ② FRONTIER
  │   read the map, then list the frontier
  │   BY NAME — never a wall of bare ids
  │
  │   nothing open, no fog? ■ the map is done
  ▼
  ③ CLAIM — one ticket, immediately
  │   the pick IS the approval; claim first,
  │   before any work, so others skip it
  ▼
  ④ RESOLVE by type
  │   grilling · prototype · research · task
  ▼
  ⑤ RECORD
  │   repo doc? gist + link
  │   no repo doc? the body IS the record
  ▼
  ⑥ FOG — graduate through the chart gate
  │   dry run → labels → a yes → --real
  ▼
  ⑦ LINT — run the check, report it
  │   exit 0 clean · exit 3 findings
  ▼
  ■ STOP — one HITL ticket per session
     wanting "just one more" = the edge
单会话,单决策。地图——而非本次对话——是状态载体:所有决策内容必须在会话结束前写入工单,这样下一个会话可以直接从地图开始,不会丢失任何信息。
会话开始时先输出以下流程图,方便用户查看完整流程:
WORK A DECISION MAP — one decision, then stop
─────────────────────────────────────────────

  ① PREFLIGHT
  │   which backend holds THIS map?
  │   local docs/decision-map/<slug>/
  │   or GitHub issues (needs --repo)
  ▼
  ② FRONTIER
  │   read the map, then list the frontier
  │   BY NAME — never a wall of bare ids
  │
  │   nothing open, no fog? ■ the map is done
  ▼
  ③ CLAIM — one ticket, immediately
  │   the pick IS the approval; claim first,
  │   before any work, so others skip it
  ▼
  ④ RESOLVE by type
  │   grilling · prototype · research · task
  ▼
  ⑤ RECORD
  │   repo doc? gist + link
  │   no repo doc? the body IS the record
  ▼
  ⑥ FOG — graduate through the chart gate
  │   dry run → labels → a yes → --real
  ▼
  ⑦ LINT — run the check, report it
  │   exit 0 clean · exit 3 findings
  ▼
  ■ STOP — one HITL ticket per session
     wanting "just one more" = the edge

Step 0 — Preflight: name the backend before you work

步骤0 — 预检查:开始工作前确定后端存储

Two backends exist, and a map lives in exactly one of them. Work out which before anything else happens — a session that reads the wrong backend reports an empty or missing map and looks like a finished effort.
backendwhere the map liveshow to tell
local markdown (default)
docs/decision-map/<slug>/
the directory exists in this repo
GitHub Issuesan issue labelled
decision-map:map
, one sub-issue per ticket
the user names a repo or a board, or no local directory exists
Azure DevOps is not available (ADR 0059). If ADO specifically is a hard requirement, stop and say decision-map cannot do that yet. Do not offer
ado-backlog
or
github-backlog
— neither can drive a decision map.
Then fix these two, and use them for every command from Step 1 down:
localGitHub
<ops>
${CLAUDE_PLUGIN_ROOT}/scripts/local_map_ops.py
${CLAUDE_PLUGIN_ROOT}/scripts/github_map_ops.py
extra flag on every callnone —
--root
defaults to
docs/decision-map
--repo <owner>/<repo>
, always
what
--map
takes
the slugthe map's issue number or its slug
Run
<ops>
with
python
, from the repo root. Everything from Step 1 down is backend-neutral — the subcommands, flags, JSON shapes and gates are identical on both (ADR 0062); only
<ops>
and that one flag change.
On GitHub the map is a shared tracker, so two things follow that do not apply locally: a claim is visible to everyone immediately (which is the point — claim before working, so a parallel session skips the ticket), and every write lands in an issue's timeline, which is why
block
and an additive
chart
do not re-write an edge that already exists.
Contract — every subcommand, flag and JSON shape used below is fixed by
${CLAUDE_PLUGIN_ROOT}/references/data-contracts.md
. Where this skill and the contract disagree, the contract wins.
The map and any repo docs a resolution produces are committed through assisted git — offer the commit at the end, never make it automatically.
有两种后端存储方式,每个地图仅属于其中一种。在开展任何工作前先确定存储位置——如果会话读取了错误的后端,会返回空地图或地图不存在的结果,导致误判为工作已完成。
后端存储地图位置判断方式
本地Markdown(默认)
docs/decision-map/<slug>/
此仓库中存在该目录
GitHub Issues标记为
decision-map:map
的主Issue,每个工单对应一个子Issue
用户指定了仓库或看板,或本地不存在对应目录
Azure DevOps 暂不支持(ADR 0059)。如果明确要求使用ADO,请停止操作并告知decision-map目前无法支持。请勿提供
ado-backlog
或
github-backlog
——二者均无法驱动决策地图。
确定后端后,配置以下两项内容,后续步骤1及之后的所有命令都将使用:
本地存储GitHub存储
<ops>
${CLAUDE_PLUGIN_ROOT}/scripts/local_map_ops.py
${CLAUDE_PLUGIN_ROOT}/scripts/github_map_ops.py
每次调用的额外参数无——
--root
默认值为
docs/decision-map
必须始终添加
--repo <owner>/<repo>
--map
参数取值
地图slug地图的Issue编号或其slug
从仓库根目录使用Python运行
<ops>
脚本。步骤1及之后的所有操作与后端无关——子命令、参数、JSON结构和关卡在两种后端下完全一致(ADR 0062);仅
<ops>
脚本和上述一个参数会变化。
在GitHub中,地图是共享跟踪器,因此有两个本地存储不具备的特性:工单认领状态会立即对所有人可见(这正是关键所在——先认领再工作,避免并行会话处理同一工单),且所有写入操作都会记录在Issue的时间线中,这也是
block
和增量式
chart
不会重写已存在的依赖关系的原因。
约定——以下使用的所有子命令、参数和JSON结构均由
${CLAUDE_PLUGIN_ROOT}/references/data-contracts.md
定义。若本技能与约定存在冲突,以约定为准。
地图及决议生成的所有仓库文档均通过辅助Git提交——会话结束时询问用户是否提交,切勿自动提交。

Step 1 — Load the map, show the frontier

步骤1 — 加载地图,展示前沿工单

If the user did not name a map, list the maps and ask which one — on local they are the directories under
docs/decision-map/
; on GitHub they are the issues labelled
decision-map:map
. If there is no map at all, this is the wrong skill: point at
/decision-map:chart
.
Read the map first, and read it before the frontier:
python "<ops>" read --map <slug>
A map that does not exist fails here — exit
2
, one line on stderr, empty stdout.
frontier
fails the same way now (ADR 0061 made it assert the map exists), so this is no longer the only guard against mistaking a missing map for a finished one. Read first anyway:
read
is what gives you the destination, every ticket's status and every gist, and the frontier alone tells you none of that.
Check for orphaned artifacts before showing the frontier. This runs in two directions, and the second one is the one that actually catches drift.
Artifact exists, map does not know — run
git status
and look for uncommitted or untracked repo docs — an ADR, a spec, a CONTEXT.md edit — that name a ticket on this map. A session that produced the artifact and then ended before Step 4 recorded it leaves the decision real but the map ignorant of it, and usually leaves the ticket
claimed
too. The map is the state carrier; an orphaned ADR is that carrier having failed. Report what you find alongside the frontier — but do not record it yourself without asking, because the claiming session may still be live.
Map says closed, artifact does not exist —
lint
checks this; do not audit it by hand.
It reports
closed-without-artifact
for every closed ticket of a type that owes a durable artifact whose resolution names none. This half was prose in an earlier revision, which was the wrong shape: the failure it exists to catch IS a complete instruction being skipped in silence, so the remedy could not be another instruction (ADR 0091).
python "<ops>" lint --map <slug>
A closed
grilling
ticket with no ADR is the more common failure and the more expensive one, because nothing about the map's state looks wrong — the frontier is clean, the destination looks nearer, and the reasoning is gone. Measured on one real map: 8 closed
grilling
tickets, 1 ADR, 0 glossary terms
, while a sibling feature run straight through
grill-then-plan
produced 14 ADRs and edited the glossary in nearly every commit. On a tracker backend the rule needs the resolution body, so it comes back under
notChecked
rather than passing quietly.
Report the findings as one line with the ticket keys, then stop and ask, and be honest about the cost of the repair: an ADR written long after the code shipped tends to record what the code does rather than what was decided and what was rejected, and a confident ADR documenting the implementation is worse than none, because a later reader trusts it. When the user does want them, draft from the ticket's own body — the question, the
Confirming exchange
, the user's own words — never from the code.
read
returns the map's
id
/
name
/
url
/
destination
, every ticket, and now the ordered
milestones
list too —
{slug, label, members}
per entry, in map order (ADR 0099). Milestones is the one region
read
does report; the fog list, the out-of-scope list and the notes are the three that still do not come back from
read
at all. Those three live only in the map document, each between its own marker pair —
decision-map:fog
,
decision-map:scope
, and now
decision-map:notes
too (ADR 0101 turned the old bare
## Notes
paragraph into an append-only bullet region, the same shape as fog and scope) — so open it and read them. You need the fog list in Step 5, and again in Step 6's report — but not this same copy of it: Step 5 changes that region twice (the gate's merge adds lines, your hand edit deletes the graduated one), and
frontier
carries no fog to refresh it. Re-read the map document in Step 6; treat what you read here as good only until Step 5 writes:
  • local: the file
    docs/decision-map/<slug>/map.md
  • GitHub: the map issue body —
    read
    's
    map.url
    links straight to it, or
    gh issue view <number> --repo <owner>/<repo> --json body
The regions are byte-identical in both, which is what lets one flow read either (ADR 0062).
("Decisions so far" you do not need to read: it is a projection of the closed tickets, and
read
returns each one's
status
and
gist
already.)
python "<ops>" frontier --map <slug>
Present it as prose, in this order:
  • the destination line, verbatim — it is what every choice below is measured against;
  • decisions so far, one gist each — the closed tickets in
    read
    's output, the same lines the map document indexes;
  • the frontier, grouped by milestone (ADR 0099): walk
    frontier.json
    's
    milestones
    list in the order it gives them — that order is the map's own declared plan, not something a session chooses — and under each show its progress (
    <closed>/<total>
    closed) plus its takeable tickets by name, one line each with its type, never a wall of bare ids. Each milestone carries its own blocked and claimed lines too, not just its takeable ones: with one global blocked line a reader cannot tell whether the second milestone is stalled or simply untouched, and cannot see another session's claims sitting in a later group. Collapse them rather than itemizing —
    2 blocked on <blocker name>
    ,
    1 claimed by <session>
    — so the group stays two or three lines. Every entry in all three buckets carries a
    milestone
    field, so no extra call is needed. After every milestone, list any ticket whose
    milestone
    is
    null
    as a final unassigned tail — takeable, blocked and claimed the same way, with no progress count, because there is no milestone for it to count against.
    read
    is what carries milestone membership (
    map.json
    's
    milestones[].members
    );
    frontier
    is what carries progress and each ticket's own
    milestone
    field. The three buckets themselves stay key-ascending exactly as before (ADR 0062) — the grouping comes from walking the
    milestones
    list, not from re-sorting a bucket. On an unmilestoned map
    milestones
    is
    []
    , so there is nothing to group and this collapses back to the flat list it replaces: takeable by name, then one line of blocked (
    <name> — waiting on <blocker name>
    ) and one line of claimed, if any — another session is holding those.
One line per item, no filler: aim to keep the whole presentation around ten lines, and on a map large enough to blow past that, group rather than itemize -- "four tickets blocked on
<name>
" beats four lines that each say it once.
A ticket sits in exactly one bucket, and the order is fixed by the contract: claimed beats blocked beats frontier. So a claimed ticket does not show its blockers here, and a blocked ticket lists only its open blockers — a blocker that has closed is no longer a reason, which is precisely how resolving one decision releases the next.
If the frontier is empty, go to Step 6 — the answer is either "the map is done", "everything left is blocked or claimed", or "that was the wrong slug", and Step 6 tells them apart.
如果用户未指定地图,列出所有地图并询问选择——本地存储的地图是
docs/decision-map/
下的目录;GitHub存储的地图是标记为
decision-map:map
的Issue。如果不存在任何地图,则本技能不适用:请转向
/decision-map:chart
技能。
先读取地图,再获取前沿工单:
python "<ops>" read --map <slug>
如果地图不存在,此命令会失败——退出码为
2
,标准错误输出一行信息,标准输出为空。
frontier
命令现在也会以同样方式失败(ADR 0061要求它先验证地图是否存在),因此这不再是唯一防止将“地图不存在”误判为“工作已完成”的检查。但仍建议先执行
read
:
read
会返回地图目标、每个工单的状态和摘要,而仅靠前沿工单无法获取这些信息。
展示前沿工单前检查孤立工件。此项检查分为两个方向,第二个方向才是真正能发现偏差的关键。
  • 工件存在,但地图未记录——运行
    git status
    查看是否有未提交或未跟踪的仓库文档(如ADR、规格说明、CONTEXT.md修改)提及此地图上的工单。如果某会话生成了工件但在步骤4前结束,会导致决策已实际完成但地图未记录,且工单通常仍处于
    claimed
    状态。地图是状态载体,孤立的ADR意味着载体失效。在展示前沿工单时一并报告发现的问题——但请勿未经询问就自行记录,因为认领该工单的会话可能仍在进行中。
  • 地图标记为已关闭,但工件不存在——此项由
    lint
    检查,无需手动审核
    。
    lint
    会为所有已关闭、且类型要求生成持久化工件但决议未关联任何工件的工单报告
    closed-without-artifact
    。此检查在早期版本中是文字说明,但这并不合理:它要捕捉的问题是完整指令被静默跳过,因此解决方案不能再是另一条指令(ADR 0091)。
python "<ops>" lint --map <slug>
已关闭的
grilling
工单未关联ADR是更常见且影响更大的问题,因为地图状态看起来一切正常——前沿工单已清理,目标似乎更近,但决策依据已丢失。在某真实地图中统计:8个已关闭的
grilling
工单,仅1个关联了ADR,0个添加了术语表条目
,而使用
grill-then-plan
直接处理的同类功能生成了14个ADR,且几乎每次提交都修改了术语表。在跟踪器后端中,该规则需要决议内容,因此未检查的工单会标记为
notChecked
而非直接通过。
将检查结果以单行形式列出工单标识,然后停止操作并询问用户,同时如实说明修复成本:在代码交付很久之后才编写的ADR往往只能记录代码的实际行为,而非决策过程和被否决的方案,而自信地记录实现细节的ADR比没有更糟,因为后续读者会信任它。如果用户需要补全ADR,请从工单本身的内容(问题、确认对话、用户原话)起草,切勿从代码反推。
read
会返回地图的
id
/
name
/
url
/
destination
、所有工单,以及现在新增的有序**
milestones
列表——每个条目包含
{slug, label, members}
,顺序与地图一致(ADR 0099)。
read
仅会返回
milestones
区域;模糊需求列表(fog list)、超出范围列表(out-of-scope list)和注释(notes)这三个区域仍不会从
read
返回。这三个区域仅存在于地图
文档中,各自位于专属标记对之间——
decision-map:fog
、
decision-map:scope
以及新增的
decision-map:notes
(ADR 0101将旧的纯文本
## Notes
段落改为仅可追加的列表区域,格式与fog和scope一致)——因此
请打开文档并读取这些内容**。步骤5需要模糊需求列表,步骤6的报告也需要——但不能使用此处读取的副本:步骤5会修改该区域两次(关卡合并会添加行,手动编辑会移除已推进的条目),而
frontier
不会返回模糊需求列表来刷新内容。步骤6中请重新读取地图文档;此处读取的内容仅在步骤5写入前有效:
  • 本地存储:文件
    docs/decision-map/<slug>/map.md
  • GitHub存储:地图Issue正文——
    read
    返回的
    map.url
    直接指向该页面,或使用命令
    gh issue view <number> --repo <owner>/<repo> --json body
    获取
两种存储中的这些区域内容完全一致,这也是同一流程可以读取两种存储的原因(ADR 0062)。
(“已完成决策”无需读取:它是已关闭工单的投影,而
read
已返回每个工单的
status
和
gist
。)
python "<ops>" frontier --map <slug>
以下列格式以自然语言展示结果:
  • 目标行,原文呈现——这是后续所有选择的衡量标准;
  • 已完成决策,每个工单显示一行摘要——
    read
    输出中的已关闭工单,与地图文档索引的内容一致;
  • 前沿工单,按里程碑分组(ADR 0099):按
    frontier.json
    中
    milestones
    列表的顺序遍历——该顺序是地图自身定义的计划,而非会话临时选择——每个里程碑下显示进度(
    <已关闭数>/<总数>
    )以及可处理工单的名称,每行显示工单类型,切勿仅展示工单ID。每个里程碑还会显示自身的阻塞和已认领工单汇总,而非仅显示可处理工单:如果只显示全局阻塞行,读者无法判断第二个里程碑是停滞还是尚未开始,也无法看到其他会话在后续组中的认领情况。汇总展示而非逐条列出——如
    2个工单阻塞于<阻塞工单名称>
    、
    1个工单被<会话>认领
    ——使每个里程碑的内容保持在2-3行。所有三个分类中的条目都包含
    milestone
    字段,因此无需额外调用。每个里程碑之后,将
    milestone
    为
    null
    的工单列为最后一个未分配组——同样分为可处理、阻塞和已认领三类,无进度统计,因为没有对应的里程碑。
    read
    返回里程碑的成员(
    map.json
    的
    milestones[].members
    );
    frontier
    返回进度和每个工单自身的
    milestone
    字段。三个分类本身仍保持按标识升序排列(ADR 0062)——分组是通过遍历
    milestones
    列表实现的,而非重新排序分类。对于无里程碑的地图,
    milestones
    为空数组,因此不会分组,直接回到原来的扁平列表:按名称展示可处理工单,然后一行阻塞工单(
    <名称> — 等待<阻塞工单名称>
    )和一行已认领工单(如有)——这些是其他会话正在处理的工单。
每个条目占一行,无冗余内容:尽量将整个展示控制在10行左右,对于过大的地图,选择汇总而非逐条列出——“4个工单阻塞于
<名称>
”比四行重复内容更好。
每个工单仅属于一个分类,顺序由约定固定:已认领优先于阻塞,阻塞优先于前沿。因此已认领工单不会在此处显示其阻塞原因,阻塞工单仅列出未关闭的阻塞工单——已关闭的阻塞工单不再是阻碍,这正是解决一个决策会释放下一个决策的原因。
如果前沿工单为空,进入步骤6——结果可能是“地图已完成”“所有剩余工单均被阻塞或认领”或“slug错误”,步骤6会区分这些情况。

Offer milestones once, only on a big unmilestoned map (ADR 0100)

仅在大型无里程碑地图中提供一次里程碑分组建议(ADR 0100)

If
frontier.json
's
milestones
is empty and more than five tickets came back open, offer one line: "this map has no milestones; want to group what ships first before picking?" Ask it once per session, never repeat it, and a decline changes nothing — go straight to Step 2 either way. Below that threshold, skip the offer outright: a small map does not need an ordering layer, and milestones must never become a toll a session pays just to pick up one ticket.
如果
frontier.json
的
milestones
为空且返回的未处理工单超过5个,提供一行建议:“此地图无里程碑;是否要先分组确定优先交付内容再选择工单?”每个会话仅询问一次,切勿重复;如果用户拒绝,无需改变——直接进入步骤2。如果未处理工单少于5个,跳过此建议:小型地图不需要额外的排序层,里程碑绝不能成为会话选择工单的额外负担。

Step 2 — Choose one, and claim it before you work

步骤2 — 选择一个工单,先认领再工作

If the user named a ticket, use it — unless it came back under
claimed
, which is the one case where you stop and ask first (below). Otherwise recommend by the two-level rule (ADR 0099): first find the earliest incomplete milestone that has something takeable — walk
frontier.json
's
milestones
list in order, skipping any milestone that is
complete
or whose frontier tickets are none, and take the first one that has at least one — then, inside it, apply the existing heuristic and recommend whichever of its frontier tickets unblocks the most. Recommend an unassigned ticket (
milestone: null
) only once every milestone is either
complete
or blocked — i.e. the walk above found nothing takeable anywhere. Say the reason in one line either way — the ordering was decided once, on the map, so this session does not re-derive it.
Before claiming, check the bucket the ticket came from. Claim only what
frontier
listed under
frontier
. A ticket under
claimed
is another session's work: there is no tool backstop, so a second
claim
exits
0
and silently replaces the first assignee, and the two sessions then resolve the same decision two different ways with no trace that it happened. Ask the user whether that session is still live, and say plainly that taking it over means the other session's answer will land on a ticket you have already changed. On the local backend the claim records whatever
--user
you passed, and an anonymous claim (the bare default) names nobody, so the files cannot tell you who holds it. Always pass a real
--user
, as the command below does; for a claim already holding an anonymous value, only the user can identify it.
Taking over a
claimed
ticket does not stop the other session.
There is no lock: the user telling you to proceed settles who should own it, not who is still writing. Expect the other session to keep resolving and to keep charting — so re-read the map before every write (Step 5), and treat a second resolution appearing on your ticket as evidence it was live, not as your own duplicate.
Then, the moment the user picks or accepts, claim it — before any work at all:
python "<ops>" claim --map <slug> --ticket <key> --user "<purpose>-<HHMM>"
The pick itself is the approval; this is a lifecycle write, not a create, so it needs no separate dialog (ADR 0039). Claiming first is the concurrency handshake: a session that starts a minute later sees the ticket under
claimed
and picks something else. Work first and claim later, and you have no handshake at all.
Claim before you IMPLEMENT, not only before you decide. The ticket is the unit of work in both phases. A session that resolves one ticket and then starts building another ticket's content without claiming it has dropped the handshake exactly where two sessions collide hardest: the source tree. Seen 2026-08-02,
nav-registry-seam
sat open and unclaimed while two sessions each built the whole nav registry, caught only because one noticed an export that had not existed twenty minutes earlier. Note a claim does not lock files; for genuine isolation give each session its own git worktree.
A claim you cannot finish must be released. There is no
unclaim
subcommand, but
claim --user ""
does release one: it sets
assignee:
back to empty and the ticket returns to the frontier. The catch is quoting — on Windows PowerShell an empty string is dropped before argparse sees it, so the command fails there. Under bash it works. If you are on PowerShell, or you would rather not rely on shell quoting, clear the value by hand instead — so if you end the session holding a ticket you did not resolve (the escalation in Step 3 is the usual reason), edit:
local:   docs/decision-map/<slug>/tickets/<key>.md   →   frontmatter `assignee:`
GitHub:  the ticket issue                            →   unassign yourself
That is a legitimate edit in both:
assignee:
is ordinary frontmatter rather than a tool-owned marker region, and a GitHub assignee is a native field the UI exists to change. Clearing it puts the ticket straight back on the frontier. Skipping it quarantines the ticket permanently, because every future session is told above not to claim over
claimed
.
如果用户指定了工单,使用该工单——除非它处于
claimed
分类
,这种情况下需要先停止操作并询问用户(如下所述)。否则按照两级规则推荐工单(ADR 0099):首先找到最早的有可处理工单的未完成里程碑——按
frontier.json
中
milestones
列表的顺序遍历,跳过已完成或无可处理工单的里程碑,选择第一个有可处理工单的里程碑——然后在该里程碑内,应用现有启发式规则,推荐能解锁最多后续工单的前沿工单。仅当所有里程碑均已完成或阻塞时,才推荐未分配工单(
milestone: null
)——即上述遍历未找到任何可处理工单。无论哪种情况,用一句话说明推荐理由——顺序已在地图中确定,会话无需重新推导。
认领前,检查工单所属的分类。仅认领
frontier
分类下的工单。处于
claimed
分类的工单属于其他会话的工作:目前没有工具阻止重复认领,第二次
claim
命令会返回退出码
0
并静默替换原认领人,导致两个会话以不同方式解决同一决策且无任何痕迹。询问用户该会话是否仍在进行,并明确说明接管该工单意味着其他会话的结果会覆盖您已修改的工单。在本地后端中,认领记录会保存您传入的
--user
值,匿名认领(默认值)不会记录任何人,因此文件无法显示谁认领了工单。请始终传入真实的
--user
值,如下方命令所示;对于已匿名认领的工单,只有用户能识别认领人。
接管
claimed
工单不会停止其他会话
。没有锁机制:用户指示您继续仅代表您应该拥有该工单,而非实际阻止其他会话继续写入。请预期其他会话会继续解决工单并更新地图——因此步骤5前每次写入前都要重新读取地图,若发现工单出现第二个决议,说明其他会话仍在进行,而非您重复操作。
用户选择或接受推荐后,立即认领工单——在任何工作开始前:
python "<ops>" claim --map <slug> --ticket <key> --user "<purpose>-<HHMM>"
选择本身就是批准;这是生命周期写入操作,而非创建操作,因此无需单独确认(ADR 0039)。先认领是并发处理的握手机制:一分钟后启动的会话会看到该工单处于
claimed
分类,从而选择其他工单。先工作后认领则完全没有握手机制。
在实现前认领,而非仅在决策前认领。工单是两个阶段(决策和实现)的工作单元。如果某会话解决了一个工单,然后开始处理另一个未认领工单的内容,就会在最容易发生冲突的地方(源代码树)丢失握手机制。2026-08-02发现的案例:
nav-registry-seam
工单处于未处理且无人认领状态,两个会话各自构建了完整的导航注册表,仅因其中一个会话注意到20分钟前不存在的导出才发现冲突。注意认领不会锁定文件;如需真正隔离,请为每个会话分配独立的Git工作区。
无法完成的认领必须释放。没有
unclaim
子命令,但
claim --user ""
可以释放认领:它会将
assignee:
重置为空,工单回到前沿分类。需要注意引号问题——在Windows PowerShell中,空字符串会在argparse处理前被丢弃,导致命令失败。在bash中该命令有效。如果使用PowerShell,或不想依赖shell引号,可以手动清除值——因此如果会话结束时持有未解决的工单(通常是步骤3中的升级情况),请编辑:
本地存储:   docs/decision-map/<slug>/tickets/<key>.md   →   前端内容`assignee:`
GitHub存储:  工单Issue                            →   取消自己的认领
这在两种存储中都是合法编辑:
assignee:
是普通的前端内容,而非工具专属标记区域;GitHub认领人是原生字段,UI专门提供了修改功能。清除认领后工单会立即回到前沿分类。如果跳过此步骤,工单会被永久隔离,因为未来所有会话都会遵循上述规则,不会认领已被认领的工单。

Step 3 — Resolve it, by type

步骤3 — 按类型解决工单

The ticket's
type
picks the resolver and the mode (ADR 0038). The ticket's Question is the scope: if resolving reveals the question was wrong, do not silently widen it — re-scope out loud, and record the re-scope in the resolution. It is the exit condition too: the moment the Question is answerable, stop grilling and go record it (Step 4). Questions that sit past it belong to other tickets, or to fog -- breadth was chart-map's job, and it is already done.
TypeModeHow you resolve it
grilling
HITLLoad
dev-workflows:sp-grill-with-doc
the way your harness loads skills — or
dev-workflows:grill-then-plan
when this ticket's outcome is meant to be a written plan, and ONLY then: its Step 6 hand-off to
sp-writing-plans
is mandatory and terminal, and its Step 0 warns (non-blocking) rather than gates on the upstream superpowers plugin, so loading it for a decision-only ticket yields a spec plus an implementation plan per ticket where the map wanted one answer. If the ticket is fix-shaped and the cause is not yet verified, verify the cause first with
debug-mantra
: never plan a fix on an unverified cause
(ADR 0003/0011). Pose every question in the user's terms - name the screen, what they press, and what they would observe; when two or more paths are in play put them in a small table, and make the stake concrete ("you save it under a parent record, you delete that parent later, it disappears"). An answer that comes back as a question ("what do you mean?", "which step of the app is this?") is a framing failure: re-pose it rather than explaining it at greater length.
prototype
HITLProduce the cheap artifact through the ui-mockup mechanism — before the first render, read
references/ui-mockup.md
as bundled with the dev-workflows plugin (in this repo,
plugins/dev-workflows/references/ui-mockup.md
; once installed it sits inside that plugin's own directory, wherever your harness put it — the plugin-root path every other file reference in this skill uses points at decision-map, so it cannot address another plugin's file). A Claude Design design-system home is preferred per ADR 0032; a rendered artifact, then a self-contained local
.html
, are fallbacks 2 and 3, used only when the ones above are unavailable. The user reacts to the artifact; their reaction is the decision. Link the artifact onto the ticket with
comment
.
research
AFKNormally already resolved by the chart-time subagents. If it is still open: dispatch a research subagent now, the way your harness runs them, and record its findings with
--body-file
in Step 4.
task
eitherDo the thing if you can do it unattended; otherwise hand the user a precise checklist and wait. Record what was done, and the facts later tickets depend on — a task ticket's value to the map is the facts it leaves behind.
HITL means the human answers. Preference, trade-off and scope questions go to the user, one at a time. Your own recommended answer is a recommendation, never an accepted answer — do not resolve a ticket on it. Silence is not acceptance, and neither is a plausible-sounding default.
Frame every question in the user's terms, whatever the ticket's
type
— name the screen, what they press, what they would observe. Restate where the session has got to immediately before asking: a frame set at session open goes stale across a long technical middle. And when a plan, ADR or standing rule already names a route, say so and justify any deviation rather than offering a neutral menu. This is not grilling-only — a
task
ticket's blocking question (which branch, which environment) fails the same way. An answer that comes back as a question is a framing failure: re-pose it, do not explain it at greater length.
If the human is not available to answer, stop and say so rather than resolving alone. A decision recorded without them is worse than an open ticket: the map presents it as settled, and the next session builds on it without knowing nobody agreed to it. Release the claim (above) so the ticket goes back on the frontier.
Research escalation (ADR 0038). If the question can only be answered from a live system — a real schema, a real org's data, the actual running code — do not answer it from outside knowledge. Leave the ticket open, note on it that it needs
study-design-verify
in its own session, and pick something else or stop:
python "<ops>" comment --map <slug> --ticket <key> --body-file <workdir>/note-<key>.md
Then release the claim — clear the ticket's
assignee:
frontmatter by hand, exactly as Step 2 describes. You could not have avoided claiming it: you learn that a question needs live-system grounding here, in Step 3, and by then the claim has already happened, because claiming first is the handshake you must not skip. An escalated ticket left claimed is a ticket no future session will pick up.
The same
comment
call is how a prototype artifact's link, or any note that is not the answer, lands on a ticket.
工单的
type
决定了解决方式和模式(ADR 0038)。工单的Question是范围边界:如果解决过程发现问题表述有误,请勿静默扩大范围——请公开重新界定范围,并将重新界定的内容记录在决议中。它也是退出条件:一旦Question可回答,立即停止grilling并进入步骤4记录。超出该范围的问题属于其他工单或模糊需求——广度规划是chart-map的工作,且已完成。
类型模式解决方式
grilling
HITL加载
dev-workflows:sp-grill-with-doc
,方式与您的工具加载技能相同——或者当该工单的结果需要是书面计划时,使用
dev-workflows:grill-then-plan
,且仅在这种情况下使用:其步骤6必须且终止性地移交至
sp-writing-plans
,且步骤0会警告(非阻塞)上游超级能力插件,因此为仅需决策的工单加载该技能会生成规格说明和每个工单的实现计划,而地图只需要一个答案。如果工单是修复类且原因尚未验证,请先使用
debug-mantra
验证原因:切勿基于未验证的原因制定修复计划
(ADR 0003/0011)。始终用用户的术语提出每个问题——命名界面、操作步骤和用户会观察到的结果;当有多个方案时,用小型表格展示,并明确说明影响(如“您将其保存到父记录下,之后删除父记录时,它会消失”)。如果用户的回复是问题(如“你指的是什么?”“这是应用的哪个步骤?”),说明问题表述失败:重新表述问题,而非进一步解释。
prototype
HITL通过ui-mockup机制生成低成本工件——首次渲染前,请阅读
dev-workflows
插件中附带的
references/ui-mockup.md
(在此仓库中为
plugins/dev-workflows/references/ui-mockup.md
;安装后位于该插件的目录中,无论您的工具将其放在何处——本技能中使用的其他文件引用的plugin-root路径指向decision-map,因此无法访问其他插件的文件)。优先使用Claude Design设计系统(ADR 0032);渲染后的工件、独立本地
.html
文件是备选方案2和3,仅在上述方案不可用时使用。用户对工件的反馈即为决策结果。使用
comment
将工件链接添加到工单。
research
AFK通常已由chart-time子代理解决。如果仍未解决:立即调度研究子代理,方式与您的工具运行子代理相同,并在步骤4中使用
--body-file
记录其发现。
task
任意如果可以无人值守完成,则直接执行;否则向用户提供精确的检查清单并等待。记录已完成的工作,以及后续工单依赖的事实——任务工单对地图的价值在于留下的事实。
HITL意味着由人类回答。偏好、权衡和范围问题需逐一提交给用户。您的推荐答案仅为建议,绝非已接受的答案——请勿自行解决工单。沉默不代表接受,看似合理的默认值也不代表接受。
无论工单
type
如何,始终用用户的术语表述每个问题
——命名界面、操作步骤和用户会观察到的结果。提问前立即重述会话当前进度:会话开始时设定的背景在较长的技术环节后会失效。如果已有计划、ADR或既定规则指定了解决方案,请说明并解释任何偏离,而非提供中立选项。这不仅适用于grilling——
task
工单的阻塞问题(如选择哪个分支、哪个环境)也遵循此规则。如果用户的回复是问题,说明问题表述失败:重新表述问题,而非进一步解释。
如果无法联系到用户回答问题,请停止操作并告知用户,切勿自行解决工单。未经用户确认的决策记录比未处理工单更糟:地图会将其显示为已解决,下一个会话会基于此开展工作,却无人知晓该决策未得到任何人同意。释放认领(如上所述),使工单回到前沿分类。
研究升级(ADR 0038)。如果问题只能从实时系统(真实架构、真实组织的数据、实际运行的代码)中获取答案,请勿凭外部知识回答。保持工单未处理,在工单上注明需要在单独会话中使用
study-design-verify
,然后选择其他工单或停止:
python "<ops>" comment --map <slug> --ticket <key> --body-file <workdir>/note-<key>.md
然后释放认领——按照步骤2所述,手动清除工单的
assignee:
前端内容。您无法避免认领:您是在步骤3中才发现问题需要实时系统支撑,此时认领已完成,而先认领是必须遵循的握手机制。如果升级后的工单仍处于认领状态,未来任何会话都不会选择它。
同样的
comment
命令也用于添加原型工件链接或任何非答案的注释到工单。

Step 4 — Record the resolution (ADR 0036)

步骤4 — 记录决议(ADR 0036)

When the user confirms the answer, or the unattended work completes, write it onto the ticket in the same turn. An answer that lives only in this conversation is lost the moment the session ends.
There are two shapes, and which one you use depends on whether repo docs exist:
1. The resolution produced repo docs — an ADR, a CONTEXT.md term, a spec. Those stay canonical. The ticket only gists, links and pictures them; it never restates them:
python "<ops>" resolve --map <slug> --ticket <key> --gist "<one line>" --link <adr-path-or-url> --body-file <workdir>/body-<key>.md
2. There is no repo doc — research findings, task facts. Then the resolution body is the record:
python "<ops>" resolve --map <slug> --ticket <key> --gist "<one line>" --body-file <workdir>/body-<key>.md
Both shapes pass
--body-file
, and for the same reason: it is the only slot a diagram can go in.
resolve
renders the block as gist, then a
Detail:
line for
--link
, then the body file — so with
--link
alone there is literally nowhere for a picture to land, and the ADR-backed path is the common one.
--gist
is required either way (without it: exit
2
, one line on stderr). It is flattened to a single line and it is what the map's index shows, so make it one sentence that answers the question — not one paragraph, and not a topic.
resolve
warns on stderr past 200 characters and records it anyway; the warning means the map index is now unreadable, not that the answer was rejected. Detail belongs in
--body-file
or behind
--link
, never in the gist.
The resolution body opens with one Mermaid diagram of the ANSWER (ADR 0065) — the first block of the
--body-file
, above any prose in it. It shows the structure the decision creates, not the options weighed and not the process followed. A reader who opens a closed ticket should see what was decided before reading a word of prose. ("Opens with" is about the body: the gist and the
Detail:
line are rendered above it, and both are one line.)
Match the diagram to the ticket's own
type
:
type
diagramwhat it shows
grilling
flowchart TD
the chosen shape and what it displaces
research
graph TD
, or
erDiagram
for a real data model
the structure that was found
prototype
sequenceDiagram
if the answer is a call order, else
graph TD
the seam that was built
task
graph TD
before → after
A ticket whose answer is an ADR draws its own diagram anyway, and it is not a copy of the ADR's. The subjects differ: the ADR draws chosen versus rejected (diagram convention, Rule 3); the ticket draws what the chosen answer changes. Two diagrams with two subjects cannot drift into contradicting each other; two copies of one diagram will. That is why shape 1 carries a
--body-file
even when the ADR holds every word of the reasoning — the body file may be nothing but the diagram.
This is separate from the position diagram the ops script generates below
## Question
— that one is the ticket's place in the map, and you never author or edit it.
Shape 1's body file holds the diagram at minimum; add prose to it when there is a confirming exchange worth keeping alongside the ADR. Quote the user's confirming words in the body: the close rides the conversation's own approval, so the quote is the audit trail that makes that safe (ADR 0039).
One call does all of it:
resolve
writes the resolution block, closes the ticket, and re-projects the map's "Decisions so far" index from every closed ticket. There is no second call, and no map edit to remember.
On local it is idempotent — re-resolving replaces the previous resolution block. On GitHub it is NOT: the resolution is an issue comment, so a second
resolve
posts a SECOND
## Resolution
comment
(the ticket's gist markers and the map index do get replaced either way). Count the gist yourself before the first call — a
--gist
past ~200 characters is warned about only after it has been recorded, and re-resolving to shorten it leaves two resolutions on the timeline. Seen 2026-08-10: ticket #16 of
claude-model-router
carries four, two from each of two concurrent sessions.
A measured gate must name the ref it was measured on. A route count, a test tally or a file count is true of one commit, and a resolution stating it bare is read as describing the trunk forever. Write "24 paths on
main
(ba323d8)", never "24 paths". On one map
carve-core-api
recorded "20 paths with ZERO module routes" — correct for its branch, false on the trunk one merge later — and three canonical docs plus four tickets had to be amended once a later session ran it.
It must name its SEARCH SCOPE and the repo set it swept, too. "0 hits" is not a measurement; "0 hits in
*.cs
across 26 refs" is. An unscoped zero reads as "the thing is not even specified", and the same grep over
*.md
will contradict it. Worse, a zero measured in ONE repo says nothing when the product is split across sibling repos — seen 2026-08-19: a map recorded a feature "absent on all seven refs" while nine sibling repos on the same disk held the CRM plugins, the Dataverse solution and three per-feature React controls, one of them a plugin writing to the very record the new code would create. Enumerate the siblings before writing any "not built" gist.
If the ticket's subject is runnable, RUN it. A compile gate and a route table cannot see a wrong runbook. That same map had 24 closed tickets, every one gated on
tsc
/
dotnet build
/ a route diff, and the first session to open a browser found the demo script wrong in six ways — a branch that had never existed, the wrong repo count, and every nav-item count off by one.
当用户确认答案,或无人值守工作完成后,立即将答案写入工单。仅存在于本次对话中的答案会在会话结束后丢失。
有两种记录格式,取决于是否存在仓库文档:
1. 决议生成了仓库文档——如ADR、CONTEXT.md术语、规格说明。这些文档是权威的。工单仅需提供摘要、链接和图示;切勿重述文档内容:
python "<ops>" resolve --map <slug> --ticket <key> --gist "<一行摘要>" --link <adr路径或url> --body-file <workdir>/body-<key>.md
2. 无仓库文档——如研究结果、任务事实。此时决议正文即为记录:
python "<ops>" resolve --map <slug> --ticket <key> --gist "<一行摘要>" --body-file <workdir>/body-<key>.md
两种格式都需要传递
--body-file
,原因相同:这是唯一可以插入图表的位置。
resolve
会先渲染摘要,然后是
Detail:
行(对应
--link
),最后是body文件内容——因此仅使用
--link
时,图表无处可放,而基于ADR的格式是常见情况。
无论哪种格式,
--gist
都是必填项(未提供时:退出码
2
,标准错误输出一行信息)。它会被压缩为单行,并显示在地图索引中,因此请将其写为回答问题的一句话——而非一段文字或一个主题。如果超过200字符,
resolve
会在标准错误输出警告,但仍会记录;警告意味着地图索引将变得难以阅读,而非答案被拒绝。详细内容应放在
--body-file
或
--link
指向的位置,切勿放在摘要中。
决议正文开头必须包含一个Mermaid图表展示答案(ADR 0065)——即
--body-file
的第一个块,位于所有 prose 内容之前。图表展示决策创建的结构,而非权衡的选项或遵循的流程。读者打开已关闭工单时,应在阅读任何文字前先看到决策结果。(“开头”指正文部分:摘要和
Detail:
行显示在正文上方,且均为单行。)
图表需与工单自身的
type
匹配:
type
图表类型展示内容
grilling
flowchart TD
选定的方案及其替代的内容
research
graph TD
,或针对真实数据模型使用
erDiagram
发现的结构
prototype
如果答案是调用顺序,使用
sequenceDiagram
,否则使用
graph TD
构建的衔接部分
task
graph TD
前后变化
答案为ADR的工单本身会有自己的图表,但切勿复制ADR的图表。二者主题不同:ADR展示选定方案与被否决方案的对比(图表约定,规则3);工单展示选定方案带来的变化。两个主题不同的图表不会相互矛盾;而复制同一图表则可能出现矛盾。这就是即使ADR包含所有推理内容,格式1仍需携带
--body-file
的原因——body文件可能仅包含图表。
这与ops脚本在
## Question
下方生成的位置图表不同——该图表显示工单在地图中的位置,您无需创建或编辑它。
格式1的body文件至少包含图表;当有值得与ADR一起保留的确认对话时,可添加prose内容。在正文中引用用户的确认话语:工单关闭依赖对话中的批准,因此引用是确保安全的审计线索(ADR 0039)。
一次调用即可完成所有操作:
resolve
会写入决议块、关闭工单,并从所有已关闭工单重新生成地图的“已完成决策”索引。无需第二次调用,也无需手动编辑地图。
本地存储中该操作是幂等的——重复调用
resolve
会替换之前的决议块。在GitHub存储中则不是:决议是Issue评论,因此第二次
resolve
会发布第二个
## Resolution
评论
(工单的摘要标记和地图索引仍会被替换)。首次调用前请自行检查摘要长度——超过约200字符的
--gist
仅在记录后才会发出警告,重新调用缩短摘要会在时间线中留下两个决议。2026-08-10发现的案例:
claude-model-router
的工单#16有四个决议,来自两个并发会话各两次调用。
量化关卡必须指定测量的引用版本。路径数量、测试计数或文件计数仅对特定提交有效,未指定版本的决议会被解读为永远适用于主干分支。请写为“
main
分支(ba323d8)上有24条路径”,而非“24条路径”。在某地图中,
carve-core-api
工单记录“20条路径,ZERO模块路由”——这在其分支上是正确的,但合并到主干后就不正确了——后来某会话运行时发现问题,不得不修改三个权威文档和四个工单。
还必须指定搜索范围和扫描的仓库集合。“0命中”不是测量结果;“在26个引用的
*.cs
文件中0命中”才是。未指定范围的零结果会被解读为“该内容甚至未被定义”,而对
*.md
文件执行相同的grep可能会得出相反结果。更糟的是,在单个仓库中测量的零结果对跨多个兄弟仓库的产品毫无意义——2026-08-19发现的案例:某地图记录某功能“在所有7个引用中不存在”,但同一磁盘上的9个兄弟仓库包含CRM插件、Dataverse解决方案和三个按功能划分的React控件,其中一个插件会写入新代码将创建的记录。在写入任何“未构建”摘要前,请枚举所有兄弟仓库。
如果工单主题可运行,请运行它。编译检查和路由表无法发现错误的运行手册。同一地图有24个已关闭工单,每个都通过
tsc
/
dotnet build
/路由差异检查,但首个打开浏览器的会话发现演示脚本有六处错误——不存在的分支、错误的仓库计数、每个导航项计数都差1。

When a CLOSED ticket's recorded fact turns out to be false

当已关闭工单的记录事实被证明是错误的

You will find these — a later session runs what an earlier one only reasoned about. Do not re-
resolve
the old ticket to overwrite its gist: the resolution is the audit trail of what was verified then, and its numbers are usually correct for the ref they were taken on. Instead:
  1. comment
    the correction onto that ticket, naming both the old claim's true scope and the new measurement.
  2. Amend the canonical doc the gist links (the ADR) with a dated amendment that scopes the original rather than deleting it.
  3. Carry the correction in your ticket's resolution too, so the map's newest entry holds the truth.
Overwriting the old gist loses the fact that the claim was ever believed, which is precisely what explains how the downstream work went wrong.
您会遇到这种情况——后续会话运行了之前仅推理过的内容。请勿重新调用
resolve
覆盖旧工单的摘要:决议是当时验证内容的审计线索,其数值通常对当时的引用版本是正确的。请按以下步骤操作:
  1. 使用
    comment
    在该工单上添加更正,同时说明旧声明的真实范围和新测量结果。
  2. 修改摘要链接的权威文档(ADR),添加日期标记的修正内容,限定原始内容的范围而非删除它。
  3. 在您的工单决议中也包含该更正,使地图的最新条目保留正确信息。
覆盖旧摘要会丢失“该声明曾被认为是正确的”这一事实,而这正是解释下游工作出错原因的关键。

Step 5 — Graduate the fog (through the same gate as charting)

步骤5 — 推进模糊需求(与charting使用相同关卡)

Now ask what the answer changed:
  • Did it make a "Not yet specified" line sharp enough to state as a question? That fog graduates into a ticket. (Can you only gesture at it still? Then it stays fog — do not pre-slice fog into ticket-sized pieces.)
  • Did it put something past the destination? Close that ticket and add one line under "Out of scope".
  • Did it reveal a new blocking relationship?
  • Did it sharpen the milestone plan — a group that should now exist, or a ticket that clearly belongs in one that already does?
Graduating fog is an additive
chart
— one operation serves both acts, so there is no separate subcommand (ADR 0057). Build a
map_input.json
in your scratch working directory containing only the new tickets, plus, for an edge onto a ticket that already exists, that edge in the new ticket's
blocks
:
json
{
  "target": { "slug": "<the existing map's slug>" },
  "map": {
    "title": "<unchanged>", "destination": "<unchanged>",
    "notes": ["<any NEW note line>"],
    "notYetSpecified": ["<any NEW fog line>"],
    "outOfScope": ["<anything newly ruled out>"],
    "milestones": [
      { "slug": "<a NEW group, or one already on the map>", "label": "<optional>",
        "members": ["relevance-metric"] }
    ]
  },
  "tickets": [
    { "key": "relevance-metric", "title": "Relevance metric - what do we measure against?",
      "type": "grilling", "question": "<the decision this resolves>",
      "blocks": ["rollout-order"] }
  ]
}
  • Repeat
    title
    /
    destination
    unchanged. An additive run does not apply a differing scalar — it reports it under
    divergence
    and leaves it alone. To change one, edit the map document by hand.
  • notes
    is not one of those any more (ADR 0101): on any map carrying the notes region it is a list that unions exactly like
    notYetSpecified
    , so pass only new lines and omit the key when there are none. Do not restate what is already there in any other shape — a string holding the existing notes joined together, or the rendered bullets pasted back, is a new line to the union, so it is appended and there is no
    divergence
    to catch it. Repeating the exact same list is a harmless no-op, but there is no reason to.
  • milestones
    is the same optional field chart-map's Step 3 fills in — a resolution can sharpen the plan just as easily as a fog line, so include it here whenever the answer named a group that should now exist, or made clear which milestone a ticket belongs in. A brand-new group appears in the plan as
    adds 1 milestone line
    ; a ticket joining one that already exists as
    adds 1 ticket to an existing milestone
    , which is worded differently because that one edits the stored milestone line instead of adding a line. Moving a ticket between milestones is different: additive means union, never move, so
    chart
    reports a member already claimed by a different milestone under
    divergence
    rather than applying it. A move is a hand edit of the milestones region instead — the same shape as deleting a graduated fog line, below — with
    lint
    as the check afterwards.
  • blocks
    is downstream — the tickets this one holds up. Readers see the upstream
    blockedBy
    . An edge may name a ticket that already exists without re-listing it in
    tickets[]
    (ADR 0058), but the target must exist either in this input or already on the map: naming one that exists in neither is a validation error, and the run exits
    2
    naming the ticket and the bad target.
  • Keys are lowercase-kebab, must match
    [A-Za-z0-9][A-Za-z0-9_-]*
    , and must not contain
    --
    .
现在询问用户答案带来了哪些变化:
  • 是否使“尚未明确”的内容足够清晰,可表述为问题?这类模糊需求可推进为工单。(如果仍只能模糊描述?则保留为模糊需求——请勿将模糊需求提前拆分为工单大小的条目。)
  • 是否使某些内容超出目标范围?关闭该工单,并在“超出范围”下添加一行。
  • 是否揭示了新的阻塞关系?
  • 是否优化了里程碑计划——需要新增分组,或明确某工单属于现有分组?
推进模糊需求是增量式
chart
——一个操作同时完成两项任务,因此无需单独的子命令(ADR 0057)。在临时工作目录中创建
map_input.json
,仅包含新增工单,如果需要与现有工单建立依赖关系,在新工单的
blocks
中添加该依赖:
json
{
  "target": { "slug": "<现有地图的slug>" },
  "map": {
    "title": "<保持不变>", "destination": "<保持不变>",
    "notes": ["<新增注释行>"],
    "notYetSpecified": ["<新增模糊需求行>"],
    "outOfScope": ["<新排除的内容>"],
    "milestones": [
      { "slug": "<新分组,或地图已有的分组>", "label": "<可选>",
        "members": ["relevance-metric"] }
    ]
  },
  "tickets": [
    { "key": "relevance-metric", "title": "Relevance metric - what do we measure against?",
      "type": "grilling", "question": "<该工单解决的决策>",
      "blocks": ["rollout-order"] }
  ]
}
  • title
    /
    destination
    保持不变。增量式运行不会应用不同的标量值——它会在
    divergence
    中报告并保留原值。如需修改,请手动编辑地图文档。
  • notes
    不再是单一字符串(ADR 0101):任何包含notes区域的地图中,它都是与
    notYetSpecified
    完全相同的列表,因此仅传递新增行,无新增行时省略该键。请勿以任何形式重述已有的内容——将现有注释连接成字符串,或粘贴已渲染的项目符号,都会被视为新行追加,且不会被
    divergence
    捕获。重复完全相同的列表是无害的空操作,但没有必要。
  • milestones
    是chart-map步骤3中填充的可选字段——决议与模糊需求一样可以优化计划,因此只要答案指定了需要新增的分组,或明确了工单所属的里程碑,就请包含此字段。全新分组会在计划中显示为
    adds 1 milestone line
    ;工单加入现有分组会显示为
    adds 1 ticket to an existing milestone
    ,表述不同是因为后者会编辑已存储的里程碑行而非添加新行。在里程碑之间移动工单则不同:增量式意味着合并,而非移动,因此
    chart
    会在
    divergence
    中报告该成员已被其他里程碑认领,而非应用移动操作。移动工单需要手动编辑里程碑区域——与删除已推进的模糊需求行操作相同——之后运行
    lint
    检查。
  • blocks
    是下游工单——即被当前工单阻塞的工单。读者看到的是上游工单的
    blockedBy
    字段。依赖关系可以引用已存在的工单,无需在
    tickets[]
    中重新列出(ADR 0058),但目标工单必须存在于本次输入或已在地图中:引用不存在的工单会导致验证错误,运行会退出并返回码
    2
    ,同时指出该工单和无效目标。
  • 标识为小写短横线格式,必须匹配
    [A-Za-z0-9][A-Za-z0-9_-]*
    ,且不得包含
    --
    。

The create-class gate (never skip it)

创建级关卡(切勿跳过)

1. Dry run.
chart
is dry-run by default;
--real
is what writes.
python "<ops>" chart --input <workdir>/map_input.json
The plan lands twice: JSON on stdout, a human rendering on stderr. Show the user the stderr rendering.
2. Read the labels — they are what the user is approving.
ActionWhat it means for that item
create
it does not exist yet and will be created
skip (exists)
it exists and nothing will be written to it
merge
it exists and will be modified in place, additively — the map body gaining fog / out-of-scope lines, or a ticket gaining one
blockedBy
entry
OVERWRITE
--force
only: it exists and will be fully rewritten, discarding its recorded state
A graduation run should read almost entirely
create
(the new tickets) and
merge
(the map body, and any existing ticket gaining an edge). Every
merge
names what it adds in its
detail
—
adds 2 fog lines, 1 out-of-scope line
on the map body,
unions blockedBy: <key>
on a ticket — so read those out. There is a third shape:
normalises the map body's list regions (no new lines)
, a rewrite that adds nothing. Expect it on the run after you delete a graduated fog line by hand and the region is left empty — the run is restoring the tool-owned
- (none)
placeholder, nothing more. A
merge
on an existing ticket adds exactly one
blockedBy
entry and changes nothing else
: its status, assignee, gist and resolution block are untouched (ADR 0058). If you see an
OVERWRITE
line and did not deliberately pass
--force
, stop and investigate.
Re-read the map before you approve — the Step 1 snapshot is stale.
read
and
frontier
describe the map as it was when you ran them, and a parallel session can create tickets, resolve them or comment after that. On GitHub the tell is cheap: issue numbers are sequential, so a ticket numbered higher than the highest you saw in Step 1 was written by someone else while you worked. Re-run
read
immediately before
--real
and diff the ticket list against your Step 1 copy. If something new appeared, check whether your graduation duplicates it BEFORE creating —
lint
reports the map clean either way. Seen 2026-08-10 on
claude-model-router
:
gate-rearm-scope
(#17) and
effort-ratchet-persistence
(#18) are the same question, charted 8 minutes apart by two sessions that had each resolved the same ticket.
3. Ask for explicit approval. Never create without it. The approval is for the plan you just showed — if the input changes at all, re-run the dry run.
Carry the end-of-session commit offer in this same ask, on local. In the same message, ask whether to commit
docs/decision-map/<slug>/
and any repo docs this session produced, once the session ends -- so the session pauses once, here, instead of twice. This does not weaken assisted git: a bundled offer is still an explicit offer the user answers, and nothing is committed without that yes. On GitHub there is nothing to commit for the map itself, but any repo docs still need the same ask.
4. On approval, re-run with
--real
:
python "<ops>" chart --input <workdir>/map_input.json --output <workdir>/map.json --real
5. Check
divergence
in the result
and report every line. The fix is a hand edit of the map document, never
--force
.
--force
is never the remedy here.
It discards recorded resolutions, claims and blocking edges on everything it rewrites — on a map you are working, that is the session history you just wrote. Additive
chart
already adds tickets, edges, fog lines and scope lines; nothing in graduation needs
--force
. Its exact blast radius is tabulated in
${CLAUDE_PLUGIN_ROOT}/references/data-contracts.md
.
Two things the gate will not do for you:
  • The graduated fog line is not removed. Union never deletes, so the line you just turned into a ticket is still sitting under "Not yet specified". Delete it by hand from between the
    decision-map:fog
    marker comments in the map document, leaving the markers themselves alone — otherwise the map keeps advertising fog that is now a real ticket.
  • An edge between two tickets that both already exist needs no create at all. That is
    block
    , a lifecycle write with no gate:
python "<ops>" block --map <slug> --ticket <blocked-key> --blocked-by <blocker-key>
Then re-run
frontier --map <slug>
and show the result. A graduated ticket that blocks an existing one pushes that ticket off the frontier into
blocked
— seeing that happen is how you know the edge was actually wired.
1. 预演。
chart
默认是预演模式;
--real
参数才会执行写入操作。
python "<ops>" chart --input <workdir>/map_input.json
计划会输出两次:标准输出为JSON格式,标准错误输出为人类可读格式。向用户展示标准错误输出的内容。
2. 读取标签——这是用户要批准的内容。
操作对该条目的影响
create
条目不存在,将被创建
skip (exists)
条目已存在,不会写入任何内容
merge
条目已存在,将就地增量修改——地图正文添加模糊需求/超出范围行,或工单新增一个
blockedBy
条目
OVERWRITE
仅
--force
模式:条目已存在,将被完全重写,丢弃已记录的状态
推进模糊需求的运行结果应几乎全部是
create
(新增工单)和
merge
(地图正文、新增依赖关系的现有工单)。每个
merge
会在
detail
中说明添加的内容——地图正文
adds 2 fog lines, 1 out-of-scope line
,工单
unions blockedBy: <key>
——请读出这些内容。还有第三种情况:
normalises the map body's list regions (no new lines)
,即重写但未添加任何内容。这种情况通常发生在您手动删除已推进的模糊需求行后,区域为空——运行会恢复工具专属的
- (none)
占位符,仅此而已。现有工单的
merge
操作仅会新增一个
blockedBy
条目,不会修改其他内容
:其状态、认领人、摘要和决议块均保持不变(ADR 0058)。如果看到
OVERWRITE
行且未故意传递
--force
,请停止操作并调查原因。
批准前重新读取地图——步骤1的快照已过期。
read
和
frontier
描述的是运行时的地图状态,并行会话可能在之后创建工单、解决工单或添加注释。在GitHub中很容易判断:Issue编号是连续的,因此编号高于步骤1中最高编号的工单是其他会话在您工作时创建的。执行
--real
前立即重新运行
read
,并将工单列表与步骤1的副本对比。如果出现新工单,请在创建前检查您的推进操作是否重复——
lint
会报告地图是否干净。2026-08-10在
claude-model-router
上发现的案例:
gate-rearm-scope
(#17)和
effort-ratchet-persistence
(#18)是同一个问题,由两个会话在8分钟内分别charted,而这两个会话都解决了同一个工单。
3. 请求明确批准。切勿未经批准就创建内容。批准针对的是您刚展示的计划——如果输入有任何变化,请重新运行预演。
在本地存储中,将会话结束时的提交请求包含在同一询问中。在同一消息中,询问用户会话结束后是否提交
docs/decision-map/<slug>/
和本次会话生成的所有仓库文档——这样会话只需暂停一次,而非两次。这不会削弱辅助Git的安全性:捆绑请求仍是用户明确回答的请求,未经同意不会提交任何内容。在GitHub存储中,地图本身无需提交,但仓库文档仍需同样的询问。
4. 获得批准后,添加
--real
参数重新运行
:
python "<ops>" chart --input <workdir>/map_input.json --output <workdir>/map.json --real
5. 检查结果中的
divergence
并报告每一行
。修复方式是手动编辑地图文档,切勿使用
--force
。
--force
永远不是解决此问题的方法
。它会丢弃所有重写内容的已记录决议、认领状态和阻塞关系——对于您正在处理的地图,这就是您刚写入的会话历史。增量式
chart
已可添加工单、依赖关系、模糊需求行和范围行;推进模糊需求无需
--force
。其精确影响范围在
${CLAUDE_PLUGIN_ROOT}/references/data-contracts.md
中有表格说明。
关卡不会自动处理两件事:
  • 已推进的模糊需求行不会被删除。合并操作不会删除内容,因此您刚转换为工单的行仍会留在“尚未明确”下。请手动从地图文档的
    decision-map:fog
    标记注释之间删除该行,保留标记本身——否则地图会继续显示已变为真实工单的模糊需求。
  • 两个已存在工单之间的依赖关系无需创建操作。这是
    block
    操作,无需关卡的生命周期写入:
python "<ops>" block --map <slug> --ticket <被阻塞工单标识> --blocked-by <阻塞工单标识>
然后重新运行
frontier --map <slug>
并展示结果。阻塞现有工单的已推进工单会将该现有工单从前沿分类移至
blocked
分类——看到此变化说明依赖关系已正确配置。

Step 6 — Stop

步骤6 — 停止

The cap counts HITL tickets: one per session (ADR 0041). Grilling and prototype tickets are always HITL, and so is any task ticket that needed the user. Research is AFK and does not count — subagents run in parallel, so dispatching several and recording their findings is one session's work whether they were fired at chart time or picked up here. The line is the user's attention, not the ticket count: the moment a ticket needs their judgement, that is this session's one, and you stop after it. If the ticket you claimed turned out to be research that resolved without them, you may go back to Step 1 and take one more.
The pull to do "just one more" past that line is not stamina, it is a signal — name it out loud rather than acting on it. Either the frontier is genuinely small and the map is nearly done, or this session is overreaching and the next decision deserves a fresh, un-anchored session. The cap is what keeps the map, rather than a long conversation, holding the state.
If the user directs work past the cap, the claim discipline does not lapse. The cap is theirs to override -- a deadline, a follow-on they want now -- and their instruction wins; do not argue it. What must NOT lapse is Step 2: every additional ticket you touch gets claimed before you implement it, and resolved before the session ends. Working on past the cap without claiming is the worst of both -- past the attention budget AND off the map, so the code lands in git while the reasoning that produced it does not. Seen 2026-08-02: after resolving one ticket the session drove two more (
frontend-relocation
,
monorepo-import-api
) to completion, unclaimed and unrecorded, and only the user asking "which ticket are we on?" surfaced it. The tell is cheap -- if you cannot name the ticket you are on, you are off the map.
Run the check before you report (ADR 0067).
lint
reads the map as it now stands and writes nothing — exit
0
clean, exit
3
with findings:
python "<ops>" lint --map <slug>
It catches what a session most plausibly just broke: the graduated fog line Step 5 told you to delete by hand, an edge left dangling by a hand-edited
assignee:
, a resolution recorded without its diagram, or a milestone move gone wrong — a line the hand edit left unparsable, a slug declared more than once, a ticket key listed more than once (in two milestones, or twice on one line), or a milestone naming a ticket that is not on this map (
milestone-line-unparsable
,
milestone-duplicate-slug
,
milestone-duplicate-member
,
milestone-unknown-ticket
— the same four rules a mistyped hand-edited move above can trip). Report every finding alongside the rest of the report, and fix the errors before you stop; a warning is the user's call. Do not skip it because the session felt clean — "felt clean" is the only signal you have otherwise, and it is the one
lint
exists to replace.
Re-run the frontier so the report describes the map as it now stands, not as it looked when the session opened:
python "<ops>" frontier --map <slug>
And re-read the fog region of the map document —
frontier
does not carry it, and the copy you read in Step 1 is stale the moment Step 5 runs: the gate's merge appended any new fog lines and your hand edit removed the graduated one. Reporting the Step 1 copy tells the user the map still asks a question they just answered.
Report, in this order:
  • any
    lint
    finding
    , errors first — and say explicitly when it came back clean, so the user knows the check ran;
  • the ticket you resolved, by name, with its gist and the link if there is one;
  • what that released — the tickets that moved onto the frontier;
  • what graduated out of the fog, and anything newly out of scope;
  • the frontier for next time, by name;
  • the fog lines still unspecified.
Same discipline as the Step 1 frontier: one line per bullet, no filler, around ten lines in total -- group rather than itemize when a bullet would otherwise run to a list of its own.
Offer to commit any repo docs the resolution produced — an ADR written during a grilling ticket is exactly the file that gets orphaned when only the map is committed — plus, on local,
docs/decision-map/<slug>/
itself
. On GitHub the map needs no commit (it is already live in the tracker), but the repo docs still do, and that is the half most easily forgotten when the map is not a file. Assisted git: offer, never automatic. If the Step 5 gate already carried this offer and the user approved it there, commit now without asking a second time -- the yes you are holding is that explicit offer, answered. If there was no graduation this session, no gate ran and no offer rode along with it, so this is the ask, exactly as before.
Then suggest
/decision-map:work
for the next session, and stop.
上限统计HITL工单:每个会话最多一个(ADR 0041)。Grilling和prototype工单始终是HITL,需要用户参与的task工单也是。Research是AFK,不计入上限——子代理并行运行,因此无论在chart time调度还是在此处处理,调度多个并记录结果都是一个会话的工作。界限是用户的注意力,而非工单数量:一旦工单需要用户判断,这就是本次会话的唯一工单,处理完后停止。如果您认领的工单是无需用户参与的research且已解决,您可以回到步骤1再处理一个工单。
想要“再做一个”的冲动不是耐力问题,而是一个信号——请明确指出,而非付诸行动。要么前沿工单确实很少,地图即将完成;要么本次会话已超出范围,下一个决策需要全新的、无偏见的会话。上限是为了确保地图而非冗长对话成为状态载体。
如果用户指示超出上限继续工作,认领规则仍需遵守。上限可由用户覆盖——如截止日期、用户希望立即处理的后续任务——用户的指示优先;请勿争辩。绝对不能忽略步骤2:您处理的每个额外工单都必须在实现前认领,并在会话结束前解决。超出上限且未认领工作是最糟糕的情况——既超出注意力预算,又脱离地图,导致代码提交到Git,但生成代码的推理过程未被记录。2026-08-02发现的案例:解决一个工单后,会话又完成了两个工单(
frontend-relocation
、
monorepo-import-api
),未认领且未记录,仅当用户询问“我们在处理哪个工单?”时才被发现。判断方法很简单——如果您无法说出正在处理的工单名称,说明您已脱离地图。
报告前运行检查(ADR 0067)。
lint
读取当前地图状态,不写入任何内容——退出码
0
表示干净,退出码
3
表示有问题:
python "<ops>" lint --map <slug>
它会捕捉会话最可能刚破坏的内容:步骤5要求手动删除的已推进模糊需求行、手动编辑
assignee:
留下的悬空依赖关系、未包含图表的决议记录,或错误的里程碑移动——手动编辑留下的无法解析的行、重复声明的slug、重复列出的工单标识(在两个里程碑中,或同一行出现两次),或里程碑引用了不在此地图中的工单(
milestone-line-unparsable
、
milestone-duplicate-slug
、
milestone-duplicate-member
、
milestone-unknown-ticket
——上述手动移动操作可能触发这四个规则)。将所有发现与其他报告内容一并展示,并在停止前修复错误;警告由用户决定是否修复。切勿因感觉会话无问题而跳过此项检查——“感觉无问题”是您唯一的判断依据,而
lint
正是为了替代这种判断而存在。
重新运行frontier,使报告描述当前地图状态,而非会话开始时的状态:
python "<ops>" frontier --map <slug>
并重新读取地图文档的模糊需求区域——
frontier
不会返回该区域,且步骤1读取的副本在步骤5运行后已过期:关卡合并会追加新的模糊需求行,您的手动编辑会移除已推进的行。报告步骤1的副本会告知用户地图仍在询问他们刚回答的问题。
按以下顺序报告:
  • 所有
    lint
    发现
    ,先错误后警告——明确说明检查结果是否干净,让用户知道已运行检查;
  • 您解决的工单,按名称,包含其摘要和链接(如有);
  • 该工单解锁的内容——移至前沿分类的工单;
  • 从模糊需求推进的内容,以及新增的超出范围内容;
  • 下次会话的前沿工单,按名称;
  • 仍未明确的模糊需求行。
遵循与步骤1前沿工单相同的原则:每个项目占一行,无冗余内容,总长度约10行——当项目本身会列出多个条目时,选择汇总而非逐条列出。
询问用户是否提交决议生成的仓库文档——grilling工单期间编写的ADR正是仅提交地图时最容易被孤立的文件——此外,在本地存储中,还需提交
docs/decision-map/<slug>/
本身
。在GitHub存储中,地图无需提交(已实时存在于跟踪器中),但仓库文档仍需提交,这也是地图非文件形式时最容易被遗忘的部分。辅助Git:仅询问,切勿自动提交。如果步骤5的关卡已包含此请求且用户已批准,现在无需再次询问即可提交——您持有的同意就是明确的请求答复。如果本次会话未推进模糊需求,未运行关卡也未附带请求,则在此处询问,与之前相同。
然后建议下次会话使用
/decision-map:work
,并停止。

When the frontier came back empty

当前沿工单为空时

Three different situations, and they need different answers:
  • Nothing open and no fog left — the map is done. Say the way is clear, and hand off to
    sp-writing-plans
    (or whatever the destination line named). decision-map plans; it does not build.
  • Nothing open but fog remains — this session's work is graduation, and it happens here. Do not send the user to
    /decision-map:chart
    : that skill charts a map that does not exist yet, and this one does. Take the sharpest fog line, grill it just far enough to state it as a question, and run Step 5's gate on it — the gate is the same additive
    chart
    whether or not a resolution came before it, and a session that opens a map, finds nothing takeable and graduates one fog line into a real ticket has done exactly one session's work. If nothing sharpens, say so and stop: fog that will not state itself is waiting on a decision that has not been made yet.
  • Everything left is blocked or claimed — another session is holding the unblocked work, or a blocker is still open under someone else's claim. Nothing to pick up; say who holds what and stop. If a
    claimed
    ticket looks abandoned, releasing it is the hand edit in Step 2, and it is the user's call.
有三种不同情况,需要不同的处理方式:
  • 无未处理工单且无模糊需求——地图已完成。告知用户路径已畅通,并移交至
    sp-writing-plans
    (或目标行指定的任何内容)。decision-map负责规划;不负责构建。
  • 无未处理工单但仍有模糊需求——本次会话的工作是推进模糊需求,且在此处完成。请勿将用户引导至
    /decision-map:chart
    :该技能用于创建不存在的地图,而此地图已存在。选择最清晰的模糊需求行,仅grill到能将其表述为问题的程度,然后对其运行步骤5的关卡——无论之前是否有决议,关卡都是相同的增量式
    chart
    ,会话打开地图、发现无可处理工单并将一个模糊需求推进为真实工单,正是一个会话的工作。如果没有模糊需求变得清晰,告知用户并停止:无法明确表述的模糊需求正在等待尚未做出的决策。
  • 所有剩余工单均被阻塞或认领——其他会话正在处理未阻塞的工作,或阻塞工单仍被其他人认领。无可处理工单;告知用户谁持有哪些工单并停止。如果某个
    claimed
    工单看起来已被遗弃,释放认领是步骤2中的手动编辑操作,由用户决定。