Skip to content
Back to skills

Archive Spec

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
developmentrustgobashgitdocumentation

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add marcioaltoe/roundfix --skill archive-spec --agent claude-code

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.

Security grade badge for Archive Spec
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/marcioaltoe-archive-spec/badge)](https://www.skillsdirectory.com/skills/marcioaltoe-archive-spec)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…