| name | aiwf-archive |
| description | Use when terminal-status entities have accumulated in the active tree and the operator wants to sweep them into per-kind `archive/` subdirs, or when `aiwf check` reports `archive-sweep-pending`. Explains dry-run vs `--apply`, the no-reverse rule, the `archive.sweep_threshold` knob, merge edge cases, and the per-kind storage layout. |
aiwf-archive
The aiwf archive verb sweeps terminal-status entities into per-kind archive/ subdirectories so the active tree reflects what's currently in-flight. Movement is decoupled from FSM promotion — aiwf promote and aiwf cancel flip status only; this verb performs the structural projection later, as a single commit per invocation.
The kernel principle the verb embodies: location is a redundant projection of status. The decoupling buys (1) a grace period after close for inspection and wrap rituals, and (2) one-purpose promotion verbs with no file-move side effects to test. The cost is bounded drift (the period between promotion and the next sweep); the drift is policed via the archive-sweep-pending advisory finding and the archive.sweep_threshold knob below.
When to use
aiwf check reports archive-sweep-pending and the operator wants the tree back to convergence.
- The active directory listing (a plain
ls work/gaps/, GitHub tree, or IDE file pane) is cluttered with terminal entries.
- A milestone or epic just closed and the operator is doing wrap rituals — running a dry-run now previews what the next sweep will move.
- First-time migration on a pre-archive-convention tree: the same verb covers the bulk first sweep and every recurring small sweep that follows.
What to run
aiwf archive
aiwf archive --dry-run
aiwf archive --apply
aiwf archive --apply --kind gap
Dry-run is the default. The verb prints the planned moves and exits without touching the tree. Re-run with --apply to commit. This is the single-flag-flip safety pattern that keeps the verb hard to misuse — the destructive shape requires a deliberate flag, not the default.
Idempotent. Re-running on a clean tree produces no commit and exits 0. There is no "force a sweep" mode; if the tree is already converged, there is nothing to do.
When a sweep skips a candidate
A sweep over a tree with uncommitted work reports some entities as skipped
and sweeps the rest. The dry run lists the skipped ones under Skipped:
alongside the planned moves; under --apply the same list rides into the
commit body rather than the terminal, so run the dry run first to read it:
Skipped:
G-NNNN: uncommitted changes in work/gaps/G-NNNN-<slug>.md
When every candidate is skipped there is nothing to commit, so the verb
says so instead and exits 0:
aiwf archive: no entities swept; 1 entity skipped: G-NNNN (uncommitted changes in work/gaps/G-NNNN-<slug>.md)
Neither is an error. The exit code is unchanged, and every candidate not
named has swept.
Why a candidate is skipped. The sweep decides where an entity belongs
from what is committed. When a file that decision rests on has uncommitted
changes, the answer is unavailable rather than negative, so that one
candidate is left in place and named. Three files can do it: the entity's
own, whose status decides whether it is terminal at all; an entity whose
body links to it, whose link the sweep would rewrite; and anything already
sitting at the destination the move lands on.
What to do. Commit or revert the named file, then re-run. Nothing was
half-done — a sweep is one commit, and a skipped candidate contributes
nothing to it.
An epic is skipped for a second reason, reported as
E-NNNN: non-terminal children (M-NNNN) rather than as a file. Its subtree
still holds a milestone that has not closed, and it sweeps once that
milestone reaches a terminal status; no file needs committing.
A skip costs one candidate, never the sweep. Uncommitted work in one
entity does not block moves that do not depend on it, so the verb stays
usable mid-edit.
Reversal — there is none
You don't reverse the sweep, deliberately. Per the archive convention §"Reversal — what verb undoes archive?", the FSM is one-directional and archive is the structural projection of FSM-terminality. The kernel does not provide an "aiwf reactivate" verb, an "un-archive" verb, or any reverse-sweep mode.
The canonical pattern when a closed entity needs revisiting is to file a new entity that references the archived one. Resolves: G-NNNN from a new gap remains valid because the loader resolves ids across both active and archive directories — references stay live indefinitely.
If a contributor hand-edits frontmatter to take a status off-terminal on an already-archived file, aiwf check fires archived-entity-not-terminal (blocking). The remediation is to revert the hand-edit, not to relocate the file.
Drift control
The tree is never strictly in convergence between promotion and the next sweep. Three layers bound the drift; the threshold knob below is the operator-tunable one.
archive.sweep_threshold (the knob this skill exists to document). Set in aiwf.yaml:
archive:
sweep_threshold: 5
When the active-tree pending-sweep count exceeds the configured value, aiwf check escalates archive-sweep-pending from a warning to an error and the pre-push hook blocks the push. At-or-below the threshold the finding stays advisory.
- Default: unset. No threshold;
archive-sweep-pending stays advisory regardless of count. Permissive by default — teams that don't want the kernel to nag don't have to opt out.
- Strictest setting:
sweep_threshold: 0. Any single pending sweep blocks. Use when the consumer prefers a strictly-converged tree.
- Common pattern:
sweep_threshold: 20 or similar. Allows the natural grace period for inspection without letting the backlog grow indefinitely.
The escalated message names both the count and the configured threshold so the human reading the failed push sees the magnitude of the breach and the policy they crossed.
Merge edge cases
The decoupled model creates one merge-conflict shape worth knowing.
Rename + modify. Branch A archives G-NNNN (renames the file from work/gaps/G-NNNN-...md to work/gaps/archive/G-NNNN-...md). Branch B edits G-NNNN in place. When the branches merge, git's rename detection usually handles this cleanly — the edit goes to the renamed path. Occasionally it surfaces as a "rename+modify" conflict; the resolution is mechanical: take the rename, take the edit. Standard kernel pattern of "merge, run check, fix findings" handles the rest.
Cross-references in body prose. Body-prose references that use file paths (rather than ids) become stale when the target archives. Standard kernel discipline prefers id-form references; a tracked gap captures the work item for a preventive check rule.
Per-kind storage layout
| Kind | Active location | Archive location | Trigger |
|---|
| Epic | work/epics/<epic>/ (directory) | work/epics/archive/<epic>/ (whole subtree) | terminal status (done, cancelled) |
| Milestone | work/epics/<epic>/M-NNNN-<slug>.md | rides with parent epic — does not archive independently | n/a |
| Contract | work/contracts/<contract>/ | work/contracts/archive/<contract>/ (whole subtree) | terminal status (retired, rejected) |
| Gap | work/gaps/G-NNNN-<slug>.md | work/gaps/archive/G-NNNN-<slug>.md | terminal status (addressed, wontfix) |
| Decision | work/decisions/D-NNNN-<slug>.md | work/decisions/archive/D-NNNN-<slug>.md | terminal status (superseded, rejected) |
| ADR | docs/adr/ADR-NNNN-<slug>.md | docs/adr/archive/ADR-NNNN-<slug>.md | terminal status (superseded, rejected) |
Milestones don't archive independently because they live as flat files inside their parent epic's directory. A done milestone under an active epic stays put until the epic itself archives, at which point the whole subtree moves in a single rename.
internal/entity/transition.go::IsTerminal is the source of truth for which statuses are terminal per kind. The verb consults it directly; the table above mirrors the rule for reference.
Don't
- Don't hand-move files into
archive/. The verb's commit carries the aiwf-verb: archive trailer so aiwf history recognizes the move. A hand-mv leaves an untrailered commit that the provenance audit flags.
- Don't try to "un-archive" by editing frontmatter. Status is the source of truth; flipping a terminal status off-terminal on a file under
archive/ produces an archived-entity-not-terminal finding. The remediation is to revert the hand-edit and file a new entity referencing the archived one.
- Don't sweep before wrap rituals. Just-closed entities benefit from the grace period — running wrap skills, browsing
aiwf show <id> paths, and skimming the dust just settled all work most naturally when the entity is still at its active path.
- Don't bypass the dry-run preview on a bulk migration. The first sweep on a pre-archive-convention tree typically moves dozens or hundreds of files. Read the dry-run output, confirm the counts, then
--apply.