Archive a completed spec — verify every task completed, QA passed, and indexed references are self-contained, then stamp the archive metadata and move <spec-root>/<slug>/ to the resolved archive root (<spec-root>/_archived/<slug>/, or docs/history/specs/<slug>/ for the built-in docs/specs root). Runs automatically at the end of the implement-spec loop after a QA pass, or whenever the user asks to archive a spec.
Installs into .claude/skills of the current project.
Are you the author of Archive Spec?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/marcioaltoe-archive-spec)
---
name: archive-spec
description: Archive a completed spec — verify every task completed, QA passed, and indexed references are self-contained, then stamp the archive metadata and move <spec-root>/<slug>/ to the resolved archive root (<spec-root>/_archived/<slug>/, or docs/history/specs/<slug>/ for the built-in docs/specs root). Runs automatically at the end of the implement-spec loop after a QA pass, or whenever the user asks to archive a spec.
argument-hint: "<spec slug>"
metadata:
category: delivery
tags: [workflow, documentation, process]
version: 0.0.4
author: Marcio Altoé
source: https://github.com/marcioaltoe/skills
version: 0.0.4
---
# Archive Spec
Move a completed spec out of the active set: `<resolved-spec-root>/<slug>/` → `<resolved-archive-root>/<slug>/`, with the completion stamped in its frontmatter. The source is the configured Spec Root; the destination is its resolved archive root — `docs/history/specs/` for the built-in `docs/specs` root, or `<spec-root>/_archived/` for an external or non-default root, matching the `roundfix archive` destination. A normal archive means _implemented, verified, and self-contained_ — every task done, QA passed, and every indexed reference owned by the Spec. A QA Archive Override records the explicit exception without claiming verification. After either disposition, one `ls <spec-root>/` separates live work from history, and the archive stays greppable as the record of what was built and why.
The trigger is spec completion, not publication: run this automatically at the end of the `implement-spec` loop once the QA gate passes, or whenever the user asks. Merge and release are separate, user-driven steps — the archive commit simply travels with the branch and ships inside the feature's own PR.
### QA settlement
The same outcome settles the authored `qa` Task and determines what archive
may move:
| Outcome | Settles | Archives |
| --- | --- | --- |
| `pass` | Settles the QA Task as `completed` and makes the Spec archive-eligible when the report has no disallowed blocked rows. | The Spec and its QA report and evidence. |
| qualifying declared `partial` | Settles the QA Task as `completed` when no row failed, was skipped or is finding-blocked, every declared-blocked row is covered by a matching `## Unreachable Acceptance` declaration, and every environment-blocked row is the pre-PR Pull Request row, recorded as `blocked (environment: no open Pull Request)` with the Pull Request row named in its provenance, or an outside-evidence row the Run sandbox could not reach, recorded as `blocked (environment: network denied: <host>)` with the outside-evidence row named in its provenance. Neither row needs an Unreachable Acceptance declaration, and a partial whose only unmet rows are such rows qualifies. | The Spec, its QA report and evidence, and the declarations' `satisfied-by` record. |
| `environment-blocked` | Leaves the row blocked; the report can still settle as `pass` when equivalent evidence satisfies the environment policy. | Nothing by itself; a qualifying report can archive the Spec. |
| `failed` | Leaves the QA Task unresolved and refuses archive unless an authorized override applies. | Nothing. |
| `missing` | Leaves the QA Task unresolved and refuses archive unless an authorized override applies. | Nothing. |
| `override` | Does not change the QA Task status or report verdict; settles archive as explicitly authorized despite failed or missing QA. | The Spec with `qa_override`, `qa_override_approval`, `qa_override_reason`, `qa_override_qa_outcome`, `qa_override_qa_task_status` when the QA Task is incomplete, and `qa_override_revision`; QA files move byte-identically. |
## Preconditions — verify, don't trust
Check all three with fresh command evidence before touching anything:
1. **Required tasks completed.** For a normal archive, read each `task_NN.md`
listed in `_tasks.md`; every `status` must be `completed`. For a QA Archive
Override, every non-QA Task must be `completed`; the QA Task may remain
`pending` or `failed`. If the QA Task is also `completed`, the newest report
must remain ineligible or the override is unnecessary and refused.
**Command:** run `grep -n '^status:' <each-task-file-listed-in-_tasks.md>` and
retain its output. Any value other than `status: completed` blocks a normal
archive. During an override, only a non-QA Task with another status blocks
the archive; name that Task file.
2. **QA passed.** The newest report in `qa/` must pass the repository's QA
verifier. Do not substitute a line grep for structured validation. In
Roundfix, use the same `internal/spec.QAVerdict` contract as the Archive
Command: select the newest report by the `qa-report-YYYY-MM-DD[-NN].md`
filename contract, parse its YAML frontmatter, require a supported
`verdict`, require both blocked-row fields to be non-negative integers when
present, and reject `verdict: pass` when `rows_blocked_finding` is nonzero.
Retain the verifier's report path and result as evidence. A missing `qa/`
directory, malformed newest report, or non-passing verdict blocks the
normal archive. Proceed only under an explicit QA Archive Override performed
through `roundfix archive <slug> --qa-override --approval <source> --reason
<text>`. Never record the override by editing stamped frontmatter. A
qualifying newest report does not make a failed or pending QA Task completed;
the override is refused only when the report qualifies and every Task is
completed.
3. **The Spec is self-contained.** Apply this precondition when
`docs/specs/<slug>/references/_index.md` exists or is a symbolic link; a
symbolic link, including a broken one, is invalid index state rather than a
legacy Spec. Only a Spec where that path neither exists nor is a symbolic
link predates this contract and passes without retrofitting historical
artifacts. For an indexed Spec, every indexed `path` must exist relative to
`_index.md`, every never-updated `source` path must be absent, and no
Markdown link destination inside the Spec may point into `docs/_inbox/` or
`docs/findings/`.
**Commands:** first run this link-destination check; its syntax deliberately
matches inline or reference-style Markdown links, not prose that merely
names either tree:
```bash
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"
```
Then parse and validate every data row. The index belongs to the current
Spec: `owner` must equal its four-digit prefix, `type` must be `inbox`,
`finding`, or `backlog`, and each `source` and `path` must appear only once. A `path` must
be one basename relative to `_index.md`; reject absolute paths, `.`, `..`,
path separators, and symbolic links instead of allowing traversal or a link
outside `references/`. Run the following from the repository root and
retain the normalized rows plus any diagnostic as evidence:
```bash
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"
```
A QA Archive Override performed only through `roundfix archive <slug>
--qa-override --approval <source> --reason <text>` overrides the unmet normal QA
prerequisite: an incomplete QA Task, an ineligible newest report, or missing or
unreadable QA evidence. The command is refused only when every Task is completed
and the newest report qualifies, because that Spec can archive normally. It
never overrides self-containment: verification can be overridden by the
maintainer, but self-containment is a property of the artifact and must be
repaired by finishing adoption.
A merged PR or release tag is **not** a precondition, and the archive never waits for one.
If any check fails, stop and report the offending Task, report, source, or link
and the adoption step that fixes a self-containment failure — the Spec stays
active.
To archive despite failed, missing or otherwise ineligible QA, use the explicit
approval and reason with the archive command:
```bash
roundfix archive <slug> --qa-override --approval <source> --reason <text>
```
The override still requires every non-QA Task to be `completed`. It accepts a
failed or pending QA Task regardless of the newest report's verdict and refuses
only when every Task is completed and the newest report qualifies, because the
same Spec can archive normally. It records the approval source, reason, observed
QA outcome and archived revision without changing the QA Task or report. When
the QA Task is not completed, it also records `qa_override_qa_task_status`. An
unreadable report's recorded outcome names its path relative to the Spec folder,
never the machine's absolute path.
## Steps
For a QA Archive Override, perform the archive only through
`roundfix archive <slug> --qa-override --approval <source> --reason <text>`.
For a normal archive, run `roundfix archive <slug>`; it verifies the
preconditions, stamps the archive metadata, and moves the folder. Never
hand-edit archive front matter; the command owns its refusals, provenance, and
archive lifecycle. The Delivery Queue writes the commit subject
`docs: archive <slug>` (Conventional Commits). Do not push unless asked.
Report the new path and anything carried over (open follow-ups from task
`## Result` sections belong in new specs, not in the archive). When the work
isn't merged yet, suggest opening the PR via `github-pr-workflow`; opening the
PR is the user's call.
## Unarchive
Rare, explicit, reversed: `git mv` back, set `status: active`, remove `release`/`archived`. Reopening usually means new work — prefer a fresh spec that references the archived one.
## Anti-patterns
- Archiving with failing or missing QA silently — the override must be the user's word, on the record.
- Blocking the archive on a merged PR or release tag — completion (tasks + QA) is the gate; publishing is a separate, user-driven step.
- Leaving a completed spec in the active set "until the PR merges" — the active folder is for live work only.
- Editing an archived spec — it is a record; new requirements get a new spec that links back.
- Deleting instead of archiving — the graveyard is where "didn't we already try this?" gets answered.