Installs into .claude/skills of the current project.
Are you the author of Retro?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nightgauge-retro)
---
name: retro
description: Analyze pipeline failures to identify root causes, recurring patterns, and
actionable remediation steps using deterministic binary classification. Also
records outcome data and lessons learned to the knowledge base when
knowledge_path is available. Use after a batch run or periodically to
understand why runs did not complete successfully.
license: Apache-2.0
metadata:
author: nightgauge
version: "1.6.0"
source: https://github.com/nightgauge/nightgauge
chainable: true
allowed-tools: Read Write Edit Glob Grep Bash Task
context: fork
agent: pipeline-optimizer
model: haiku
---
# Nightgauge Pipeline Retro
## Description
Scrubs session logs, pipeline context files, batch state, and execution history
to surface failure events, classify them into one of 7 categories, and generate
a root-cause retrospective with remediation recommendations. Produces actionable
recommendation templates with config snippets, recovery guidance, and optional
GitHub issue creation. Uses deterministic Bash/Python3 for data extraction and
failure event collection; AI interprets patterns and produces the narrative
findings.
**Use Cases:**
- Post-batch failure triage after a multi-issue run
- Identifying recurring failure modes across issues
- Understanding why a specific issue's pipeline did not complete
- Tracking whether past remediations are working
- Creating actionable issues for systemic pipeline problems
**When to Use:**
- After any batch run where one or more issues did not reach `complete` status
- When a single issue's pipeline stalled or failed unexpectedly
- Periodically (post-sprint) to review accumulated failure patterns
- Before planning pipeline reliability improvements
## Invocation
| Tool | Command |
| ----------- | --------------------------------------- |
| Claude Code | `/nightgauge:retro [options]` |
| Copilot | Invoke via Agent Skills extension |
| Cursor | Run via Agent Skills or direct SKILL.md |
| Headless | `claude -p "/nightgauge:retro"` |
## Arguments
### Core Options
| Argument | Description | Default |
| ------------------ | ----------------------------------------------------------------------------- | ------- |
| `--issue N` | Analyze failures for a specific issue number | - |
| `--since DATE` | Analyze failures since date (YYYY-MM-DD) | - |
| `--period N` | Analyze last N days | `7` |
| `--all-failures` | Include all failure events, not just last batch run | `false` |
| `--format FORMAT` | Output format: `summary`, `json`, `both` | `both` |
| `--dry-run` | Preview issues that would be created (default) | `true` |
| `--create-issues` | Auto-create GitHub issues for high findings | `false` |
| `--severity LEVEL` | Minimum severity for `--create-issues` | `high` |
| `--record-outcome` | Record outcome to knowledge base (auto-detected when `knowledge_path` is set) | `false` |
| `--epic N` | Run post-epic synthesis: aggregate patterns across all sub-issues of epic #N | - |
| `--skill-feedback` | Run skill self-assessment synthesis (aggregate friction from assessments dir) | `false` |
### Examples
```bash
# Retro on the last batch run (default scope)
/nightgauge:retro
# Retro on a specific issue
/nightgauge:retro --issue 960
# Failures from the last 14 days
/nightgauge:retro --period 14
# Failures since a specific date
/nightgauge:retro --since 2026-02-01
# All failures in history (not just last batch)
/nightgauge:retro --all-failures
# JSON output only (for programmatic consumption)
/nightgauge:retro --format json
# Preview what issues would be created (default behavior)
/nightgauge:retro --dry-run
# Auto-create GitHub issues for high-severity findings
/nightgauge:retro --create-issues
# Create issues for medium+ severity findings
/nightgauge:retro --create-issues --severity medium
# Post-epic synthesis: aggregate patterns across all sub-issues
/nightgauge:retro --epic 347
# Post-epic synthesis with auto-issue creation for recurring friction
/nightgauge:retro --epic 347 --create-issues
# Skill self-assessment synthesis (aggregate all friction records)
/nightgauge:retro --skill-feedback
# Record outcome to knowledge base for a specific completed issue
/nightgauge:retro --issue 960 --record-outcome
# Record outcome automatically (auto-detected when knowledge_path is set in context)
/nightgauge:retro --issue 960
```
---
## Prerequisites
- `python3` installed (for reliable JSONL/log parsing)
- nightgauge binary installed and configured (`nightgauge forge auth status`)
- `jq` installed for JSON processing (fallback data sources)
- Git repository with `.nightgauge/` directory
- At least one data source populated (see Data Sources below)
**CRITICAL**: This skill runs headless. Do NOT use AskUserQuestion. Make
autonomous decisions or fail with clear error.
---
## Data Sources
| Source | Location | What It Provides |
| ----------------- | --------------------------------- | -------------------------------------------------------------------- |
| Session Logs | `logs/YYYY-MM-DD_NNN_session.log` | Timestamped stage events, error messages, token budget warnings |
| Pipeline Context | `pipeline/{stage}-{N}.json` | Per-issue stage outputs, test results, validation status |
| Batch State | `checkout/batch-state.json` | Per-issue completion status, failed stages, token usage per run |
| Execution History | `pipeline/history/*.jsonl` | Structured per-run records with outcome, stage statuses, durations |
| Gate Spend | `pipeline/exit-records/*.jsonl` | Full gates per stage and run: `nightgauge exit-records gate-summary` |
Locations are relative to the clone's per-clone class directories, or this
checkout's directory for `checkout/...`; resolve one with
`nightgauge layout path <class> [name]` (for example
`nightgauge layout path checkout batch-state.json`).
Each data source is optional. The skill analyzes whatever is available and notes
which sources were absent in the output report.
---
## Failure Categories
| Category | Description | Example Signals |
| -------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `budget-exceeded` | Run terminated because token or cost budget was exhausted | Log: `budget exceeded`, `token limit`, `costUsd > budget`; stage killed mid-execution |
| `state-management` | Context file missing, corrupt, or schema mismatch caused stage to abort | Missing `dev-N.json` when `feature-validate` starts; JSON parse failure on handoff file |
| `ci-infrastructure` | External CI system (GitHub Actions, test runner) failed or was unavailable | Log: `CI checks failed`, `workflow run failed`, forge `pr checks --wait` non-zero exit |
| `model-capability` | AI model produced output that did not meet task requirements after retries | Log: `model returned empty`, `unexpected output format`, repeated re-prompt loops |
| `timeout` | Stage or full pipeline exceeded configured time limit before completing | Log: `timed out`, `exceeded ci_timeout`, `stage duration > max`; partial context file |
| `validation-failure` | Tests, TypeScript check, or manual checklist blocked stage progression | Log: `tests failed`, `tsc error`, `build failed`; `feature-validate` status `failed` |
| `unknown` | Failure event detected but does not match any other category | Unexpected process exit, missing log entries, outcome `failed` with no clear cause in logs |
---
## Workflow
### Phase 0: Environment Preflight
### Phase 0: Environment Preflight
**PURPOSE**: Verify the nightgauge binary is available and the environment
is healthy before executing any pipeline stage. Skills halt immediately with
actionable instructions when this check fails.
Three-layer check:
1. **Binary discovery** — shell-level, runs before invoking the binary
2. **Doctor check** — binary-level, full environment health via `doctor --json`
3. **Project environment** — the checks the project declares under
`preflight.environment` (#101); opt-in, silent when none are declared
The binary discovery cascade below is **provider-neutral** (issue 4029): it works
identically under every adapter (Claude, Codex, Gemini, …) and references no
VSCode-extension-specific path. The host that spawns the skill (the VSCode
extension's `skillRunner`, or the Go `nightgauge` binary in auto/CLI mode)
resolves the binary authoritatively and exports `NIGHTGAUGE_BIN`, which
this cascade honors first. The remaining steps (PATH, the development-only repo
`bin/` and worktree canonical-repo `bin/`, `~/go/bin`) are fallbacks for direct
or terminal invocation.
This intentionally **diverges** from
`claude-plugins/nightgauge/hooks/lib/guard.sh` (issue 3262 → issue 4029): guard.sh is
Claude-Code-only and retains a trailing `~/.vscode/extensions/...` glob to serve
the standalone-terminal-Claude case (where no host exports `NIGHTGAUGE_BIN`).
Skills must stay portable Markdown and cannot reference that VSCode-only path —
so the shared resolution order (`NIGHTGAUGE_BIN` → PATH → gated repo → gated
canonical → `~/go/bin`) stays in sync, and only guard.sh carries the extra Claude-only
fallback. The `nightgauge preflight skill-portability` gate enforces that
no skill reintroduces a `.vscode/extensions` path, and that every cascade keeps
the `NIGHTGAUGE_DEV_REPO_BIN` gate (#2758): a foreign repository's
`bin/nightgauge` must never run.
After the cascade resolves `$BINARY`, the preflight prepends
`dirname($BINARY)` to `PATH` so subsequent bare `nightgauge ...`
invocations later in the same skill body resolve through PATH rather than
failing with "command not found" (issue 3262).
```bash
# Layer 1: provider-neutral binary discovery (NIGHTGAUGE_BIN → PATH → gated repo
# bin → gated canonical-repo bin → ~/go/bin). No VSCode-extension path — see issue 4029.
# Repo bins need NIGHTGAUGE_DEV_REPO_BIN=1 and a verified core checkout (#2758).
BINARY="${NIGHTGAUGE_BIN:-}"
[ -n "$BINARY" ] && [ ! -x "$BINARY" ] && BINARY=""
[ -z "$BINARY" ] && BINARY=$(command -v nightgauge 2>/dev/null || echo "")
if [ -z "$BINARY" ] && [ "${NIGHTGAUGE_DEV_REPO_BIN:-}" = "1" ]; then
for NG_ROOT in "$(git rev-parse --show-toplevel 2>/dev/null)" \
"$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)"; do
[ -x "$NG_ROOT/bin/nightgauge" ] || continue
grep -qx 'module github.com/nightgauge/nightgauge' "$NG_ROOT/go.mod" 2>/dev/null || continue
NG_ORIGIN="$(git -C "$NG_ROOT" config --get remote.origin.url 2>/dev/null)"
case "${NG_ORIGIN%.git}" in
git@github.com:nightgauge/nightgauge | https://github.com/nightgauge/nightgauge | \
ssh://git@github.com/nightgauge/nightgauge) BINARY="$NG_ROOT/bin/nightgauge"; break ;;
esac
done
fi
[ -z "$BINARY" ] && [ -x "$HOME/go/bin/nightgauge" ] && BINARY="$HOME/go/bin/nightgauge"
if [ -z "$BINARY" ]; then
echo "ERROR: nightgauge binary not found via NIGHTGAUGE_BIN, PATH or ~/go/bin" >&2
echo "" >&2
echo "Install via: go install github.com/nightgauge/nightgauge/cmd/nightgauge@latest" >&2
echo "Or download from: https://github.com/nightgauge/nightgauge/releases" >&2
echo "Run \`nightgauge doctor\` after installing to verify your environment." >&2
exit 1
fi
# Make the resolved binary callable via bare `nightgauge ...` later in this skill body (issue 3262).
export PATH="$(dirname "$BINARY"):$PATH"
# Per-repo GitHub token: export GH_TOKEN so every `gh` call in this skill
# authenticates as the *current repo's* configured user instead of the machine-
# global `gh auth` active account. `forge auth token` resolves per-repo (config
# github_user/token → GITHUB_TOKEN → `gh auth token --user <github_user>`), so
# concurrent sessions/workspaces owned by different GitHub users each use their
# own token. GH_TOKEN outranks GITHUB_TOKEN and the gh keyring. Only resolve when
# unset (an upstream env value — e.g. the VSCode extension's terminal env —
# wins); never fail preflight over it. Mirrors hooks/lib/guard.sh (sync, issue 3262).
if [ -z "${GH_TOKEN:-}" ]; then
IB_REPO_TOKEN="$("$BINARY" forge auth token 2>/dev/null || true)"
if [ -n "$IB_REPO_TOKEN" ]; then
export GH_TOKEN="$IB_REPO_TOKEN"
export GITHUB_TOKEN="${GITHUB_TOKEN:-$IB_REPO_TOKEN}"
fi
unset IB_REPO_TOKEN
fi
# Layer 2: full environment health check via doctor --json
DOCTOR_RESULT=$("$BINARY" doctor --json 2>/dev/null)
DOCTOR_EXIT=$?
if [ "$DOCTOR_EXIT" -eq 2 ]; then
echo "ERROR: Environment check failed — nightgauge doctor reports broken environment." >&2
FAILED_CHECKS=$(printf '%s\n' "$DOCTOR_RESULT" | jq -r '.failed_checks[]? // empty' 2>/dev/null)
if [ -n "$FAILED_CHECKS" ]; then
echo "Failing check(s): $(echo "$FAILED_CHECKS" | tr '\n' ',' | sed 's/,$//')" >&2
fi
printf '%s\n' "$DOCTOR_RESULT" | jq -r '.errors[]' >&2 2>/dev/null || true
INSTALL_MSG=$(printf '%s\n' "$DOCTOR_RESULT" | jq -r '.install_instructions // empty' 2>/dev/null)
[ -n "$INSTALL_MSG" ] && echo "$INSTALL_MSG" >&2
exit 1
fi
if [ "$DOCTOR_EXIT" -eq 1 ]; then
echo "WARNING: Environment has non-critical issues:" >&2
printf '%s\n' "$DOCTOR_RESULT" | jq -r '.warnings[]' >&2 2>/dev/null || true
# Continue — warnings do not block skill execution
fi
# Layer 3: project-declared environment checks (preflight.environment, #101).
# Opt-in: with none declared this prints nothing and continues. The verb
# writes one `[environment-halt]` line per failed check, with its remedy.
if ! "$BINARY" preflight environment >/dev/null; then
echo "[environment-halt] Project environment check failed — fix the environment, not the code." >&2
exit 1
fi
```
**Exit codes from `nightgauge doctor`**:
| Code | Meaning | Skill behavior |
| ---- | ------------------------------- | --------------------------------------- |
| 0 | Healthy | Continue |
| 1 | Degraded (warnings only) | Continue with warning printed to stderr |
| 2 | Broken (required checks failed) | Halt immediately with error details |
**Layer 3** halts the stage when any check declared under
`preflight.environment` in the workspace's `.nightgauge/config.yaml` fails
(exit 1) or the declaration is invalid (exit 2). The failure is in the
operator's environment, not the code: do not edit code to work around it, do
not run the remedy yourself, and end the stage. The pipeline records it as
`environment_halt` and holds the issue for an operator.
Do not theorize about the cause beyond what is printed here — the failing
check name (`failed_checks`) and its `.errors[]` message are the complete
diagnosis (#277).
---
### Phase 1: Parse Arguments and Determine Scope
#### Step 1.1: Parse Arguments
```bash
ISSUE_FILTER=""
SINCE_DATE=""
PERIOD_DAYS=7
ALL_FAILURES=false
OUTPUT_FORMAT="both"
DRY_RUN=true
CREATE_ISSUES=false
MIN_SEVERITY="high"
RECORD_OUTCOME=false
# Parse arguments from invocation:
# --issue N → ISSUE_FILTER=N
# --since YYYY-MM-DD → SINCE_DATE
# --period N → PERIOD_DAYS=N
# --all-failures → ALL_FAILURES=true
# --format FORMAT → OUTPUT_FORMAT
# --dry-run → DRY_RUN=true (default, explicit for clarity)
# --create-issues → CREATE_ISSUES=true, DRY_RUN=false
# --severity LEVEL → MIN_SEVERITY
# --record-outcome → RECORD_OUTCOME=true
```
#### Step 1.2: Validate Arguments
```bash
if [ -n "$SINCE_DATE" ]; then
if ! printf '%s\n' "$SINCE_DATE" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
echo "ERROR: --since must be YYYY-MM-DD format (got: $SINCE_DATE)"
exit 1
fi
fi
VALID_FORMATS="summary json both"
if ! printf '%s\n' "$VALID_FORMATS" | grep -qw "$OUTPUT_FORMAT"; then
echo "ERROR: --format must be one of: $VALID_FORMATS"
exit 1
fi
```
#### Step 1.3: Determine Scope
Default scope is the **last batch run** (batch-state.json). If batch state is
absent or `--all-failures` is passed, fall back to date-based scope using
`--period` or `--since`:
```bash
LOGS_DIR="$(nightgauge layout path logs)"
PIPELINE_DIR="$(nightgauge layout path pipeline)"
HISTORY_DIR="$(nightgauge layout path pipeline history)"
BATCH_STATE="$(nightgauge layout path checkout batch-state.json)"
SCOPE="batch"
# Override scope
if [ "$ALL_FAILURES" = true ] || [ -n "$SINCE_DATE" ] || [ -n "$ISSUE_FILTER" ]; then
SCOPE="date-range"
fi
if [ ! -f "$BATCH_STATE" ] && [ "$SCOPE" = "batch" ]; then
echo "No batch-state.json found. Falling back to date-range scope (last ${PERIOD_DAYS} days)."
SCOPE="date-range"
fi
# Compute SINCE_DATE from --period if not provided
if [ -z "$SINCE_DATE" ]; then
SINCE_DATE=$(python3 -c "
from datetime import datetime, timedelta
import sys
print((datetime.now() - timedelta(days=${PERIOD_DAYS})).strftime('%Y-%m-%d'))
")
fi
# Load knowledge_path from pipeline context when ISSUE_FILTER is set.
# Also auto-enable RECORD_OUTCOME when knowledge_path is found.
KNOWLEDGE_PATH=""
if [ -n "$ISSUE_FILTER" ]; then
KNOWLEDGE_PATH=$(jq -r '.knowledge_path // empty' \
"$(nightgauge layout path pipeline issue-${ISSUE_FILTER}.json)" 2>/dev/null)
# Auto-detect: if knowledge_path is set, record outcome even without flag
if [ -n "$KNOWLEDGE_PATH" ]; then
RECORD_OUTCOME=true
fi
fi
```
---
### Phase 2: Locate and Read Data Sources (Deterministic Binary)
**IMPORTANT**: All extraction is now performed by the deterministic
`nightgauge` Go binary — `pipeline batch-failures` (consolidates batch
state, JSONL history, and context-files fallback) and `logs scan-failures`
(16-pattern session-log scan). Do NOT re-implement these in Python; the
binary owns the schema and the regex set as single sources of truth (audit
row B29).
#### Step 2.1: Extract Pipeline Failures
Replaces the previous inline-Python parsers for batch state (old Phase 2.1),
execution history (old Phase 2.2), and context-files fallback (old Phase 2.4).
The binary reads the checkout's `batch-state.json`, and `history/*.jsonl` and the
`issue-*.json` / `pr-*.json` set from the clone's pipeline state directory
(`nightgauge layout path pipeline`), in one call, emitting a stable v1 JSON schema.
```bash
ALL_FAILURES_FLAG=""
[ "${ALL_FAILURES}" = "true" ] && ALL_FAILURES_FLAG="--all-failures"
ISSUE_FLAG=""
[ -n "${ISSUE_FILTER}" ] && ISSUE_FLAG="--issue ${ISSUE_FILTER}"
SINCE_FLAG=""
[ -n "${SINCE_DATE}" ] && SINCE_FLAG="--since ${SINCE_DATE}"
"$BINARY" pipeline batch-failures \
${SINCE_FLAG} ${ISSUE_FLAG} ${ALL_FAILURES_FLAG} \
--json > /tmp/retro_pipeline_failures.json
# Quick summary for the run log
jq -r '
"Pipeline failures: batch=\(.batch_failures|length) " +
"history=\(.history_failures|length) " +
"context=\(.context_failures|length) " +
"skipped_records=\(.skipped_records)"
' /tmp/retro_pipeline_failures.json
```
The output schema (top-level keys):
- `batch` — batch-state.json metadata (`null` when absent)
- `batch_failures[]` — failure rows from `batch-state.json`
- `history_failures[]` — failure rows from JSONL history
- `context_failures[]` — fallback rows for incomplete pipeline context
- `skipped_records` — count of malformed JSONL lines
- `warnings[]` — non-fatal issues (e.g. unreadable history file)
See `docs/GO_BINARY.md` `### Pipeline Operations` →
`pipeline batch-failures` for the full schema.
#### Step 2.2: Extract Session Log Failure Signals
Replaces the previous inline-Python regex scan (old Phase 2.3). The 16-pattern
set lives in `internal/cmd/scanfailures/scanner.go` (`var FailurePatterns`)
as the single Go source of truth. Lines are bounded to 300 bytes and per-file
matches capped at 50 — same as the old Python parser.
```bash
"$BINARY" logs scan-failures \
${SINCE_FLAG} ${ISSUE_FLAG} \
--json > /tmp/retro_logs.json
jq -r '
"Logs: scanned \(.log_files_scanned) files, " +
"\(.files_with_signals) with failure signals"
' /tmp/retro_logs.json
```
The output schema preserves the existing `log_signals[]` collection (with
`log_file`, `issue_number`, `date`, `failure_signals[].{line,text}`). See
`docs/GO_BINARY.md` `### Logs Operations` → `logs scan-failures` for the full
schema.
---
### Phase 3: Extract Failure Events
Read the binary-emitted intermediates and merge into a unified failure event
list. Deduplicate by `issue_number` — prefer `history` source over
`batch-state` over `context-files` for the same issue.
```bash
# Read the two binary outputs into AI context (small, pre-aggregated)
cat /tmp/retro_pipeline_failures.json 2>/dev/null || \
echo '{"batch_failures":[],"history_failures":[],"context_failures":[]}'
cat /tmp/retro_logs.json 2>/dev/null || echo '{"log_signals":[]}'
```
The pipeline-failures file groups failure rows by source under three top-level
arrays (`batch_failures`, `history_failures`, `context_failures`); the logs
file remains a single `log_signals` array. Iterate each array when building
the unified event list.
Build the unified failure event list with deduplication. For each unique
`issue_number`, merge all available signals into one event record:
```json
{
"issue_number": 960,
"title": "feat: ...",
"outcome": "failed",
"failed_stages": ["feature-validate", "pr-create"],
"log_signals": ["[ERROR] tests failed: 3 failures", "[ERROR] tsc error: ..."],
"estimated_cost_usd": 1.23,
"total_duration_ms": 480000,
"sources": ["batch-state", "logs"]
}
```
If no failure events are found across all data sources, output:
```
No pipeline failures found in the specified scope.
Scope: last batch run (or last N days)
Issues analyzed: X (all completed successfully)
```
Then exit 0.
---
### Phase 4: Categorize Failures (Deterministic Classification)
Use the deterministic `nightgauge failure classify` binary verb to categorize each failure event. The binary implements fixed rules for 7 failure categories (budget-exceeded, timeout, ci-infrastructure, validation-failure, state-management, model-capability, unknown).
**Invocation:**
```bash
CLASSIFICATION=$("$BINARY" failure classify \
--stage <stage-name> \
--stderr "<error-output-text>" 2>&1)
```
**Output format** (JSON):
```json
{
"category": "validation-failure",
"severity": "high",
"retryable": false,
"maxRetries": 1,
"escalate": false,
"description": "..."
}
```
Produce a categorized failure record for each event:
```json
{
"issue_number": 960,
"title": "feat: ...",
"category": "validation-failure",
"severity": "high",
"evidence": ["[ERROR] tests failed: 3 failures"],
"failed_stages": ["feature-validate"],
"outcome": "failed",
"estimated_cost_usd": 1.23
}
```
(For full category definitions and pattern rules, see `internal/intelligence/failure/taxonomy.go` in the binary codebase.)
---
### Phase 5: AI Interpretation (Root Cause Analysis)
**PURPOSE**: AI reads the categorized failure events and interprets patterns,
root causes, and actionable recommendations. This is the probabilistic phase.
Read `/tmp/retro_batch.json`, `/tmp/retro_history.json`, `/tmp/retro_logs.json`,
`/tmp/retro_context.json` (all pre-aggregated — safe to load into AI context).
For each failure category present in the event list, generate a finding:
```json
{
"category": "validation-failure",
"severity": "high",
"count": 3,
"affected_issues": [960, 961, 963],
"title": "Test failures blocking feature-validate stage",
"root_cause": "Detailed explanation of what is causing this failure category",
"pattern": "Recurring pattern description (e.g., TypeScript strict mode errors on async handlers)",
"recommendation": "Specific actionable step to remediate",
"estimated_recurrence_risk": "high | medium | low",
"cause": { "kind": "product_code | skill_instruction | agent_config" }
}
```
When the root cause is a skill instruction or agent configuration, also add a
`skill_regression` block (schema: `docs/SKILL_EVALUATION.md` § Generated
scenarios) built from the transcript evidence you read. Omit it when the
evidence does not show the decision the skill made — a guessed pre-fix
decision would produce a scenario that proves nothing.
**Severity Classification Rules:**
| Severity | Criteria |
| ---------- | ------------------------------------------------------------ |
| `critical` | >50% of issues in scope failed; or same failure on 3+ issues |
| `high` | >25% failure rate; or 2+ issues share the same root cause |
| `medium` | Single failure with known root cause and clear fix available |
| `low` | Single failure, ambiguous cause, or one-off event |
| `info` | Successful runs with minor warning signals worth noting |
Also identify:
- **Cross-issue patterns**: failures that share the same root cause
- **Stage hotspots**: stages that appear most frequently in failed runs
- **Cost impact**: total cost of failed runs (wasted spend)
- **Remediation priority**: order recommendations by highest impact first
---
### Phase 6: Generate Report
#### Step 6.1: Structured JSON Report
Assemble the full retro report:
```json
{
"schema_version": "1.0",
"scope": {
"type": "batch | date-range | single-issue",
"from": "YYYY-MM-DD",
"to": "YYYY-MM-DD",
"issue_filter": null,
"issues_analyzed": 12,
"failure_count": 3,
"data_sources": ["batch-state", "history", "logs"]
},
"summary": {
"failure_rate": 0.25,
"total_wasted_cost_usd": 3.69,
"most_common_category": "validation-failure",
"stage_hotspot": "feature-validate",
"categories_found": ["validation-failure", "ci-infrastructure"]
},
"failures": [
{
"issue_number": 960,
"title": "feat: ...",
"categories": ["validation-failure"],
"confidence": "high",
"evidence": ["[ERROR] tests failed: 3 failures"],
"failed_stages": ["feature-validate"],
"estimated_cost_usd": 1.23
}
],
"findings": [
{
"category": "validation-failure",
"severity": "high",
"count": 3,
"affected_issues": [960, 961, 963],
"title": "Recurring test failures in feature-validate",
"root_cause": "...",
"pattern": "...",
"recommendation": "...",
"estimated_recurrence_risk": "high"
}
],
"recommendations": [
{
"priority": 1,
"action": "Fix failing test suite for async handlers",
"impact": "Eliminates 3 recurring failures",
"effort": "medium",
"category": "validation-failure",
"config_snippet": null,
"recovery_commands": ["nightgauge forge pr create --head feat/960-... --base main"],
"affected_issues": [960, 961, 963],
"estimated_cost_savings_usd": 3.69
}
],
"created_at": "ISO-8601 timestamp"
}
```
If `--format json` or `--format both`, write it with
`nightgauge layout write pipeline retro-report-YYYY-MM-DD.json` (content on
stdin, or `--from FILE` after writing a temp file outside the git directory);
the command prints the path it wrote.
#### Step 6.2: Human-Readable Summary
Output a formatted summary to the console:
```
PIPELINE RETRO REPORT
═══════════════════════════════════════════════════════════
Scope: Last batch run (2026-02-21, 12 issues)
Data Sources: batch-state, execution history, session logs
Failure Rate: 25% (3/12 issues)
Wasted Cost: $3.69 across failed runs
FAILURE BREAKDOWN BY CATEGORY
───────────────────────────────────────────────────────────
validation-failure ████████░░ 2 issues (#960, #963)
ci-infrastructure ████░░░░░░ 1 issue (#961)
budget-exceeded ░░░░░░░░░░ 0 issues
state-management ░░░░░░░░░░ 0 issues
model-capability ░░░░░░░░░░ 0 issues
timeout ░░░░░░░░░░ 0 issues
unknown ░░░░░░░░░░ 0 issues
STAGE HOTSPOT: feature-validate (appears in 2 of 3 failures)
FINDINGS
───────────────────────────────────────────────────────────
[HIGH] validation-failure: Recurring test failures blocking 2 issues
Issues: #960, #963
Root Cause: TypeScript strict mode errors on async handler return types
Pattern: Both issues modified the same module (src/pipeline/skillRunner.ts)
→ Recommendation: Add strict type coverage to skillRunner test suite before
merging any changes to that module
→ Recurrence risk: HIGH
[MEDIUM] ci-infrastructure: GitHub Actions workflow timeout on #961
Issues: #961
Root Cause: Intermittent runner timeout on macOS-latest pool
Pattern: Single occurrence, likely transient
→ Recommendation: Re-run pipeline for #961; add ci_timeout buffer to config
→ Recurrence risk: LOW
RECOMMENDATIONS (sorted by impact)
───────────────────────────────────────────────────────────
# │ Action │ Category │ Effort │ Issues
1 │ Fix async handler type errors │ validation-failure │ medium │ #960, #963
2 │ Increase ci_timeout in config │ ci-infrastructure │ low │ #961
Config snippet for #2:
pipeline:
ci_timeout: 600
RECOVERY GUIDANCE
───────────────────────────────────────────────────────────
#960 │ Branch has commits, no PR → nightgauge forge pr create --head feat/960-... --base main
#961 │ PR #187 exists, CI failed → nightgauge forge pr merge --node-id $ID --strategy squash
ESTIMATED RECOVERY EFFORT: 1 medium code fix + 1 low-effort config change
───────────────────────────────────────────────────────────
Report saved: <pipeline dir>/retro-report-2026-02-21.json
Run with --create-issues to auto-create GitHub issues for findings.
Next: /nightgauge:retro --create-issues --severity medium
```
If `--format summary`, output only the console summary. If `--format json`,
write JSON only and output the file path. If `--format both`, write JSON and
output the console summary.
---
### Phase 7: Recommendation Template Generation
**PURPOSE**: Transform categorized findings into structured, actionable
recommendation templates with config snippets, recovery guidance, and
deduplication.
#### Step 7.1: Deduplicate Findings
Before generating recommendations, group findings by root cause:
1. Group findings sharing the same `category` AND `pattern` text
2. Merge affected issues into a single recommendation
3. Sum `estimated_cost_usd` across merged findings
4. Take the highest `severity` from merged findings
5. Combine `evidence` arrays (deduplicated)
```python
# /tmp/retro_dedup.py — group findings by category + pattern
import json
findings = [] # loaded from Phase 5 output
seen = {}
for f in findings:
key = f"{f['category']}::{f['pattern']}"
if key in seen:
existing = seen[key]
existing["affected_issues"] = list(
set(existing["affected_issues"]) | set(f["affected_issues"])
)
existing["count"] += f["count"]
existing["estimated_cost_usd"] = (
existing.get("estimated_cost_usd", 0)
+ f.get("estimated_cost_usd", 0)
)
# Take highest severity
SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"]
if SEVERITY_ORDER.index(f["severity"]) > SEVERITY_ORDER.index(
existing["severity"]
):
existing["severity"] = f["severity"]
else:
seen[key] = {**f}
deduped_findings = list(seen.values())
```
#### Step 7.1b: Generate Regression Scenarios for Skill Causes (#1287)
For each deduplicated finding whose `cause.kind` is `skill_instruction` or
`agent_config` and that carries `skill_regression`, write the finding to a temp
file and run the generator. It writes `evals/scenarios/<skill>/` and
`evals/fixtures/<skill>/` only when the scenario passes on the corrected
decision and fails on the non-answer sentinel and on the pre-fix decision:
```bash
FINDING_JSON=$(mktemp)
printf '%s\n' "$FINDING" > "$FINDING_JSON"
GEN_FLAGS=""
[ "$DRY_RUN" = "true" ] && GEN_FLAGS="--dry-run"
npx tsx scripts/generate-eval-scenario.ts --from "$FINDING_JSON" $GEN_FLAGS
GEN_EXIT=$?
rm -f "$FINDING_JSON"
```
Exit 0 → report the `scenario_path` in the finding's recommendation
(`verified` under dry-run). Exit 1 → report the rejection reason; the
scenario must be hand-authored or the diagnosis improved. Exit 2 → the
`skill_regression` block is malformed; report it, do not guess a fix.
#### Step 7.2: Generate Recommendation Templates
For each deduplicated finding, generate a recommendation template based on its
category. Each template maps to specific config snippets and recovery actions.
**Category-to-recommendation mappings:**
##### budget-exceeded
- **Primary action**: Set `budget_preset: generous` in config.yaml
- **Secondary action**: Per-stage budget override for the specific stage
- **Tertiary action**: Size label correction if issue was under-sized
- **Config snippet**:
```yaml
# Option A: Apply generous preset (2x all budgets)
pipeline:
budget_preset: generous
# Option B: Override specific stage budget
pipeline:
stage_budgets:
feature-dev:
M: 16.00
L: 50.00
```
##### state-management
- **Primary action**: Suggest model upgrade for the failing stage
- **Secondary action**: Suggest context file rebuild or state validation fix
- **Config snippet**:
```yaml
# Upgrade model for affected stage to improve state handling
model_routing:
mode: hybrid
minimum_model:
<stage>: sonnet
pipeline:
stage_models:
<stage>: opus
```
##### ci-infrastructure
- **Primary action**: Suggest token refresh (`nightgauge forge auth login`)
- **Secondary action**: Suggest CI timeout increase
- **Config snippet**:
```yaml
# Increase CI timeout to accommodate slow runners
pipeline:
ci_timeout: 600
```
- **Recovery command**: `nightgauge forge auth login` (configure via env var `GITHUB_TOKEN` for non-interactive)
##### model-capability
- **Primary action**: Suggest model upgrade (haiku → sonnet or sonnet → opus)
- **Config snippet**:
```yaml
# Upgrade model for affected stage
model_routing:
mode: hybrid
minimum_model:
<stage>: sonnet
pipeline:
stage_models:
<stage>: opus
```
##### timeout
- **Primary action**: Suggest effort reduction or scope split
- **Secondary action**: Increase timeout config
- **Config snippet**:
```yaml
# Increase CI timeout
pipeline:
ci_timeout: 600
# Or reduce effort for the stage
model_routing:
stage_efforts:
<stage>: low
```
##### validation-failure
- **Primary action**: Include specific test failure details from evidence
- **Secondary action**: Suggest running tests locally before pipeline
- **No config snippet** — requires code fixes
- **Recovery command**: `npm test` or relevant test command from evidence
##### unknown
- **Primary action**: Manual investigation with log file references
- **No config snippet**
- **Recovery command**: Review session log at
`<date>_<issue>_session.log` in the clone's logs directory
(`nightgauge layout path logs`)
#### Step 7.3: Generate Recovery Guidance
For each failed issue with branch work, detect branch/PR state and generate
recovery commands:
```bash
# For each affected issue number:
BRANCH_NAME="feat/${ISSUE_NUM}-..."
# 1. Check if remote branch exists with commits
git branch -r --list "origin/${BRANCH_NAME}*"
# 2. Check for existing PR
nightgauge forge pr list --head "${BRANCH_NAME}" --state all --json number,state,url
# 3. Generate recovery command based on state:
# - Branch with commits, no PR → nightgauge forge pr create --head <branch> --base main
# - PR exists, not merged → nightgauge forge pr merge --node-id $ID --strategy squash (or re-run CI)
# - PR merged but pipeline killed → no recovery needed
# - No branch found → re-run pipeline from issue-pickup
```
#### Step 7.4: Assemble Recommendation Records
For each recommendation, produce a structured record:
```json
{
"priority": 1,
"action": "Increase budget for feature-dev stage",
"impact": "Eliminates 2 budget-exceeded failures",
"effort": "low",
"category": "budget-exceeded",
"config_snippet": "pipeline:\n budget_preset: generous",
"recovery_commands": ["nightgauge forge pr create --head feat/960-... --base main"],
"affected_issues": [960, 963],
"estimated_cost_savings_usd": 2.46
}
```
Sort recommendations by:
1. Severity (critical > high > medium > low > info)
2. Number of affected issues (descending)
3. Effort (low > medium > high — prefer quick wins)
---
### Phase 8: Issue Creation / Dry Run
**PURPOSE**: Preview or create GitHub issues for actionable recommendations.
#### Step 8.1: Filter by Severity Threshold
Only process recommendations at or above the `--severity` threshold (default:
`high`):
```bash
SEVERITY_ORDER="info low medium high critical"
# Filter recommendations where severity >= MIN_SEVERITY
```
#### Step 8.2: Dry Run Output (Default)
When `DRY_RUN=true` (default, or `--create-issues` not passed), output a preview
table:
```text
ISSUE CREATION PREVIEW (dry-run)
═══════════════════════════════════════════════════════════
# │ Title │ Labels │ Issues │ Effort
1 │ fix(pipeline): Increase feature-dev │ chore, priority:hi │ 2 │ low
│ budget for budget-exceeded failures │ │ │
2 │ fix(pipeline): Upgrade pr-create model │ chore, priority:hi │ 1 │ low
│ for state-management failures │ │ │
To create these issues: /nightgauge:retro --create-issues
To include medium+ severity: /nightgauge:retro --create-issues --severity medium
```
#### Step 8.3: Create Issues (`--create-issues`)
When `CREATE_ISSUES=true`:
For each recommendation at or above the severity threshold:
**Step 8.3.1: Check for duplicate issues**
```bash
# Search for existing issues with similar title
SEARCH_TITLE="fix(pipeline): ${ACTION_SUMMARY}"
REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
# `issue list --search` is scoped to open issues
EXISTING=$(nightgauge issue list --repo "$REPO" --search "${SEARCH_TITLE}" --limit 5 --json | jq '. // []')
# If duplicate found, skip and report
if printf '%s\n' "$EXISTING" | jq -e 'length > 0' > /dev/null 2>&1; then
echo "SKIP: Similar issue already exists: $(printf '%s\n' "$EXISTING" | jq -r '.[0] | "#\(.number) \(.title)"')"
continue
fi
```
**Step 8.3.2: Create the issue**
```bash
ISSUE_BODY=$(cat <<'ISSUE_EOF'
## Summary
${ACTION_DESCRIPTION}
## Motivation
Pipeline retro analysis identified ${COUNT} failure(s) in category
\`${CATEGORY}\` affecting issue(s) ${AFFECTED_ISSUES_LIST}.
Estimated wasted cost: $${ESTIMATED_COST_SAVINGS_USD}
## Recommended Fix
${RECOMMENDATION_DETAIL}
### Config Change
\`\`\`yaml
${CONFIG_SNIPPET}
\`\`\`
### Recovery Commands
${RECOVERY_COMMANDS_LIST}
## Acceptance Criteria
- [ ] Config change applied and validated
- [ ] Affected issues re-run successfully (or manually recovered)
- [ ] No regression in pipeline success rate
## Technical Notes
- Category: \`${CATEGORY}\`
- Severity: ${SEVERITY}
- Affected issues: ${AFFECTED_ISSUES_LIST}
- Source: Pipeline retro report (${REPORT_DATE})
## Related Issues
${RELATED_ISSUES_REFS}
---
_Generated by \`/nightgauge:retro --create-issues\`_
ISSUE_EOF
)
REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
nightgauge issue create --repo "$REPO" \
--title "fix(pipeline): ${ACTION_SUMMARY}" \
--body "$ISSUE_BODY" \
--labels "chore,priority:high"
```
**Step 8.3.3: Report created issues**
```text
ISSUES CREATED
═══════════════════════════════════════════════════════════
✓ #1201 fix(pipeline): Increase feature-dev budget (2 affected issues)
✓ #1202 fix(pipeline): Upgrade pr-create model (1 affected issue)
✗ SKIP fix(pipeline): Fix async handler errors (duplicate: #1180)
Created: 2 issues | Skipped: 1 (duplicate)
```
---
### Phase 9: Record Outcome to Knowledge Base
**PURPOSE**: Append an `## Outcome` section to the knowledge base for the
completed issue, closing the knowledge loop started by issue-pickup (scaffold)
and feature-planning (decisions). This section becomes corpus for the
pipeline learning system.
**No-op when**:
- `RECORD_OUTCOME` is false, AND
- `KNOWLEDGE_PATH` is empty or unset
#### Step 9.1: Load Pipeline Context for Outcome Data
When `ISSUE_FILTER` is set and `RECORD_OUTCOME=true`:
```bash
ISSUE_NUMBER="${ISSUE_FILTER}"
# Load outcome metrics from dev context (best-effort)
DEV_CONTEXT="$(nightgauge layout path pipeline dev-${ISSUE_NUMBER}.json)"
PIPELINE_DURATION_MINS=0
TOTAL_TOKENS=0
ESTIMATED_COST_USD=0
OUTCOME_STATUS="complete"
if [ -f "$DEV_CONTEXT" ]; then
TOTAL_TOKENS=$(jq -r '.token_usage.total // 0' "$DEV_CONTEXT" 2>/dev/null || echo "0")
ESTIMATED_COST_USD=$(jq -r '.token_usage.estimated_cost_usd // 0' "$DEV_CONTEXT" 2>/dev/null || echo "0")
fi
# Agent fills in narrative from the retro analysis above:
# - WHAT_WENT_WELL: positive signals, stages that completed quickly, tests passing
# - WHAT_DIDNT: failures, retries, unexpected scope, CI issues
# - LESSONS_LEARNED: actionable guidance for future similar work
WHAT_WENT_WELL=""
WHAT_DIDNT=""
LESSONS_LEARNED=""
```
#### Step 9.1b: Graduation Candidates (auto-detected)
When `ISSUE_NUMBER` is set, call `nightgauge knowledge graduate-candidates`
to deterministically rank ADR blocks in this issue's `decisions.md`. The
binary uses telemetry signals (recall_hit events) plus structural heuristics
to score each ADR and returns only those at or above the threshold.
```bash
GRADUATION_SECTION=""
if [ -n "$ISSUE_NUMBER" ]; then
CANDIDATES_JSON=$(nightgauge knowledge graduate-candidates "$ISSUE_NUMBER" --json 2>/dev/null || echo '{"issue":0,"candidates":[]}')
CAND_COUNT=$(printf '%s\n' "$CANDIDATES_JSON" | jq '.candidates | length' 2>/dev/null || echo 0)
if [ "$CAND_COUNT" -gt 0 ]; then
GRADUATION_SECTION=$(printf '%s\n' "$CANDIDATES_JSON" | jq -r '
"## Graduation Candidates\n\nThese ADRs from this issue look like good candidates to graduate to permanent docs. Run `nightgauge knowledge graduate \(.issue) --section <docs-path> --adr ADR-NNN` to start the ritual.\n\n| ADR | Score | Signals | Suggested Dest |\n|-----|-------|---------|----------------|\n" +
(.candidates | map("| \"\(.adr_title)\" (ADR-\(.adr_index | tostring | (if length < 3 then ("000" + .) | .[-3:] else . end))) | \(.score) | \(.signals | join(", ")) | \(.suggested_dest) |") | join("\n"))
')
fi
fi
```
The agent appends `$GRADUATION_SECTION` to the retro summary output when set.
When `CAND_COUNT == 0`, the section is omitted entirely — no static
"Graduation checklist" blockquote is emitted. The candidate threshold and
scoring rubric live in
[docs/GO_BINARY.md](https://github.com/nightgauge/nightgauge/blob/main/docs/GO_BINARY.md#knowledge-graduate-candidates).
> **Manual fallback**: When the binary is unavailable on stale checkouts the
> command silently returns an empty list; reviewers can still scan
> `decisions.md` manually and run `nightgauge knowledge graduate <issue>
--section <docs-path> --adr ADR-NNN` to start the ritual. See
> [docs/KNOWLEDGE_BASE.md#graduation-workflow](https://github.com/nightgauge/nightgauge/blob/main/docs/KNOWLEDGE_BASE.md#graduation-workflow).
**Content guidelines** — the agent fills in the narrative fields:
| Field | Source | Format |
| ----------------- | -------------------------------------------------- | ---------------------------------- |
| `What Went Well` | Phases 3–5 analysis: stages that completed cleanly | 2–5 bullet points or short prose |
| `What Didn't` | Failure events, retries, validation failures | 2–5 bullet points or "None" if N/A |
| `Lessons Learned` | Actionable guidance distilled from the above | 2–5 actionable bullet points |
When the retro scope had **no failures** for the issue (all stages completed),
`What Didn't Go Well` should be "None — all stages completed successfully."
#### Step 9.2: Record Outcome via Go Binary
Call the deterministic Go binary to append the outcome block. The binary
handles directory creation, idempotency detection, and Markdown formatting.
The outcome always lands in `decisions.md`; a missing one is created carrying
the frontmatter contract.
The same call records the **verification event** — `verified: {by:
process:retro}` — that promotes the entry from `unverified` to
`machine-confirmed`. Retro runs after the PR merged, so the decisions it
records survived a real merge rather than being a model's unreviewed first
draft.
**Do not call `knowledge stamp` here.** `record-outcome` is the writer for this
event; a separate stamp would either duplicate it or, worse, record a
provenance the outcome path did not actually establish. The other stages stamp
what they _produce_ (`generated`); retro records what it _confirms_
(`verified`), and those are different fields with different meanings.
Read the merged PR URL out of the PR context file first, when it exists, so
the entry cites what confirmed it:
```bash
PR_CONTEXT="$(nightgauge layout path pipeline pr-${ISSUE_NUMBER}.json)"
PR_URL=""
if [ -f "$PR_CONTEXT" ]; then
PR_URL=$(jq -r '.pr_url // empty' "$PR_CONTEXT")
fi
nightgauge knowledge record-outcome \
--issue "$ISSUE_NUMBER" \
--status "$OUTCOME_STATUS" \
--duration "$PIPELINE_DURATION_MINS" \
--tokens "$TOTAL_TOKENS" \
--cost "$ESTIMATED_COST_USD" \
--what-went-well "$WHAT_WENT_WELL" \
--what-didnt "$WHAT_DIDNT" \
--lessons-learned "$LESSONS_LEARNED" \
${PR_URL:+--pr-url "$PR_URL"}
```
Exit code 0 = success (appended or idempotent no-op). Exit code 1 = error
(invalid status, permission failure, etc.). The binary prints the target file
path on success; errors go to stderr.
---
### Phase 10: Post-Epic Synthesis (Skill Feedback Loop)
**PURPOSE**: After an epic completes (all sub-issues merged), aggregate findings
across all sub-issues to detect systemic patterns that individual retros miss.
This closes the feedback loop: epic failures → pattern detection → skill/config
improvements → fewer failures next epic.
**Trigger**: Runs when `--epic N` is passed OR when batch state shows an epic
with all sub-issues complete.
**No-op when**: No epic context, or epic is still in progress.
#### Step 10.1: Gather Cross-Issue Data
```bash
EPIC_NUMBER="${ARG_EPIC:-}"
# Auto-detect epic if not specified
BATCH_STATE="$(nightgauge layout path checkout batch-state.json)"
if [ -z "$EPIC_NUMBER" ] && [ -f "$BATCH_STATE" ]; then
EPIC_NUMBER=$(jq -r '.epic_number // empty' "$BATCH_STATE" 2>/dev/null)
fi
if [ -z "$EPIC_NUMBER" ]; then
echo "No epic context — skipping post-epic synthesis"
else
echo "Running post-epic synthesis for epic #$EPIC_NUMBER..."
# Collect all assessment records for this epic's sub-issues
ASSESSMENT_DIR="$(nightgauge layout path pipeline assessments)"
RETRO_DIR="$(nightgauge layout path retros)"
HISTORY_DIR="$(nightgauge layout path pipeline history)"
# Get sub-issue numbers from batch state or GitHub
SUB_ISSUES=$(jq -r '.issues[]?.number // empty' "$BATCH_STATE" 2>/dev/null)
if [ -z "$SUB_ISSUES" ]; then
REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
SUB_ISSUES=$(nightgauge forge issue view "$EPIC_NUMBER" --repo "$REPO" --json 2>/dev/null | \
jq -r '.body // empty' | \
grep -oE '#[0-9]+' | grep -oE '[0-9]+' | sort -u)
fi
echo "Sub-issues: $SUB_ISSUES"
fi
```
#### Step 10.2: Aggregate Patterns Across Sub-Issues
```bash
if [ -n "$EPIC_NUMBER" ] && [ -n "$SUB_ISSUES" ]; then
# Collect all assessment records
ASSESSMENT_FILES=""
for ISSUE_NUM in $SUB_ISSUES; do
for f in "$ASSESSMENT_DIR"/*-"$ISSUE_NUM".json; do
[ -f "$f" ] && ASSESSMENT_FILES="$ASSESSMENT_FILES $f"
done
done
# Collect all retro records
RETRO_FILES=""
for ISSUE_NUM in $SUB_ISSUES; do
for f in "$RETRO_DIR"/*_"$ISSUE_NUM"_retro.json; do
[ -f "$f" ] && RETRO_FILES="$RETRO_FILES $f"
done
done
# Parse and aggregate with Python3
SYNTHESIS_RESULT=$(python3 << 'PYEOF'
import json, glob, sys, os
from collections import defaultdict
assessment_files = os.environ.get("ASSESSMENT_FILES", "").split()
retro_files = os.environ.get("RETRO_FILES", "").split()
# Aggregate friction patterns from assessments
friction_patterns = defaultdict(lambda: {"count": 0, "issues": set(), "suggestions": [], "severity": "low"})
for f in assessment_files:
if not f or not os.path.isfile(f):
continue
try:
with open(f) as fh:
data = json.load(fh)
issue_num = data.get("issue_number", 0)
for friction in data.get("friction", []):
key = f"{friction.get('type', 'unknown')}:{friction.get('description', '')[:80]}"
friction_patterns[key]["count"] += 1
friction_patterns[key]["issues"].add(issue_num)
if friction.get("suggested_fix"):
friction_patterns[key]["suggestions"].append(friction["suggested_fix"])
sev = friction.get("severity", "low")
if sev == "high" or (sev == "medium" and friction_patterns[key]["severity"] == "low"):
friction_patterns[key]["severity"] = sev
except (json.JSONDecodeError, KeyError):
continue
# Aggregate failure categories from retros
failure_categories = defaultdict(lambda: {"count": 0, "issues": set()})
for f in retro_files:
if not f or not os.path.isfile(f):
continue
try:
with open(f) as fh:
data = json.load(fh)
issue_num = data.get("issue_number", 0)
for finding in data.get("findings", []):
cat = finding.get("category", "unknown")
failure_categories[cat]["count"] += 1
failure_categories[cat]["issues"].add(issue_num)
except (json.JSONDecodeError, KeyError):
continue
# Build synthesis output
recurring = []
for pattern, info in friction_patterns.items():
info["issues"] = sorted(info["issues"])
if info["count"] >= 2: # Two-strike rule
recurring.append({
"pattern": pattern,
"count": info["count"],
"affected_issues": info["issues"],
"severity": info["severity"],
"suggested_fix": info["suggestions"][0] if info["suggestions"] else None,
})
recurring.sort(key=lambda x: (-{"high": 3, "medium": 2, "low": 1}.get(x["severity"], 0), -x["count"]))
result = {
"recurring_friction": recurring,
"failure_categories": {k: {"count": v["count"], "issues": sorted(v["issues"])} for k, v in failure_categories.items()},
"total_assessments": len(assessment_files),
"total_retros": len(retro_files),
}
print(json.dumps(result, indent=2))
PYEOF
) || SYNTHESIS_RESULT='{}'
echo "$SYNTHESIS_RESULT"
fi
```
#### Step 10.3: Generate Improvement Proposals
For each **recurring** friction pattern (count >= 2), generate a
`SkillImprovementProposal`:
```bash
if [ -n "$EPIC_NUMBER" ] && [ -n "$SYNTHESIS_RESULT" ]; then
RECURRING_COUNT=$(printf '%s\n' "$SYNTHESIS_RESULT" | jq '.recurring_friction | length' 2>/dev/null || echo "0")
if [ "$RECURRING_COUNT" -gt 0 ]; then
echo "=== RECURRING SKILL FRICTION (${RECURRING_COUNT} patterns) ==="
printf '%s\n' "$SYNTHESIS_RESULT" | jq -r '.recurring_friction[] |
"[\(.severity | ascii_upcase)] \(.pattern) — seen \(.count)x across issues \(.affected_issues | join(", "))
Fix: \(.suggested_fix // "No suggestion")\n"'
# Write synthesis to assessments dir for dashboard consumption
SYNTHESIS_FILE=$(printf '%s\n' "$SYNTHESIS_RESULT" | jq --arg epic "$EPIC_NUMBER" \
'. + {"epic_number": ($epic | tonumber), "synthesized_at": (now | todate)}' \
| nightgauge layout write pipeline "assessments/synthesis-epic-${EPIC_NUMBER}.json")
echo "Synthesis written to: $SYNTHESIS_FILE"
else
echo "No recurring friction patterns detected across epic #$EPIC_NUMBER sub-issues."
fi
fi
```
#### Step 10.4: Auto-Create Improvement Issues (When Configured)
When `--create-issues` is active and recurring friction has `severity >= medium`:
```bash
REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
if [ "$CREATE_ISSUES" = "true" ] && [ "$RECURRING_COUNT" -gt 0 ]; then
printf '%s\n' "$SYNTHESIS_RESULT" | jq -c '.recurring_friction[] | select(.severity == "high" or .severity == "medium")' | \
while IFS= read -r proposal; do
PATTERN=$(printf '%s\n' "$proposal" | jq -r '.pattern')
FIX=$(printf '%s\n' "$proposal" | jq -r '.suggested_fix // "Review and update skill instructions"')
COUNT=$(printf '%s\n' "$proposal" | jq -r '.count')
ISSUES=$(printf '%s\n' "$proposal" | jq -r '.affected_issues | map("#" + tostring) | join(", ")')
SEVERITY=$(printf '%s\n' "$proposal" | jq -r '.severity')
TITLE="fix(skills): ${PATTERN}"
BODY="## Recurring Skill Friction
**Pattern**: \`${PATTERN}\`
**Severity**: ${SEVERITY}
**Occurrences**: ${COUNT} (across ${ISSUES})
**Source**: Post-epic synthesis for epic #${EPIC_NUMBER}
## Suggested Fix
${FIX}
## Evidence
Detected by self-assessment epilogues across multiple pipeline runs.
See \`assessments/synthesis-epic-${EPIC_NUMBER}.json\` in the clone's pipeline state directory
(\`nightgauge layout path pipeline\`) for details.
---
*Auto-created by /nightgauge:retro --epic ${EPIC_NUMBER} --create-issues*"
# Check for duplicate
EXISTING=$(nightgauge issue list --repo "$REPO" --search "\"${PATTERN}\" label:skill-drift" --json 2>/dev/null \
| jq -r '(. // [])[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "Duplicate exists: #$EXISTING — skipping"
else
if [ "$DRY_RUN" = "true" ]; then
echo "[DRY RUN] Would create: $TITLE"
else
nightgauge issue create --repo "$REPO" --title "$TITLE" --body "$BODY" --labels "skill-drift,priority:medium" 2>/dev/null || \
echo "WARNING: Failed to create issue for pattern: $PATTERN"
fi
fi
done
fi
```
---
### Self-Assessment Epilogue
### Self-Assessment Epilogue
**PURPOSE**: Evaluate whether this skill's instructions matched reality during
this execution. This phase is **non-blocking** — skip entirely if any main phase
failed. A perfectly working skill produces **no output** from this phase.
> See [docs/SKILL_SELF_ASSESSMENT.md](https://github.com/nightgauge/nightgauge/blob/main/docs/SKILL_SELF_ASSESSMENT.md) for
> the full strategy, synthesis algorithm, and integration architecture.
#### When to Skip
- Any prior phase exited with an error before completing
- The skill was cancelled or timed out
- Running in `--dry-run` mode
#### Step 1: Evaluate Execution Friction
Review the execution that just completed. Answer these questions honestly:
1. **Command failures**: Did any command, script, or binary call in the skill
instructions fail? (e.g., script not found, wrong flags, missing binary)
2. **Workarounds**: Did you have to deviate from the skill instructions to
accomplish the goal? (e.g., used `gh` directly because a referenced script
didn't exist, skipped a step that referenced a nonexistent file)
3. **Stale references**: Did any file path, function name, API endpoint, or tool
referenced in the instructions not exist in the current codebase?
4. **Unclear instructions**: Were any instructions ambiguous enough that you had
to guess at the intended behavior?
5. **Missing instructions**: Was there a significant step you had to figure out
on your own that should have been documented in this skill?
**If ALL answers are "no" — write nothing and complete the skill normally.** The
goal is silence when everything works.
#### Step 2: Write Assessment Record
Only if friction was detected in Step 1. Write a single JSON file,
`assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json` in the clone's pipeline state
directory, through the binary (JSON on stdin; never a direct file write):
```bash
ISSUE_NUMBER="${NIGHTGAUGE_ISSUE_NUMBER:-$(git branch --show-current | sed -n 's#^[^/]*/\([0-9]*\)-.*#\1#p')}"
: "${ISSUE_NUMBER:?set NIGHTGAUGE_ISSUE_NUMBER or check out the issue branch}"
nightgauge layout write pipeline "assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json" <<'JSON'
{ ... the record below ... }
JSON
```
The assessment record MUST follow this schema:
```json
{
"schema_version": "1",
"skill": "{STAGE_NAME}",
"skill_file": "skills/nightgauge-{STAGE_NAME}/SKILL.md",
"issue_number": 42,
"timestamp": "2026-03-10T14:30:00Z",
"friction": [
{
"type": "command_failure",
"severity": "high",
"description": "hooks/lib/add-to-project.sh not found — script was deleted",
"skill_line_hint": "claude-plugins/nightgauge/hooks/lib/add-to-project.sh <issue-number>",
"actual_resolution": "Used gh api graphql to add issue to project board directly",
"suggested_fix": "Replace with: nightgauge project add <issue-number>"
}
]
}
```
**Friction types**: `command_failure`, `workaround`, `stale_reference`,
`unclear_instruction`, `missing_instruction`
**Severity levels**:
- `high` — instruction is **broken**. Required manual workaround to complete.
- `medium` — instruction is **misleading**. Agent adapted without user help.
- `low` — instruction is **suboptimal**. No functional impact.
**Rules**:
- **One record per execution** — multiple friction items go in the `friction`
array, not separate files.
- **Be specific** — quote the exact instruction text that was wrong. Not "some
commands didn't work" but "Step 5.2 calls `hooks/lib/add-to-project.sh` but
this script was deleted in commit 65915701."
- **Suggest the fix** — every finding MUST include `suggested_fix` with the
concrete SKILL.md change needed. Not "update the docs" but "replace
`hooks/lib/add-to-project.sh <N>` with `nightgauge project add <N>`."
- **Don't invent friction** — only report issues you actually encountered during
this execution. Do not speculate about potential problems.
#### Step 3: Validate and Complete
```bash
# Validate JSON if written
ISSUE_NUMBER="${NIGHTGAUGE_ISSUE_NUMBER:-$(git branch --show-current | sed -n 's#^[^/]*/\([0-9]*\)-.*#\1#p')}"
: "${ISSUE_NUMBER:?set NIGHTGAUGE_ISSUE_NUMBER or check out the issue branch}"
ASSESSMENT_FILE="$(nightgauge layout path pipeline "assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json")"
if [ -f "$ASSESSMENT_FILE" ]; then
python3 -m json.tool "$ASSESSMENT_FILE" > /dev/null 2>&1 || \
echo "WARNING: Assessment record is not valid JSON" >&2
fi
```
---
## Output Format
### JSON Schema
```json
{
"schema_version": "string (1.0)",
"scope": {
"type": "string (batch | date-range | single-issue)",
"from": "string (YYYY-MM-DD)",
"to": "string (YYYY-MM-DD)",
"issue_filter": "number | null",
"issues_analyzed": "number",
"failure_count": "number",
"data_sources": "string[]"
},
"summary": {
"failure_rate": "number (0-1)",
"total_wasted_cost_usd": "number",
"most_common_category": "string",
"stage_hotspot": "string",
"categories_found": "string[]"
},
"failures": [
{
"issue_number": "number",
"title": "string",
"categories": "string[]",
"confidence": "string (high | medium | low)",
"evidence": "string[]",
"failed_stages": "string[]",
"estimated_cost_usd": "number"
}
],
"findings": [
{
"category": "string",
"severity": "string (critical | high | medium | low | info)",
"count": "number",
"affected_issues": "number[]",
"title": "string",
"root_cause": "string",
"pattern": "string",
"recommendation": "string",
"estimated_recurrence_risk": "string (high | medium | low)"
}
],
"recommendations": [
{
"priority": "number",
"action": "string",
"impact": "string",
"effort": "string (low | medium | high)",
"category": "string",
"config_snippet": "string | null",
"recovery_commands": "string[]",
"affected_issues": "number[]",
"estimated_cost_savings_usd": "number"
}
],
"created_at": "string (ISO-8601)"
}
```
---
## Error Handling
| Condition | Action |
| ----------------------------------- | ------------------------------------------------------------------------- |
| No data sources found | Output friendly message listing expected paths, exit 0 |
| No failures found in scope | Output "No failures found", list total runs analyzed |
| Malformed JSONL records | Skip bad records, report count at end |
| Malformed log lines | Skip unreadable lines, continue scanning |
| python3 not installed | Error with install instructions, exit 1 |
| forge auth not configured | Error with `nightgauge forge auth login` instructions |
| `--create-issues` without findings | Output "No findings at severity threshold" |
| `--issue N` not found in any source | Output "Issue #N not found in available data sources" |
| Duplicate finding exists | Skip creation, report as already tracked |
| `issue create-sub` label error | Use `nightgauge forge graphql` `addLabelsToLabelable` mutation separately |
| Batch state and history both empty | Fall back to context file scan, note limited coverage |
---
## Pipeline Position
```
UTILITIES (not part of main pipeline)
/nightgauge:retro
↑
Use after batch runs or periodically to triage failures
Also records outcome data to knowledge base when knowledge_path is set
(class paths resolve with `nightgauge layout path <class>`)
Reads: logs/*_session.log
Reads: checkout/batch-state.json
Reads: pipeline/history/*.jsonl
Reads: pipeline/{stage}-{N}.json
Reads: pipeline/issue-{N}.json (for knowledge_path)
Reads: pipeline/pr-{N}.json (for pr_url, recorded as a source)
Reads: pipeline/assessments/*.json (--epic, --skill-feedback)
Reads: retros/*_retro.json (--epic cross-issue aggregation)
Writes: pipeline/retro-report-YYYY-MM-DD.json (optional, via layout write)
Writes: pipeline/assessments/synthesis-epic-{N}.json (--epic, via layout write)
Writes: {knowledge_path}/decisions.md (when knowledge_path set)
Creates: GitHub issues via nightgauge forge issue create (--create-issues only)
```
This is a standalone utility skill. It does not affect pipeline state and can be
run at any time without interfering with active pipeline runs.
---
## Configuration
This skill reads configuration from `.nightgauge/config.yaml`:
| Config Key | Default | Description |
| ------------------------ | ------- | ----------------------------------- |
| `project.number` | - | GitHub Project number for issues |
| `retro.default_period` | `7` | Default `--period` value (days) |
| `retro.default_severity` | `high` | Default severity for issue creation |
| `retro.auto_create` | `false` | Auto-create issues without flag |
---
**Author:** nightgauge **License:** Apache-2.0