关于 archive-spec
archive-spec 是一项用于归档已完成或取消的规格文档的 Agent 技能,适用于遵循 Roundfix 规格与 QA 流程的项目。它会检查归档前提、重写引用,并保留归档记录及来源修订信息。
主要功能
- 01归档已完成的规格
- 02支持经授权的 QA 覆盖归档
- 03归档前验证任务与 QA 状态
- 04整理引用并提升关联文件
- 05取消规格并恢复关联条目
适用场景
- 01QA 通过后归档已实现的规格
- 02按授权例外记录未通过 QA 的归档
- 03将引用文件移出即将删除的规格目录
- 04取消规格并将条目恢复至待办目录
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill archive-spec安装后在 Claude Code 中输入/archive-spec确认已加载。
详细介绍
归档规范
通过在解析后的归档根目录下写入 <slug>.md 并移除 <resolved-spec-root>/<slug>/ 来归档已完成的规范;该目录会在 Git 中保留于记录的 source_revision。源目录是已配置的 Spec Root;目标目录是其解析后的归档根目录:内置的 docs/specs 根目录对应 docs/history/specs/,外部或非默认根目录对应 <spec-root>/_archived/,与 roundfix archive 的目标目录一致。正常归档意味着 已实现、已验证且内容自包含——所有任务均已完成、QA 已通过,且所有已索引引用均由该 Spec 所有。QA 归档例外用于记录明确的例外情况,但不声称已完成验证。无论采用哪种处置方式,之后执行一次 ls <spec-root>/,即可区分进行中的工作与历史记录;归档内容可供搜索,作为已构建内容及其原因的记录。
触发归档的条件是规范完成,而非发布:在 QA 门禁通过后,于 implement-spec 循环结束时自动执行,或在用户提出要求时执行。合并和发布是由用户单独决定的步骤——归档提交只需随分支一起流转,并随功能自身的 PR 发布。
归档重链接
写入归档记录之前,归档过程会执行归档重链接。对于每个由 Live Document 引用、且由所属 Spec 提供的非核心文件,归档过程会自动将其提升至 docs/references/<basename>,然后将每个 Live Document 中的引用改为指向提升后的副本;对 Spec 文件夹或核心产物的引用则指向归档记录。如果存在重写的文件,记录会在 relinked 下列出这些文件。源文件只保留一个所属方,因此次要 Spec 无需副本(ADR-0260、ADR-0083)。
Spec 文件夹通过删除而退役。在复制结果和每个提升后的副本中,原本指向已删除文件夹内的目标会改为指向提升后的副本,或变为 Git 定位符 `<path>@<commit>`;绝不会指向记录本身。该提交保留此路径,并可从默认分支或标签访问。如果不存在这样的提交,定位符会指明该路径以及记录对应的拉取请求。其他目标仍保留其在代码库中的目标位置(ADR-0278)。
如果提升目标已存在、两个待提升文件的基本文件名相同,或待重写的 Live Document 存在未在 HEAD 提交的更改,归档过程会拒绝写入。非 Live Document 文件中的引用,或重写后仍保留的引用,会作为警告列出文件、行号和目标;归档过程不会修改这些文件。
Squash 合并
Squash 合并只会在拉取请求的 head ref 中保留 source_revision;记录会在 pull_request 中注明该拉取请求。
QA 结算
同一结果会结算已编写的 qa Task,并决定归档时保留的内容:
| 结果 | 结算 | 归档 |
|---|---|---|
pass | 将 QA Task 结算为 completed;如果报告没有不允许的阻塞行,则使 Spec 符合归档条件。 | Archive Record,其中注明 QA Report 和判定结果;Spec、其 QA 报告和证据保留在 Git 的记录 source_revision 中。 |
符合条件的声明式 partial | 满足以下条件时,将 QA Task 结算为 completed:没有行失败、跳过或因发现问题而阻塞;每个声明为阻塞的行都由匹配的 ## Unreachable Acceptance 声明覆盖;每个环境阻塞的行都是 PR 前的 Pull Request 行,记录为 blocked (environment: no open Pull Request),且其溯源中注明 Pull Request 行;或者是 Run 沙盒无法访问的外部证据行,记录为 blocked (environment: network denied: <host>),且其溯源中注明外部证据行。这两类行都不需要 Unreachable Acceptance 声明;如果未满足的行只有这两类行,则该 partial 符合条件。 | Archive Record,其中注明 QA Report 和判定结果,并将各声明的 satisfied-by 记录为 unproven;Spec、其 QA 报告和证据保留在 Git 的记录 source_revision 中。 |
environment-blocked | 保持该行阻塞;如果等效证据满足环境策略,报告仍可结算为 pass。 | 仅此项不会归档任何内容;符合条件的报告仍可归档 Spec。 |
failed | QA Task 保持未解决状态;除非适用已授权的覆盖,否则拒绝归档。 | 无。 |
missing | QA Task 保持未解决状态;除非适用已授权的覆盖,否则拒绝归档。 | 无。 |
override | 不更改 QA Task 状态或报告判定结果;即使 QA 失败或缺失,也会在明确授权的情况下结算归档。 | Archive Record,其中包含 qa_override、qa_override_approval、qa_override_reason、qa_override_qa_outcome、当 QA Task 未完成时的 qa_override_qa_task_status,以及 qa_override_revision;QA Task 和 Reports 在 Git 的记录 source_revision 中保持不变。 |
Spec 授权要求保持离线的行
上述符合条件的声明式 partial 也接受一种外部证据行:由于 Spec 自身的授权禁止查询,门禁未进行查询;该行记录为 blocked (environment: <subject> prohibited by Spec authorization),且其溯源中注明外部证据行。subject 按授权原文说明禁止的事项:网络查询对应 network,或 another repository;长度为 1 到 80 个字符,且不含圆括号。与网络拒绝行一样,该行不会影响符合条件的 partial 判定,也不需要 Unreachable Acceptance 声明;每行最多计入一种豁免类型,结算、roundfix qa-report accept 和 roundfix archive 都按同一策略处理(ADR-0276、ADR-0335)。
取消 Spec
不再交付的 Spec 应使用 roundfix cancel <slug> --reason <text> 取消,绝不能手动删除。该命令会写入 disposition 为 cancelled 的 Archive Record,将每个已采用的 Backlog Entry 以 status: open 退回 docs/backlog/,并将每个已采用的 Finding 以 status: pending 退回 docs/findings/,随后移除 Spec 文件夹;其字节仍保留在 Git 的 source_revision 中。已取消的记录永远不满足任何前置条件,不会取代其他 Spec,且 Delivery Retry 会拒绝该记录。
被忽略的文件和移除失败
只要 Git 忽略了 Spec 文件夹下的任何文件,归档命令和取消命令就会拒绝执行。每条命令都会列出所有被忽略的文件,且不会写入任何内容。操作人员必须移除每个文件,或使用 git add -f 将其提交,然后重试。
如果最终无法移除 Spec 文件夹,两条命令都会保留所有写入内容、以 1 退出,并打印完成移除操作的命令:
chmod -R u+w '<folder>' && rm -rf '<folder>'。
前置条件 — 验证,不要轻信
动手前,使用新鲜的命令证据检查以下三项:
-
必需的任务已完成。 对于常规归档,读取
_tasks.md中列出的每个task_NN.md;所有status都必须为completed。对于 QA 归档覆盖,所有非 QA 任务都必须为completed;QA 任务可以仍为pending或failed。如果 QA 任务也已completed,则最新报告必须仍不符合资格;否则不需要覆盖,且覆盖会被拒绝。命令: 运行
grep -n '^status:' <each-task-file-listed-in-_tasks.md>并保留其输出。任何不是status: completed的值都会阻止常规归档。执行覆盖时,只有状态不同的非 QA 任务会阻止归档;请指出该任务文件。 -
QA 已通过。
qa/中的最新报告必须通过仓库的 QA 验证器。不要用逐行 grep 代替结构化验证。在 Roundfix 中,使用与归档命令相同的internal/spec.QAVerdict契约:按照qa-report-YYYY-MM-DD[-NN].md文件名契约选出最新报告,解析其 YAML 前置内容,要求verdict为受支持的值,要求存在时两个 blocked-row 字段都为非负整数,并在rows_blocked_finding非零时拒绝verdict: pass。保留验证器的报告路径和结果作为证据。缺少qa/目录、最新报告格式错误或判定未通过,都会阻止常规归档。只有通过roundfix archive <slug> --qa-override --approval <source> --reason <text>明确执行 QA 归档覆盖时,才可继续。切勿通过编辑归档记录来记录覆盖。符合条件的最新报告并不代表失败或待处理的 QA 任务已完成;只有报告符合条件且所有任务均已完成时,才会拒绝覆盖。 -
Spec 内容完整自足。 当
docs/specs/<slug>/references/_index.md存在或为符号链接时,应用此项前置条件;符号链接(包括失效链接)属于无效索引状态,不应视为旧版 Spec。只有该路径既不存在也不是符号链接的 Spec 才早于此契约,不经补充历史制品即可通过。对于已建立索引的 Spec,每个索引中的path都必须相对于_index.md存在;每个从未更新的source路径都必须不存在;Spec 内的 Markdown 链接目标都不得指向docs/_inbox/或docs/findings/。命令: 首先运行此链接目标检查;其语法特意匹配行内或引用式 Markdown 链接,而不是仅在文字中提及任一目录树的内容:
spec_dir=docs/specs/<slug> link_hits=$(grep --include='*.md' -RInE '(\]\([^)]*(docs/)?(_inbox|findings)/[^)]*\)|^[[:space:]]*\[[^]]+\]:[[:space:]]*<?[^[:space:]>]*(docs/)?(_inbox|findings)/)' "$spec_dir") link_status=$? if test "$link_status" -eq 0; then printf '%s\n' "$link_hits" echo "self-containment failed: rewrite each listed link at adoption step 8" exit 1 fi test "$link_status" -eq 1 || exit "$link_status"然后解析并验证每一条数据行。索引必须属于当前 Spec:
owner必须等于其四位数字前缀,type必须为inbox、finding或backlog,且每个source和path都只能出现一次。path必须是相对于_index.md的单个文件名;拒绝绝对路径、.、..、路径分隔符和符号链接,不能允许路径遍历或链接到references/之外。从仓库根目录运行以下命令,并保留规范化后的行以及任何诊断作为证据:slug=<slug> index="docs/specs/$slug/references/_index.md" expected_owner=${slug%%-*} parsed_index=$(mktemp) || exit 1 trap 'rm -f "$parsed_index"' EXIT HUP INT TERM if test -L "$(dirname "$index")"; then printf 'self-containment failed: references/ must not be a symbolic link\n' >&2 exit 1 fi if test -L "$index" || test ! -f "$index"; then printf 'self-containment failed: references/_index.md must be a regular, non-symbolic-link file\n' >&2 exit 1 fi awk -F '|' -v expected_owner="$expected_owner" ' function trim(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) return value } function reject(message) { print "self-containment failed: " message > "/dev/stderr" invalid = 1 } function separator(value) { value = trim(value) return value ~ /^:?-{3,}:?$/ } /^[[:space:]]*$/ || /^[[:space:]]*#/ { next } !header { if (NF != 7 || trim($2) != "source" || trim($3) != "type" || trim($4) != "owner" || trim($5) != "adopted date" || trim($6) != "path") { reject("_index.md must use the fixed source | type | owner | adopted date | path header") } else { header = 1 } next } !divider { if (NF != 7 || !separator($2) || !separator($3) || !separator($4) || !separator($5) || !separator($6)) { reject("_index.md has an invalid table separator") } else { divider = 1 } next } { source = trim($2) type = trim($3) owner = trim($4) adopted = trim($5) path = trim($6) if (NF != 7 || source == "" || type == "" || owner == "" || adopted == "" || path == "") { reject("invalid index row at line " NR ": " $0) next } if (type != "inbox" && type != "finding" && type != "backlog") { reject("type must be `inbox`, `finding`, or `backlog` at line " NR ": " type) } if (owner != expected_owner) { reject("owner must be " expected_owner " at line " NR ": " owner) } if (seen_source[source]++) { reject("duplicate source at line " NR ": " source) } if (seen_path[path]++) { reject("duplicate path at line " NR ": " path) } if (adopted !~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) { reject("adopted date must be YYYY-MM-DD at line " NR ": " adopted) } if ((type == "inbox" && source !~ /^docs\/_inbox\/[^\/]+\.md$/) || (type == "finding" && source !~ /^docs\/findings\/[^\/]+\.md$/) || (type == "backlog" && source !~ /^docs\/backlog\/[^\/]+\.md$/)) { reject("source does not match type at line " NR ": " source) } print source "|" type "|" owner "|" adopted "|" path } END { if (!header || !divider) { reject("_index.md is missing its fixed table header") } if (invalid) { exit 1 } } ' "$index" > "$parsed_index" || exit $? while IFS='|' read -r source type owner adopted path; do case "$path" in ""|.|..|/*|*/*|*\\*) printf 'self-containment failed: path must be one basename relative to `_index.md`: %s\n' "$path" >&2 exit 1 ;; esac source_basename=${source##*/} if test "$path" != "$source_basename"; then printf 'self-containment failed: path must equal source basename for %s: %s != %s\n' "$source" "$path" "$source_basename" >&2 exit 1 fi current="$(dirname "$index")/$path" if test -L "$current" || test ! -f "$current"; then printf 'self-containment failed: invalid or missing path %s; finish adoption step 7\n' "$path" >&2 exit 1 fi if test -e "$source" || test -L "$source"; then printf 'self-containment failed: source still exists at %s; finish adoption step 7\n' "$source" >&2 exit 1 fi done < "$parsed_index" cat "$parsed_index"
仅通过 roundfix archive <slug> --qa-override --approval <source> --reason <text> 执行的 QA 归档覆盖,可以覆盖未满足的常规 QA 前置条件:QA 任务未完成、最新报告不符合资格,或 QA 证据缺失或无法读取。仅当所有任务均已完成且最新报告符合资格时,命令才会被拒绝,因为该规格可以正常归档。它绝不会覆盖自包含性:维护者可以覆盖验证要求,但自包含性是制品本身的属性,必须通过完成采纳来修复。
已合并的 PR 或发布标签不是前置条件,归档也绝不会等待它们。
如果任何检查失败,请停止并报告有问题的任务、报告、来源或链接,以及修复自包含性问题所需的采纳步骤——该规格会保持活跃状态。
若要在 QA 失败、缺失或不符合其他资格条件时归档,请在归档命令中明确提供批准信息和原因:
roundfix archive <slug> --qa-override --approval <source> --reason <text>
此覆盖仍要求所有非 QA 任务均为 completed。无论最新报告的判定结果如何,它都接受失败或待处理的 QA 任务;仅当所有任务均已完成且最新报告符合资格时才会拒绝,因为同一规格可以正常归档。它会记录批准来源、原因、观察到的 QA 结果和归档修订版本,但不会更改 QA 任务或报告。QA 任务未完成时,它还会记录 qa_override_qa_task_status。无法读取的报告,其记录的结果会写明相对于规格文件夹的路径,而不会写入计算机上的绝对路径。
步骤
对于 QA 归档覆盖,只能通过
roundfix archive <slug> --qa-override --approval <source> --reason <text> 执行归档。对于常规归档,请运行 roundfix archive <slug>;它会验证前置条件、写入归档记录并移除规格文件夹。切勿手动编辑归档记录;拒绝条件、溯源信息和归档生命周期均由该命令管理。交付队列会写入提交主题 docs: archive <slug>(Conventional Commits);如果这会超过 100 个字符,则写入 docs: archive the <spec number> Spec(对于不含规格编号的 slug,则写入 docs: archive the Spec)。除非收到请求,否则不要推送。
报告新路径以及任何移交内容(任务 ## Result 部分中的未完成后续事项应放入新规格,而不是归档)。如果工作尚未合并,建议通过 github-pr-workflow 创建 PR;是否创建 PR 由用户决定。
归档记录
归档会在解析后的归档根目录下写入 <slug>.md。记录会列出 QA 报告及其判定结果、处理方式,以及 source_revision,即被移除的规格文件夹仍可恢复时所对应的 Git 修订版本。归档前,请使用 roundfix archive <slug> --plan 查看将要归档的内容和归档建议中的候选文件。使用可重复执行的 --promote <path> 来提升持久知识;提升操作会在归档变更中将该文件复制到 docs/references/。
取消归档
罕见、明确、可逆:从 Git 中的记录 source_revision 恢复规格文件夹,并删除记录。重新开启通常意味着新的工作——优先创建引用已归档规格的新规格。
反模式
- 在 QA 失败或缺失时悄悄归档——必须由用户明确授权覆盖,并记录在案。
- 因 PR 尚未合并或发布标签尚未创建而阻止归档——完成情况(任务 + QA)才是门槛;发布是单独的、由用户决定的步骤。
- 将已完成的规格留在活跃集合中,“等 PR 合并后再处理”——活跃文件夹只用于正在进行的工作。
- 编辑已归档的规格——它是记录;新要求应放入链接回原规格的新规格中。
- 删除而不是归档——这里就是回答“我们之前不是已经试过了吗?”的地方。