feat(plan-01): Phase 0.5 — path indirection via casan-paths.sh (no file moves)
Task 1.2: introduce a single path resolver so no harness script hardcodes `.specify/...` scattered across the tree. casan-paths.sh resolves four roots (HARNESS/STATE/GOVERNANCE/APP) by marker-based walk-up from its own location — never `git rev-parse` (git root is the repo PARENT here, not the app dir). - 101 bash scripts/tests: 238 hardcoded `$PROJECT_ROOT/.specify/...` refs rewritten to CASAN_HARNESS_ROOT (code) / CASAN_STATE_ROOT (logs,state) / CASAN_GOVERNANCE_ROOT. Sandbox test vars ($WORK/$TP/$FP/$T1_WORK) left untouched. - Roots are NOT exported: each script/subprocess self-resolves from its own tree, matching the original per-script semantics and preserving hermetic sandbox isolation (node casan-step.mjs, copied telemetry/rollback scripts must not inherit real roots). - Sandbox tests that copy a harness script now also copy casan-paths.sh (its new sibling dependency): adversarial (verify-audit-chain/verify-tool-audit/rollback) + track-a (security-check/telemetry-integrity). - control-plane-settings.json reclassified as STATE (untracked runtime store). Roots all still resolve to `.specify` in this monolithic layout, so behavior is unchanged. Full gate: PASS=64 FAIL=0 SKIP=3 (adversarial 44/0, track-a 25/0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
61c3a7c648
commit
2c765c9a45
@@ -23,8 +23,9 @@ set -uo pipefail
|
||||
# approver identity (audited) — approval is never silent.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG="$PROJECT_ROOT/.specify/logs/level5/action-gate.jsonl"
|
||||
LOG="$CASAN_STATE_ROOT/logs/level5/action-gate.jsonl"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
# shellcheck source=casan-log.sh
|
||||
source "$SCRIPT_DIR/casan-log.sh"
|
||||
|
||||
@@ -21,13 +21,14 @@ if [[ -z "$INPUT_FILE" || -z "$OUTPUT_FILE" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs"
|
||||
TRACE_DIR="$LOG_DIR/trace"
|
||||
# SEC-23 (MT-01): telemetry dir is tenant-scoped when CASAN_METRICS_DIR is set
|
||||
# (tenant-paths.sh exports it per tenant); default is the shared path.
|
||||
METRICS_DIR="${CASAN_METRICS_DIR:-$LOG_DIR/cost}"
|
||||
ALERT_LOG="$PROJECT_ROOT/.specify/agentops/alerts.log"
|
||||
ALERT_LOG="$CASAN_HARNESS_ROOT/agentops/alerts.log"
|
||||
METRICS_LOG="$METRICS_DIR/metrics.jsonl"
|
||||
mkdir -p "$TRACE_DIR" "$METRICS_DIR" "$(dirname "$OUTPUT_FILE")" "$(dirname "$ALERT_LOG")"
|
||||
|
||||
@@ -126,7 +127,7 @@ COST_SOURCE="word_count_estimate"
|
||||
|
||||
# Prefer real provider usage when telemetry has been imported; the word-count
|
||||
# figure above is an explicit fallback, not presented as a real billed cost.
|
||||
PROVIDER_LOG="$PROJECT_ROOT/.specify/logs/level5/provider-usage.jsonl"
|
||||
PROVIDER_LOG="$CASAN_STATE_ROOT/logs/level5/provider-usage.jsonl"
|
||||
if [[ -f "$PROVIDER_LOG" ]] && command -v python >/dev/null 2>&1; then
|
||||
# Use real provider telemetry ONLY when a record genuinely matches this step.
|
||||
# Do NOT fall back to an arbitrary record (that would reuse one sample's cost
|
||||
@@ -140,7 +141,7 @@ if [[ -f "$PROVIDER_LOG" ]] && command -v python >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
# Real hallucination-signal detection (populates hallucination-tracking.yaml's metric).
|
||||
HALLU_YAML="$PROJECT_ROOT/.specify/agentops/hallucination-tracking.yaml"
|
||||
HALLU_YAML="$CASAN_HARNESS_ROOT/agentops/hallucination-tracking.yaml"
|
||||
HALLUCINATION_SIGNALS=0
|
||||
HALLUCINATION_MATCHED="[]"
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
|
||||
@@ -21,8 +21,9 @@ set -uo pipefail
|
||||
# ALERT_DEADLETTER_FLUSHED | ALERT_WEBHOOK_UNSET
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
AGENTOPS_DIR="${CASAN_AGENTOPS_DIR:-$PROJECT_ROOT/.specify/agentops}"
|
||||
AGENTOPS_DIR="${CASAN_AGENTOPS_DIR:-$CASAN_HARNESS_ROOT/agentops}"
|
||||
STATE="$AGENTOPS_DIR/alert-dispatch-state.jsonl"
|
||||
DEADLETTER="$AGENTOPS_DIR/alert-deadletter.jsonl"
|
||||
WEBHOOK="${CASAN_ALERT_WEBHOOK:-}"
|
||||
|
||||
@@ -28,8 +28,9 @@ set -uo pipefail
|
||||
# Exit: 0 ok (prints "APPROVAL_OK role=<role>"), 3 deny (reason on stderr), 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
GOV_DIR="$PROJECT_ROOT/.specify/level5/central-governance"
|
||||
GOV_DIR="$CASAN_GOVERNANCE_ROOT"
|
||||
REVIEWERS_FILE="${CASAN_REVIEWERS_FILE:-$GOV_DIR/reviewers.registry}"
|
||||
REVIEWERS_DIR="${CASAN_REVIEWERS_DIR:-$GOV_DIR/reviewers}"
|
||||
|
||||
@@ -52,7 +53,7 @@ sha_stdin() {
|
||||
record_nonce_or_deny() {
|
||||
local nonce="$1"
|
||||
[[ "${CASAN_PROFILE:-}" == "prod" || -n "${CASAN_APPROVAL_NONCE_FILE:-}" ]] || return 0
|
||||
local ledger="${CASAN_APPROVAL_NONCE_FILE:-$PROJECT_ROOT/.specify/logs/level5/approval-nonces.txt}"
|
||||
local ledger="${CASAN_APPROVAL_NONCE_FILE:-$CASAN_STATE_ROOT/logs/level5/approval-nonces.txt}"
|
||||
mkdir -p "$(dirname "$ledger")" 2>/dev/null || true
|
||||
if [[ -f "$ledger" ]] && grep -qxF "$nonce" "$ledger" 2>/dev/null; then
|
||||
deny "approval_replayed(nonce=${nonce:0:12}…)"
|
||||
|
||||
@@ -11,9 +11,10 @@ set -uo pipefail
|
||||
# Env: CASAN_WORM_LEDGER (default .specify/logs/worm/anchor-ledger.jsonl)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
HEAD_FILE="${1:-$PROJECT_ROOT/.specify/logs/audit/audit-head.txt}"
|
||||
LEDGER="${2:-${CASAN_WORM_LEDGER:-$PROJECT_ROOT/.specify/logs/worm/anchor-ledger.jsonl}}"
|
||||
HEAD_FILE="${1:-$CASAN_STATE_ROOT/logs/audit/audit-head.txt}"
|
||||
LEDGER="${2:-${CASAN_WORM_LEDGER:-$CASAN_STATE_ROOT/logs/worm/anchor-ledger.jsonl}}"
|
||||
mkdir -p "$(dirname "$LEDGER")"
|
||||
|
||||
[[ -f "$HEAD_FILE" ]] || { echo "AUDIT_SHIP_NO_HEAD file=$HEAD_FILE" >&2; exit 1; }
|
||||
|
||||
@@ -20,8 +20,9 @@ set -uo pipefail
|
||||
# Exit: 0 within budget (or --no-gate), 1 budget exceeded, 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
SEC_DIR="$PROJECT_ROOT/.specify/security"
|
||||
SEC_DIR="$CASAN_HARNESS_ROOT/security"
|
||||
CORPUS_DIR="$SEC_DIR/benign-corpus"
|
||||
VECTORS="$SEC_DIR/redteam-vectors.jsonl"
|
||||
|
||||
|
||||
@@ -22,14 +22,15 @@ if [[ -z "$INPUT_FILE" || -z "$FINAL_OUTPUT" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# SEC-23 (MT-01): make state (control-plane settings, telemetry, audit) tenant-scoped
|
||||
# when CASAN_TENANT_ID is set, so a run for tenant A never touches tenant B's state.
|
||||
# No-op when no tenant is set (baseline unchanged); invalid tenant fails closed.
|
||||
# shellcheck source=tenant-paths.sh
|
||||
source "$SCRIPT_DIR/tenant-paths.sh"
|
||||
TMP_DIR="$PROJECT_ROOT/.specify/logs/tmp"
|
||||
CACHE_DIR="$PROJECT_ROOT/.specify/logs/idempotency"
|
||||
TMP_DIR="$CASAN_STATE_ROOT/logs/tmp"
|
||||
CACHE_DIR="$CASAN_STATE_ROOT/logs/idempotency"
|
||||
mkdir -p "$TMP_DIR" "$CACHE_DIR" "$(dirname "$FINAL_OUTPUT")"
|
||||
|
||||
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL).
|
||||
@@ -117,7 +118,7 @@ fi
|
||||
# prod-without-a-manifest are unaffected); a present-but-drifted bundle fails closed.
|
||||
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
|
||||
&& -f "$SCRIPT_DIR/bundle-integrity.py" ]]; then
|
||||
BUNDLE_MANIFEST="${CASAN_BUNDLE_MANIFEST:-$PROJECT_ROOT/.specify/level5/central-governance/harness-bundle-manifest.json}"
|
||||
BUNDLE_MANIFEST="${CASAN_BUNDLE_MANIFEST:-$CASAN_GOVERNANCE_ROOT/harness-bundle-manifest.json}"
|
||||
if [[ -f "$BUNDLE_MANIFEST" ]]; then
|
||||
if ! python "$SCRIPT_DIR/bundle-integrity.py" verify >/dev/null 2>&1; then
|
||||
casan_log error harness "BUNDLE_INTEGRITY_DRIFT — refusing to run $ACTION_NAME (harness/policy modified vs signed manifest)"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# CASAN path resolver — single source of truth for harness / state / governance roots.
|
||||
#
|
||||
# Plan-01 restructure: physical file moves (`.specify/` -> `packages/casan-harness/`,
|
||||
# domain -> `apps/okr/domain/`) are absorbed HERE. Callers must reference the
|
||||
# CASAN_*_ROOT variables below instead of hardcoding ".specify/...", so a move only
|
||||
# changes this file, not the 37+ scripts that consume the paths.
|
||||
#
|
||||
# Resolution rules (per root):
|
||||
# - An explicit env override always wins (e.g. CASAN_HARNESS_ROOT=... bash foo.sh).
|
||||
# - CASAN_HARNESS_ROOT derives from THIS file's own location: casan-paths.sh always
|
||||
# lives at <harness-root>/scripts/bash/casan-paths.sh, so `../..` is the harness
|
||||
# root wherever the tree is moved. No edit needed when the package relocates.
|
||||
# - CASAN_APP_ROOT is found by walking UP for the `.specify` state marker.
|
||||
# NEVER use `git rev-parse --show-toplevel`: in this checkout the git root is the
|
||||
# repo PARENT, not the app dir, which would shift every path up one level.
|
||||
#
|
||||
# Idempotent and `set -e` safe: sourcing multiple times is a no-op; nothing here
|
||||
# returns a non-zero status to a caller running under `set -euo pipefail`.
|
||||
|
||||
if [[ -n "${CASAN_PATHS_SOURCED:-}" ]]; then
|
||||
return 0 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_casan_paths_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Walk up from a starting dir until a dir containing `.specify/` is found.
|
||||
_casan_find_app_root() {
|
||||
local d="$1"
|
||||
while [[ -n "$d" && "$d" != "/" ]]; do
|
||||
if [[ -d "$d/.specify" ]]; then
|
||||
printf '%s\n' "$d"
|
||||
return 0
|
||||
fi
|
||||
d="$(dirname "$d")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Harness (CODE) root: the tree that holds scripts/, security/, templates/, tests/,
|
||||
# config/, level5/*.yaml. Derived from this file's location so it follows the move.
|
||||
if [[ -z "${CASAN_HARNESS_ROOT:-}" ]]; then
|
||||
CASAN_HARNESS_ROOT="$(cd "$_casan_paths_self/../.." && pwd)"
|
||||
fi
|
||||
|
||||
# App root: where runtime state lives (holds the `.specify` marker). Walk up from the
|
||||
# harness root; fall back to the classic `<harness>/..`-style layout if not found.
|
||||
if [[ -z "${CASAN_APP_ROOT:-}" ]]; then
|
||||
CASAN_APP_ROOT="$(_casan_find_app_root "$_casan_paths_self" || true)"
|
||||
fi
|
||||
if [[ -z "${CASAN_APP_ROOT:-}" ]]; then
|
||||
CASAN_APP_ROOT="$(cd "$CASAN_HARNESS_ROOT/.." && pwd)"
|
||||
fi
|
||||
|
||||
# State (RUNTIME) root: logs/, state/, agentops/ — stays with the app, not the package.
|
||||
if [[ -z "${CASAN_STATE_ROOT:-}" ]]; then
|
||||
CASAN_STATE_ROOT="$CASAN_APP_ROOT/.specify"
|
||||
fi
|
||||
|
||||
# Governance root: central-governance mixes harness pub-keys/registries, runtime
|
||||
# policy-manifest state, and a private key. Rooted under STATE (not HARNESS) so the
|
||||
# runtime-regenerated policy-manifest.{json,sig} and the private key never land inside
|
||||
# the shipped code package; it stays at .specify/level5/central-governance across the
|
||||
# move. Plan-01 can still split individual files later via per-file overrides
|
||||
# (CASAN_AUDIT_PUB, CASAN_AGENT_REGISTRY, CASAN_AGENT_KEYS_DIR, CASAN_BUNDLE_MANIFEST).
|
||||
if [[ -z "${CASAN_GOVERNANCE_ROOT:-}" ]]; then
|
||||
CASAN_GOVERNANCE_ROOT="$CASAN_STATE_ROOT/level5/central-governance"
|
||||
fi
|
||||
|
||||
# NOTE: roots are deliberately NOT exported. Each script/test sources this resolver
|
||||
# and self-derives its roots from its OWN location (BASH_SOURCE), matching the original
|
||||
# per-script `PROJECT_ROOT="$SCRIPT_DIR/../../.."` semantics. Exporting them would leak
|
||||
# the caller's (real-repo) roots into sandbox subprocesses (e.g. `node casan-step.mjs`,
|
||||
# copied telemetry/rollback scripts), breaking hermetic test isolation. The idempotency
|
||||
# flag is likewise NOT exported, so every subprocess re-resolves against its own tree.
|
||||
CASAN_PATHS_SOURCED=1
|
||||
@@ -12,8 +12,9 @@ set -uo pipefail
|
||||
# CASAN_CI_SUITE_FILTER optional regex; run matching suite names only
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
TESTS="$ROOT/.specify/tests"
|
||||
TESTS="$CASAN_HARNESS_ROOT/tests"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
@@ -22,8 +22,9 @@ set -uo pipefail
|
||||
# Exit: 0 all OK, 1 bypass found or circuit open.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
PROVIDER_LOG="${CASAN_PROVIDER_LOG:-$ROOT/.specify/logs/level5/provider-usage.jsonl}"
|
||||
PROVIDER_LOG="${CASAN_PROVIDER_LOG:-$CASAN_STATE_ROOT/logs/level5/provider-usage.jsonl}"
|
||||
CIRCUIT_BREAKER_THRESHOLD="${CIRCUIT_BREAKER_THRESHOLD:-5}"
|
||||
CIRCUIT_WINDOW="${CIRCUIT_WINDOW:-10}"
|
||||
CIRCUIT_WINDOW_FAIL_PCT="${CIRCUIT_WINDOW_FAIL_PCT:-50}"
|
||||
@@ -39,8 +40,8 @@ echo "=== CASAN WP-S6: no-bypass + circuit breaker ==="
|
||||
if [[ "$MODE" != "--breaker-only" ]]; then
|
||||
echo "--- no-bypass scan ---"
|
||||
SCAN_DIRS=(
|
||||
"$ROOT/.specify/scripts/bash"
|
||||
"$ROOT/.specify/tests"
|
||||
"$CASAN_HARNESS_ROOT/scripts/bash"
|
||||
"$CASAN_HARNESS_ROOT/tests"
|
||||
"$ROOT/scripts"
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ set -uo pipefail
|
||||
# 3 not enough data for the RELATIVE test and no absolute/cumulative cap set.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# SEC-23 (MT-03): per-tenant cost/quota. With no explicit log arg, a tenant run
|
||||
# evaluates its OWN usage log so one tenant's spend never counts against another's
|
||||
@@ -29,7 +30,7 @@ elif [[ -n "${CASAN_TENANT_ID:-}" ]]; then
|
||||
LOG="$(bash "$SCRIPT_DIR/tenant-store.sh" resolve telemetry/provider-usage.jsonl 2>/dev/null)" \
|
||||
|| { echo "COST_SPIKE_TENANT_DENIED" >&2; exit 3; }
|
||||
else
|
||||
LOG="$PROJECT_ROOT/.specify/logs/level5/provider-usage.jsonl"
|
||||
LOG="$CASAN_STATE_ROOT/logs/level5/provider-usage.jsonl"
|
||||
fi
|
||||
MULT="${2:-3.0}"
|
||||
|
||||
|
||||
@@ -1,313 +1,314 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
arg="${!i}"
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--short-name)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
# Check if the next argument is another option (starts with --)
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_NAME="$next_arg"
|
||||
;;
|
||||
--number)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
|
||||
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to find the repository root by searching for existing project markers
|
||||
find_repo_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to get highest number from specs directory
|
||||
get_highest_from_specs() {
|
||||
local specs_dir="$1"
|
||||
local highest=0
|
||||
|
||||
if [ -d "$specs_dir" ]; then
|
||||
for dir in "$specs_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from git branches
|
||||
get_highest_from_branches() {
|
||||
local highest=0
|
||||
|
||||
# Get all branches (local and remote)
|
||||
branches=$(git branch -a 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$branches" ]; then
|
||||
while IFS= read -r branch; do
|
||||
# Clean branch name: remove leading markers and remote prefixes
|
||||
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||
|
||||
# Extract feature number if branch matches pattern ###-*
|
||||
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done <<< "$branches"
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to check existing branches (local and remote) and return next available number
|
||||
check_existing_branches() {
|
||||
local specs_dir="$1"
|
||||
|
||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||
git fetch --all --prune 2>/dev/null || true
|
||||
|
||||
# Get highest number from ALL branches (not just matching short name)
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
|
||||
# Get highest number from ALL specs (not just matching short name)
|
||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||
|
||||
# Take the maximum of both
|
||||
local max_num=$highest_branch
|
||||
if [ "$highest_spec" -gt "$max_num" ]; then
|
||||
max_num=$highest_spec
|
||||
fi
|
||||
|
||||
# Return next number
|
||||
echo $((max_num + 1))
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# Resolve repository root. Prefer git information when available, but fall back
|
||||
# to searching for repository markers so the workflow still functions in repositories that
|
||||
# were initialised with --no-git.
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
HAS_GIT=true
|
||||
else
|
||||
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
HAS_GIT=false
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
# Function to generate branch name with stop word filtering and length filtering
|
||||
generate_branch_name() {
|
||||
local description="$1"
|
||||
|
||||
# Common stop words to filter out
|
||||
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
||||
|
||||
# Convert to lowercase and split into words
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
# Skip empty words
|
||||
[ -z "$word" ] && continue
|
||||
|
||||
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
||||
# Keep short words if they appear as uppercase in original (likely acronyms)
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If we have meaningful words, use first 3-4 of them
|
||||
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
||||
local max_words=3
|
||||
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
||||
|
||||
local result=""
|
||||
local count=0
|
||||
for word in "${meaningful_words[@]}"; do
|
||||
if [ $count -ge $max_words ]; then break; fi
|
||||
if [ -n "$result" ]; then result="$result-"; fi
|
||||
result="$result$word"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$result"
|
||||
else
|
||||
# Fallback to original logic if no meaningful words found
|
||||
local cleaned=$(clean_branch_name "$description")
|
||||
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate branch name
|
||||
if [ -n "$SHORT_NAME" ]; then
|
||||
# Use provided short name, just clean it up
|
||||
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||
else
|
||||
# Generate from description with smart filtering
|
||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||
fi
|
||||
|
||||
# Determine branch number
|
||||
if [ -z "$BRANCH_NUMBER" ]; then
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
# Check existing branches on remotes
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||
else
|
||||
# Fall back to local directory check
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
MAX_BRANCH_LENGTH=244
|
||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
||||
|
||||
# Truncate suffix at word boundary if possible
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
# Remove trailing hyphen if truncation created one
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
fi
|
||||
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||
# Check if branch already exists
|
||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||
exit 1
|
||||
else
|
||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||
|
||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||
export SPECIFY_FEATURE="$BRANCH_NAME"
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||
fi
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
arg="${!i}"
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--short-name)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
# Check if the next argument is another option (starts with --)
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_NAME="$next_arg"
|
||||
;;
|
||||
--number)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
|
||||
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to find the repository root by searching for existing project markers
|
||||
find_repo_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to get highest number from specs directory
|
||||
get_highest_from_specs() {
|
||||
local specs_dir="$1"
|
||||
local highest=0
|
||||
|
||||
if [ -d "$specs_dir" ]; then
|
||||
for dir in "$specs_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from git branches
|
||||
get_highest_from_branches() {
|
||||
local highest=0
|
||||
|
||||
# Get all branches (local and remote)
|
||||
branches=$(git branch -a 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$branches" ]; then
|
||||
while IFS= read -r branch; do
|
||||
# Clean branch name: remove leading markers and remote prefixes
|
||||
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||
|
||||
# Extract feature number if branch matches pattern ###-*
|
||||
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done <<< "$branches"
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to check existing branches (local and remote) and return next available number
|
||||
check_existing_branches() {
|
||||
local specs_dir="$1"
|
||||
|
||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||
git fetch --all --prune 2>/dev/null || true
|
||||
|
||||
# Get highest number from ALL branches (not just matching short name)
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
|
||||
# Get highest number from ALL specs (not just matching short name)
|
||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||
|
||||
# Take the maximum of both
|
||||
local max_num=$highest_branch
|
||||
if [ "$highest_spec" -gt "$max_num" ]; then
|
||||
max_num=$highest_spec
|
||||
fi
|
||||
|
||||
# Return next number
|
||||
echo $((max_num + 1))
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# Resolve repository root. Prefer git information when available, but fall back
|
||||
# to searching for repository markers so the workflow still functions in repositories that
|
||||
# were initialised with --no-git.
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
HAS_GIT=true
|
||||
else
|
||||
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
HAS_GIT=false
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
# Function to generate branch name with stop word filtering and length filtering
|
||||
generate_branch_name() {
|
||||
local description="$1"
|
||||
|
||||
# Common stop words to filter out
|
||||
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
||||
|
||||
# Convert to lowercase and split into words
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
# Skip empty words
|
||||
[ -z "$word" ] && continue
|
||||
|
||||
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
||||
# Keep short words if they appear as uppercase in original (likely acronyms)
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If we have meaningful words, use first 3-4 of them
|
||||
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
||||
local max_words=3
|
||||
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
||||
|
||||
local result=""
|
||||
local count=0
|
||||
for word in "${meaningful_words[@]}"; do
|
||||
if [ $count -ge $max_words ]; then break; fi
|
||||
if [ -n "$result" ]; then result="$result-"; fi
|
||||
result="$result$word"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$result"
|
||||
else
|
||||
# Fallback to original logic if no meaningful words found
|
||||
local cleaned=$(clean_branch_name "$description")
|
||||
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate branch name
|
||||
if [ -n "$SHORT_NAME" ]; then
|
||||
# Use provided short name, just clean it up
|
||||
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||
else
|
||||
# Generate from description with smart filtering
|
||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||
fi
|
||||
|
||||
# Determine branch number
|
||||
if [ -z "$BRANCH_NUMBER" ]; then
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
# Check existing branches on remotes
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||
else
|
||||
# Fall back to local directory check
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
MAX_BRANCH_LENGTH=244
|
||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
||||
|
||||
# Truncate suffix at word boundary if possible
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
# Remove trailing hyphen if truncation created one
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
fi
|
||||
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||
# Check if branch already exists
|
||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||
exit 1
|
||||
else
|
||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
TEMPLATE="$CASAN_HARNESS_ROOT/templates/spec-template.md"
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||
|
||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||
export SPECIFY_FEATURE="$BRANCH_NAME"
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
@@ -17,11 +17,12 @@ set -uo pipefail
|
||||
# Greppable outputs: DASHBOARD_HOSTED | DASHBOARD_STOPPED | DASHBOARD_NOT_RUNNING
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
AGENTOPS_DIR="${CASAN_AGENTOPS_DIR:-$PROJECT_ROOT/.specify/agentops}"
|
||||
AGENTOPS_DIR="${CASAN_AGENTOPS_DIR:-$CASAN_HARNESS_ROOT/agentops}"
|
||||
PID_FILE="$AGENTOPS_DIR/dashboard.pid"
|
||||
PORT_FILE="$AGENTOPS_DIR/dashboard.port"
|
||||
GENERATOR="$PROJECT_ROOT/.specify/tests/generate-agentops-dashboard.py"
|
||||
GENERATOR="$CASAN_HARNESS_ROOT/tests/generate-agentops-dashboard.py"
|
||||
CMD="${1:-start}"
|
||||
mkdir -p "$AGENTOPS_DIR"
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ if [[ -z "$GOLDEN" || -z "$CANDIDATE" || -z "$REPORT" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
mkdir -p "$(dirname "$REPORT")" "$PROJECT_ROOT/.specify/logs/level5"
|
||||
mkdir -p "$(dirname "$REPORT")" "$CASAN_STATE_ROOT/logs/level5"
|
||||
|
||||
python - "$GOLDEN" "$CANDIDATE" "$REPORT" <<'PY'
|
||||
import difflib
|
||||
|
||||
@@ -21,6 +21,7 @@ set -uo pipefail
|
||||
# Exit: 0 ok, 1 verify failed, 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=casan-log.sh
|
||||
source "$SCRIPT_DIR/casan-log.sh"
|
||||
@@ -38,8 +39,8 @@ done
|
||||
[[ -z "$CMD" || -z "$RUN_ID" ]] && { echo "Usage: evidence-pack.sh {pack|verify-pack} <run-id> [--out/--dir <dir>]" >&2; exit 64; }
|
||||
[[ -z "$PACK_DIR" ]] && PACK_DIR="$PACKS_ROOT/$RUN_ID"
|
||||
|
||||
AUDIT_PRIV="${CASAN_AUDIT_PRIV:-$PROJECT_ROOT/.specify/level5/central-governance/audit-private.pem}"
|
||||
AUDIT_PUB="${CASAN_AUDIT_PUB:-$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem}"
|
||||
AUDIT_PRIV="${CASAN_AUDIT_PRIV:-$CASAN_GOVERNANCE_ROOT/audit-private.pem}"
|
||||
AUDIT_PUB="${CASAN_AUDIT_PUB:-$CASAN_GOVERNANCE_ROOT/audit-public.pem}"
|
||||
|
||||
run_status() { # <command...> -> prints "<first-stdout-line>|<rc>"
|
||||
local out rc
|
||||
|
||||
@@ -18,8 +18,9 @@ if [[ -z "$INPUT_FILE" || -z "$OUTPUT_FILE" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs"
|
||||
TRACE_DIR="$LOG_DIR/trace"
|
||||
AUDIT_DIR="$LOG_DIR/audit"
|
||||
mkdir -p "$TRACE_DIR" "$AUDIT_DIR" "$(dirname "$OUTPUT_FILE")"
|
||||
@@ -200,7 +201,7 @@ fi
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
# Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the
|
||||
# public key is committed. Production: replace with KMS/HSM.
|
||||
PUB_DIR="$PROJECT_ROOT/.specify/level5/central-governance"
|
||||
PUB_DIR="$CASAN_GOVERNANCE_ROOT"
|
||||
PRIV_DIR="${CASAN_AUDIT_KEY_DIR:-$HOME/.casan/audit-keys}"
|
||||
AUDIT_PRIV="$PRIV_DIR/audit-private.pem"
|
||||
AUDIT_PUB="$PUB_DIR/audit-public.pem"
|
||||
|
||||
@@ -12,8 +12,9 @@ if [[ -z "$INPUT_JSON" || ! -f "$INPUT_JSON" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs/level5"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||
OUT="$LOG_DIR/provider-usage.jsonl"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ set -uo pipefail
|
||||
# Exit: 0 recorded (LOW/MED) · 2 kill-switch engaged (HIGH/CRIT) · 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
SEC_DIR="$PROJECT_ROOT/.specify/security"
|
||||
LOG="$PROJECT_ROOT/.specify/logs/level5/incidents.jsonl"
|
||||
SEC_DIR="$CASAN_HARNESS_ROOT/security"
|
||||
LOG="$CASAN_STATE_ROOT/logs/level5/incidents.jsonl"
|
||||
RUNBOOK="$SEC_DIR/incident-runbook.md"
|
||||
SEVMAP="$SEC_DIR/incident-severity.map"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
|
||||
@@ -17,8 +17,9 @@ set -uo pipefail
|
||||
# Env: CASAN_KILLSWITCH_DIR (default .specify/logs/level5/kill-switch)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
KS_DIR="${CASAN_KILLSWITCH_DIR:-$PROJECT_ROOT/.specify/logs/level5/kill-switch}"
|
||||
KS_DIR="${CASAN_KILLSWITCH_DIR:-$CASAN_STATE_ROOT/logs/level5/kill-switch}"
|
||||
mkdir -p "$KS_DIR"
|
||||
|
||||
CMD="${1:-}"; SCOPE="${2:-}"; ID="${3:-}"; REASON="${4:-unspecified}"
|
||||
|
||||
@@ -21,8 +21,9 @@ set -uo pipefail
|
||||
# Exit: 0 match/pinned/warned · 2 MISMATCH in block mode · 3 unpinned/undeterminable.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
PIN_FILE="${CASAN_MODEL_DIGEST_PIN:-$PROJECT_ROOT/.specify/security/model-digest.pin}"
|
||||
PIN_FILE="${CASAN_MODEL_DIGEST_PIN:-$CASAN_HARNESS_ROOT/security/model-digest.pin}"
|
||||
mkdir -p "$(dirname "$PIN_FILE")"
|
||||
CMD="${1:-verify}"
|
||||
MODEL="${2:-${CASAN_MODEL:-ornith:9b}}"
|
||||
|
||||
@@ -34,8 +34,9 @@ if [[ -z "$OUTPUT_FILE" || -z "$PRIMARY_CMD" || -z "$FALLBACK_CMD" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs/level5"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||
mkdir -p "$LOG_DIR" "$(dirname "$OUTPUT_FILE")"
|
||||
FALLBACK_LOG="$LOG_DIR/fallback.jsonl"
|
||||
TRACE_ID="$(uuidgen 2>/dev/null | tr '[:upper:]' '[:lower:]' || printf 'fallback-%s-%s' "$(date +%s)" "$$")"
|
||||
|
||||
@@ -20,8 +20,9 @@ if [[ -z "$API_URL" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
OUT="${2:-$PROJECT_ROOT/.specify/logs/level5/provider-usage.jsonl}"
|
||||
OUT="${2:-$CASAN_STATE_ROOT/logs/level5/provider-usage.jsonl}"
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
|
||||
# SEC-13 (M-09): SSRF guard on the fetch URL. ALWAYS reject non-http(s) schemes
|
||||
|
||||
@@ -12,8 +12,9 @@ ACTION="${2:-}"
|
||||
ROLLBACK_COMMAND="${3:-}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs/level5"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||
BACKUP_DIR="$LOG_DIR/rollback-backups"
|
||||
TX_LOG="$LOG_DIR/rollback-transactions.jsonl"
|
||||
mkdir -p "$LOG_DIR" "$BACKUP_DIR"
|
||||
|
||||
@@ -18,11 +18,12 @@ if [[ -z "$INPUT_FILE" || -z "$OUTPUT_FILE" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs"
|
||||
TRACE_DIR="$LOG_DIR/trace"
|
||||
AUDIT_DIR="$LOG_DIR/audit"
|
||||
SECURITY_DIR="$PROJECT_ROOT/.specify/security"
|
||||
SECURITY_DIR="$CASAN_HARNESS_ROOT/security"
|
||||
|
||||
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL). Used to
|
||||
# make semantic skips loud (never silent) — stderr only, stdout contract intact.
|
||||
|
||||
@@ -7,6 +7,7 @@ set -uo pipefail
|
||||
# Exit: 0 all required gates green, 1 a required gate failed.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
PASS=0; FAIL=0; SKIP=0
|
||||
run() { # <name> <command...>
|
||||
@@ -16,21 +17,21 @@ run() { # <name> <command...>
|
||||
}
|
||||
|
||||
echo "== CASAN security gate =="
|
||||
run "run-casan4 harness suite" bash "$ROOT/.specify/tests/run-casan4-harness-tests.sh"
|
||||
run "adversarial suite" bash "$ROOT/.specify/tests/adversarial-harness-tests.sh"
|
||||
run "run-casan4 harness suite" bash "$CASAN_HARNESS_ROOT/tests/run-casan4-harness-tests.sh"
|
||||
run "adversarial suite" bash "$CASAN_HARNESS_ROOT/tests/adversarial-harness-tests.sh"
|
||||
# Wave 5: sign audit head via Vault KMS (or local fallback) before verifying.
|
||||
# This changes verify output from anchor=unsigned to anchor=signed when Vault is configured.
|
||||
run "sign audit-chain head (KMS)" bash "$ROOT/.specify/scripts/bash/sign-audit-head.sh"
|
||||
run "audit hash-chain (signed)" bash "$ROOT/.specify/scripts/bash/verify-audit-chain.sh"
|
||||
run "tool-call audit (signed)" bash "$ROOT/.specify/scripts/bash/verify-tool-audit.sh"
|
||||
run "sign audit-chain head (KMS)" bash "$CASAN_HARNESS_ROOT/scripts/bash/sign-audit-head.sh"
|
||||
run "audit hash-chain (signed)" bash "$CASAN_HARNESS_ROOT/scripts/bash/verify-audit-chain.sh"
|
||||
run "tool-call audit (signed)" bash "$CASAN_HARNESS_ROOT/scripts/bash/verify-tool-audit.sh"
|
||||
# Wave 3 additions
|
||||
run "secrets scan (WP-S4)" bash "$ROOT/.specify/scripts/bash/secrets-scan.sh"
|
||||
run "no-bypass + circuit breaker" bash "$ROOT/.specify/scripts/bash/circuit-breaker-check.sh"
|
||||
run "secrets scan (WP-S4)" bash "$CASAN_HARNESS_ROOT/scripts/bash/secrets-scan.sh"
|
||||
run "no-bypass + circuit breaker" bash "$CASAN_HARNESS_ROOT/scripts/bash/circuit-breaker-check.sh"
|
||||
|
||||
if curl -sS -m 5 http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
|
||||
run "model router tests" bash "$ROOT/.specify/tests/phase3-model-router-tests.sh"
|
||||
run "red-team H4 metrics (30 samples)" bash "$ROOT/.specify/tests/phase3-redteam-metrics.sh"
|
||||
run "judge gate tests (WP-B)" bash "$ROOT/.specify/tests/phase3-judge-gate-tests.sh"
|
||||
run "model router tests" bash "$CASAN_HARNESS_ROOT/tests/phase3-model-router-tests.sh"
|
||||
run "red-team H4 metrics (30 samples)" bash "$CASAN_HARNESS_ROOT/tests/phase3-redteam-metrics.sh"
|
||||
run "judge gate tests (WP-B)" bash "$CASAN_HARNESS_ROOT/tests/phase3-judge-gate-tests.sh"
|
||||
else
|
||||
echo " GATE SKIP model router + red-team + judge-gate (Ollama tunnel down)"; SKIP=$((SKIP+1))
|
||||
fi
|
||||
|
||||
@@ -1,61 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json]"
|
||||
echo " --json Output results in JSON format"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if we're on a proper feature branch (only for git repos)
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# Ensure the feature directory exists
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy plan template if it exists
|
||||
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
|
||||
if [[ -f "$TEMPLATE" ]]; then
|
||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
echo "Copied plan template to $IMPL_PLAN"
|
||||
else
|
||||
echo "Warning: Plan template not found at $TEMPLATE"
|
||||
# Create a basic plan file if template doesn't exist
|
||||
touch "$IMPL_PLAN"
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
||||
else
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "SPECS_DIR: $FEATURE_DIR"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "HAS_GIT: $HAS_GIT"
|
||||
fi
|
||||
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json]"
|
||||
echo " --json Output results in JSON format"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if we're on a proper feature branch (only for git repos)
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# Ensure the feature directory exists
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy plan template if it exists
|
||||
TEMPLATE="$CASAN_HARNESS_ROOT/templates/plan-template.md"
|
||||
if [[ -f "$TEMPLATE" ]]; then
|
||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
echo "Copied plan template to $IMPL_PLAN"
|
||||
else
|
||||
echo "Warning: Plan template not found at $TEMPLATE"
|
||||
# Create a basic plan file if template doesn't exist
|
||||
touch "$IMPL_PLAN"
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
||||
else
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "SPECS_DIR: $FEATURE_DIR"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "HAS_GIT: $HAS_GIT"
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,13 +21,14 @@ set -euo pipefail
|
||||
# VAULT_TOKEN — token with transit/sign/casan-audit-key capability
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
AUDIT_LOG="${1:-$PROJECT_ROOT/.specify/logs/audit/audit.jsonl}"
|
||||
AUDIT_LOG="${1:-$CASAN_STATE_ROOT/logs/audit/audit.jsonl}"
|
||||
AUDIT_DIR="$(dirname "$AUDIT_LOG")"
|
||||
HEAD_FILE="$AUDIT_DIR/audit-head.txt"
|
||||
HEAD_SIG="$AUDIT_DIR/audit-head.sig"
|
||||
AUDIT_PUB="$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem"
|
||||
AUDIT_PUB="$CASAN_GOVERNANCE_ROOT/audit-public.pem"
|
||||
|
||||
if [[ ! -f "$AUDIT_LOG" ]]; then
|
||||
echo "SIGN_AUDIT_HEAD_SKIP audit.jsonl not found" >&2
|
||||
@@ -102,7 +103,7 @@ PY
|
||||
fi
|
||||
else
|
||||
# Fallback — local key (dev environment without Vault)
|
||||
AUDIT_PRIV="$PROJECT_ROOT/.specify/level5/central-governance/audit-private.pem"
|
||||
AUDIT_PRIV="$CASAN_GOVERNANCE_ROOT/audit-private.pem"
|
||||
if [[ ! -f "$AUDIT_PRIV" ]]; then
|
||||
echo "SIGN_AUDIT_HEAD_SKIP no private key and VAULT_ADDR not set — verify will show anchor=unsigned" >&2
|
||||
exit 0
|
||||
|
||||
@@ -8,8 +8,9 @@ set -euo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
GOV_DIR="$PROJECT_ROOT/.specify/level5/central-governance"
|
||||
GOV_DIR="$CASAN_GOVERNANCE_ROOT"
|
||||
BUNDLE="$GOV_DIR/policy-bundle.yaml"
|
||||
MANIFEST="$GOV_DIR/policy-manifest.json"
|
||||
PRIVATE_KEY="$GOV_DIR/policy-private.pem"
|
||||
|
||||
@@ -20,14 +20,15 @@ set -uo pipefail
|
||||
# If baseline is omitted, the manifest's committed git version is used.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
SEC_DIR="$PROJECT_ROOT/.specify/security"
|
||||
SEC_DIR="$CASAN_HARNESS_ROOT/security"
|
||||
# shellcheck source=casan-log.sh
|
||||
source "$SCRIPT_DIR/casan-log.sh"
|
||||
|
||||
MANIFEST="${1:-}"
|
||||
BASELINE="${2:-}"
|
||||
REPORT="${3:-$PROJECT_ROOT/.specify/logs/level5/supply-chain-report.json}"
|
||||
REPORT="${3:-$CASAN_STATE_ROOT/logs/level5/supply-chain-report.json}"
|
||||
mkdir -p "$(dirname "$REPORT")"
|
||||
|
||||
if [[ -z "$MANIFEST" || ! -f "$MANIFEST" ]]; then
|
||||
|
||||
@@ -24,17 +24,18 @@ set -uo pipefail
|
||||
# Exit: 0 ok, 1 tamper/mismatch/invalid-signature, 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
L5_DIR="$PROJECT_ROOT/.specify/logs/level5"
|
||||
COST_DIR="$PROJECT_ROOT/.specify/logs/cost"
|
||||
L5_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||
COST_DIR="$CASAN_STATE_ROOT/logs/cost"
|
||||
mkdir -p "$L5_DIR"
|
||||
|
||||
CMD="${1:-}"
|
||||
MANIFEST="$L5_DIR/telemetry-manifest.json"
|
||||
HEAD_FILE="$L5_DIR/telemetry-head.txt"
|
||||
HEAD_SIG="$L5_DIR/telemetry-head.sig"
|
||||
AUDIT_PRIV="${CASAN_AUDIT_PRIV:-$PROJECT_ROOT/.specify/level5/central-governance/audit-private.pem}"
|
||||
AUDIT_PUB="${CASAN_AUDIT_PUB:-$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem}"
|
||||
AUDIT_PRIV="${CASAN_AUDIT_PRIV:-$CASAN_GOVERNANCE_ROOT/audit-private.pem}"
|
||||
AUDIT_PUB="${CASAN_AUDIT_PUB:-$CASAN_GOVERNANCE_ROOT/audit-public.pem}"
|
||||
|
||||
# Telemetry files to bind. Missing files hash to the literal "MISSING" so the
|
||||
# manifest is stable and a deletion is itself a detectable change.
|
||||
|
||||
@@ -23,8 +23,9 @@ set -uo pipefail
|
||||
# Exit: 0 ok · 3 denied (reason on stderr) · 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
STATE_ROOT="${CASAN_TENANT_STATE_ROOT:-$PROJECT_ROOT/.specify/state/tenants}"
|
||||
STATE_ROOT="${CASAN_TENANT_STATE_ROOT:-$CASAN_STATE_ROOT/state/tenants}"
|
||||
|
||||
die() { echo "TENANT_DENIED reason=$1" >&2; exit 3; }
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
append_tool_audit() {
|
||||
local record_json="$1"
|
||||
local project_root="$2"
|
||||
local audit_dir="$project_root/.specify/logs/audit"
|
||||
local audit_dir="$CASAN_STATE_ROOT/logs/audit"
|
||||
local log="$audit_dir/tool-calls.jsonl"
|
||||
mkdir -p "$audit_dir"
|
||||
|
||||
@@ -45,7 +45,7 @@ PY
|
||||
command -v openssl >/dev/null 2>&1 || return 0
|
||||
# Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the
|
||||
# public key is committed, for verification. Production: replace with KMS/HSM.
|
||||
local pub_dir="$project_root/.specify/level5/central-governance"
|
||||
local pub_dir="$CASAN_GOVERNANCE_ROOT"
|
||||
local priv_dir="${CASAN_AUDIT_KEY_DIR:-$HOME/.casan/audit-keys}"
|
||||
local priv="$priv_dir/audit-private.pem" pub="$pub_dir/audit-public.pem"
|
||||
mkdir -p "$pub_dir" "$priv_dir"
|
||||
|
||||
@@ -12,10 +12,11 @@ if [[ -z "$TOOL_ID" ]]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
REGISTRY="$PROJECT_ROOT/.specify/level5/tool-registry.yaml"
|
||||
LOG_DIR="$PROJECT_ROOT/.specify/logs/level5"
|
||||
AUDIT_DIR="$PROJECT_ROOT/.specify/logs/audit"
|
||||
REGISTRY="$CASAN_HARNESS_ROOT/level5/tool-registry.yaml"
|
||||
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||
AUDIT_DIR="$CASAN_STATE_ROOT/logs/audit"
|
||||
mkdir -p "$LOG_DIR" "$AUDIT_DIR"
|
||||
TRACE_ID="$(uuidgen 2>/dev/null | tr '[:upper:]' '[:lower:]' || printf 'tool-%s-%s' "$(date +%s)" "$$")"
|
||||
|
||||
@@ -36,8 +37,8 @@ if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_IDENTITY_STRICT:-}" == "1" ]];
|
||||
EFFECTIVE_AGENT="" # unauthenticated until a valid token proves otherwise
|
||||
CLAIM="${CASAN_AGENT:-}"
|
||||
SIG="${CASAN_AGENT_SIG:-}"
|
||||
REG="${CASAN_AGENT_REGISTRY:-$PROJECT_ROOT/.specify/level5/central-governance/agent-identities.registry}"
|
||||
KEYS_DIR="${CASAN_AGENT_KEYS_DIR:-$PROJECT_ROOT/.specify/level5/central-governance/agents}"
|
||||
REG="${CASAN_AGENT_REGISTRY:-$CASAN_GOVERNANCE_ROOT/agent-identities.registry}"
|
||||
KEYS_DIR="${CASAN_AGENT_KEYS_DIR:-$CASAN_GOVERNANCE_ROOT/agents}"
|
||||
if [[ -n "$CLAIM" && -n "$SIG" && -f "$SIG" && -f "$REG" ]] && command -v openssl >/dev/null 2>&1; then
|
||||
PUB_REL="$(awk -v id="$CLAIM" '$1=="agent" && $2==id {print $3; exit}' "$REG")"
|
||||
if [[ -n "$PUB_REL" ]]; then
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,9 @@ set -euo pipefail
|
||||
# verify-audit-chain.sh [audit-jsonl]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
AUDIT_LOG="${1:-$PROJECT_ROOT/.specify/logs/audit/audit.jsonl}"
|
||||
AUDIT_LOG="${1:-$CASAN_STATE_ROOT/logs/audit/audit.jsonl}"
|
||||
|
||||
if [[ ! -f "$AUDIT_LOG" ]]; then
|
||||
echo "AUDIT_CHAIN_MISSING file=$AUDIT_LOG" >&2
|
||||
@@ -70,7 +71,7 @@ PY
|
||||
AUDIT_DIR="$(dirname "$AUDIT_LOG")"
|
||||
HEAD_FILE="$AUDIT_DIR/audit-head.txt"
|
||||
HEAD_SIG="$AUDIT_DIR/audit-head.sig"
|
||||
AUDIT_PUB="$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem"
|
||||
AUDIT_PUB="$CASAN_GOVERNANCE_ROOT/audit-public.pem"
|
||||
|
||||
if [[ -f "$HEAD_FILE" && -f "$HEAD_SIG" && -f "$AUDIT_PUB" ]] && command -v openssl >/dev/null 2>&1; then
|
||||
STORED_HEAD="$(cat "$HEAD_FILE")"
|
||||
|
||||
@@ -11,9 +11,10 @@ set -uo pipefail
|
||||
# Exit: 0 in-sync, 1 tamper/gap/unshipped, 64 usage.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
HEAD_FILE="${1:-$PROJECT_ROOT/.specify/logs/audit/audit-head.txt}"
|
||||
LEDGER="${2:-${CASAN_WORM_LEDGER:-$PROJECT_ROOT/.specify/logs/worm/anchor-ledger.jsonl}}"
|
||||
HEAD_FILE="${1:-$CASAN_STATE_ROOT/logs/audit/audit-head.txt}"
|
||||
LEDGER="${2:-${CASAN_WORM_LEDGER:-$CASAN_STATE_ROOT/logs/worm/anchor-ledger.jsonl}}"
|
||||
|
||||
[[ -f "$HEAD_FILE" ]] || { echo "AUDIT_GAP_NO_HEAD file=$HEAD_FILE" >&2; exit 1; }
|
||||
python "$SCRIPT_DIR/worm-ledger.py" verify "$HEAD_FILE" "$LEDGER"
|
||||
|
||||
@@ -4,9 +4,10 @@ set -euo pipefail
|
||||
# Verify CASAN Level 5 shared harness reuse across more than one project.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
REGISTRY="$PROJECT_ROOT/.specify/level5/project-registry.json"
|
||||
PACKAGE="$PROJECT_ROOT/.specify/level5/harness-package.json"
|
||||
REGISTRY="$CASAN_HARNESS_ROOT/level5/project-registry.json"
|
||||
PACKAGE="$CASAN_HARNESS_ROOT/level5/harness-package.json"
|
||||
|
||||
python - "$REGISTRY" "$PACKAGE" <<'PY'
|
||||
import json
|
||||
|
||||
@@ -8,8 +8,9 @@ set -euo pipefail
|
||||
# Usage: verify-tool-audit.sh [tool-calls.jsonl]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
LOG="${1:-$PROJECT_ROOT/.specify/logs/audit/tool-calls.jsonl}"
|
||||
LOG="${1:-$CASAN_STATE_ROOT/logs/audit/tool-calls.jsonl}"
|
||||
|
||||
if [[ ! -f "$LOG" ]]; then
|
||||
echo "TOOL_AUDIT_MISSING file=$LOG" >&2
|
||||
@@ -43,7 +44,7 @@ PY
|
||||
AUDIT_DIR="$(dirname "$LOG")"
|
||||
HEAD_FILE="$AUDIT_DIR/tool-calls-head.txt"
|
||||
HEAD_SIG="$AUDIT_DIR/tool-calls-head.sig"
|
||||
AUDIT_PUB="$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem"
|
||||
AUDIT_PUB="$CASAN_GOVERNANCE_ROOT/audit-public.pem"
|
||||
|
||||
if [[ -f "$HEAD_FILE" && -f "$HEAD_SIG" && -f "$AUDIT_PUB" ]] && command -v openssl >/dev/null 2>&1; then
|
||||
if [[ "$(cat "$HEAD_FILE")" != "$COMPUTED_HEAD" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user