feat: add automated PDCA loop, domain adapters, cost tracking, DAG renderer
- skills/run: automated PDCA execution loop with --start-from, --dry-run - skills/artifact-routing: inter-phase artifact protocol with context injection - skills/act-phase: structured review→fix pipeline with cycle feedback - skills/domains: domain adapter system (writing, code, research) - skills/cost-tracking: per-agent cost estimation, budget enforcement - lib/archeflow-dag.sh: ASCII DAG renderer from JSONL events - lib/archeflow-report.sh: updated with DAG section, cycle diff, --dag/--summary flags
This commit is contained in:
261
lib/archeflow-dag.sh
Executable file
261
lib/archeflow-dag.sh
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env bash
|
||||
# archeflow-dag.sh — Render an ASCII DAG from ArcheFlow JSONL events.
|
||||
#
|
||||
# Usage: ./lib/archeflow-dag.sh <events.jsonl> [--color] [--no-color]
|
||||
#
|
||||
# Reads a JSONL event file and renders the causal DAG as ASCII art.
|
||||
# Each event shows: #seq description (phase) [metadata]
|
||||
# Tree drawing uses Unicode box-drawing characters for branches.
|
||||
#
|
||||
# The rendering uses a "logical grouping" strategy: phase transitions and
|
||||
# structural events appear as top-level siblings under root, with agents
|
||||
# and sub-events nested beneath their phase section. This gives a readable
|
||||
# timeline view while preserving DAG relationships.
|
||||
#
|
||||
# Requires: jq
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <events.jsonl> [--color] [--no-color]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EVENT_FILE="$1"
|
||||
shift
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$EVENT_FILE" ]]; then
|
||||
echo "Error: Event file not found: $EVENT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Color support: auto-detect terminal, allow override
|
||||
USE_COLOR=auto
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--color) USE_COLOR=yes ;;
|
||||
--no-color) USE_COLOR=no ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$USE_COLOR" == "auto" ]]; then
|
||||
if [[ -t 1 ]]; then
|
||||
USE_COLOR=yes
|
||||
else
|
||||
USE_COLOR=no
|
||||
fi
|
||||
fi
|
||||
|
||||
# ANSI color codes
|
||||
if [[ "$USE_COLOR" == "yes" ]]; then
|
||||
C_RESET="\033[0m"
|
||||
C_SEQ="\033[1;37m" # bold white for seq numbers
|
||||
C_PLAN="\033[1;34m" # blue for plan phase
|
||||
C_DO="\033[1;32m" # green for do phase
|
||||
C_CHECK="\033[1;33m" # yellow for check phase
|
||||
C_ACT="\033[1;35m" # magenta for act phase
|
||||
C_TRANS="\033[0;36m" # cyan for phase transitions
|
||||
C_DIM="\033[0;90m" # dim for metadata
|
||||
C_DECISION="\033[1;33m" # yellow for decisions
|
||||
C_VERDICT="\033[1;31m" # red for verdicts
|
||||
else
|
||||
C_RESET="" C_SEQ="" C_PLAN="" C_DO="" C_CHECK="" C_ACT=""
|
||||
C_TRANS="" C_DIM="" C_DECISION="" C_VERDICT=""
|
||||
fi
|
||||
|
||||
phase_color() {
|
||||
case "$1" in
|
||||
plan) printf "%s" "$C_PLAN" ;;
|
||||
do) printf "%s" "$C_DO" ;;
|
||||
check) printf "%s" "$C_CHECK" ;;
|
||||
act) printf "%s" "$C_ACT" ;;
|
||||
*) printf "%s" "$C_RESET" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Pre-process all events with jq into a structured format for bash consumption.
|
||||
# Output: seq|type|phase|agent|parents_csv|label
|
||||
# This avoids calling jq per-event in a loop.
|
||||
EVENTS_PARSED=$(jq -r '
|
||||
def mklabel:
|
||||
if .type == "run.start" then "run.start"
|
||||
elif .type == "agent.complete" then
|
||||
(.data.archetype // .agent // "unknown") + " (" + .phase + ")" +
|
||||
(if (.data.tokens // 0) > 0 then " [" + (.data.tokens | tostring) + " tok]" else "" end)
|
||||
elif .type == "decision" then
|
||||
"decision: " + (.data.what // "unknown") + " → " + (.data.chosen // "unknown")
|
||||
elif .type == "phase.transition" then
|
||||
"─── " + (.data.from // "?") + " → " + (.data.to // "?") + " ───"
|
||||
elif .type == "review.verdict" then
|
||||
(.data.archetype // .agent // "unknown") + " (" + .phase + ") → " +
|
||||
((.data.verdict // "unknown") | ascii_upcase | gsub("_"; " "))
|
||||
elif .type == "fix.applied" then
|
||||
"fix (" + (.data.source // "unknown") + "): " + (.data.finding // "unknown")
|
||||
elif .type == "cycle.boundary" then
|
||||
"cycle " + ((.data.cycle // 0) | tostring) + "/" + ((.data.max_cycles // 0) | tostring) +
|
||||
" → " + (.data.next_action // "continue")
|
||||
elif .type == "shadow.detected" then
|
||||
"shadow: " + (.data.archetype // "unknown") + " — " + (.data.shadow // "unknown")
|
||||
elif .type == "run.complete" then
|
||||
"run.complete [" + ((.data.agents_total // .data.agents // 0) | tostring) +
|
||||
" agents, " + ((.data.fixes_total // .data.fixes // 0) | tostring) + " fixes]"
|
||||
else .type
|
||||
end;
|
||||
[.seq, .type, .phase,
|
||||
(.agent // "_NONE_"),
|
||||
(((.parent // []) | map(tostring) | join(",")) | if . == "" then "_NONE_" else . end),
|
||||
mklabel]
|
||||
| join("§")
|
||||
' "$EVENT_FILE")
|
||||
|
||||
# Parse into arrays
|
||||
declare -A EVENT_TYPE EVENT_PHASE EVENT_LABEL EVENT_PARENTS
|
||||
declare -A CHILDREN_OF # parent_seq -> space-separated child seqs
|
||||
MAX_SEQ=0
|
||||
|
||||
while IFS='§' read -r seq type phase agent parents label; do
|
||||
[[ "$agent" == "_NONE_" ]] && agent=""
|
||||
[[ "$parents" == "_NONE_" ]] && parents=""
|
||||
EVENT_TYPE[$seq]="$type"
|
||||
EVENT_PHASE[$seq]="$phase"
|
||||
EVENT_LABEL[$seq]="$label"
|
||||
EVENT_PARENTS[$seq]="$parents"
|
||||
|
||||
# Register parent-child relationships
|
||||
if [[ -z "$parents" ]]; then
|
||||
CHILDREN_OF[0]="${CHILDREN_OF[0]:-} $seq"
|
||||
else
|
||||
IFS=',' read -ra parent_arr <<< "$parents"
|
||||
for p in "${parent_arr[@]}"; do
|
||||
CHILDREN_OF[$p]="${CHILDREN_OF[$p]:-} $seq"
|
||||
done
|
||||
fi
|
||||
|
||||
if (( seq > MAX_SEQ )); then
|
||||
MAX_SEQ=$seq
|
||||
fi
|
||||
done <<< "$EVENTS_PARSED"
|
||||
|
||||
# Sort and deduplicate children
|
||||
for key in "${!CHILDREN_OF[@]}"; do
|
||||
CHILDREN_OF[$key]=$(echo "${CHILDREN_OF[$key]}" | tr ' ' '\n' | sort -un | tr '\n' ' ' | xargs)
|
||||
done
|
||||
|
||||
# Determine display parent for each event.
|
||||
# Strategy: structural events (phase.transition, cycle.boundary, run.complete) are promoted
|
||||
# to be direct children of #1 (run.start), creating a flat timeline backbone.
|
||||
# All other events use their first (lowest-numbered) parent for display.
|
||||
declare -A DISPLAY_PARENT # seq -> parent seq for display (0 = root)
|
||||
declare -A DISPLAY_CHILDREN # parent -> ordered children for display
|
||||
|
||||
for seq_i in $(seq 1 "$MAX_SEQ"); do
|
||||
[[ -z "${EVENT_TYPE[$seq_i]:-}" ]] && continue
|
||||
local_type="${EVENT_TYPE[$seq_i]}"
|
||||
parents_csv="${EVENT_PARENTS[$seq_i]:-}"
|
||||
|
||||
if [[ -z "$parents_csv" ]]; then
|
||||
# Root event (run.start)
|
||||
DISPLAY_PARENT[$seq_i]=0
|
||||
elif [[ "$local_type" == "phase.transition" || "$local_type" == "cycle.boundary" || "$local_type" == "run.complete" ]]; then
|
||||
# Promote structural events to be children of run.start (#1)
|
||||
DISPLAY_PARENT[$seq_i]=1
|
||||
else
|
||||
# Use first (lowest) parent as display parent
|
||||
IFS=',' read -ra parr <<< "$parents_csv"
|
||||
DISPLAY_PARENT[$seq_i]="${parr[0]}"
|
||||
fi
|
||||
|
||||
dp="${DISPLAY_PARENT[$seq_i]}"
|
||||
DISPLAY_CHILDREN[$dp]="${DISPLAY_CHILDREN[$dp]:-} $seq_i"
|
||||
done
|
||||
|
||||
# Sort display children
|
||||
for key in "${!DISPLAY_CHILDREN[@]}"; do
|
||||
DISPLAY_CHILDREN[$key]=$(echo "${DISPLAY_CHILDREN[$key]}" | tr ' ' '\n' | sort -n | tr '\n' ' ' | xargs)
|
||||
done
|
||||
|
||||
# Render the tree recursively using display hierarchy
|
||||
render_node() {
|
||||
local seq="$1"
|
||||
local prefix="$2"
|
||||
local is_last="$3"
|
||||
|
||||
local label="${EVENT_LABEL[$seq]:-unknown}"
|
||||
local phase="${EVENT_PHASE[$seq]:-}"
|
||||
local type="${EVENT_TYPE[$seq]:-}"
|
||||
local pc
|
||||
pc=$(phase_color "$phase")
|
||||
|
||||
# Format seq number with padding
|
||||
local seq_str
|
||||
seq_str=$(printf "#%-3s" "${seq}")
|
||||
|
||||
# Connector
|
||||
local connector
|
||||
if [[ -z "$prefix" && "$seq" == "1" ]]; then
|
||||
connector=""
|
||||
elif [[ "$is_last" == "true" ]]; then
|
||||
connector="└── "
|
||||
else
|
||||
connector="├── "
|
||||
fi
|
||||
|
||||
# Color the label based on type
|
||||
local colored_label
|
||||
case "$type" in
|
||||
phase.transition) colored_label="${C_TRANS}${label}${C_RESET}" ;;
|
||||
decision) colored_label="${C_DECISION}${label}${C_RESET}" ;;
|
||||
review.verdict) colored_label="${C_VERDICT}${label}${C_RESET}" ;;
|
||||
*) colored_label="${pc}${label}${C_RESET}" ;;
|
||||
esac
|
||||
|
||||
if [[ "$seq" == "1" ]]; then
|
||||
printf "%b\n" "${C_SEQ}#1${C_RESET} ${colored_label}"
|
||||
else
|
||||
printf "%b\n" "${prefix}${connector}${C_SEQ}${seq_str}${C_RESET}${colored_label}"
|
||||
fi
|
||||
|
||||
# Render children
|
||||
local children="${DISPLAY_CHILDREN[$seq]:-}"
|
||||
if [[ -z "$children" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local child_arr=($children)
|
||||
local count=${#child_arr[@]}
|
||||
local i=0
|
||||
|
||||
for c in "${child_arr[@]}"; do
|
||||
i=$((i + 1))
|
||||
local child_is_last="false"
|
||||
if [[ $i -eq $count ]]; then
|
||||
child_is_last="true"
|
||||
fi
|
||||
|
||||
local child_prefix
|
||||
if [[ "$seq" == "1" ]]; then
|
||||
child_prefix=""
|
||||
elif [[ "$is_last" == "true" ]]; then
|
||||
child_prefix="${prefix} "
|
||||
else
|
||||
child_prefix="${prefix}│ "
|
||||
fi
|
||||
|
||||
render_node "$c" "$child_prefix" "$child_is_last"
|
||||
done
|
||||
}
|
||||
|
||||
# Find root nodes (display parent == 0 means top-level)
|
||||
root_children="${DISPLAY_CHILDREN[0]:-}"
|
||||
if [[ -z "$root_children" ]]; then
|
||||
echo "No events found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The first root child should be #1 (run.start), render from there
|
||||
render_node 1 "" "true"
|
||||
@@ -1,26 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# archeflow-report.sh — Generate a Markdown process report from ArcheFlow JSONL events.
|
||||
#
|
||||
# Usage: ./lib/archeflow-report.sh <events.jsonl> [--output <file.md>]
|
||||
# Usage: ./lib/archeflow-report.sh <events.jsonl> [--output <file.md>] [--dag] [--summary]
|
||||
#
|
||||
# Reads a JSONL event file and produces a structured Markdown report showing
|
||||
# the full orchestration process: phases, decisions, reviews, fixes, metrics.
|
||||
#
|
||||
# Flags:
|
||||
# --output <file.md> Write report to file instead of stdout
|
||||
# --dag Output ONLY the ASCII DAG (for quick terminal viewing)
|
||||
# --summary Output a one-line summary (for session logs)
|
||||
#
|
||||
# Requires: jq
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <events.jsonl> [--output <file.md>]" >&2
|
||||
echo "Usage: $0 <events.jsonl> [--output <file.md>] [--dag] [--summary]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EVENT_FILE="$1"
|
||||
OUTPUT=""
|
||||
shift
|
||||
|
||||
if [[ "${2:-}" == "--output" && -n "${3:-}" ]]; then
|
||||
OUTPUT="$3"
|
||||
fi
|
||||
OUTPUT=""
|
||||
MODE="full" # full | dag | summary
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
OUTPUT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dag)
|
||||
MODE="dag"
|
||||
shift
|
||||
;;
|
||||
--summary)
|
||||
MODE="summary"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed." >&2
|
||||
@@ -45,7 +71,74 @@ TASK=$(echo "$RUN_START" | jq -r '.data.task // "unknown"')
|
||||
WORKFLOW=$(echo "$RUN_START" | jq -r '.data.workflow // "unknown"')
|
||||
TEAM=$(echo "$RUN_START" | jq -r '.data.team // "unknown"')
|
||||
|
||||
# Generate report
|
||||
# --summary mode: one-line output and exit
|
||||
if [[ "$MODE" == "summary" ]]; then
|
||||
if [[ -n "$RUN_COMPLETE" ]]; then
|
||||
STATUS=$(echo "$RUN_COMPLETE" | jq -r '.data.status // "unknown"')
|
||||
CYCLES=$(echo "$RUN_COMPLETE" | jq -r '.data.cycles // "?"')
|
||||
# Handle both agents_total and agents field names
|
||||
AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // .data.agents // "?"')
|
||||
FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // .data.fixes // "?"')
|
||||
DURATION_MS=$(echo "$RUN_COMPLETE" | jq -r '.data.duration_ms // "0"')
|
||||
if [[ "$DURATION_MS" != "0" && "$DURATION_MS" != "null" ]]; then
|
||||
DURATION_MIN=$(( DURATION_MS / 60000 ))
|
||||
echo "[${STATUS}] ${TASK} — ${CYCLES} cycles, ${AGENTS} agents, ${FIXES} fixes (~${DURATION_MIN}min) [${RUN_ID}]"
|
||||
else
|
||||
echo "[${STATUS}] ${TASK} — ${CYCLES} cycles, ${AGENTS} agents, ${FIXES} fixes [${RUN_ID}]"
|
||||
fi
|
||||
else
|
||||
echo "[in-progress] ${TASK} [${RUN_ID}]"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --dag mode: output DAG and exit
|
||||
if [[ "$MODE" == "dag" ]]; then
|
||||
if [[ -x "${SCRIPT_DIR}/archeflow-dag.sh" ]]; then
|
||||
"${SCRIPT_DIR}/archeflow-dag.sh" "$EVENT_FILE" "$@"
|
||||
else
|
||||
echo "Error: archeflow-dag.sh not found at ${SCRIPT_DIR}/archeflow-dag.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Full report mode ---
|
||||
|
||||
# Collect cycle data for cycle diff section
|
||||
CYCLE_BOUNDARIES=$(events_of_type "cycle.boundary" | jq -r '.data.cycle' 2>/dev/null || true)
|
||||
CYCLE_COUNT=0
|
||||
if [[ -n "$CYCLE_BOUNDARIES" ]]; then
|
||||
CYCLE_COUNT=$(echo "$CYCLE_BOUNDARIES" | grep -c '[0-9]' 2>/dev/null || true)
|
||||
CYCLE_COUNT=${CYCLE_COUNT:-0}
|
||||
fi
|
||||
|
||||
# Collect review findings per cycle for diff
|
||||
# A cycle's reviews are between two cycle.boundary events (or between start and first boundary)
|
||||
collect_cycle_findings() {
|
||||
# Returns JSON array of {cycle, archetype, findings[]} for all review.verdict events
|
||||
jq -s '
|
||||
# Assign cycle number to each event based on cycle.boundary positions
|
||||
(
|
||||
[.[] | select(.type == "cycle.boundary") | .seq] | sort
|
||||
) as $boundaries |
|
||||
[.[] | select(.type == "review.verdict")] |
|
||||
[.[] | {
|
||||
seq: .seq,
|
||||
archetype: (.data.archetype // .agent // "unknown"),
|
||||
verdict: .data.verdict,
|
||||
findings: (.data.findings // []),
|
||||
cycle: (
|
||||
.seq as $s |
|
||||
if ($boundaries | length) == 0 then 1
|
||||
else
|
||||
([1] + [$boundaries | to_entries[] | select(.value < $s) | .key + 2] | max)
|
||||
end
|
||||
)
|
||||
}]
|
||||
' "$EVENT_FILE"
|
||||
}
|
||||
|
||||
generate_report() {
|
||||
cat <<HEADER
|
||||
# Process Report: ${TASK}
|
||||
@@ -63,11 +156,17 @@ HEADER
|
||||
if [[ -n "$RUN_COMPLETE" ]]; then
|
||||
STATUS=$(echo "$RUN_COMPLETE" | jq -r '.data.status // "unknown"')
|
||||
CYCLES=$(echo "$RUN_COMPLETE" | jq -r '.data.cycles // "?"')
|
||||
AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // "?"')
|
||||
FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // "?"')
|
||||
# Handle both agents_total and agents field names
|
||||
AGENTS=$(echo "$RUN_COMPLETE" | jq -r '.data.agents_total // .data.agents // "?"')
|
||||
FIXES=$(echo "$RUN_COMPLETE" | jq -r '.data.fixes_total // .data.fixes // "?"')
|
||||
SHADOWS=$(echo "$RUN_COMPLETE" | jq -r '.data.shadows // "0"')
|
||||
DURATION_MS=$(echo "$RUN_COMPLETE" | jq -r '.data.duration_ms // "0"')
|
||||
DURATION_MIN=$(( DURATION_MS / 60000 ))
|
||||
if [[ "$DURATION_MS" != "0" && "$DURATION_MS" != "null" ]]; then
|
||||
DURATION_MIN=$(( DURATION_MS / 60000 ))
|
||||
DURATION_DISPLAY="~${DURATION_MIN} min"
|
||||
else
|
||||
DURATION_DISPLAY="n/a"
|
||||
fi
|
||||
|
||||
cat <<TABLE
|
||||
| Field | Value |
|
||||
@@ -77,7 +176,7 @@ HEADER
|
||||
| **Agents** | ${AGENTS} |
|
||||
| **Fixes** | ${FIXES} |
|
||||
| **Shadows** | ${SHADOWS} |
|
||||
| **Duration** | ~${DURATION_MIN} min |
|
||||
| **Duration** | ${DURATION_DISPLAY} |
|
||||
|
||||
TABLE
|
||||
fi
|
||||
@@ -95,6 +194,21 @@ TABLE
|
||||
echo "---"
|
||||
echo ""
|
||||
|
||||
# Process Flow (DAG)
|
||||
echo "## Process Flow"
|
||||
echo ""
|
||||
echo '```'
|
||||
if [[ -x "${SCRIPT_DIR}/archeflow-dag.sh" ]]; then
|
||||
"${SCRIPT_DIR}/archeflow-dag.sh" "$EVENT_FILE" --no-color
|
||||
else
|
||||
echo "(DAG renderer not available)"
|
||||
fi
|
||||
echo '```'
|
||||
echo ""
|
||||
|
||||
echo "---"
|
||||
echo ""
|
||||
|
||||
# Phase sections — iterate through phase transitions
|
||||
echo "## Phases"
|
||||
echo ""
|
||||
@@ -183,7 +297,7 @@ TABLE
|
||||
SHADOW=$(echo "$event" | jq -r '.data.shadow // "unknown"')
|
||||
ACTION=$(echo "$event" | jq -r '.data.action // "unknown"')
|
||||
|
||||
echo "- **Shadow** ⚠️ ${ARCHETYPE}: ${SHADOW} → ${ACTION}"
|
||||
echo "- **Shadow** ${ARCHETYPE}: ${SHADOW} → ${ACTION}"
|
||||
echo ""
|
||||
;;
|
||||
|
||||
@@ -203,6 +317,65 @@ TABLE
|
||||
|
||||
done < "$EVENT_FILE"
|
||||
|
||||
# Cycle Comparison section (only if multiple cycles detected)
|
||||
if [[ "$CYCLE_COUNT" -ge 2 ]]; then
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Cycle Comparison"
|
||||
echo ""
|
||||
|
||||
# Collect all review findings with cycle assignment
|
||||
CYCLE_FINDINGS=$(collect_cycle_findings)
|
||||
|
||||
# Get unique cycle numbers
|
||||
CYCLE_NUMS=$(echo "$CYCLE_FINDINGS" | jq -r '[.[].cycle] | unique | .[]')
|
||||
|
||||
# Compare consecutive cycles
|
||||
PREV_CYCLE=""
|
||||
for CURR_CYCLE in $CYCLE_NUMS; do
|
||||
if [[ -n "$PREV_CYCLE" ]]; then
|
||||
echo "### Cycle ${PREV_CYCLE} → Cycle ${CURR_CYCLE}"
|
||||
echo ""
|
||||
|
||||
# Get findings for each cycle as JSON arrays
|
||||
PREV_FINDINGS=$(echo "$CYCLE_FINDINGS" | jq --argjson c "$PREV_CYCLE" \
|
||||
'[.[] | select(.cycle == $c) | .findings[] | {desc: .description, sev: .severity}]' 2>/dev/null || echo "[]")
|
||||
CURR_FINDINGS=$(echo "$CYCLE_FINDINGS" | jq --argjson c "$CURR_CYCLE" \
|
||||
'[.[] | select(.cycle == $c) | .findings[] | {desc: .description, sev: .severity}]' 2>/dev/null || echo "[]")
|
||||
|
||||
# Compute new, resolved, and persistent findings
|
||||
DIFF_OUTPUT=$(jq -rn --argjson prev "$PREV_FINDINGS" --argjson curr "$CURR_FINDINGS" '
|
||||
def descs: [.[].desc];
|
||||
($prev | descs) as $pd |
|
||||
($curr | descs) as $cd |
|
||||
($curr | [.[] | select(.desc as $d | $pd | all(. != $d))]) as $new |
|
||||
($prev | [.[] | select(.desc as $d | $cd | all(. != $d))]) as $resolved |
|
||||
($curr | [.[] | select(.desc as $d | $pd | any(. == $d))]) as $persistent |
|
||||
(
|
||||
(if ($new | length) > 0 then
|
||||
["**New findings:**"] + [$new[] | "- [" + .sev + "] " + .desc]
|
||||
else [] end) +
|
||||
(if ($resolved | length) > 0 then
|
||||
["", "**Resolved findings:**"] + [$resolved[] | "- [" + .sev + "] " + .desc]
|
||||
else [] end) +
|
||||
(if ($persistent | length) > 0 then
|
||||
["", "**Persistent findings:**"] + [$persistent[] | "- [" + .sev + "] " + .desc]
|
||||
else [] end)
|
||||
) | .[]
|
||||
' 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$DIFF_OUTPUT" ]]; then
|
||||
echo "$DIFF_OUTPUT"
|
||||
else
|
||||
echo "(No findings to compare)"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
PREV_CYCLE="$CURR_CYCLE"
|
||||
done
|
||||
fi
|
||||
|
||||
# Artifacts list from run.complete
|
||||
if [[ -n "$RUN_COMPLETE" ]]; then
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user