| name | skill-todo |
| description | Archive completed, abandoned, and expanded tasks with CHANGE_LOG.md updates and memory harvest suggestions |
| allowed-tools | Bash, Edit, Read, Write, Grep, AskUserQuestion |
| context | direct |
Todo Skill
Direct execution skill for archiving tasks, updating CHANGE_LOG.md, and suggesting memory harvesting.
OpenCode task archival with changelog tracking and memory suggestions.
Archive completed/abandoned/expanded tasks and track changes.
Direct execution skill for task archival operations with automated CHANGE_LOG updates and memory harvest suggestions.
Parse arguments, scan for archivable tasks (completed, abandoned, expanded), update states, generate CHANGE_LOG entries, suggest memory harvesting from completed task artifacts.
Parse command arguments
1. Check for --dry-run flag
2. Set dry_run = true if present
3. Validate no other arguments expected
Dry-run scan for status-stranded tasks whose artifact for the in-flight phase already
exists on disk -- these are invisible to Stage 2's literal completed/abandoned match today and
can never be archived until something promotes them
This stage never auto-repairs status: `/todo` performs the system's most irreversible
operations (moving directories, rewriting CHANGE_LOG.md), and a silent status promotion
immediately before a silent archive move would compound two mutations with no visibility.
Every call this stage makes is `--dry-run`; only a user-approved selection in Stage 9 ever
calls the script live.
1. Generate a session ID via `common_session_id` (skill-todo does not source
`command-gate-in.sh` and has no session ID of its own):
```bash
source .claude/scripts/lib/common.sh
todo_session_id="$(common_session_id)"
```
2. Select the same four reconcilable statuses used by the `/task --sync` and `/orchestrate`
triggers (positive-match against the four statuses `reconcile-task-status.sh` knows how
to reconcile -- no `!=`/negation selector needed):
```bash
reconcile_scan_targets=$(jq -r '
.active_projects[] |
select(.status == "researching" or .status == "planning" or .status == "implementing" or .status == "partial") |
.project_number
' specs/state.json)
```
3. For each candidate, dry-run the script and collect any task whose output reports a
would-promote line into `reconcile_candidates`, keeping each candidate's task number,
current status, the artifact basename found, and the full dry-run output for display in
Stage 8/9:
```bash
reconcile_candidates=()
for task_num in $reconcile_scan_targets; do
recon_out=$(bash .claude/scripts/reconcile-task-status.sh "$task_num" "$todo_session_id" --dry-run 2>&1)
if echo "$recon_out" | grep -q "Would promote"; then
reconcile_candidates+=("$task_num")
# associate $recon_out with $task_num (e.g. via an associative array) for Stage 9's
# AskUserQuestion description text
fi
done
```
4. If `reconcile_scan_targets` is empty, `reconcile_candidates` is `()` -- proceed straight
to Stage 2. This stage is genuinely side-effect-free: `--dry-run` never writes
`state.json` or any other file.
</process>
Scan for archivable tasks
1. Read specs/state.json
2. Identify tasks with status = "completed"
3. Identify tasks with status = "abandoned"
4. Identify tasks with status = "expanded"
5. Read specs/TODO.md and cross-reference (including entries marked [EXPANDED])
6. Track counts: completed_count, abandoned_count, expanded_count
**Subtasks-defer guard**: identical semantics to `commands/todo.md`'s Step 3 guard (see that
file's "Prepare Archive List" section, which is the reference implementation this mirrors).
Partition the tasks identified above into `archivable_tasks[]` (proceeds) and
`deferred_expanded[]` (held back for a later `/todo` run) — an expanded parent is deferred
while any task in its `subtasks[]` is still present in `active_projects` with a non-terminal
status. Use a `case` statement for status classification (never `!=`):
```bash
archivable_tasks=()
deferred_expanded=()
deferred_expanded_nums=()
for task in "${candidate_tasks[@]}"; do
status=$(echo "$task" | jq -r '.status')
project_num=$(echo "$task" | jq -r '.project_number')
case "$status" in
expanded)
# A missing, null, or empty subtasks array means nothing is blocking - archive normally.
subtasks=$(echo "$task" | jq -c '.subtasks // []')
subtask_count=$(echo "$subtasks" | jq 'length')
if [ "$subtask_count" -eq 0 ]; then
archivable_tasks+=("$task")
continue
fi
blocking_count=0
for subtask_num in $(echo "$subtasks" | jq -r '.[]'); do
subtask_status=$(jq -r --argjson n "$subtask_num" \
'.active_projects[] | select(.project_number == $n) | .status' \
specs/state.json)
# An empty result means the subtask is already archived - not blocking.
if [ -z "$subtask_status" ]; then
continue
fi
case "$subtask_status" in
completed|abandoned|expanded)
# Terminal - not blocking.
;;
*)
# Any other status blocks.
((blocking_count++))
;;
esac
done
if [ "$blocking_count" -gt 0 ]; then
deferred_expanded+=("$task")
deferred_expanded_nums+=("$project_num")
else
archivable_tasks+=("$task")
fi
;;
*)
# Non-expanded tasks pass through untouched.
archivable_tasks+=("$task")
;;
esac
done
```
Track `deferred_expanded[]` and `deferred_count` (`= ${#deferred_expanded[@]}`). Stage 10
(`ArchiveTasks`) consumes `archivable_tasks[]` — never a freshly-recomputed status match —
so a deferred parent's `active_projects` entry survives this run.
</process>
Optional: backfill topics on active tasks missing the topic field
Detect active tasks without a topic:
```bash
missing=$(jq -r '.active_projects[] |
select(.status == "completed" | not) |
select(.status == "abandoned" | not) |
select(.status == "expanded" | not) |
select(.topic == null or .topic == "") |
"\(.project_number)|\(.project_name)"' specs/state.json)
```
If no tasks need backfill, skip this stage.
For each task needing a topic, follow the topic assignment pattern from
@.claude/context/patterns/topic-assignment-pattern.md (Mode A, per-task backfill).
Use header "Topic Backfill ({i} of {total})".
After each selection:
```bash
bash .claude/scripts/manage-topics.sh set "$task_num" "$topic"
```
</process>
Detect orphaned directories and TODO.md orphans
1. Scan specs/ for directories not tracked in state files:
```bash
for dir in specs/OC_[0-9]*_*/ specs/[0-9]*_*/; do
[ -d "$dir" ] || continue
basename_dir=$(basename "$dir")
project_num=$(echo "$basename_dir" | sed 's/^OC_//' | cut -d_ -f1)
in_active=$(jq -r --arg n "$project_num" \
'.active_projects[] | select(.project_number == ($n | tonumber)) | .project_number' \
specs/state.json 2>/dev/null)
in_archive=$(jq -r --arg n "$project_num" \
'.completed_projects[] | select(.project_number == ($num | tonumber)) | .project_number' \
specs/archive/state.json 2>/dev/null)
if [ -z "$in_active" ] && [ -z "$in_archive" ]; then
orphaned_in_specs+=("$dir")
fi
done
```
2. Scan specs/archive/ for orphaned directories:
```bash
for dir in specs/archive/OC_[0-9]*_*/ specs/archive/[0-9]*_*/; do
[ -d "$dir" ] || continue
basename_dir=$(basename "$dir")
project_num=$(echo "$basename_dir" | sed 's/^OC_//' | cut -d_ -f1)
in_archive=$(jq -r --arg n "$project_num" \
'.completed_projects[] | select(.project_number == ($num | tonumber)) | .project_number' \
specs/archive/state.json 2>/dev/null)
if [ -z "$in_archive" ]; then
orphaned_in_archive+=("$dir")
fi
done
```
3. Scan TODO.md for completed/abandoned tasks not tracked in state.json or archive:
- Parse task headers (`### {N}.` or `### OC_{N}.`) and status lines (`[COMPLETED]`/`[ABANDONED]`)
- Cross-reference each against active_projects and archive completed_projects
- Collect as `todo_md_orphans[]` if: status is completed/abandoned, not in either state file, and has a directory in specs/
</process>
Detect misplaced directories
1. Scan specs/ for directories tracked in archive state:
```bash
for dir in specs/OC_[0-9]*_*/ specs/[0-9]*_*/; do
[ -d "$dir" ] || continue
basename_dir=$(basename "$dir")
project_num=$(echo "$basename_dir" | sed 's/^OC_//' | cut -d_ -f1)
in_active=$(jq -r --arg n "$project_num" \
'.active_projects[] | select(.project_number == ($num | tonumber)) | .project_number' \
specs/state.json 2>/dev/null)
in_archive=$(jq -r --arg n "$project_num" \
'.completed_projects[] | select(.project_number == ($num | tonumber)) | .project_number' \
specs/archive/state.json 2>/dev/null)
if [ -z "$in_active" ] && [ -n "$in_archive" ]; then
misplaced_in_specs+=("$dir")
fi
done
```
</process>
Scan for roadmap references
0. Ensure specs/ROADMAP.md exists. If the file does not exist, create it with the default template:
```markdown
# Project Roadmap
## Phase 1: Current Priorities (High Priority)
- [ ] (No items yet -- add roadmap items here)
## Success Metrics
- (Define success metrics here)
```
1. Partition `archivable_tasks[]` into roadmap-excluded (meta tasks, and expanded tasks —
an expanded task has no `completion_summary` of its own by construction, since its
subtasks carry the deliverables; do not "fix" this by requiring one) and
roadmap-eligible tasks, exactly as `commands/todo.md`'s Step 3.5.1 does.
2. This stage performs no matching of its own. Invoke `roadmap-integration.sh` parse-only
(no `--annotate`) against `specs/ROADMAP.md`/`specs/state.json`, capturing
`roadmap_structure`, `warnings`, and `roadmap_matches` from the payload. Filter
`roadmap_matches` to only the roadmap-eligible tasks from step 1 before treating any
match as an annotation candidate — this filter is where meta/expanded exclusion is
enforced, since the script has no `task_type` filter of its own (see the script's header
"Caller contract").
3. **Error-handling contract** (identical to `commands/todo.md`'s Step 3.5 and
`commands/review.md`'s Step 2.5): a missing script, a non-zero exit, or empty output all
produce the same visible warning and the same fully-defined `parseable: false` fallback
— never silence.
</process>
Scan meta tasks for README.md suggestions
1. For each archived meta task:
- Check completion_data.readme_suggestions
- Filter out "none" values
- Track actionable suggestions by type:
* Add: Insert new content
* Update: Replace existing content
* Remove: Delete content
Collect, deduplicate, and classify memory candidates from state.json
Note: this stage stays scoped to completed tasks and is not widened to expanded tasks —
an expanded task's work product and memory candidates belong to its subtasks, which are
harvested (or already were harvested) in their own right when they complete.
1. Collect candidates from state.json:
- For each completed task in the archival batch:
- Read `memory_candidates // []` from the task's state.json entry
- Flatten into a single list, tagging each candidate with `task_number` provenance
- If no candidates across all tasks, set `harvest_candidates = []` and skip to Stage 8
2. Deduplicate against existing memory-index.json: