You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/.trae/skills/comet/scripts/comet-archive.sh

312 lines
7.1 KiB

#!/bin/bash
# Comet Archive — automates the archive phase in one command
# Usage: comet-archive.sh <change-name> [--dry-run]
# Exit 0 = archive complete, exit 1 = fatal error
set -euo pipefail
COMET_BASH="${COMET_BASH:-${BASH:-bash}}"
COMET_OPENSPEC="${COMET_OPENSPEC:-openspec}"
red() { echo -e "\033[31m$1\033[0m" >&2; }
green() { echo -e "\033[32m$1\033[0m" >&2; }
yellow() { echo -e "\033[33m$1\033[0m" >&2; }
DRY_RUN=0
if [[ "${2:-}" == "--dry-run" ]]; then
DRY_RUN=1
fi
# Input validation
validate_change_name() {
local name="$1"
if [ -z "$name" ]; then
red "FATAL: Change name cannot be empty"
exit 1
fi
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
red "FATAL: Invalid change name: '$name'"
red "Valid characters: a-z, A-Z, 0-9, -, _"
exit 1
fi
if [[ "$name" =~ \.\. ]]; then
red "FATAL: Change name cannot contain '..'"
exit 1
fi
}
CHANGE="$1"
validate_change_name "$CHANGE"
CHANGE_DIR="openspec/changes/$CHANGE"
YAML="$CHANGE_DIR/.comet.yaml"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
STATE_SH="$SCRIPT_DIR/comet-state.sh"
TODAY=$(date -u +%Y-%m-%d)
ARCHIVE_NAME="${TODAY}-${CHANGE}"
ARCHIVE_DIR="openspec/changes/archive/${ARCHIVE_NAME}"
STEPS_OK=0
STEPS_TOTAL=0
step_ok() {
green " [OK] $1"
STEPS_OK=$((STEPS_OK + 1))
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
step_fail() {
red " [FAIL] $1"
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
step_dry_run() {
yellow " [DRY-RUN] $1"
STEPS_OK=$((STEPS_OK + 1))
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
echo "=== Comet Archive: $CHANGE ===" >&2
# --- Step 1: Read .comet.yaml, extract paths ---
yaml_field() {
local field="$1"
if [ -f "$STATE_SH" ]; then
"$COMET_BASH" "$STATE_SH" get "$CHANGE" "$field" 2>/dev/null
else
if [ -f "$YAML" ]; then
local value
value=$(grep "^${field}:" "$YAML" 2>/dev/null | sed "s/^${field}: *//" || true)
value=$(strip_inline_comment "$value")
strip_wrapping_quotes "$value"
fi
fi
}
strip_inline_comment() {
local value="$1"
printf '%s\n' "$value" | awk -v squote="'" '
{
out = ""
quote = ""
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1)
if (quote == "") {
if (c == "\"" || c == squote) {
quote = c
} else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
sub(/[[:space:]]+$/, "", out)
print out
next
}
} else if (c == quote) {
quote = ""
}
out = out c
}
print out
}
'
}
strip_wrapping_quotes() {
local value="$1"
case "$value" in
\"*\") printf '%s\n' "${value:1:${#value}-2}" ;;
\'*\') printf '%s\n' "${value:1:${#value}-2}" ;;
*) printf '%s\n' "$value" ;;
esac
}
if [ ! -f "$YAML" ]; then
red "FATAL: .comet.yaml not found in $CHANGE_DIR/"
exit 1
fi
DESIGN_DOC=$(yaml_field "design_doc")
PLAN_PATH=$(yaml_field "plan")
# --- Step 2: Validate entry state ---
PHASE_VAL=$(yaml_field "phase")
VERIFY_VAL=$(yaml_field "verify_result")
ARCHIVED_VAL=$(yaml_field "archived")
if [ "$PHASE_VAL" != "archive" ]; then
red "FATAL: phase is '$PHASE_VAL', expected 'archive'"
exit 1
fi
if [ "$VERIFY_VAL" != "pass" ]; then
red "FATAL: verify_result is '$VERIFY_VAL', expected 'pass'. Run comet-verify first."
exit 1
fi
if [ "$ARCHIVED_VAL" = "true" ]; then
red "FATAL: change already archived"
exit 1
fi
step_ok "Entry state verified"
# --- Step 3: Check archive target ---
if [ -d "$ARCHIVE_DIR" ]; then
red "FATAL: archive target already exists: $ARCHIVE_DIR"
exit 1
fi
step_ok "Archive target available"
# --- Step 4: Prepare document frontmatter annotation ---
annotate_frontmatter() {
local file="$1"
local extra_fields="$2"
if [ ! -f "$file" ]; then
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would annotate: $file"
return 0
fi
if head -1 "$file" | grep -q '^---'; then
local tmp_file
tmp_file=$(mktemp)
chmod 600 "$tmp_file"
awk -v archive="$ARCHIVE_NAME" -v extra="$extra_fields" '
/^archived-with:/ { next }
NR==1 && /^---/ { print; next }
/^---/ && NR>1 {
print "archived-with: " archive
if (extra != "") print extra
print; next
}
{ print }
' "$file" > "$tmp_file"
mv "$tmp_file" "$file"
else
local tmp_file
tmp_file=$(mktemp)
chmod 600 "$tmp_file"
{
echo "---"
echo "archived-with: $ARCHIVE_NAME"
if [ -n "$extra_fields" ]; then
echo "$extra_fields"
fi
echo "status: final"
echo "---"
cat "$file"
} > "$tmp_file"
mv "$tmp_file" "$file"
fi
step_ok "Annotated: $file"
}
# --- Step 5: Run OpenSpec archive for delta merge and move ---
verify_main_specs_clean() {
if [ ! -d "openspec/specs" ]; then
return 0
fi
local found=0
local matches
for spec_file in openspec/specs/*/spec.md; do
[ -f "$spec_file" ] || continue
matches=$(grep -nE '^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements$' "$spec_file" 2>/dev/null || true)
if [ -n "$matches" ]; then
red "FATAL: delta-only section heading leaked into main spec: $spec_file"
printf '%s\n' "$matches" >&2
found=1
fi
done
if [ "$found" -ne 0 ]; then
return 1
fi
return 0
}
resolve_archive_dir() {
if [ -d "$ARCHIVE_DIR" ]; then
return 0
fi
# Fallback: search for any directory matching *-$CHANGE in archive
local found
found=$(find "openspec/changes/archive" -maxdepth 1 -mindepth 1 -type d -name "*-$CHANGE" 2>/dev/null | head -1 || true)
if [ -n "$found" ]; then
ARCHIVE_DIR="$found"
ARCHIVE_NAME=$(basename "$found")
return 0
fi
return 1
}
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would run OpenSpec archive: $CHANGE"
else
if ! command -v "$COMET_OPENSPEC" >/dev/null 2>&1; then
red "FATAL: OpenSpec CLI not found: $COMET_OPENSPEC"
red "Install OpenSpec or set COMET_OPENSPEC to the openspec executable."
exit 1
fi
"$COMET_OPENSPEC" archive "$CHANGE" --yes >&2
if ! resolve_archive_dir; then
step_fail "OpenSpec archive output not found"
exit 1
else
step_ok "OpenSpec archive completed: $ARCHIVE_DIR"
fi
verify_main_specs_clean
step_ok "Main specs verified clean"
fi
# --- Step 6: Annotate design doc and plan frontmatter ---
if [ -n "$DESIGN_DOC" ] && [ "$DESIGN_DOC" != "null" ]; then
annotate_frontmatter "$DESIGN_DOC" "status: final"
fi
if [ -n "$PLAN_PATH" ] && [ "$PLAN_PATH" != "null" ]; then
annotate_frontmatter "$PLAN_PATH" ""
fi
# --- Step 7: Mark archived via comet-state transition ---
ARCHIVE_YAML="$ARCHIVE_DIR/.comet.yaml"
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would set archived: true in $ARCHIVE_YAML"
else
if [ -f "$ARCHIVE_YAML" ]; then
"$COMET_BASH" "$STATE_SH" transition "$ARCHIVE_NAME" archived >/dev/null
step_ok "archived: true"
else
step_fail "archived: true (.comet.yaml not found after move)"
fi
fi
# --- Step 8: Print summary ---
echo "" >&2
if [ "$DRY_RUN" -eq 1 ]; then
yellow "Dry run complete. $STEPS_OK/$STEPS_TOTAL steps would succeed."
else
green "Archive complete. $STEPS_OK/$STEPS_TOTAL steps succeeded."
fi
if [ "$STEPS_OK" -lt "$STEPS_TOTAL" ]; then
exit 1
fi
exit 0