Use when all implementation work is finished and the project is ready to close. Marks the OAT project lifecycle as complete.
Mark the active OAT project lifecycle as complete.
When executing this skill, provide lightweight progress feedback so the user can tell what's happening after they confirm.
Print a phase banner once at start using horizontal separators, e.g.:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ OAT ▸ COMPLETE PROJECT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Before multi-step work, print step indicators, e.g.:
[1/6] Resolving project + collecting user choices…[2/6] Checking completion gates…[3/6] Completing lifecycle…[4/6] Generating PR description + archiving…[5/6] Refreshing dashboard + committing…[6/6] Opening PR…PROJECT_PATH=$(oat config get activeProject 2>/dev/null || true)
if [[ -z "$PROJECT_PATH" ]]; then
echo "Error: No active project set. Use the oat-project-open skill first." >&2
exit 1
fi
PROJECT_NAME=$(basename "$PROJECT_PATH")
PROJECTS_ROOT="${OAT_PROJECTS_ROOT:-$(oat config get projects.root 2>/dev/null || echo ".oat/projects/shared")}"
PROJECTS_ROOT="${PROJECTS_ROOT%/}"
IS_SHARED_PROJECT="false"
case "$PROJECT_PATH" in
"${PROJECTS_ROOT}/"*) IS_SHARED_PROJECT="true" ;;
esac
Ask all user questions at once so the user can answer them in a single interaction, then the rest of the skill runs without further prompts.
Host-specific structured input guidance:
AskUserQuestion when availableBefore asking the batched questions, read oat_pr_status and oat_pr_url from state.md frontmatter.
Workflow preference checks (before asking questions):
Some questions can be answered automatically from workflow preferences. Read each preference before deciding whether to include its question in the batched prompt:
ARCHIVE_PREF=$(oat config get workflow.archiveOnComplete 2>/dev/null || true)
PR_ON_COMPLETE=$(oat config get workflow.createPrOnComplete 2>/dev/null || true)
ARCHIVE_PREF is true: Set SHOULD_ARCHIVE="true". Skip the archive question. Print Archive on complete: enabled (from workflow.archiveOnComplete).ARCHIVE_PREF is false: Set SHOULD_ARCHIVE="false". Skip the archive question. Print Archive on complete: disabled (from workflow.archiveOnComplete).PR_ON_COMPLETE is true AND no tracked open PR exists: Set SHOULD_OPEN_PR="true". Skip the Open PR question. Print PR on complete: enabled (from workflow.createPrOnComplete).PR_ON_COMPLETE is false: Set SHOULD_OPEN_PR="false". Skip the Open PR question. Print PR on complete: disabled (from workflow.createPrOnComplete).PR_ON_COMPLETE is unset: Include the Open PR question in the batched prompt as normal (backward compatible).oat_pr_status is open, do not ask the Open PR question and do not honor PR_ON_COMPLETE=true — the PR already exists.The "Ready to mark complete?" confirmation is always asked — it is a meaningful "are you sure" moment, not a preference.
Also preflight summary status using the same freshness rules as oat-project-summary:
summary.md is missing when {PROJECT_PATH}/summary.md does not existsummary.md is stale when the tracking frontmatter fields oat_summary_last_task, oat_summary_revision_count, or oat_summary_includes_revisions no longer match current_last_task, current_rev_count, or current_rev_list as defined in oat-project-summary Step 3summary.md is current when those tracking fields still match the oat-project-summary Step 3 comparison inputsQuestions to ask (in a single prompt):
IS_SHARED_PROJECT is true): "Archive the project after completion?"missing or stale): present the status explicitly:
If oat_pr_status is open, do not ask the Open PR question. Set SHOULD_OPEN_PR="false" and treat the existing PR as already tracked.
Present all applicable questions together. Example combined prompt:
Ready to complete project **{PROJECT_NAME}**?
1. Archive the project after completion? (yes/no)
2. A summary has not been generated yet. Generate it now as part of completion? (yes/no)
3. Open a PR in GitHub? (yes/no)
If the user declines the completion confirmation, exit gracefully.
Store the answers as SHOULD_ARCHIVE, SHOULD_GENERATE_SUMMARY, and SHOULD_OPEN_PR for use in later steps.
If the summary status is current, set SHOULD_GENERATE_SUMMARY="false" and note that a current summary is already available.
If oat_pr_url is present, show it in the completion summary.
Read oat_phase_status from state.md frontmatter and handle permissively:
pr_open: Proceed normally. This is the expected entry point after oat-project-pr-final.complete: Proceed normally. Implementation is done.in_progress: Note: "Project is still in progress. Completing anyway." — proceed without additional confirmation.All three are valid starting states for completion. Do not block on any phase status value.
Run all gate checks and collect warnings. These are informational — they don't require individual user answers.
PLAN_FILE="${PROJECT_PATH}/plan.md"
if [[ -f "$PLAN_FILE" ]]; then
final_row=$(grep -E "^\|\s*final\s*\|" "$PLAN_FILE" | head -1 || true)
if [[ -z "$final_row" ]]; then
echo "Warning: No final review row found in plan.md."
elif ! echo "$final_row" | grep -qE "\|\s*passed\s*\|"; then
echo "Warning: Final code review is not marked passed."
echo "Recommendation: run the oat-project-review-provide skill with code final and oat-project-review-receive before completing."
fi
else
echo "Warning: plan.md not found, unable to verify final review status."
fi
IMPL_FILE="${PROJECT_PATH}/implementation.md"
if [[ -f "$IMPL_FILE" ]]; then
medium_items=$(awk '
BEGIN { in_medium = 0 }
/^\*\*Deferred Findings \(Medium\):\*\*/ { in_medium = 1; next }
/^\*\*Deferred Findings \(Medium\/Minor\):\*\*/ { in_medium = 1; next }
in_medium && /^\*\*/ { in_medium = 0; next }
in_medium && /^[[:space:]]*-[[:space:]]+/ { print }
' "$IMPL_FILE")
has_unresolved_medium="false"
while IFS= read -r line; do
item=$(echo "$line" | sed -E 's/^[[:space:]]*-[[:space:]]+//')
if ! echo "$item" | grep -qiE '^none([[:space:]]|[[:punct:]]|$)'; then
has_unresolved_medium="true"
break
fi
done <<< "$medium_items"
if [[ "$has_unresolved_medium" == "true" ]]; then
echo "Warning: Deferred Medium findings are recorded in implementation.md."
echo "Recommendation: resurface via final review and explicitly disposition before completion."
fi
fi
STATE_FILE="${PROJECT_PATH}/state.md"
DOCS_UPDATED=$(grep "^oat_docs_updated:" "$STATE_FILE" 2>/dev/null | awk '{print $2}' || true)
# Read policy from config (default: false = soft suggestion)
REQUIRE_DOCS=$(oat config get documentation.requireForProjectCompletion 2>/dev/null || echo "false")
if [[ "$DOCS_UPDATED" == "null" || -z "$DOCS_UPDATED" ]]; then
if [[ "$REQUIRE_DOCS" == "true" ]]; then
echo "Gate: Documentation sync required (documentation.requireForProjectCompletion is true)."
echo "Action: Run oat-project-document first, or choose to skip."
else
echo "Suggestion: Consider running oat-project-document to sync documentation before completing."
fi
fi
If oat_docs_updated is null or empty:
requireForProjectCompletion is true: Hard gate — ask user to run oat-project-document or explicitly skip. If user chooses to skip, update state.md frontmatter to set oat_docs_updated: skipped.requireForProjectCompletion is false (default): Soft suggestion — inform user about oat-project-document and allow proceeding. If user wants to skip, set oat_docs_updated: skipped.If oat_docs_updated is skipped or complete: proceed normally.
After collecting all warnings from 3.1, 3.2, and 3.3:
passed, unresolved deferred Medium findings, or documentation gate blocking), present all warnings together and ask one confirmation:
Check if {PROJECT_PATH}/summary.md exists and whether it is current against the implementation state:
summary.md is missing or stale and SHOULD_GENERATE_SUMMARY="true", generate or refresh it before completing.oat-project-summary skill when skill-to-skill invocation is available in the current host/runtime.summary.md inline by following the same synthesis rules as oat-project-summary (validate implementation state, read the same project artifacts, apply the same freshness checks, update the same frontmatter tracking fields, and write a complete summary.md before continuing).oat-project-summary is a shell command on PATH. Only execute a shell command with that name if the environment explicitly provides a real executable.summary.md is missing or stale and SHOULD_GENERATE_SUMMARY="false", emit: Warning: Proceeding without summary generation.summary.md available for PR and archive steps.summary.md already exists and is current, note it as available. Summary.md will be:
Detect any leftover active review artifacts in the top level of "$PROJECT_PATH/reviews/":
find "$PROJECT_PATH/reviews" -maxdepth 1 -type f -name "*.md" 2>/dev/null
If any active review artifacts exist:
"$PROJECT_PATH/reviews/archived" if needed.reviews/{filename}.md to reviews/archived/{filename}.md in:
"$PROJECT_PATH/plan.md""$PROJECT_PATH/implementation.md""$PROJECT_PATH/state.md"reviews/archived/, adding a timestamp suffix if needed to avoid overwriting prior history.Rules:
reviews/archived/ untouched.reviews/archived/; do not route them through the shared-project archive destination logic in Step 6.Delegate the canonical state.md completion mutation to the CLI:
COMPLETE_STATE_ARGS=("$PROJECT_PATH")
if [[ "$SHOULD_ARCHIVE" == "true" && "$IS_SHARED_PROJECT" == "true" ]]; then
COMPLETE_STATE_ARGS+=("--archived")
fi
oat project complete-state "${COMPLETE_STATE_ARGS[@]}"
The CLI command owns both the frontmatter completion fields and the canonical markdown body updates for state.md.
It must set oat_lifecycle: complete, completion timestamps, **Status:** Complete, **Last Updated:**, the canonical ## Current Phase body, normalized ## Progress, and ## Next Milestone.
Clear the active project pointer immediately. If the user is completing a project, clearing the pointer is implicit — no confirmation needed.
oat config set activeProject ""
echo "Active project pointer cleared."
PR description generation is automatic — it always runs as part of project completion. This must happen before archiving so that project artifacts are still at their tracked paths and blob links resolve correctly.
Follow the oat-project-pr-final skill's process (Steps 0.5 through 4) inline:
plan.md, implementation.md, spec.md, design.md, discovery.md) based on workflow mode from state.md.summary.md exists (from Step 3.5), use it as the primary source for the PR description's Summary section (per oat-project-pr-final Step 3.0). Read remaining artifacts and collect git context:BRANCH=$(git rev-parse --abbrev-ref HEAD)
MERGE_BASE=$(git merge-base origin/main HEAD 2>/dev/null || git merge-base main HEAD 2>/dev/null || echo "")
if [[ -n "$MERGE_BASE" ]]; then
git log --oneline "${MERGE_BASE}..HEAD"
git diff --shortstat "${MERGE_BASE}..HEAD"
fi
{PROJECT_PATH}/pr/project-pr-YYYY-MM-DD.md following the template and policies from oat-project-pr-final Step 4 (frontmatter policy, reference links policy, local path exclusion).If a PR description artifact already exists at {PROJECT_PATH}/pr/project-pr-*.md, skip generation and use the existing one instead.
Skip if SHOULD_ARCHIVE is false or IS_SHARED_PROJECT is false.
Archive happens after PR description generation (so artifacts are readable at tracked paths) but before commit+push (so the archive deletion is included in the commit).
The archive-side effects in this step are CLI-owned. Follow the canonical behavior from packages/cli/src/commands/project/archive/archive-utils.ts rather than inventing separate S3 or summary-export logic inside the skill.
ARCHIVED_ROOT=".oat/projects/archived"
PRIMARY_REPO_ARCHIVE=""
LOCAL_ARCHIVED_ROOT=".oat/projects/archived"
USE_PRIMARY_REPO_ARCHIVE="false"
# Heuristic: if this checkout is a worktree and the primary repo archive parent
# exists, use that durable archive path as the canonical archive destination.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
GIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null || true)
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || true)
if [[ -n "$GIT_COMMON_DIR" && -n "$GIT_DIR" && "$GIT_COMMON_DIR" != "$GIT_DIR" ]]; then
PRIMARY_REPO_ROOT=$(cd "$(dirname "$GIT_COMMON_DIR")" && pwd)
PRIMARY_REPO_ARCHIVE="${PRIMARY_REPO_ROOT}/.oat/projects/archived"
if [[ -d "$(dirname "$PRIMARY_REPO_ARCHIVE")" ]]; then
USE_PRIMARY_REPO_ARCHIVE="true"
ARCHIVED_ROOT="$PRIMARY_REPO_ARCHIVE"
else
echo "Warning: Running in a worktree, but the primary repo archive path is unavailable: $PRIMARY_REPO_ARCHIVE"
echo "A worktree-local archive may be deleted when the worktree is removed and is not a durable archive."
echo "Require explicit confirmation before proceeding with local-only archive."
fi
fi
fi
if [[ "$USE_PRIMARY_REPO_ARCHIVE" != "true" ]]; then
ARCHIVED_ROOT="$LOCAL_ARCHIVED_ROOT"
fi
mkdir -p "$ARCHIVED_ROOT"
ARCHIVE_PATH="${ARCHIVED_ROOT}/${PROJECT_NAME}"
if [[ -e "$ARCHIVE_PATH" ]]; then
ARCHIVE_PATH="${ARCHIVED_ROOT}/${PROJECT_NAME}-$(date +%Y%m%d-%H%M%S)"
fi
mv "$PROJECT_PATH" "$ARCHIVE_PATH"
PROJECT_PATH="$ARCHIVE_PATH"
echo "Project archived to $ARCHIVE_PATH"
Canonical helper behaviors (required):
archive.summaryExportPath is configured and summary.md exists after archive, copy it to {repoRoot}/{archive.summaryExportPath}/YYYYMMDD-{PROJECT_NAME}.md.archive.s3SyncOnComplete=true and archive.s3Uri is configured, sync the archived project to {archive.s3Uri}/{repo-slug}/{PROJECT_NAME}/. The S3 sync excludes process artifacts (reviews/*, pr/*) — only core deliverables (discovery, spec, design, plan, implementation, summary, state) are uploaded. The CLI enforces this via S3_ARCHIVE_SYNC_EXCLUDES in archive-utils.ts.archive.s3SyncOnComplete is false or archive.s3Uri is unset, skip remote sync without prompting.Worktree durability guard (required):
git rev-parse --git-common-dir and git rev-parse --git-dir, matching the CLI helper in packages/cli/src/commands/project/archive/archive-utils.ts. Do not rely on a main checkout or default-branch naming.Git handling after archive:
If the archived directory is gitignored (check with git check-ignore -q "$ARCHIVE_PATH"), the move looks like a deletion to git — the original tracked files disappear and the archived copy is local-only. To commit:
git add -A "$PROJECTS_ROOT/$PROJECT_NAME" 2>/dev/null || true
This stages the deletions from the shared directory. The archived copy is preserved locally but not tracked by git.
Worktree archive target (required when available):
If running from a git worktree, the primary repo archive directory is the canonical/durable archive destination.
Reference path:
GIT_COMMON_DIR=$(git rev-parse --git-common-dir)
PRIMARY_REPO_ROOT=$(cd "$(dirname "$GIT_COMMON_DIR")" && pwd)
PRIMARY_REPO_ARCHIVE="${PRIMARY_REPO_ROOT}/.oat/projects/archived"
Guidance:
PRIMARY_REPO_ARCHIVE when the archive destination is local-only/gitignored in the current checkout. If .oat/projects/archived/ is version controlled on the current branch, archive in the current checkout instead.archive.summaryExportPath copy into the current checkout (repoRoot), even when the project archive itself is written to the primary checkout.Regenerate the repo state dashboard so the completion status is reflected before committing.
oat state refresh
Completion is not done until bookkeeping changes are committed and pushed. This prevents local-only state.md updates that leave project status stale for later sessions/reviews.
Expected changes may include:
{PROJECT_PATH}/state.md{PROJECT_PATH}/implementation.md (if touched earlier in the lifecycle closeout){PROJECT_PATH}/plan.md (if review receive just ran){PROJECT_PATH}/pr/project-pr-*.md (PR description artifact).oat/state.md (dashboard regenerated in Step 9).oat/config.local.json (if activeProject cleared){PROJECTS_ROOT}/{PROJECT_NAME} (if archived)Run:
git status --short
git add -A
git commit -m "chore(oat): complete project lifecycle for ${PROJECT_NAME}"
git push
Rules:
Skip if SHOULD_OPEN_PR is false.
CRITICAL — Strip YAML frontmatter before submitting to GitHub.
The local artifact file contains YAML frontmatter (--- delimited block at the top) for OAT metadata. This frontmatter MUST NOT appear in the GitHub PR body. Before passing the file to gh pr create, strip everything from the start of the file through and including the closing --- line. Verify the resulting body starts with the markdown heading (e.g., # feat: ...), not YAML keys.
Steps:
{PROJECT_PATH}/pr/project-pr-*.md.--- through the closing ---, inclusive).git push -u origin "$(git rev-parse --abbrev-ref HEAD)"
gh pr create --base main --title "{title}" --body-file "$TMP_BODY"
Do not assume gh is installed; if missing, instruct manual PR creation using the file contents.
Show user:
oat_pr_url is present, show it in the completion summary even when PR creation was skipped because the project already tracked an open PR.