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-hook-guard.sh

337 lines
12 KiB

#!/bin/bash
# comet-hook-guard.sh — PreToolUse hook for Comet phase enforcement
#
# Blocks file writes (Write/Edit) when the active Comet change is in
# a phase that does not allow source code modifications (open/design/archive).
#
# Usage (called by harness, not directly):
# PreToolUse matcher "Write|Edit" → this script
# Stdin: JSON {"tool_name":"Write|Edit","tool_input":{"file_path":"..."}}
# Exit 0 = allow
# Exit 2 = blocked (stderr message shown to user)
#
# Cross-platform: macOS / Linux / Windows Git Bash
# shellcheck disable=SC2329
set -euo pipefail
# ── Extract target file path ──────────────────────────────────────
TARGET=""
# Method 1: FILE_PATH environment variable (set by some harnesses)
if [ -n "${FILE_PATH:-}" ]; then
TARGET="$FILE_PATH"
fi
# Method 2: Parse stdin JSON
if [ -z "$TARGET" ]; then
INPUT=""
if [ ! -t 0 ]; then
INPUT=$(cat 2>/dev/null || true)
fi
if [ -n "$INPUT" ]; then
# Extract file_path value — works for both Write and Edit tool inputs
TARGET=$(printf '%s' "$INPUT" \
| grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null \
| head -1 \
| sed 's/^"file_path"[[:space:]]*:[[:space:]]*"//' \
| sed 's/"$//' \
|| true)
fi
fi
# No target found — allow (not a file-path-bearing operation)
if [ -z "$TARGET" ]; then
echo "[COMET-HOOK] allowed: no file path in tool input" >&2
exit 0
fi
# Normalize to forward slashes, collapse doubles from JSON escaping (\\ → //)
TARGET=$(printf '%s' "$TARGET" | sed 's|\\|/|g' | sed 's|///*|/|g')
# ── Resolve to project-relative path ─────────────────────────────
# Normalize helper: forward slashes only
norm() { printf '%s' "$1" | sed 's|\\|/|g'; }
RELPATH=$(norm "$TARGET")
# If already relative, use as-is
case "$RELPATH" in
/*|[A-Za-z]:/*)
# Absolute — try stripping CWD prefixes
CWD_UNIX=$(norm "$(pwd)")
CWD_PHYS=$(norm "$(pwd -P 2>/dev/null || pwd)")
# Try: TARGET as-is vs CWD logical
if [ "${RELPATH#"$CWD_UNIX"/}" != "$RELPATH" ]; then
RELPATH="${RELPATH#"$CWD_UNIX"/}"
# Try: TARGET as-is vs CWD physical (macOS /var → /private/var)
elif [ "${RELPATH#"$CWD_PHYS"/}" != "$RELPATH" ]; then
RELPATH="${RELPATH#"$CWD_PHYS"/}"
else
# Resolve TARGET's parent through filesystem (handles symlinked TARGET path)
_PDIR=$(cd "$(dirname "$TARGET")" 2>/dev/null && pwd -P 2>/dev/null || true)
if [ -n "$_PDIR" ]; then
_TRESOLVED=$(norm "${_PDIR}/$(basename "$TARGET")")
if [ "${_TRESOLVED#"$CWD_UNIX"/}" != "$_TRESOLVED" ]; then
RELPATH="${_TRESOLVED#"$CWD_UNIX"/}"
elif [ "${_TRESOLVED#"$CWD_PHYS"/}" != "$_TRESOLVED" ]; then
RELPATH="${_TRESOLVED#"$CWD_PHYS"/}"
fi
fi
fi
;;
esac
# ── Helpers to read .comet.yaml fields ───────────────────────────
is_archived() {
grep "^archived:" "$1" 2>/dev/null \
| awk '{print $2}' | tr -d '[:space:][:cntrl:]' || true
}
read_phase() {
grep "^phase:" "$1" 2>/dev/null \
| awk '{print $2}' | tr -d '[:space:][:cntrl:]' || true
}
read_field() {
grep "^$1:" "$2" 2>/dev/null \
| head -1 | awk '{print $2}' | tr -d '[:space:][:cntrl:]' || true
}
# ── Determine the governing Comet change + phase ─────────────────
#
# A write targeting a specific change directory (openspec/changes/<name>/...)
# must be governed by THAT change's own phase — never by an unrelated
# active change. Otherwise a change left in the `archive` phase would
# wrongly block artifact writes for a brand-new change created alongside it.
PHASE=""
# Path to the .comet.yaml that governs this write (used for deeper invariant checks)
GOV_YAML=""
case "$RELPATH" in
openspec/changes/*/*)
_rest="${RELPATH#openspec/changes/}"
_own_change="${_rest%%/*}"
if [ -n "$_own_change" ] && [ "$_own_change" != "archive" ]; then
_own_yaml="openspec/changes/${_own_change}/.comet.yaml"
if [ -f "$_own_yaml" ]; then
if [ "$(is_archived "$_own_yaml")" = "true" ]; then
# This change is already archived — its own writes are unrestricted
echo "[COMET-HOOK] allowed: $RELPATH (own change archived)" >&2
exit 0
fi
PHASE=$(read_phase "$_own_yaml")
GOV_YAML="$_own_yaml"
else
# Change directory exists but state file not yet written
# (artifacts are created before .comet.yaml during /comet-open).
# Treat as `open` so proposal/design/tasks/specs are allowed.
PHASE="open"
fi
fi
;;
esac
# Fallback: writes outside a specific change directory are governed by
# the first active (non-archived) change.
if [ -z "$PHASE" ]; then
YAML_FILE=""
if [ -d "openspec/changes" ]; then
for dir in openspec/changes/*/; do
[ -d "$dir" ] || continue
# Skip archived changes directory
case "$dir" in
*/archive/*) continue ;;
esac
if [ -f "${dir}.comet.yaml" ]; then
# Skip changes already marked as archived
if [ "$(is_archived "${dir}.comet.yaml")" = "true" ]; then
continue
fi
YAML_FILE="${dir}.comet.yaml"
break
fi
done
fi
# No active change — allow all writes
if [ -z "$YAML_FILE" ]; then
echo "[COMET-HOOK] allowed: no active comet change" >&2
exit 0
fi
PHASE=$(read_phase "$YAML_FILE")
GOV_YAML="$YAML_FILE"
fi
if [ -z "$PHASE" ]; then
echo "[COMET-HOOK] allowed: no phase in .comet.yaml" >&2
exit 0
fi
# ── Whitelist: phase-aware allowed paths ─────────────────────────
case "$RELPATH" in
openspec/*)
# OpenSpec artifacts — phase-aware sub-check
case "$PHASE" in
open)
# open: allow proposal, design, tasks, yaml, handoff, specs
case "$RELPATH" in
*/proposal.md|*/design.md|*/tasks.md|*/.openspec.yaml|*/.comet.yaml|*/.comet/*|*/specs/*)
echo "[COMET-HOOK] allowed: $RELPATH (phase: open, openspec artifacts)" >&2
exit 0
;;
esac
;;
design)
# design: allow handoff, delta spec (Spec Patch), proposal/design/tasks (minor refinements), .comet.yaml
case "$RELPATH" in
*/proposal.md|*/design.md|*/tasks.md|*/.comet/*|*/specs/*|*/.comet.yaml|*/.openspec.yaml)
echo "[COMET-HOOK] allowed: $RELPATH (phase: design, handoff/spec)" >&2
exit 0
;;
esac
;;
build)
# build: allow delta spec (incremental update), tasks, .comet.yaml
case "$RELPATH" in
*/specs/*|*/tasks.md|*/.comet.yaml|*/.openspec.yaml)
echo "[COMET-HOOK] allowed: $RELPATH (phase: build, spec/tasks)" >&2
exit 0
;;
esac
;;
verify)
# verify: allow tasks (post-check), .comet.yaml
case "$RELPATH" in
*/tasks.md|*/.comet.yaml|*/.openspec.yaml)
echo "[COMET-HOOK] allowed: $RELPATH (phase: verify, tasks/state)" >&2
exit 0
;;
esac
;;
archive)
# archive: allow .comet.yaml state updates only
case "$RELPATH" in
*/.comet.yaml|*/.openspec.yaml)
echo "[COMET-HOOK] allowed: $RELPATH (phase: archive, state)" >&2
exit 0
;;
esac
;;
esac
;;
docs/superpowers/*)
# Superpowers artifacts — phase-aware sub-check
case "$PHASE" in
design)
echo "[COMET-HOOK] allowed: $RELPATH (phase: design, superpowers)" >&2
exit 0
;;
build)
echo "[COMET-HOOK] allowed: $RELPATH (phase: build, superpowers)" >&2
exit 0
;;
verify)
echo "[COMET-HOOK] allowed: $RELPATH (phase: verify, superpowers)" >&2
exit 0
;;
esac
# open/archive: block docs/superpowers writes
;;
.comet/*|*/.comet/*)
# Comet config
echo "[COMET-HOOK] allowed: $RELPATH (whitelist: comet config)" >&2
exit 0
;;
.claude/*)
# Claude settings/rules
echo "[COMET-HOOK] allowed: $RELPATH (whitelist: claude config)" >&2
exit 0
;;
CLAUDE.md|CHANGELOG.md|README.md|*.md)
# Root-level markdown files
case "$RELPATH" in
*/*) ;; # subdirectory .md — NOT whitelisted, fall through
*)
echo "[COMET-HOOK] allowed: $RELPATH (whitelist: root markdown)" >&2
exit 0
;;
esac
;;
.comet.yaml|comet.yaml|.comet.yml|comet.yml)
# Project-level comet config
echo "[COMET-HOOK] allowed: $RELPATH (whitelist: comet config)" >&2
exit 0
;;
esac
# ── Phase-based enforcement ──────────────────────────────────────
case "$PHASE" in
build|verify)
# Full workflow must have a Design Doc before any source write in build/verify.
# Catches illegal open→build / design→build jumps that skipped the design phase
# (e.g. misclassified preset, direct `set phase`, or bare transition).
if [ -n "$GOV_YAML" ]; then
_wf=$(read_field "workflow" "$GOV_YAML")
_dd=$(read_field "design_doc" "$GOV_YAML")
if [ "$_wf" = "full" ] && { [ -z "$_dd" ] || [ "$_dd" = "null" ]; }; then
echo "" >&2
echo "╔══════════════════════════════════════════╗" >&2
echo "║ COMET PHASE GUARD — WRITE BLOCKED ║" >&2
echo "╚══════════════════════════════════════════╝" >&2
echo "" >&2
echo " Current phase: $PHASE (workflow: full), but design_doc is empty" >&2
echo " Target file: $RELPATH" >&2
echo "" >&2
echo " ❌ Illegal phase jump detected: full workflow entered $PHASE without a Design Doc" >&2
echo " ✅ Correct flow: create the Design Doc in design phase, then run comet-guard design --apply" >&2
echo " 💡 Run /comet-design to fill the missing design; for repair, set design_doc with comet-state" >&2
echo "" >&2
exit 2
fi
fi
# Code writes allowed in build and verify
echo "[COMET-HOOK] allowed: $RELPATH (phase: $PHASE)" >&2
exit 0
;;
open|design|archive)
echo "" >&2
echo "╔══════════════════════════════════════════╗" >&2
echo "║ COMET PHASE GUARD — WRITE BLOCKED ║" >&2
echo "╚══════════════════════════════════════════╝" >&2
echo "" >&2
echo " Current phase: $PHASE" >&2
echo " Target file: $RELPATH" >&2
echo "" >&2
case "$PHASE" in
open)
echo " ❌ open phase does not allow source code writes" >&2
echo " ✅ Allowed: create proposal/design/tasks and run guard" >&2
echo " 💡 After clarification and artifact creation, run guard --apply" >&2
;;
design)
echo " ❌ design phase does not allow source code writes" >&2
echo " ✅ Allowed: brainstorming, create the Design Doc, and run guard" >&2
echo " 💡 After the Design Doc is ready, run comet-guard design --apply to enter build" >&2
;;
archive)
echo " ❌ archive phase does not allow source code writes" >&2
echo " ✅ Allowed: confirm archive intent and run the archive script" >&2
;;
esac
echo "" >&2
exit 2
;;
esac
echo "[COMET-HOOK] allowed: $RELPATH (phase: $PHASE)" >&2
exit 0