- name
- skill-cite
- description
- Verify citation claims against Literature/ index and Zotero library. Invoke for /cite command.
- allowed-tools
- Bash, Read, Write, Edit, AskUserQuestion
# Cite Skill (Direct Execution)
Direct execution skill for verifying citation claims in task artifacts against the Literature/ index and Zotero library. Extracts citations, searches for source matches, scores confidence, presents findings interactively, and creates tasks for unverified claims.
**Key behavior**: Users always see citation findings BEFORE any tasks are created. Users select which unverified/gap claims to address via interactive prompts.
## Context References
Reference (do not load eagerly):
- Path: `@specs/state.json` - Machine state
- Path: `@specs/TODO.md` - Current task list
- Path: `@specs/literature/index.json` - Literature index for keyword matching
---
## Execution
### Step 1: Parse Arguments
Extract task number, optional description text, and flags from command input:
```bash
# Parse from command input
args="$ARGUMENTS"
# Extract task number (first numeric argument)
task_num=$(echo "$args" | grep -oP '^\s*\K\d+' | head -1)
# Check for --gaps flag (also show gap items separately)
show_gaps=false
if echo "$args" | grep -q -- '--gaps'; then
show_gaps=true
fi
# Check for optional description text (everything after flags/number)
description_override=$(echo "$args" | sed 's/--[a-z-]*//g' | sed "s/$task_num//" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
```
If no task number is provided and no direct file path is given, report and exit:
```
## Error: No Task Specified
Usage: /cite N [--gaps]
N Task number to verify citations for
--gaps Also flag citations found but with no PDF source
Example: /cite 42
/cite 42 --gaps
```
### Step 2: Generate Session ID
Generate session ID for tracking:
```bash
source .claude/scripts/lib/common.sh
session_id="$(common_session_id)"
```
### Step 3: Locate Task Artifacts
Read state.json to find the task slug and locate artifacts:
```bash
project_root="$(pwd)"
state_file="$project_root/specs/state.json"
# Find task slug from state.json
task_slug=$(jq -r --argjson num "$task_num" \
'.active_projects[] | select(.project_number == $num) | .project_name' \
"$state_file")
# Also check archive
if [ -z "$task_slug" ] || [ "$task_slug" = "null" ]; then
task_slug=$(jq -r --argjson num "$task_num" \
'.archive[]? | select(.project_number == $num) | .project_name' \
"$state_file" 2>/dev/null)
fi
# Construct task directory path (zero-padded 3 digits)
task_dir=$(printf "specs/%03d_%s" "$task_num" "$task_slug")
if [ ! -d "$project_root/$task_dir" ]; then
echo "Error: Task directory not found: $task_dir"
exit 1
fi
# Glob all artifact files (reports, plans, summaries)
artifact_files=()
while IFS= read -r -d '' f; do
artifact_files+=("$f")
done < <(find "$project_root/$task_dir" -name "*.md" -print0 2>/dev/null)
if [ ${#artifact_files[@]} -eq 0 ]; then
echo "No artifact files found in $task_dir"
echo "Nothing to verify."
exit 0
fi
```
### Step 4: Extract Citations
For each artifact file, run `cite-extract.sh` and aggregate results:
```bash
script_dir="$project_root/.claude/extensions/literature/scripts"
all_citations=() # JSON objects as strings
total_found=0
for artifact_file in "${artifact_files[@]}"; do
rel_path="${artifact_file#$project_root/}"
# Run cite-extract.sh on the file
raw_output=$("$script_dir/cite-extract.sh" --format=json "$artifact_file" 2>/dev/null) || {
exit_code=$?
# exit code 2 = no citations found (normal), anything else = error
if [ $exit_code -ne 2 ]; then
echo "Warning: cite-extract.sh failed for $rel_path (exit $exit_code)" >&2
fi
continue
}
# Add source_file field to each result and accumulate
# cite-extract.sh output schema: [{claim, source_text, line_number, confidence, pattern_type}]
enriched=$(echo "$raw_output" | jq --arg file "$rel_path" \
'[.[] | . + {source_file: $file}]' 2>/dev/null) || continue
count=$(echo "$enriched" | jq 'length' 2>/dev/null || echo 0)
total_found=$((total_found + count))
# Append to all_citations (collect as JSON array strings to merge later)
all_citations+=("$enriched")
done
# Merge all citation arrays into one
if [ ${#all_citations[@]} -eq 0 ]; then
combined_citations="[]"
else
combined_citations=$(printf '%s\n' "${all_citations[@]}" | jq -s 'add // []')
fi
```
### Step 5: Handle No Citations Found
If no citations were extracted:
```
## No Citations Found
**Task**: #{N} — {task_slug}
**Artifacts Scanned**: {count} files in {task_dir}
No citation patterns detected across task artifacts.
Patterns searched: author_year, parenthetical, phrase_attribution,
theorem_attr, direct_quote, numeric_bracket, alpha_num_bracket, latex_cite
```
Exit gracefully without prompts.
### Step 6: Search Literature/ Index
For each unique citation claim, extract query terms and search `specs/literature/index.json`:
```bash
if [ -n "${LITERATURE_DIR:-}" ] && [ -d "$LITERATURE_DIR" ]; then
lit_index="$LITERATURE_DIR/index.json"
else
lit_index="$project_root/specs/literature/index.json"
fi
index_available=false
if [ -f "$lit_index" ]; then
index_available=true
fi
# For each citation, extract key terms from source_text and match against index
# index.json entry schema: {id, title, keywords[], file, summary}
# Match strategy: count how many query terms appear in (title + keywords joined)
score_against_index() {
local source_text="$1"
local query_terms
# Extract significant words (strip common stop words, lowercase, split)
query_terms=$(echo "$source_text" | tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9 ]/ /g' \
| tr ' ' '\n' \
| grep -vE '^(a|an|the|in|on|at|of|to|for|and|or|by|as|is|are|was|were|be|that|this|from|with|it|its|et|al|pp|vol|no|doi)$' \
| grep -v '^[0-9]\{1,4\}$' \
| sort -u | tr '\n' ' ')
if [ -z "$query_terms" ] || [ "$index_available" = "false" ]; then
echo "0"
return
fi
# Count term overlaps in title + keywords fields
jq -r --arg terms "$query_terms" '
.entries // [] | map(
(.title + " " + ((.keywords // []) | join(" "))) |
ascii_downcase |
. as $haystack |
($terms | split(" ") | map(select(length > 2)) |
map(if (($haystack | test(.; "i")) // false) then 1 else 0 end) |
add // 0)
) | max // 0
' "$lit_index" 2>/dev/null || echo "0"
}
```
### Step 7: Search Zotero
For each unique citation claim, search via `zotero-search.sh`:
```bash
# Check if Zotero is configured (zotero-search.sh exits 1 if library not found)
zotero_available=false
if "$script_dir/zotero-search.sh" --limit=1 --format=json "test" &>/dev/null; then
zotero_available=true
elif [ $? -eq 2 ]; then
# exit 2 = no results (library exists, query returned nothing)
zotero_available=true
fi
search_zotero() {
local source_text="$1"
if [ "$zotero_available" = "false" ]; then
echo "[]"
return
fi
# zotero-search.sh accepts query terms as positional args
# Output schema: [{citation_key, title, authors, year, score, pdf_paths, abstract_snippet}]
local query_words
query_words=$(echo "$source_text" | tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9 ]/ /g' \
| tr ' ' '\n' \
| grep -vE '^(a|an|the|in|on|at|of|to|for|and|or|by|as|is|et|al|pp)$' \
| grep -v '^[0-9]\{1,4\}$' \
| sort -u | head -8 | tr '\n' ' ')
if [ -z "$query_words" ]; then
echo "[]"
return
fi
# shellcheck disable=SC2086
"$script_dir/zotero-search.sh" --limit=5 --format=json $query_words 2>/dev/null \
|| echo "[]"
}
```
### Step 8: Score Confidence
Apply scoring to each extracted citation and classify it:
**Scoring thresholds** (based on search results):
- **confirmed**: Zotero top result score >= 3 OR index keyword overlap >= 2
- **partial**: Zotero top result score 1–2 OR index keyword overlap == 1
- **unconfirmed**: No match in either Zotero or Literature/ index
- **gap**: Citation pattern found but source text suggests a specific work that exists in index/Zotero yet has no associated PDF
```bash
score_citation() {
local source_text="$1"
local pattern_type="$2"
local extract_confidence="$3" # from cite-extract.sh (0.5–0.9)
# Get index overlap score
local index_overlap
index_overlap=$(score_against_index "$source_text")
# Get Zotero results
local zotero_results zotero_top_score
zotero_results=$(search_zotero "$source_text")
zotero_top_score=$(echo "$zotero_results" | jq '.[0].score // 0' 2>/dev/null || echo 0)
# Classify
local status best_match
if [ "$zotero_top_score" -ge 3 ] 2>/dev/null || [ "$index_overlap" -ge 2 ] 2>/dev/null; then
status="confirmed"
best_match=$(echo "$zotero_results" | jq -r '.[0] | "\(.authors[0] // "Unknown") (\(.year // "n.d.")). \(.title)"' 2>/dev/null \
|| echo "Literature index match (overlap: $index_overlap)")
elif [ "$zotero_top_score" -ge 1 ] 2>/dev/null || [ "$index_overlap" -ge 1 ] 2>/dev/null; then
status="partial"
best_match=$(echo "$zotero_results" | jq -r '.[0] | "\(.authors[0] // "Unknown") (\(.year // "n.d.")). \(.title)"' 2>/dev/null \
|| echo "Weak literature index match (overlap: $index_overlap)")
else
status="unconfirmed"
best_match="No match found"
fi
# Check for gap: pattern found but PDF unavailable
if [ "$status" != "unconfirmed" ] && [ "$show_gaps" = "true" ]; then
local pdf_count
pdf_count=$(echo "$zotero_results" | jq '.[0].pdf_paths // [] | length' 2>/dev/null || echo 0)
if [ "$pdf_count" -eq 0 ] && [ "$zotero_top_score" -ge 1 ] 2>/dev/null; then
status="gap"
fi
fi
Auf GitHub ansehen