name: mark description: Move PRDs and task files between lifecycle states (active, done, superseded) — retire active items or revive archived ones. requires: [config, prd-index:optional, tasks-index:optional]
Mark
/xavier mark [name] [state]
Manually move PRDs and task files between lifecycle states (active, done, superseded). Pairs with the choice-list count hint surfaced by the router — mark is what users run to revive an archived item or retire an active one.
Lifecycle States
There are three states for a PRD or task file:
- active — file lives at top level (
<vault>/prd/<name>.mdor<vault>/tasks/<name>.md); frontmatter has nostatusfield. Indexes (prd-index,tasks-index) only ever surface active items. - done — file lives at
<vault>/<kind>/done/<name>.md; frontmatter hasstatus: done. - superseded — file lives at
<vault>/<kind>/done/<name>.md; frontmatter hasstatus: superseded. Conceptually distinct fromdone(replaced rather than completed) but stored alongside done items.
<kind> is prd or tasks. Knowledge notes, research, investigations, and dependency notes are out of scope for this skill — mark only operates on PRDs and tasks.
Name Validation
Before performing any filesystem operation, the <name> argument MUST be validated as a basename. Reject anything that is not a basename:
- The name must match the regex
^[a-z0-9][a-z0-9-]{0,63}$(lowercase letters, digits, hyphens; must start with a letter or digit; total length 1–64 characters to keep resulting filenames well below the 255-byte filesystem limit). - Reject names containing
/,\,.., leading., whitespace, absolute paths, or any character outside[a-z0-9-]. - Names sourced from frontmatter wikilinks (e.g., a
source: "[[prd/<name>]]"field) are not trusted — apply the same validation after extracting<name>from the wikilink.
If <name> fails validation, abort with: Invalid name '<name>': must match [a-z0-9][a-z0-9-]{0,63}. Aborting — no filesystem changes made. This rule applies in arg mode (Step 3), one-arg picker pre-filter (Step 2), backfill (Step 5), and any sibling-scan that consumes a source field.
Transition Operations
When transitioning a file, perform exactly the actions listed for the target state, in the order given. Each transition is idempotent: running a transition whose end state matches the current state is a no-op (no frontmatter edit, no mv). Each transition is atomic: if any step fails, prior steps are rolled back so on-disk state remains as it was before the transition started.
The contract below is canonical — loop/SKILL.md and tasks/SKILL.md reference these operations rather than restating them. Do not duplicate transition logic in other skills.
→ done
- Validate name per the rules above. Abort on failure.
- Idempotency check. If the file is already at
<vault>/<kind>/done/<name>.mdAND its frontmatterstatusis alreadydone: no-op. Stop. - Move precondition. If the file currently lives at
<vault>/<kind>/<name>.md(top-level), verify that<vault>/<kind>/done/<name>.mddoes not already exist. If it does, abort with:Cannot transition <kind>/<name> → done: destination <vault>/<kind>/done/<name>.md already exists. Resolve the conflict manually and re-run.Do not overwrite —mvwould silently destroy the existing done-side file. - Frontmatter write (first). At the file's current path, set the frontmatter
statusfield todone(insert if absent; overwrite ifsuperseded). Bumpupdated:to today's ISO date. Save the priorstatusvalue (which may be absent orsuperseded) and the priorupdated:value in memory in case rollback is needed. - Move (second). If the file was at the top level, run
mv <vault>/<kind>/<name>.md <vault>/<kind>/done/<name>.md. - Rollback on move failure. If
mvreturns non-zero, revert the frontmatter changes from step 4: restore the priorstatusvalue (re-insertsupersededif that was the prior value, or remove thestatusfield entirely if it was previously absent) and restore the priorupdated:value. The file ends in exactly the on-disk state it had before the transition started. Surface the originalmverror to the user.
→ superseded
- Validate name per the rules above. Abort on failure.
- Idempotency check. If the file is already at
<vault>/<kind>/done/<name>.mdAND its frontmatterstatusis alreadysuperseded: no-op. Stop. - Move precondition. If the file lives at
<vault>/<kind>/<name>.md(top-level), verify<vault>/<kind>/done/<name>.mddoes not exist. If it does, abort with the equivalent error from→ donestep 3. Do not overwrite. - Frontmatter write. Set
status: superseded(insert if absent; overwrite ifdone). Bumpupdated:. Save the priorstatusvalue (absent ordone) and priorupdated:. - Move. If at top-level,
mvto<vault>/<kind>/done/<name>.md. - Rollback on move failure. Restore the prior
statusvalue (re-insertdoneif that was the prior value, or remove the field if previously absent) and priorupdated:. The file ends exactly as it was before the transition started.
→ active
- Validate name per the rules above. Abort on failure.
- Idempotency check. If the file is already at
<vault>/<kind>/<name>.md(top-level) AND has nostatusfield: no-op. Stop. - Move precondition. If the file lives in
<vault>/<kind>/done/<name>.md, verify<vault>/<kind>/<name>.md(top-level) does not already exist. If it does, abort with:Cannot transition <kind>/<name> → active: destination <vault>/<kind>/<name>.md already exists. Resolve the conflict manually and re-run.Do not overwrite. - Frontmatter write. Remove the
statusfield entirely (do not leave an empty value). Bumpupdated:. Save prior state. - Move. If in
done/,mvto<vault>/<kind>/<name>.md. - Rollback on move failure. Re-insert the prior
statusvalue and restore the priorupdated:so the file remains indone/with its original status.
Step 1: Parse Arguments
Match the invocation against the rules below in this exact order — earlier rules win. The --backfill flag is checked before any other one-arg interpretation so that /xavier mark --backfill always dispatches the migration flow, never the picker pre-filter.
--backfill(alone) → backfill mode (Step 5). One-shot migration that walks loop-state evidence, sibling inference, and a manual sweep to retire pre-existing items in a vault that predates the lifecycle feature.--backfillcombined with any other argument → error:--backfill must be the only argument.Stop.- No arguments → picker mode (Step 2).
- Two arguments (
<name> <state>) → arg mode (Step 3).<state>must be one ofdone,superseded,active. If the second argument is not in that set, error:Invalid state '<state>'. Allowed: done, superseded, active.Stop. - One argument (and it is not
--backfill) → treat as picker mode (Step 2) but pre-filter the list to entries matching the single argument as a name. Step 2 covers the zero/one/many match branches. - Three or more arguments, or any other unrecognized form → error:
Unrecognized arguments. Usage: /xavier mark [<name> <state>] | --backfill.Stop.
Step 2: Picker Mode
From the resolved
prd-indexandtasks-indexcontexts, gather the active items (top-level*.mdonly — these contexts already excludedone/).Glob
<vault>/prd/done/*.mdand<vault>/tasks/done/*.mddirectly to gather the archived items. The indexes do not surface these. Read each archived file's frontmatter to recover its current state — bothdoneandsupersededitems live in the samedone/directory, so the path alone cannot distinguish them. Read only the frontmatter (stop at the second---); never read the full body.One-arg pre-filter (entered when Step 1 detected exactly one argument that was already validated as a basename): apply the filter to the combined active + archived set before showing any picker. Match a candidate when its
<name>(basename without.md) exactly equals the argument — substring matches and fuzzy matches are explicitly out of scope. Branch on the filtered count:- Zero matches → error:
No PRD or task named '<arg>' found in active or archived sets.Stop. Do NOT fall through to a full picker; the user explicitly named something that does not exist. - Exactly one match → skip the multi-select; immediately prompt for the target state via AskUserQuestion (
done/superseded/active) and dispatch the matching transition operation. This is the auto-prompt path documented in Step 1. - More than one match (e.g., the same basename in both
prd/andtasks/trees, or both top-level and archived in one tree) → render the multi-select picker pre-filtered to just those matches so the user disambiguates.
- Zero matches → error:
Present the choices using AskUserQuestion with
multiSelect: true. Format the options into two clearly separated sections, active first:Active 1. prd/<name> (active) 2. tasks/<name> (active) ... Done / Superseded N. prd/done/<name> (done) N+1. tasks/done/<name> (superseded) ...Show the
<kind>/<name>form so the user can disambiguate items with the same base name. Append the current state in parentheses for each row. If a section is empty, render the section header with(none)rather than skipping it.After the user selects one or more items, prompt with AskUserQuestion for the target state. Options:
done,superseded,active.For each selected item, dispatch the transition operation from the matching state above. Apply transitions sequentially.
Step 3: Arg Mode
Validate
<name>per the Name Validation rules above. Abort if it fails.Resolve
<name>against all four candidate paths:<vault>/prd/<name>.md<vault>/prd/done/<name>.md<vault>/tasks/<name>.md<vault>/tasks/done/<name>.md
Zero matches → error:
No PRD or task named '<name>' found.List the closest matches by basename if any. Stop.Multiple matches across
<kind>/and<kind>/done/(i.e., the same<name>.mdexists at both top level and indone/for the same kind, OR exists in bothprd/andtasks/trees) → error:Ambiguous: '<name>' matches multiple files: - <vault>/prd/<name>.md - <vault>/prd/done/<name>.md Use picker mode (no args) to pick one explicitly.Stop. Do not perform any transition. Never accept a path-style argument as a workaround —
<name>is always a basename. If the user truly has duplicates, they must resolve via the picker.Exactly one match → dispatch the transition operation for
<state>.
Step 4: Report
After transitions complete, summarize for the user:
- For each item processed, print:
<kind>/<name>: <prior-state> → <new-state>(or<kind>/<name>: no change (already <state>)for no-ops). - If any transition failed (e.g., write error), surface the error per-item and continue with the remaining items.
Step 5: Backfill Mode (--backfill)
Backfill is a one-shot migration meant to retire items in a vault that predates the lifecycle feature. It runs three independent sub-phases in sequence. Each sub-phase is independently abortable: answering no at one sub-phase's confirm prompt proceeds to the next sub-phase with the vault unchanged so far. Re-running --backfill is idempotent — items already at <vault>/<kind>/done/ (or already carrying status: done / status: superseded) are skipped, so a second run does nothing additional.
Begin by announcing to the user that backfill is a three-step migration and that each step is individually skippable. Then run sub-phases 1 → 2 → 3.
Sub-phase 5a: auto-batch tasks from loop-state evidence
What it scans. Glob ~/.xavier/loop-state/*.md. For each loop-state file, look for any of the following completion signals (a permissive heuristic — older loop-state files predate a structured marker):
- A line matching
^##\s*Status:\s*COMPLETE(case-insensitive) - A line matching
^##\s*Current Phase:\s*COMPLETE - A frontmatter-style or list-style
Status:field whose value iscompleteordone - The literal string
Loop completeanywhere in the file - A line
status: complete(the marker written by the loop skill's Step 5 success path going forward)
If any of those patterns matches, the loop-state's basename is a candidate. Validate <name> as a basename per the Name Validation rules above (^[a-z0-9][a-z0-9-]{0,63}$) before using it in any filesystem lookup. Loop-state files are in principle written only by /xavier loop, but legacy or hand-edited filenames may not satisfy the allowlist; rejecting them here keeps backfill from probing arbitrary paths even in defense-in-depth scenarios. For each validated candidate basename <name>, look up <vault>/tasks/<name>.md:
- If it exists at top-level → eligible for the batch.
- If it already lives in
<vault>/tasks/done/<name>.md→ skip (idempotent). - If neither path exists → skip (orphan loop-state, not actionable).
- If validation failed → skip and log a warning naming the loop-state file so the user can rename it manually.
What prompt it shows. Present the eligible basenames as a single bulk-confirm via AskUserQuestion:
Found N tasks with completed loop-state evidence:
<name1>,<name2>, ... Mark them all asdone?Options:
yes,no.
Show the list in full (one per line). Do not paginate.
Consequence of "no". The vault is unchanged. Proceed to sub-phase 5b — do not abort the entire backfill.
Consequence of "yes". For each eligible task, dispatch the → done transition documented above. Apply transitions sequentially; surface per-item failures and continue with remaining items (same contract as Step 4 reporting).
Sub-phase 5b: PRD inference from sibling tasks
Runs after sub-phase 5a regardless of its outcome.
What it scans. First, build a source → {total, done} map in a single pass over <vault>/tasks/*.md and <vault>/tasks/done/*.md so the eligibility check below runs in O(P) instead of O(P × T):
- For each task file, read its frontmatter
sourcefield — a wikilink of the form"[[prd/<prd-name>]]"(any quoting style — single-quoted, unquoted, or double-quoted). - Extract
<prd-name>and validate it as a basename (must match^[a-z0-9][a-z0-9-]{0,63}$per the Name Validation rules). Skip the task and log a warning if validation fails — never derive filesystem operations from an unvalidatedsourcevalue. - Resolve
<prd-name>to an actual PRD file. Verify that at least one of<vault>/prd/<prd-name>.mdor<vault>/prd/done/<prd-name>.mdexists. If neither exists, the task references a non-existent PRD — typically because legacy task notes (predating the lifecycle feature) recordedsourceas the task's own filename rather than the PRD basename. Skip the task entirely and log a warning naming the file and the unresolvablesourcevalue:Skipping <task-file>: source [[prd/<prd-name>]] does not resolve to any PRD. Update the task's source field to point at an existing PRD.Do not include this task in the map; do not propose any PRD retirement based on it. - Increment
map[<prd-name>].total. Then classify the task and update the map's lifecycle counters — the canonical signal is location, not the frontmatter status:- Task lives in
<vault>/tasks/done/AND frontmatterstatus: done→ incrementmap[<prd-name>].done - Task lives in
<vault>/tasks/done/AND frontmatterstatus: superseded→ incrementmap[<prd-name>].superseded - Task lives in
<vault>/tasks/done/with missing or invalid status → log a warning, skip — the validator should already catch this drift; do not infer past it - Task lives at top-level (
<vault>/tasks/<name>.md) — regardless of any status field, classify as active (do not incrementdoneorsuperseded). A top-level file withstatus: done|supersededis non-canonical drift; log a warning and let the user reconcile via/xavier mark. Never silently count it as completed.
- Task lives in
Note: sub-phase 5a will have just moved tasks into tasks/done/, so re-glob — do not rely on a snapshot taken before sub-phase 5a ran.
Then, evaluate eligibility for each PRD via a single map lookup:
- Glob
<vault>/prd/*.md(top-level, active PRDs only). For each PRD basename<name>, look upmap[<name>]. - A PRD is eligible only when every sibling task is canonical
status: done— i.e.,map[<name>].total >= 1ANDmap[<name>].done == map[<name>].totalANDmap[<name>].superseded == 0. Auto-batching mixed-state PRDs asdonewould lose the lifecycle distinction this PR introduces. - PRDs whose siblings include any
supersededtask (i.e.,map[<name>].superseded > 0) are not eligible for the auto-batch — they fall through to sub-phase 5c, where the user can pick the correct retirement state per PRD. - PRDs with no derived tasks (
map[<name>]is missing ortotal == 0) are also not eligible here — they are surfaced in sub-phase 5c instead. - Skip PRDs already at
<vault>/prd/done/<name>.md(idempotent).
Cache the map for sub-phase 5c — it surfaces both the no-derived-tasks PRDs and the mixed-state PRDs, and reusing the map avoids re-scanning.
What prompt it shows. Bulk-confirm via AskUserQuestion:
Found N PRDs whose every derived task is
done:<name1>,<name2>, ... Mark them all asdone?Options:
yes,no.
(Mixed-state PRDs intentionally do not appear here — see sub-phase 5c.)
Consequence of "no". The vault is unchanged. Proceed to sub-phase 5c — do not abort.
Consequence of "yes". For each eligible PRD, dispatch the → done transition above.
Sub-phase 5c: manual sweep
Runs after sub-phase 5b regardless of its outcome. This sub-phase is skippable.
What it scans. All remaining items still active after sub-phases 5a and 5b:
- All top-level files in
<vault>/prd/*.md(excluding any retired this run) - All top-level files in
<vault>/tasks/*.md(excluding any retired this run)
What prompt it shows.
Present a multi-select picker via AskUserQuestion (
multiSelect: true) listing every remaining item with metadata:<kind>/<name>last-updated:<value>from the frontmatterupdated:field (or?if absent)has-derived-tasks:<true|false>— for PRDs only,trueiff any task file references this PRD viasource: "[[prd/<name>]]". For tasks, omit this column or printn/a.draft:<true|false>—trueiff the frontmattertags:array containsdraft.
Sort by
last-updatedascending (oldest first) so stale items rise to the top.After the user selects items (zero or more), prompt for the target state per the entire selection:
done,superseded,skip. (skipexits sub-phase 5c without touching the selection.)If users want different states for different items they should run
--backfillagain or use the regular picker mode after backfill exits —--backfilldeliberately offers a single bulk action per sub-phase to keep the migration short.
Consequence of "no" / empty selection / skip. Sub-phase 5c exits without touching the vault. Backfill ends.
Consequence of done or superseded. Dispatch the corresponding transition for each selected item.
Step 6: Backfill Report
After all three sub-phases run, print a single consolidated summary:
Backfill summary:
Sub-phase 5a: <N> tasks marked done (<list>) | <skipped reason>
Sub-phase 5b: <N> PRDs marked done (<list>) | <skipped reason>
Sub-phase 5c: <N> items marked <state> (<list>) | <skipped>
Total moves: <N>. Re-running --backfill is a no-op until new evidence appears.
If a sub-phase was answered no or selected nothing, record skipped (user declined) or skipped (no candidates) accordingly.
Notes
- Do NOT read or modify
done/files for any kind other thanprdandtasks. - Do NOT touch the
created:field — onlyupdated:is bumped. - Do NOT commit changes — the router handles the vault commit after the skill completes.
- The
prd-indexandtasks-indexcontexts already glob top-level only; archived items are reached by direct filesystem globs in the picker. - Backfill is intended for one-shot migration. After the migration lands, fresh loops auto-mark via
xavier/skills/loop/SKILL.mdStep 5 and PRD prompts inxavier/skills/loop/SKILL.mdStep 6 /xavier/skills/tasks/SKILL.md, so manual--backfilluse should be rare.