scripts/bash/helpers.sh
#!/usr/bin/env bash
# helpers.sh — Shared utilities for team-levelup skill
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
###############################################################################
# 1. PATH RESOLUTION
###############################################################################
# JSON output always goes through jq -n (no manual string building).
resolve_team_levelup_paths() {
PROJECT_ROOT="${PROJECT_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
BRANCH="${BRANCH:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
# jq is required here (no silent fallback to the default path when it is missing).
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required but not installed (https://jqlang.github.io/jq/download/)" >&2
return 1 2>/dev/null || exit 1
fi
TEAM_AI_DIRECTIVES="${TEAM_AI_DIRECTIVES:-}"
if [[ -z "$TEAM_AI_DIRECTIVES" && -f "${PROJECT_ROOT}/.adlc/init-options.json" ]]; then
TEAM_AI_DIRECTIVES=$(jq -r '.team_ai_directives // empty' "${PROJECT_ROOT}/.adlc/init-options.json" 2>/dev/null || true)
fi
[[ -z "$TEAM_AI_DIRECTIVES" || "$TEAM_AI_DIRECTIVES" = "null" ]] && TEAM_AI_DIRECTIVES="${PROJECT_ROOT}/team-ai-directives"
ADLC_BRANCH="adlc"
ADLC_WORKTREE="/tmp/adlc-worktree-$$"
CDR_DRAFTS_DIR="drafts/cdr"
CDR_INDEX_FILE="drafts/cdr/cdr.md"
REPORTS_DIR="reports"
SESSIONS_DIR="reports/sessions"
PROJECTS_DIR="reports/projects"
CONFIDENCE_FILE="reports/confidence-scores.json"
echo "PROJECT_ROOT=$PROJECT_ROOT"
echo "TEAM_AI_DIRECTIVES=$TEAM_AI_DIRECTIVES"
echo "BRANCH=$BRANCH"
echo "ADLC_BRANCH=$ADLC_BRANCH"
echo "ADLC_WORKTREE=$ADLC_WORKTREE"
echo "CDR_DRAFTS_DIR=$CDR_DRAFTS_DIR"
echo "CDR_INDEX_FILE=$CDR_INDEX_FILE"
echo "REPORTS_DIR=$REPORTS_DIR"
echo "SESSIONS_DIR=$SESSIONS_DIR"
echo "PROJECTS_DIR=$PROJECTS_DIR"
echo "CONFIDENCE_FILE=$CONFIDENCE_FILE"
}
output_json() {
resolve_team_levelup_paths >/dev/null
jq -n \
--arg repo_root "$PROJECT_ROOT" \
--arg team_ai_directives "$TEAM_AI_DIRECTIVES" \
--arg branch "$BRANCH" \
--arg adlc_branch "$ADLC_BRANCH" \
--arg adlc_worktree "$ADLC_WORKTREE" \
'{"REPO_ROOT":$repo_root,"TEAM_AI_DIRECTIVES":$team_ai_directives,"BRANCH":$branch,"ADLC_BRANCH":$adlc_branch,"ADLC_WORKTREE":$adlc_worktree}'
}
###############################################################################
# 2. ADLC ORPHAN BRANCH MANAGEMENT
###############################################################################
ensure_adlc_branch() {
local td="${1:-$TEAM_AI_DIRECTIVES}"
if ! git -C "$td" show-ref --verify --quiet "refs/heads/$ADLC_BRANCH"; then
git -C "$td" branch --orphan "$ADLC_BRANCH"
git -C "$td" worktree add "$ADLC_WORKTREE" "$ADLC_BRANCH" 2>/dev/null
mkdir -p "$ADLC_WORKTREE/drafts/cdr" "$ADLC_WORKTREE/reports/sessions" "$ADLC_WORKTREE/reports/projects"
git -C "$ADLC_WORKTREE" add -A
git -C "$ADLC_WORKTREE" commit --allow-empty -m "Initialize adlc orphan branch"
git -C "$ADLC_WORKTREE" push origin "$ADLC_BRANCH" 2>/dev/null || true
git -C "$td" worktree remove "$ADLC_WORKTREE" 2>/dev/null || true
fi
}
###############################################################################
# 3. CDR NUMBERING
###############################################################################
next_cdr_number() {
local td="${1:-$TEAM_AI_DIRECTIVES}"
local max=0
for f in $(git -C "$td" ls-tree --name-only "$ADLC_BRANCH" "drafts/cdr/" 2>/dev/null | sort); do
local base num
# A record's number is the LEADING DIGITS of its filename stem:
# CDR-010-amendment-2.md is record 010. Suffixes are preserved and
# never arithmetically parsed -- the same rule next_record_number in
# product-clarify's pdr-lib.sh implements on the filesystem. The old
# "CDR-<digits>.md" match silently skipped stemmed records and handed
# out a slot that was already taken.
base=$(basename "$f")
[[ "$base" == *.md ]] || continue
num=$(printf '%s' "${base%.md}" | sed -n 's/^CDR-\([0-9]\{1,\}\).*/\1/p')
[[ -n "$num" ]] || continue
((10#$num > max)) && max=$((10#$num))
done
printf '%03d' $((max + 1))
}
###############################################################################
# 4. ADLC BRANCH READ/WRITE
###############################################################################
read_adlc_file() {
local td="${1:-$TEAM_AI_DIRECTIVES}"
local path="$2"
git -C "$td" show "${ADLC_BRANCH}:${path}" 2>/dev/null || echo ""
}
write_adlc_file() {
local td="${1:-$TEAM_AI_DIRECTIVES}"
local path="$2"
local content="$3"
git -C "$td" worktree add "$ADLC_WORKTREE" "$ADLC_BRANCH" 2>/dev/null || git -C "$td" worktree add --force "$ADLC_WORKTREE" "$ADLC_BRANCH"
mkdir -p "$(dirname "$ADLC_WORKTREE/$path")"
echo "$content" > "$ADLC_WORKTREE/$path"
git -C "$ADLC_WORKTREE" add "$path"
git -C "$ADLC_WORKTREE" commit -m "team-levelup: update $path"
git -C "$ADLC_WORKTREE" push origin "$ADLC_BRANCH" 2>/dev/null || true
git -C "$td" worktree remove "$ADLC_WORKTREE" 2>/dev/null || true
}
###############################################################################
# 5. CDR INDEX GENERATION
###############################################################################
regenerate_cdr_index() {
local td="${1:-$TEAM_AI_DIRECTIVES}"
local index_path="drafts/cdr/cdr.md"
local today
today=$(date +%Y-%m-%d)
local entries=""
local count=0
for f in $(git -C "$td" ls-tree --name-only "$ADLC_BRANCH" "drafts/cdr/" 2>/dev/null | sort); do
[[ "$f" =~ CDR-[0-9]+\.md$ ]] || continue
local id target type status created verified age descriptor
id=$(basename "$f" .md)
local content
content=$(read_adlc_file "$td" "$f")
target=$(echo "$content" | grep -m1 '^### Target Module:' | sed 's/^### Target Module:[[:space:]]*//' | sed 's/`//g' | head -c 40 || true)
type=$(echo "$content" | grep -m1 '^### Context Type:' | sed 's/^### Context Type:[[:space:]]*//' | head -c 20 || true)
status=$(echo "$content" | grep -m1 '^### Status:' | sed 's/^### Status:[[:space:]]*//' | sed 's/\*\*//g' | head -c 12 || true)
created=$(echo "$content" | grep -m1 '^### Date:' | sed 's/^### Date:[[:space:]]*//' | head -c 10 || true)
verified=$(echo "$content" | grep -m1 '^verified:' | sed 's/verified:[[:space:]]*//' | head -c 10 || true)
age=$(echo "$content" | grep -m1 '^age_days:' | sed 's/age_days:[[:space:]]*//' | head -c 5 || true)
descriptor=$(echo "$content" | grep -m1 '^### Descriptor:' | sed 's/^### Descriptor:[[:space:]]*//' | head -c 60 || true)
[[ -z "$created" ]] && created="$today"
[[ -z "$verified" ]] && verified="-"
[[ -z "$age" ]] && age="-"
entries="${entries}| ${id} | ${target:- } | ${type:- } | ${status:-Discovered} | ${created} | ${verified} | ${age} | ${descriptor:- } |
"
count=$((count + 1))
done
local index_content
index_content=$(cat << EOF
# Context Directive Records
Context Directive Records (CDRs) track proposed and accepted contributions to team-ai-directives.
## CDR Index
| ID | Target Module | Type | Status | Created | Verified | Age | Descriptor |
|----|---------------|------|--------|---------|----------|-----|------------|
${entries}
**Stats**: ${count} entries | Last Updated: ${today}
EOF
)
write_adlc_file "$td" "$index_path" "$index_content"
echo "$index_path"
}
###############################################################################
# 6. SIGNAL GATE VALIDATION
###############################################################################
signal_gate() {
local file="$1"
local reasons=()
if grep -qi 'project-specific\|only applies to this project\|single project' "$file" 2>/dev/null; then
reasons+=("project-specific")
fi
if grep -qi 'duplicate of\|overlaps with existing\|already in team-ai-directives' "$file" 2>/dev/null; then
reasons+=("duplicate")
fi
if ! grep -Eq '(src/|lib/|test/|docs/|commit|file)' "$file" 2>/dev/null; then
reasons+=("no evidence")
fi
if grep -qi 'low value\|nice-to-have\|minor convenience' "$file" 2>/dev/null; then
reasons+=("low value")
fi
if [[ ${#reasons[@]} -gt 0 ]]; then
echo "SKIP: ${reasons[*]}"
return 1
fi
echo "PASS"
return 0
}
###############################################################################
# CLI ENTRY
###############################################################################
if [[ "${1:-}" == "--json" ]]; then
output_json
elif [[ "${1:-}" == "--next-cdr" ]]; then
# resolve first: the bare form reads $TEAM_AI_DIRECTIVES/$ADLC_BRANCH globals
# (they were unbound here — set -u aborted the bare call)
resolve_team_levelup_paths >/dev/null
next_cdr_number "${2:-}"
elif [[ "${1:-}" == "--index" ]]; then
resolve_team_levelup_paths >/dev/null
regenerate_cdr_index "${2:-}"
elif [[ "${1:-}" == "--signal-gate" ]]; then
if [[ -z "${2:-}" ]]; then
echo "ERROR: --signal-gate requires a file path" >&2
exit 1
fi
signal_gate "$2"
fi
scripts/bash/score-confidence.sh
#!/usr/bin/env bash
# score-confidence.sh — Calculate confidence score for a CDR
set -euo pipefail
# Simple integer math with awk for floating point
# Usage: calc "0.60 + 0.20" → 0.80
calc() {
awk "BEGIN { printf \"%.2f\", $1 }"
}
# Compare two floats: returns 0 if $1 > $2
gt() {
awk "BEGIN { exit !($1 > $2) }"
}
# Compare two floats: returns 0 if $1 >= $2
ge() {
awk "BEGIN { exit !($1 >= $2) }"
}
# Compare two floats: returns 0 if $1 < $2
lt() {
awk "BEGIN { exit !($1 < $2) }"
}
calculate_confidence() {
local cdr_file="$1"
local confidence_file="${2:-}"
local base_score="0.60"
local bonuses="0.00"
local usage_multiplier="1.0"
# Determine base score by type
local type
type=$(grep -m1 '^### Context Type:' "$cdr_file" 2>/dev/null | sed 's/^### Context Type:[[:space:]]*//' | tr '[:upper:]' '[:lower:]' || echo "rule")
case "$type" in
rule) base_score="0.60" ;;
persona) base_score="0.50" ;;
example) base_score="0.50" ;;
constitution) base_score="0.70" ;;
*) base_score="0.60" ;;
esac
# Bonus: paired eval exists
if grep -q '### Paired Eval' "$cdr_file" 2>/dev/null; then
bonuses=$(calc "$bonuses + 0.20")
fi
# Bonus: evidence includes file paths/commits
if grep -Eq '(src/|lib/|test/|docs/|commit|file)' "$cdr_file" 2>/dev/null; then
bonuses=$(calc "$bonuses + 0.10")
fi
# Bonus: multiple projects
local project_field
project_field=$(grep -m1 '^project:' "$cdr_file" 2>/dev/null | sed 's/^project:[[:space:]]*//' || echo "")
if echo "$project_field" | grep -q ',' 2>/dev/null; then
bonuses=$(calc "$bonuses + 0.10")
fi
# Usage multiplier from confidence-scores.json
if [[ -n "$confidence_file" && -f "$confidence_file" ]]; then
local cdr_id
cdr_id=$(basename "$cdr_file" .md)
# Parse success_rate from JSON using grep/sed (no python3)
# Expected format: "CDR-001": { "success_rate": 0.75, ... }
# (|| true guards the pipeline: an id with no entry makes the first
# grep exit 1, and under pipefail + set -e that used to abort the
# script before the documented 1.0 default could apply.)
local success_rate
success_rate=$(grep -A5 "\"$cdr_id\"" "$confidence_file" 2>/dev/null \
| grep 'success_rate' \
| sed 's/.*"success_rate"[[:space:]]*:[[:space:]]*\([0-9.]*\).*/\1/' \
| head -1 || true)
[[ -z "$success_rate" ]] && success_rate="1.0"
if gt "$success_rate" "0.8"; then
usage_multiplier="1.2"
elif lt "$success_rate" "0.5"; then
usage_multiplier="0.8"
fi
fi
local final_score
final_score=$(calc "($base_score + $bonuses) * $usage_multiplier")
# Cap at 1.0
if gt "$final_score" "1.0"; then
final_score="1.00"
fi
local tier="LOW"
if ge "$final_score" "0.8"; then
tier="HIGH"
elif ge "$final_score" "0.5" && lt "$final_score" "0.8"; then
tier="MEDIUM"
fi
echo "$final_score $tier"
}
# CLI entry
if [[ "${1:-}" == "--calculate" ]]; then
calculate_confidence "$2" "${3:-}"
fi
scripts/bash/scrub-session.sh
#!/usr/bin/env bash
# scrub-session.sh — Privacy-scrub a session summary
set -euo pipefail
scrub_session() {
local input="$1"
# Remove common secret patterns
echo "$input" | \
sed -E 's/(api[_-]?key[=: ]+)[^ ]+/\1REDACTED/gi' | \
sed -E 's/(token[=: ]+)[^ ]+/\1REDACTED/gi' | \
sed -E 's/(password[=: ]+)[^ ]+/\1REDACTED/gi' | \
sed -E 's/(secret[=: ]+)[^ ]+/\1REDACTED/gi' | \
sed -E 's/(AKIA[A-Z0-9]{16})/REDACTED/g' | \
sed -E 's/(ghp_[A-Za-z0-9]{36})/REDACTED/g' | \
sed -E 's/(glpat-[A-Za-z0-9_-]{20})/REDACTED/g'
}
if [[ "${1:-}" == "--scrub" ]]; then
scrub_session "$2"
fi
scripts/bash/team-levelup.sh
#!/usr/bin/env bash
# team-levelup.sh — Session-end CDR lifecycle entry point
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/helpers.sh"
if [[ "${1:-}" == "--setup" ]]; then
output_json
exit 0
fi
# Resolve paths (function sets PROJECT_ROOT/TEAM_AI_DIRECTIVES/BRANCH globals
# directly; output is informational only — never eval it)
resolve_team_levelup_paths >/dev/null
if [[ -z "$TEAM_AI_DIRECTIVES" ]] || [[ ! -d "$TEAM_AI_DIRECTIVES" ]]; then
echo "Team AI directives repository not configured."
echo "Run: /team-setup"
exit 1
fi
# Ensure adlc orphan branch exists
ensure_adlc_branch "$TEAM_AI_DIRECTIVES"
# Get next CDR number
NEXT_CDR=$(next_cdr_number "$TEAM_AI_DIRECTIVES")
# Output setup info
echo "NEXT_CDR=$NEXT_CDR"
echo "ADLC_BRANCH=$ADLC_BRANCH"
echo "TEAM_AI_DIRECTIVES=$TEAM_AI_DIRECTIVES"
scripts/powershell/helpers.ps1
# helpers.ps1 — Shared utilities for the team-levelup skill
# (PowerShell twin of bash/helpers.sh; one behavior, two platform scripts;
# keep in lockstep.)
#
# CLI entry (parity with bash/helpers.sh):
# --json emit the resolved paths as JSON
# --next-cdr [DIR] next CDR number (3-digit, zero-padded)
# --index [DIR] regenerate the CDR index on the adlc branch
# --signal-gate FILE run the contribution signal gate on a draft
#
# The bash twin's output_json shells out to python3 for JSON safety; this
# twin uses native ConvertTo-Json (no python dependency on Windows).
$ErrorActionPreference = 'Stop'
$script:ErrorView = 'ConciseView'
###############################################################################
# 1. PATH RESOLUTION
###############################################################################
function Resolve-TeamLevelupPaths {
# Sets the script-scoped globals (parity with the bash twin's globals);
# also writes them to stdout as KEY=VALUE lines — informational only,
# never eval it.
if (-not $env:PROJECT_ROOT) {
$root = @(git rev-parse --show-toplevel 2>$null)
if ($LASTEXITCODE -eq 0 -and $root.Count -gt 0 -and $root[0]) {
$script:PROJECT_ROOT = [string]$root[0]
} else {
$script:PROJECT_ROOT = (Get-Location).Path
}
} else {
$script:PROJECT_ROOT = $env:PROJECT_ROOT
}
if (-not $env:BRANCH) {
$b = @(git branch --show-current 2>$null)
if ($LASTEXITCODE -eq 0 -and $b.Count -gt 0 -and $b[0]) {
$script:BRANCH = [string]$b[0]
} else {
$script:BRANCH = 'unknown'
}
} else {
$script:BRANCH = $env:BRANCH
}
$script:TEAM_AI_DIRECTIVES = if ($env:TEAM_AI_DIRECTIVES) { $env:TEAM_AI_DIRECTIVES } else { '' }
if ($script:TEAM_AI_DIRECTIVES -eq '' -and (Test-Path (Join-Path $script:PROJECT_ROOT '.adlc/init-options.json'))) {
$m = Select-String -LiteralPath (Join-Path $script:PROJECT_ROOT '.adlc/init-options.json') -Pattern '"team_ai_directives"\s*:\s*"([^"]*)"' | Select-Object -First 1
if ($m -and $m.Matches.Count -gt 0) { $script:TEAM_AI_DIRECTIVES = $m.Matches[0].Groups[1].Value }
}
if ($script:TEAM_AI_DIRECTIVES -eq '' -or $script:TEAM_AI_DIRECTIVES -eq 'null') {
$script:TEAM_AI_DIRECTIVES = Join-Path $script:PROJECT_ROOT 'team-ai-directives'
}
$script:ADLC_BRANCH = 'adlc'
$script:ADLC_WORKTREE = "/tmp/adlc-worktree-$PID"
$script:CDR_DRAFTS_DIR = 'drafts/cdr'
$script:CDR_INDEX_FILE = 'drafts/cdr/cdr.md'
$script:REPORTS_DIR = 'reports'
$script:SESSIONS_DIR = 'reports/sessions'
$script:PROJECTS_DIR = 'reports/projects'
$script:CONFIDENCE_FILE = 'reports/confidence-scores.json'
Write-Output "PROJECT_ROOT=$($script:PROJECT_ROOT)"
Write-Output "TEAM_AI_DIRECTIVES=$($script:TEAM_AI_DIRECTIVES)"
Write-Output "BRANCH=$($script:BRANCH)"
Write-Output "ADLC_BRANCH=$($script:ADLC_BRANCH)"
Write-Output "ADLC_WORKTREE=$($script:ADLC_WORKTREE)"
Write-Output "CDR_DRAFTS_DIR=$($script:CDR_DRAFTS_DIR)"
Write-Output "CDR_INDEX_FILE=$($script:CDR_INDEX_FILE)"
Write-Output "REPORTS_DIR=$($script:REPORTS_DIR)"
Write-Output "SESSIONS_DIR=$($script:SESSIONS_DIR)"
Write-Output "PROJECTS_DIR=$($script:PROJECTS_DIR)"
Write-Output "CONFIDENCE_FILE=$($script:CONFIDENCE_FILE)"
}
function Output-Json {
$null = Resolve-TeamLevelupPaths
$payload = [ordered]@{
REPO_ROOT = $script:PROJECT_ROOT
TEAM_AI_DIRECTIVES = $script:TEAM_AI_DIRECTIVES
BRANCH = $script:BRANCH
ADLC_BRANCH = $script:ADLC_BRANCH
ADLC_WORKTREE = $script:ADLC_WORKTREE
}
Write-Output ($payload | ConvertTo-Json -Compress)
}
###############################################################################
# 2. ADLC ORPHAN BRANCH MANAGEMENT
###############################################################################
function Ensure-AdlcBranch {
param([string]$Td)
if (-not $Td) { $Td = $script:TEAM_AI_DIRECTIVES }
git -C $Td show-ref --verify --quiet "refs/heads/$($script:ADLC_BRANCH)" *> $null
if ($LASTEXITCODE -eq 0) { return }
git -C $Td branch --orphan $script:ADLC_BRANCH *> $null
git -C $Td worktree add $script:ADLC_WORKTREE $script:ADLC_BRANCH *> $null
New-Item -ItemType Directory -Force -Path (Join-Path $script:ADLC_WORKTREE 'drafts/cdr'), (Join-Path $script:ADLC_WORKTREE 'reports/sessions'), (Join-Path $script:ADLC_WORKTREE 'reports/projects') | Out-Null
git -C $script:ADLC_WORKTREE add -A *> $null
git -C $script:ADLC_WORKTREE commit --allow-empty -m 'Initialize adlc orphan branch' *> $null
git -C $script:ADLC_WORKTREE push origin $script:ADLC_BRANCH *> $null
git -C $Td worktree remove $script:ADLC_WORKTREE *> $null
}
###############################################################################
# 3. CDR NUMBERING
###############################################################################
function Get-NextCdrNumber {
param([string]$Td)
if (-not $Td) { $Td = $script:TEAM_AI_DIRECTIVES }
$max = 0
$files = @(git -C $Td ls-tree --name-only $script:ADLC_BRANCH 'drafts/cdr/' 2>$null)
foreach ($f in ($files | Sort-Object)) {
# A record's number is the LEADING DIGITS of its filename stem:
# CDR-010-amendment-2.md is record 010. Suffixes are preserved and
# never arithmetically parsed -- the same rule Get-NextRecordNumber in
# product-clarify's pdr-lib.ps1 implements on the filesystem. The old
# '.*CDR-(\d+)\.md$' match silently skipped stemmed records and handed
# out a slot that was already taken.
$name = [System.IO.Path]::GetFileName($f)
if ($name -cnotlike '*.md') { continue }
if ($name.Substring(0, $name.Length - 3) -cmatch '^CDR-(\d+)') {
$num = [int]$Matches[1]
if ($num -gt $max) { $max = $num }
}
}
return ('{0:d3}' -f ($max + 1))
}
###############################################################################
# 4. ADLC BRANCH READ/WRITE
###############################################################################
function Read-AdlcFile {
param([string]$Td, [string]$Path)
if (-not $Td) { $Td = $script:TEAM_AI_DIRECTIVES }
$out = @(git -C $Td show "$($script:ADLC_BRANCH):$Path" 2>$null)
if ($LASTEXITCODE -ne 0) { return '' }
return ($out -join "`n")
}
function Write-AdlcFile {
param([string]$Td, [string]$Path, [string]$Content)
if (-not $Td) { $Td = $script:TEAM_AI_DIRECTIVES }
git -C $Td worktree add $script:ADLC_WORKTREE $script:ADLC_BRANCH *> $null
if ($LASTEXITCODE -ne 0) {
git -C $Td worktree add --force $script:ADLC_WORKTREE $script:ADLC_BRANCH *> $null
}
$dest = Join-Path $script:ADLC_WORKTREE $Path
New-Item -ItemType Directory -Force -Path (Split-Path $dest -Parent) | Out-Null
Set-Content -LiteralPath $dest -Value $Content -NoNewline
git -C $script:ADLC_WORKTREE add $Path *> $null
git -C $script:ADLC_WORKTREE commit -m "team-levelup: update $Path" *> $null
git -C $script:ADLC_WORKTREE push origin $script:ADLC_BRANCH *> $null
git -C $Td worktree remove $script:ADLC_WORKTREE *> $null
}
###############################################################################
# 5. CDR INDEX GENERATION
###############################################################################
function Regenerate-CdrIndex {
param([string]$Td)
if (-not $Td) { $Td = $script:TEAM_AI_DIRECTIVES }
$indexPath = 'drafts/cdr/cdr.md'
$today = Get-Date -Format 'yyyy-MM-dd'
$entries = ''
$count = 0
$files = @(git -C $Td ls-tree --name-only $script:ADLC_BRANCH 'drafts/cdr/' 2>$null)
foreach ($f in ($files | Sort-Object)) {
if ($f -cnotmatch 'CDR-\d+\.md$') { continue }
$id = [IO.Path]::GetFileNameWithoutExtension($f)
$content = Read-AdlcFile $Td $f
$target = Get-Field $content '^### Target Module:' 0
$target = $target -replace '`', ''
if ($target.Length -gt 40) { $target = $target.Substring(0, 40) }
$type = Get-Field $content '^### Context Type:' 20
$status = (Get-Field $content '^### Status:' 12) -replace '\*\*', ''
$created = Get-Field $content '^### Date:' 10
$verified = Get-Field $content '^verified:' 10
$age = Get-Field $content '^age_days:' 5
$descriptor = Get-Field $content '^### Descriptor:' 60
if (-not $created) { $created = $today }
if (-not $verified) { $verified = '-' }
if (-not $age) { $age = '-' }
$entries += "| $id | $(If($target){$target}else{' '}) | $(If($type){$type}else{' '}) | $(If($status){$status}else{'Discovered'}) | $created | $verified | $age | $(If($descriptor){$descriptor}else{' '}) |`n"
$count++
}
$indexContent = @"
# Context Directive Records
Context Directive Records (CDRs) track proposed and accepted contributions to team-ai-directives.
## CDR Index
| ID | Target Module | Type | Status | Created | Verified | Age | Descriptor |
|----|---------------|------|--------|---------|----------|-----|------------|
$entries
**Stats**: $count entries | Last Updated: $today
"@
# the bash twin's echo adds the final newline; Set-Content -NoNewline does not
$indexContent += "`n"
Write-AdlcFile $Td $indexPath $indexContent
return $indexPath
}
# Extract the first line matching a ^header pattern, strip the header, cap
# the length (parity with the bash twin's grep -m1 | sed | head -c pipeline).
# Cap 0 or negative = uncapped.
function Get-Field {
param([string]$Content, [string]$Pattern, [int]$Cap)
foreach ($line in ($Content -split "`n")) {
if ($line -cmatch $Pattern) {
$v = ($line -creplace $Pattern, '').Trim()
# bash head -c counts BYTES; ASCII-safe here — chars == bytes
if ($Cap -gt 0 -and $v.Length -gt $Cap) { $v = $v.Substring(0, $Cap) }
return $v
}
}
return ''
}
###############################################################################
# 6. SIGNAL GATE VALIDATION
###############################################################################
function Invoke-SignalGate {
param([string]$File)
$reasons = @()
if (-not (Test-Path -LiteralPath $File -PathType Leaf)) {
# bash grep on a missing file: every grep fails → only the
# "no evidence" reason fires (its grep -Eq also fails).
return 'SKIP: no evidence'
}
$content = Get-Content -Raw -LiteralPath $File
if ($content -imatch 'project-specific|only applies to this project|single project') { $reasons += 'project-specific' }
if ($content -imatch 'duplicate of|overlaps with existing|already in team-ai-directives') { $reasons += 'duplicate' }
if ($content -cnotmatch '(src/|lib/|test/|docs/|commit|file)') { $reasons += 'no evidence' }
if ($content -imatch 'low value|nice-to-have|minor convenience') { $reasons += 'low value' }
if ($reasons.Count -gt 0) {
return "SKIP: $($reasons -join ' ')"
}
return 'PASS'
}
###############################################################################
# CLI ENTRY
###############################################################################
switch ($args.Count) {
0 { }
default {
switch ([string]$args[0]) {
'--json' { Output-Json }
'--next-cdr' {
# resolve first: the bare form reads the path globals
$null = Resolve-TeamLevelupPaths
Get-NextCdrNumber $(if ($args.Count -ge 2) { [string]$args[1] } else { '' })
}
'--index' {
$null = Resolve-TeamLevelupPaths
Regenerate-CdrIndex $(if ($args.Count -ge 2) { [string]$args[1] } else { '' })
}
'--signal-gate' {
if ($args.Count -lt 2) {
[Console]::Error.WriteLine('ERROR: --signal-gate requires a file path')
exit 1
}
# parity: the bash twin's function return code IS the exit
# code (PASS → 0, SKIP → 1)
$verdict = Invoke-SignalGate ([string]$args[1])
Write-Output $verdict
if ($verdict -eq 'PASS') { exit 0 } else { exit 1 }
}
}
}
}
scripts/powershell/score-confidence.ps1
# score-confidence.ps1 — Calculate confidence score for a CDR
# (PowerShell twin of bash/score-confidence.sh; one behavior, two platform
# scripts. The bash twin uses awk for float math; this twin uses [double].)
$ErrorActionPreference = 'Stop'
$script:ErrorView = 'ConciseView'
function Get-Calc {
param([double]$Value)
return ('{0:F2}' -f $Value)
}
function Get-CdrField {
param([string]$CdrFile, [string]$Pattern)
if (-not (Test-Path -LiteralPath $CdrFile -PathType Leaf)) { return '' }
foreach ($line in (Get-Content -LiteralPath $CdrFile)) {
if ($line -cmatch $Pattern) {
return ($line -creplace $Pattern, '').Trim()
}
}
return ''
}
function Get-CalculateConfidence {
param([string]$CdrFile, [string]$ConfidenceFile = '')
$baseScore = 0.60
$bonuses = 0.00
$usageMultiplier = 1.0
# Determine base score by type
$type = (Get-CdrField $CdrFile '^### Context Type:').ToLower()
if (-not $type) { $type = 'rule' }
switch ($type) {
'rule' { $baseScore = 0.60 }
'persona' { $baseScore = 0.50 }
'example' { $baseScore = 0.50 }
'constitution' { $baseScore = 0.70 }
default { $baseScore = 0.60 }
}
$content = if (Test-Path -LiteralPath $CdrFile -PathType Leaf) { Get-Content -Raw -LiteralPath $CdrFile } else { '' }
# Bonus: paired eval exists
if ($content -cmatch '### Paired Eval') { $bonuses += 0.20 }
# Bonus: evidence includes file paths/commits
if ($content -cmatch '(src/|lib/|test/|docs/|commit|file)') { $bonuses += 0.10 }
# Bonus: multiple projects
$projectField = if (Test-Path -LiteralPath $CdrFile -PathType Leaf) { Get-CdrField $CdrFile '^project:' } else { '' }
if ($projectField -cmatch ',') { $bonuses += 0.10 }
# Usage multiplier from confidence-scores.json
if ($ConfidenceFile -ne '' -and (Test-Path -LiteralPath $ConfidenceFile -PathType Leaf)) {
$cdrId = [IO.Path]::GetFileNameWithoutExtension($CdrFile)
# Parse success_rate from JSON (no jq dependency — same grep/sed
# contract as the bash twin: "CDR-001": { "success_rate": 0.75, ... })
$successRate = 1.0
$raw = Get-Content -Raw -LiteralPath $ConfidenceFile
$m = [regex]::Match($raw, ('"' + [regex]::Escape($cdrId) + '"[\s\S]{0,300}?"success_rate"\s*:\s*([0-9.]+)'))
if ($m.Success) { $successRate = [double]$m.Groups[1].Value }
if ($successRate -gt 0.8) { $usageMultiplier = 1.2 }
elseif ($successRate -lt 0.5) { $usageMultiplier = 0.8 }
}
$finalScore = ($baseScore + $bonuses) * $usageMultiplier
# Cap at 1.0
if ($finalScore -gt 1.0) { $finalScore = 1.00 }
$tier = 'LOW'
if ($finalScore -ge 0.8) { $tier = 'HIGH' }
elseif ($finalScore -ge 0.5 -and $finalScore -lt 0.8) { $tier = 'MEDIUM' }
return "$(Get-Calc $finalScore) $tier"
}
# CLI entry
if ($args.Count -ge 2 -and [string]$args[0] -eq '--calculate') {
Write-Output (Get-CalculateConfidence ([string]$args[1]) $(if ($args.Count -ge 3) { [string]$args[2] } else { '' }))
}
scripts/powershell/scrub-session.ps1
# scrub-session.ps1 — Privacy-scrub a session summary
# (PowerShell twin of bash/scrub-session.sh; one behavior, two platform scripts.)
$ErrorActionPreference = 'Stop'
$script:ErrorView = 'ConciseView'
function Invoke-ScrubSession {
param([string]$Text)
$out = $Text
$out = [regex]::Replace($out, '(?i)(api[_-]?key[=: ]+)[^ ]+', '$1REDACTED')
$out = [regex]::Replace($out, '(?i)(token[=: ]+)[^ ]+', '$1REDACTED')
$out = [regex]::Replace($out, '(?i)(password[=: ]+)[^ ]+', '$1REDACTED')
$out = [regex]::Replace($out, '(?i)(secret[=: ]+)[^ ]+', '$1REDACTED')
$out = [regex]::Replace($out, '(AKIA[A-Z0-9]{16})', 'REDACTED')
$out = [regex]::Replace($out, '(ghp_[A-Za-z0-9]{36})', 'REDACTED')
$out = [regex]::Replace($out, '(glpat-[A-Za-z0-9_-]{20})', 'REDACTED')
return $out
}
if ($args.Count -ge 2 -and [string]$args[0] -eq '--scrub') {
Write-Output (Invoke-ScrubSession ([string]$args[1]))
}
scripts/powershell/team-levelup.ps1
# team-levelup.ps1 — Session-end CDR lifecycle entry point
# (PowerShell twin of bash/team-levelup.sh; one behavior, two platform scripts.)
$ErrorActionPreference = 'Stop'
$script:ErrorView = 'ConciseView'
$SCRIPT_DIR = $PSScriptRoot
. (Join-Path $SCRIPT_DIR 'helpers.ps1')
if ($args.Count -ge 1 -and [string]$args[0] -eq '--setup') {
Output-Json
exit 0
}
# Resolve paths (function sets the script-scoped globals directly; output is
# informational only — never eval it)
$null = Resolve-TeamLevelupPaths
if (-not $script:TEAM_AI_DIRECTIVES -or -not (Test-Path -LiteralPath $script:TEAM_AI_DIRECTIVES -PathType Container)) {
[Console]::Error.WriteLine('Team AI directives repository not configured.')
[Console]::Error.WriteLine('Run: /team-setup')
exit 1
}
# Ensure adlc orphan branch exists
Ensure-AdlcBranch $script:TEAM_AI_DIRECTIVES
# Get next CDR number
$NEXT_CDR = Get-NextCdrNumber $script:TEAM_AI_DIRECTIVES
# Output setup info
Write-Output "NEXT_CDR=$NEXT_CDR"
Write-Output "ADLC_BRANCH=$($script:ADLC_BRANCH)"
Write-Output "TEAM_AI_DIRECTIVES=$($script:TEAM_AI_DIRECTIVES)"
SKILL.md
---
name: team-levelup
description: Use when a session ends to extract CDRs, score confidence, batch review, and publish accepted CDRs to team-ai-directives. Auto-triggers on session_end event. Also invoked from team-boot's Class Boots catalog for CDR descriptor matches.
disable-model-invocation: true
scripts:
sh: scripts/bash/team-levelup.sh
ps: scripts/powershell/team-levelup.ps1
---
# team-levelup
## What this skill does
Session-end CDR lifecycle — extracts reusable patterns from the completed
session, scores them by confidence, presents for batch review, and publishes
accepted CDRs as a draft PR to `team-ai-directives`.
Replaces the former `levelup-specify`, `levelup-clarify`, and `levelup-publish`
skills with a single streamlined workflow.
_Searched N CDRs, K matched._ 0 rows matched → emit the section heading +
the searched line only — no table, never a table header.
When no local context-module records exist in the current working directory
and the directory sits inside a workspace (detected via a `.gitmodules` marker
in an ancestor), read the workspace root's records instead.
## When to use
Invoke at the START of a matching task — before planning the todo list and
before implementation — so the CDR context informs planning.
Never defer to session end.
- **Session end (automatic)**: `.events.json` maps `session_end` → this skill
- **Manual invocation**: `/team-levelup` after completing work
- **Before closing a branch**: Extract team-wide learnings
### When NOT to use
- **Brownfield discovery**: Use `/team-init` to scan existing code
- **ADR/PDR/ChDR capture**: Use the respective clarify skills (architect-clarify, product-clarify, change-clarify)
## Storage
CDR drafts live in the `adlc` orphan branch of `team-ai-directives`:
```
team-ai-directives (adlc branch, orphan)
├── drafts/cdr/
│ ├── CDR-001.md # Pending CDR
│ └── cdr.md # Draft index
└── reports/
├── sessions/<user>/<YYYY-MM>.md
├── projects/<project>.json
└── confidence-scores.json
```
Main branch has NO drafts directory — only accepted CDRs in `context_modules/`.
## Process
### Phase 0: Environment Setup
Run the setup script:
```bash
scripts/bash/team-levelup.sh --setup
```
Parse JSON for `REPO_ROOT`, `TEAM_AI_DIRECTIVES`, `NEXT_CDR`, `ADLC_BRANCH_EXISTS`.
If `TEAM_AI_DIRECTIVES` is not configured, exit with message to run `/team-setup`.
### Phase 1: Extract CDRs from Session
Review the current session to identify reusable patterns:
1. What did the user ask for?
2. What did the agent do? (file changes, key decisions, approach)
3. What reusable patterns emerged?
4. What files were created/modified? (`git diff --stat`, `git log --oneline -10`)
For each pattern, create a CDR draft using the shared template at `skills/team/templates/cdr-draft-template.md`.
Write session trace to `adlc` branch: `reports/sessions/<user>/<YYYY-MM>.md`.
### Phase 2: Score Confidence
For each extracted CDR, calculate confidence:
**Base scores by type:**
- Rule: 0.60
- Persona: 0.50
- Example: 0.50
- Constitution: 0.70
**Bonuses:**
- +0.20 if paired eval exists
- +0.10 if evidence includes file paths/commits
- +0.10 if multiple projects reference same pattern
**Usage multiplier** (from `reports/confidence-scores.json`):
- success_rate > 0.8: ×1.2
- success_rate 0.5-0.8: ×1.0
- success_rate < 0.5: ×0.8
**Thresholds:**
- ≥ 0.8: HIGH — batch review, auto-accept after review
- 0.5-0.79: MEDIUM — batch review required
- < 0.5: LOW — keep as draft
### Phase 3: Batch Review
Present CDRs one at a time (same as former team-learn logic):
```markdown
## CDR-{ID}: {Title}
**Context Type**: {type}
**Confidence**: {score} ({HIGH/MEDIUM/LOW})
**Current Status**: {status}
### Current Content
...
### Choose Action
| Option | Action |
|---|---|
| A | Accept — Approve for implementation |
| B | Reject — Decline with reason |
| C | Defer — Skip for now, keep pending |
| D | Accept all remaining — Accept this and all pending CDRs |
| P | Promote to check (mechanical rules only) |
Reply with your choice (A/B/C/D/P).
```
Wait for user input before proceeding. Update CDR file after each decision.
## Absent Context
If `team-boot` injected no team context this session (no Team Context & Decisions section in the first user message — unconfigured project or hook failure): say so in one line, maintain rows in Team Context & Decisions with a `0 CDRs matched (no team context injected — run /team-setup)` source line, and continue the task on directly-read CDR modules. Never treat a missing injection as an empty record set. Recovery: run `/team-diagnose`.
### Phase 4: Publish
For accepted CDRs, create a draft PR to `team-ai-directives` main branch:
1. Switch to `adlc` branch worktree
2. Read accepted CDRs from `drafts/cdr/`
3. Transform to OKF v0.2 format with confidence frontmatter
4. Write to `context_modules/` in main branch
5. Create branch, commit, push, open draft PR
### Phase 5: Write Usage Data
Write to `adlc` branch:
- `reports/sessions/<user>/<YYYY-MM>.md` — privacy-scrubbed session summary
- `reports/projects/<project>.json` — CDR match/apply counts
- Update `reports/confidence-scores.json`
### Phase 6: Notify
Write report to `.adlc/team-levelup-report.md` (local, not committed):
```markdown
## Team-Levelup Report
**Date**: {date}
**CDRs Extracted**: N
**CDRs Accepted**: N
**CDRs Rejected**: N
**CDRs Deferred**: N
**PR**: {URL or "none — no accepted CDRs"}
### Next Steps
1. Review PR (if created)
2. Run `/team-repair --update-confidence` after merge
```
## Conflict Resolution
When two projects draft the same pattern:
1. First draft creates CDR-001 with `project: project-a`
2. Second draft detects existing CDR with matching descriptor → merges:
- Adds project-b to `project:` field
- Appends evidence section
- Increments confidence (multi-project validation)
## Team Context & Decisions
team-levelup integrates with Team Context & Decisions maintained by team-boot.
CDR-class decisions detected during the session are surfaced as capture
suggestions (run `/team-levelup` to write the draft — the boot never writes
it) and
tracked as table rows in the same section. Mirror each CDR-class decision as a task-list todo
(suggest capture → `/team-levelup` at session end); after code-modifying tasks, add a
trailing todo to sweep Team Context & Decisions until _Unrecorded: 0 pending · Unclarified: 0 drafts_ (a draft leaves Unclarified only via its clarify skill or an explicit user handoff to a named clarify or execute skill). At session end, deliver the clarify prompt naming each uncaptured decision (ID + capture skill) and each captured draft (ID + clarify skill); if the user defers clarify, mark those rows handed off. Do not fabricate
table rows — only record decisions that actually emerged from the session.
## Verification
- [ ] CDRs written to `adlc` branch `drafts/cdr/`
- [ ] Session summary written to `adlc` branch `reports/sessions/`
- [ ] Usage counts written to `adlc` branch `reports/projects/`
- [ ] Accepted CDRs published as draft PR to main branch
- [ ] team-levelup-report.md written locally
- [ ] No drafts in main branch