Skip to content
Back to skills

Nightgauge Issue Audit

ASecurity

Deterministic post-creation gate that verifies every issue created by issue-create

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 5, 2026
developmentgoshellbashnodegitapisecurity

Works with

  • terminal
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned November 5, 2026

npx -y skills add nightgauge/nightgauge --skill nightgauge-issue-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nightgauge Issue Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nightgauge Issue Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nightgauge-nightgauge-issue-audit/badge)](https://www.skillsdirectory.com/skills/nightgauge-nightgauge-issue-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nightgauge-issue-audit
description: Deterministic post-creation gate that verifies every issue created by issue-create
  (or any creation flow) has the labels, board membership, body sections, sub-issue
  links, blockedBy edges, cross-repo references, and knowledge scaffold the pipeline
  depends on. Reports findings by severity and offers --fix / --fix-interactive
  auto-repair using existing Go binary primitives. Use after every issue-creation
  flow or as a CI safety net.
license: Apache-2.0
metadata:
  author: nightgauge
  version: "1.3.0"
  source: https://github.com/nightgauge/nightgauge
  chainable: true
allowed-tools: Read Write Edit Glob Grep Bash Task AskUserQuestion
---

# Issue Audit

Deterministic post-creation gate for every issue-creation flow. See
[docs/ISSUE_AUDIT.md](../../docs/ISSUE_AUDIT.md) for the finding taxonomy,
severity rules, manifest schema, and repair-primitive table. This skill
encodes the orchestration; the doc encodes the spec.

## When to Use

- **Terminal pass after `/nightgauge:issue-create`** — runs automatically
  unless `--no-audit` is passed
- **After manually creating issues** with `nightgauge forge issue create` followed by board
  sync
- **CI safety net** with `--all-recent 1h` to catch any creation flow that
  bypassed audit
- **Replaces `/nightgauge:epic-validate`** — that skill is now a thin
  wrapper that delegates to `issue-audit --epic <N>`

## Outcomes

- Markdown finding report `issue-audit-<timestamp>.md`, JSON findings
  `issue-audit-<timestamp>.json` and JSONL audit trail
  `issue-audit-<timestamp>.audit.jsonl`, all in the clone's pipeline state
  directory (`nightgauge layout path pipeline`), written through
  `nightgauge layout write|append pipeline`
- Exit 0 (READY) when no CRITICAL findings remain; exit 1 (NEEDS FIXES) when
  any CRITICAL persists; exit 2 on skill-level failure
- Auto-repair via `--fix` for safe categories (board, fields, links,
  blockedBy edges, body Part-of insertion); `--fix-interactive` for
  `closed-as-not-planned` blockers
- Hard rules: never auto-rewrite human-authored content; spike-contract
  violations stay CRITICAL even with `--fix`

## Input

```
/nightgauge:issue-audit --manifest <path>          # strict mode
/nightgauge:issue-audit --epic <N> [--repo <O/R>]  # inferential, single epic
/nightgauge:issue-audit --issues <N1,N2,N3> [--repo <O/R>]
/nightgauge:issue-audit --all-recent <duration>    # e.g. 1h, 30m
```

Run modes: dry-run (default), `--fix`, `--fix-interactive`.

Output flags: `--json`, `--no-audit-trail`, `--allow-closed`.

## Workflow

### Phase 0: Environment Preflight

<!-- include: ../_shared/PREFLIGHT.md -->

---

### Phase 1: Parse Arguments and Resolve Targets

Distinguish invocation mode from the skill arguments (the text after the
skill name):

```bash
MODE=""
MANIFEST_PATH=""
EPIC_NUMBER=""
ISSUE_LIST=""
RECENT_DURATION=""
RUN_MODE="dry-run"
JSON_OUTPUT=false
WRITE_TRAIL=true
ALLOW_CLOSED=false
REPO_OVERRIDE=""

while [ $# -gt 0 ]; do
  case "$1" in
    --manifest) MODE="manifest"; MANIFEST_PATH="$2"; shift 2 ;;
    --epic) MODE="epic"; EPIC_NUMBER="$2"; shift 2 ;;
    --issues) MODE="issues"; ISSUE_LIST="$2"; shift 2 ;;
    --all-recent) MODE="all-recent"; RECENT_DURATION="$2"; shift 2 ;;
    --fix) RUN_MODE="fix"; shift ;;
    --fix-interactive) RUN_MODE="fix-interactive"; shift ;;
    --json) JSON_OUTPUT=true; shift ;;
    --no-audit-trail) WRITE_TRAIL=false; shift ;;
    --allow-closed) ALLOW_CLOSED=true; shift ;;
    --repo) REPO_OVERRIDE="$2"; shift 2 ;;
    *) echo "ERROR: unknown flag $1" >&2; exit 2 ;;
  esac
done

if [ -z "$MODE" ]; then
  echo "ERROR: must specify one of --manifest, --epic, --issues, --all-recent" >&2
  exit 2
fi
```

#### Resolve target issues

- **`--manifest`**: read JSON, parse via `jq`, fail-exit 2 on malformed JSON.
  Each entry's `repo` and `number` becomes a target.
- **`--epic <N>`**: read the epic's sub-issues with
  `nightgauge issue list --epic <N> --repo <owner/repo> --json`; the epic +
  every entry of `.subIssues` (each in its own `.repo`) are targets. The
  binary reads the whole list or exits non-zero, which is a skill failure
  (exit 2), never a shorter target list (#1701).
- **`--issues N1,N2,N3`**: split on comma; each is a target in `--repo` (or
  current repo).
- **`--all-recent <dur>`**: query
  `nightgauge forge issue list --repo $REPO --search 'created:>=<since>' --json number,repository`
  across the configured repos.

For inferential modes, the audit synthesizes a per-target _expected_ shape
from issue type, project config, and body content. Strict mode uses the
manifest as the source of truth.

#### Initialize report files

```bash
TS=$(date -u +%Y%m%dT%H%M%SZ)
# Read-only paths for display; every write goes through
# `nightgauge layout write|append pipeline <name>`.
REPORT_DIR="$(nightgauge layout path pipeline)"
REPORT_MD="$REPORT_DIR/issue-audit-${TS}.md"
REPORT_JSON="$REPORT_DIR/issue-audit-${TS}.json"
TRAIL_JSONL="$REPORT_DIR/issue-audit-${TS}.audit.jsonl"
FINDINGS=()  # populated as JSON-line strings throughout the run
```

---

### Phase 2: Existence & Repo Placement

For each target `(owner/repo, number)`:

```bash
ISSUE_JSON=$(nightgauge forge issue view "$NUM" --repo "$OWNER_REPO" --json 2>/dev/null) || {
  FINDINGS+=("$(jq -nc --arg r "$OWNER_REPO" --arg n "$NUM" '
    {phase:1,type:"MISSING_ISSUE",severity:"CRITICAL",
     issue:{repo:$r,number:($n|tonumber)},
     detail:"Issue not found via nightgauge forge issue view"}')")
  continue
}

# WRONG_REPO: the api response's repository.nameWithOwner ≠ expected
ACTUAL_REPO=$(printf '%s\n' "$ISSUE_JSON" | jq -r '.repository.nameWithOwner // empty')
if [ -n "$ACTUAL_REPO" ] && [ "$ACTUAL_REPO" != "$OWNER_REPO" ]; then
  # emit WRONG_REPO finding
  ...
fi

# UNEXPECTED_STATE: closed but not allowed
STATE=$(printf '%s\n' "$ISSUE_JSON" | jq -r '.state')
if [ "$STATE" = "CLOSED" ] && [ "$ALLOW_CLOSED" != "true" ]; then
  # emit UNEXPECTED_STATE finding (WARNING)
  ...
fi
```

---

### Phase 3: Label Completeness

For each target's labels:

- `MISSING_TYPE_LABEL` (CRITICAL) when no `type:*` present
- `MULTIPLE_TYPE_LABELS` (CRITICAL) when more than one
- `MISSING_COMPONENT_LABEL` (INFO) when body cites a component path but no
  `component:*` label

Priority and Size are project board fields; Phase 4 checks them.

---

### Phase 4: Project Board Membership and Fields

```bash
PROJECT_QUERY=$(nightgauge forge graphql -f query='query($owner:String!,$name:String!,$num:Int!){
  repository(owner:$owner,name:$name){
    issue(number:$num){
      projectItems(first:10){
        nodes{
          project{number}
          fieldValues(first:30){
            nodes{
              ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2SingleSelectField { name } } }
            }
          }
        }
      }
    }
  }
}' -f owner="$OWNER" -f name="$NAME" -F num="$NUM")
```

Findings:

- `MISSING_FROM_BOARD` (CRITICAL): no `projectItems` entry
- `WRONG_BOARD` (CRITICAL): on a project that does not match the expected
  `project_number` (manifest or workspace yaml mapping)
- `MISSING_STATUS_FIELD` (CRITICAL): item exists but Status is unset
- `MISSING_PRIORITY_FIELD` (WARNING): Priority unset
- `MISSING_SIZE_FIELD` (WARNING): Size unset

---

### Phase 5: Body Section Completeness

The table below is the **canonical body-section contract** for this repository.
`nightgauge-issue-create` authors bodies against it rather than restating a
shape of its own (see that skill's `_includes/environment-and-content.md`,
Phase 2), and [docs/ISSUE_AUDIT.md](../../docs/ISSUE_AUDIT.md) mirrors it for
readers. All three copies are pinned against each other by
`scripts/check-issue-body-contract.py` — edit this table and that gate stays red
until the other two follow. Before #711 the three disagreed on every row, so
Phase 5 fired on every issue the pipeline itself authored.

| Type     | Required headings                                           |
| -------- | ----------------------------------------------------------- |
| feature  | Summary, Acceptance Criteria, Verification                  |
| bug      | Summary, Steps to Reproduce, Expected, Actual, Verification |
| docs     | Summary, Acceptance Criteria, Verification                  |
| refactor | Summary, Acceptance Criteria, Verification                  |
| spike    | Summary, Acceptance Criteria, Recommendations, Verification |
| chore    | Summary, Verification                                       |
| epic     | Summary, Sub-Issues, Acceptance Criteria, Verification      |

`Verification` is required of every type (2026-08-29): the workspace backlog
groom found 167 of 224 open issues carried no falsifiable way to tell the
feature working from the feature claimed, and a pipeline dispatched against
such an issue self-grants "done". The section names, per acceptance
criterion, the test or command, the input, and the expected observable
output — see `issue-create`'s authoring rules for the bar.

**Matching is exact and case-sensitive.** The regex below is `grep -E` without
`-i`, so `## Acceptance criteria` does not satisfy a required
`Acceptance Criteria`. Reproduce every heading with the casing shown above.
Headings the table does not list are ignored, so an issue may carry any extra
sections it likes (`Business/user value`, `Technical notes`, `Related work`)
without affecting this phase.

For each required heading:

```bash
BODY=$(printf '%s\n' "$ISSUE_JSON" | jq -r '.body // ""')
if ! printf '%s\n' "$BODY" | grep -qE "^##[[:space:]]+${HEADING}\\s*$"; then
  # Mode-dependent severity — see "Severity of MISSING_REQUIRED_HEADING" below.
  HEADING_SEV=$([ "$MODE" = "manifest" ] && echo "CRITICAL" || echo "WARNING")
  FINDINGS+=( /* MISSING_REQUIRED_HEADING ($HEADING_SEV) */ )
  continue
fi

# Non-empty content check between this heading and the next ##
SECTION=$(printf '%s\n' "$BODY" | awk -v h="^##[[:space:]]+${HEADING}\\s*$" '
  $0 ~ h {flag=1; next}
  flag && /^##[[:space:]]/ {flag=0}
  flag {print}')
if [ -z "$(echo "$SECTION" | tr -d '[:space:]')" ]; then
  FINDINGS+=( /* EMPTY_REQUIRED_HEADING (WARNING) */ )
fi
```

Quality riders on the body, all WARNING and never auto-fixed — they are
heuristics, and a false positive must cost a glance, not a block. Two run on
every type; the epic riders below run only for `type: epic`:

```bash
# WEAK_VERIFICATION — the section exists but names no test, command, or file.
VSEC=$(printf '%s\n' "$BODY" | awk '
  $0 ~ /^##[[:space:]]+Verification[[:space:]]*$/ {flag=1; next}
  flag && /^##[[:space:]]/ {flag=0}
  flag {print}')
if [ -n "$(echo "$VSEC" | tr -d '[:space:]')" ] \
   && ! printf '%s\n' "$VSEC" | grep -qE '`[^`]+`|go test|vitest|flutter test|pytest|npm (run|test)'; then
  FINDINGS+=( /* WEAK_VERIFICATION (WARNING) — "add tests" is not verification */ )
fi

# SECURITY_SURFACE_UNADDRESSED — the design touches a security surface and
# neither a Security section nor a security-shaped acceptance criterion exists.
if printf '%s\n' "$BODY" | grep -qiE 'auth|token|secret|credential|webhook|endpoint|retry|retries|spawn|exec|shell|symlink|upload|permission' \
   && ! printf '%s\n' "$BODY" | grep -qiE '^##[[:space:]]+Security|^- \[ \].*(secur|sanitiz|validat|authoriz|authent|bound|rate.?limit|scope|confine|redact)'; then
  FINDINGS+=( /* SECURITY_SURFACE_UNADDRESSED (WARNING) — never auto-fix */ )
fi
```

For `type: epic`, two more riders check that `## Summary` justifies the epic's
existence. Each is a presence check only — a cheap signal, not a quality
judge — and stays WARNING in every mode, `--manifest` included:

```bash
# MISSING_EPIC_FALSIFIER / MISSING_EPIC_DEMAND — the Summary lacks a non-empty
# `Falsifier:` / `Demand:` line (optionally bulleted or bolded). A line outside
# `## Summary`, or with nothing after the colon, does not count.
TYPE=$(printf '%s\n' "$ISSUE_JSON" | jq -r '[.labels[].name | select(startswith("type:"))][0] // "" | sub("^type:"; "")')
if [ "$TYPE" = "epic" ]; then
  SSEC=$(printf '%s\n' "$BODY" | awk '
    $0 ~ /^##[[:space:]]+Summary[[:space:]]*$/ {flag=1; next}
    flag && /^##[[:space:]]/ {flag=0}
    flag {print}')
  for LABEL in Falsifier Demand; do
    if ! printf '%s\n' "$SSEC" \
       | grep -qE "^[[:space:]]*(-[[:space:]]+)?(\*\*)?${LABEL}(\*\*)?:(\*\*)?[[:space:]]*[^[:space:]*]"; then
      # Falsifier: no answer to the motivating question would stop the work.
      # Demand: nobody is recorded as having asked for it.
      FINDINGS+=( /* MISSING_EPIC_FALSIFIER | MISSING_EPIC_DEMAND (WARNING) — never auto-fix */ )
    fi
  done
fi
```

For `type: spike`:

````bash
# Verify the yaml recommendations block exists and parses
if ! printf '%s\n' "$BODY" | awk '/```yaml recommendations/,/```/' | head -2 | grep -q '```yaml recommendations'; then
  FINDINGS+=( /* MISSING_SPIKE_RECS_BLOCK (CRITICAL) — never auto-fix */ )
fi
````

The `MISSING_SPIKE_RECS_BLOCK` finding has **no repair primitive**. Even with
`--fix`, this remains CRITICAL — encoded in Phase 9 logic and pinned by the
`spike-contract-violation` test fixture.

#### Severity of `MISSING_REQUIRED_HEADING`

CRITICAL in strict (`--manifest`) mode, WARNING everywhere else. The split is
deliberate and was re-decided in #711 — the reasoning is recorded in
[docs/ISSUE_AUDIT.md](../../docs/ISSUE_AUDIT.md#severity-tiers).

In strict mode the manifest was written moments earlier by `issue-create`,
which authors against the table above. A missing heading there is not a
degraded issue; it is proof the authoring contract was not followed, on the one
run where following it was guaranteed. That is what CRITICAL means in this
skill's tier table, and it makes Phase 6 of `issue-create` a gate that can
actually fail.

In inferential mode (`--epic`, `--issues`, `--all-recent`) the target is
frequently a hand-filed or pre-#711 issue that no creation flow ever shaped.
Blocking those is `backlog-groom`'s job, not this skill's, so the finding stays
a WARNING and the run still exits 0.

The finding has no repair primitive in either mode: headings are human-authored
prose, and inventing a `## Actual` section is exactly the kind of fabrication
`--fix` must never do.

#### Oversized-scope check (all issue types)

Catches issues that bundle many independent refactors into a single executable
ticket — the root cause of pipeline runaways (incident #3811: $112.77 of
feature-dev churn on one issue that meant "refactor ~18 skills"). This mirrors
the `issue-create` Phase 2.85 oversized-scope gate so manually-created issues
(which never pass through issue-create) are flagged post-creation.

```bash
# Signal 1: distinct CHANGE-TARGET files via the shared deterministic
# extractor (#79) — the SAME implementation the epic wave planner uses, so
# audit counting can never drift from wave planning. Markdown-link
# destinations are citations and never count; an explicit `file_ownership`
# list in the `nightgauge:dependency-metadata` block replaces prose inference.
DISTINCT_TARGETS=$(printf '%s' "$BODY" \
  | nightgauge issue extract-targets --json 2>/dev/null \
  | jq -r '.count' 2>/dev/null || echo 0)

# Signal 2: predicted size == XL (objective estimate via the Go binary)
PREDICTED_SIZE=$(nightgauge size predict "$NUM" --json 2>/dev/null \
  | jq -r '.SizeLabel // empty')

# Signal 3: independent acceptance-criteria groups (refactor/migrate/... bullets)
AC_GROUP_COUNT=$(printf '%s\n' "$BODY" \
  | grep -ciE '^[[:space:]]*([-*]|[0-9]+\.)[[:space:]]+(refactor|migrate|convert|split|rewrite|extract|decompose|reduce|trim)[[:space:]]')

# Override marker (same marker honored by issue-create Phase 2.85)
SCOPE_OVERRIDE=false
if printf '%s' "$BODY" | grep -qi "nightgauge:oversized-scope-accepted\|oversized scope accepted"; then
  SCOPE_OVERRIDE=true
fi

# An epic with native sub-issues is the correct shape for large scope — exempt.
# Labels come from ISSUE_JSON (Phase 2). totalCount counts the whole
# subIssues connection, so no page of it needs reading.
IS_DECOMPOSED_EPIC=false
if printf '%s\n' "$ISSUE_JSON" | jq -e '.labels[]? | select(.name == "type:epic")' >/dev/null 2>&1; then
  SUB_ISSUE_COUNT=$(nightgauge forge graphql -f query='query($owner:String!,$name:String!,$num:Int!){
    repository(owner:$owner,name:$name){issue(number:$num){subIssues{totalCount}}}
  }' -f owner="$OWNER" -f name="$NAME" -F num="$NUM" \
    -q '.data.repository.issue.subIssues.totalCount' 2>/dev/null || echo 0)
  if [ "${SUB_ISSUE_COUNT:-0}" -gt 0 ]; then
    IS_DECOMPOSED_EPIC=true
  fi
fi

# Trigger: >=6 distinct targets OR size==XL OR >=6 independent AC groups,
# AND not already a decomposed epic AND no override marker.
if [ "$IS_DECOMPOSED_EPIC" = "false" ] && [ "$SCOPE_OVERRIDE" = "false" ] && {
     [ "${DISTINCT_TARGETS:-0}" -ge 6 ] || \
     printf '%s\n' "$PREDICTED_SIZE" | grep -qiE '^XL$' || \
     [ "${AC_GROUP_COUNT:-0}" -ge 6 ]; }; then
  FINDINGS+=( /* OVERSIZED_SCOPE (WARNING) — no auto-fix; requires manual decomposition */ )
fi
```

Finding:

- `OVERSIZED_SCOPE` (WARNING): the issue has ≥6 distinct change-target files
  (per the shared extractor — markdown-link citations excluded, explicit
  `file_ownership` declarations win, #79), OR has predicted size `XL`, OR
  enumerates ≥6 independent refactor/migration acceptance-criteria groups —
  and is neither a decomposed epic (`type:epic` with native sub-issues) nor
  carries the `<!-- nightgauge:oversized-scope-accepted -->` override marker. This
  finding has **no repair primitive**: decomposition into sub-issues under an
  epic is a human/planning decision, never auto-applied by `--fix`. Even with
  `--fix`, `OVERSIZED_SCOPE` remains a WARNING.

#### Step 5.x: Declared `file_ownership` substantiation (#561)

When the body carries a `nightgauge:dependency-metadata` block with a
`file_ownership` list, check it against the ACs with the deterministic verb
(no block means no-op):

```bash
OWNERSHIP_JSON=$(printf '%s' "$BODY" | nightgauge issue check-ownership --json 2>/dev/null \
  || echo '{"findings":[],"count":0}')
# One FILE_OWNERSHIP_UNSUBSTANTIATED (WARNING) per entry in .findings[]
```

- `FILE_OWNERSHIP_UNSUBSTANTIATED` (WARNING): a declared set cannot satisfy the
  issue's own ACs — `test-missing`, `generated-mirror` (names the canonical
  path), or `path-missing`. **No repair primitive**: correcting a declared set
  is a planning edit. "No edit needed" and "provably inert" sets are not
  detected; see [docs/ISSUE_AUDIT.md](../../docs/ISSUE_AUDIT.md).

---

### Phase 6: Sub-Issue & Parent Linking

Read every sub-issue through the binary, which pages the connection to its
end or exits non-zero (#1701). A fixed first page reported each sub-issue
past it as `MISSING_SUB_ISSUE_LINK`. Do not pipe the binary into `jq`: the
pipe would take `jq`'s exit status and hide a failed read.

```bash
SUB_ISSUES_JSON=$(nightgauge issue list --epic "$NUM" --repo "$OWNER/$NAME" --json) || {
  echo "ERROR: could not read every sub-issue of $OWNER/$NAME#$NUM" >&2
  exit 2
}
# [{number, repo}, ...]; the binary writes subIssues as null when there are none.
SUB_ISSUES=$(printf '%s\n' "$SUB_ISSUES_JSON" | jq -c '.subIssues // [] | map({number, repo})')
```

Findings:

- `MISSING_SUB_ISSUE_LINK` (CRITICAL): manifest declares sub-issue #M but no
  entry in `SUB_ISSUES` matches its number and repo
- `MISSING_PART_OF_ANNOTATION` (CRITICAL): cross-repo sub-issue body lacks
  `Part of <owner>/<repo>#<epic>`
- `ORPHAN_SUB_ISSUE` (WARNING): body has `Part of #X` but parent `X` has no
  matching native sub-issue

---

### Phase 7: blockedBy Alignment

Delegate the structural check to the Go binary:

```bash
if [ "$MODE" = "epic" ] && [ -n "$EPIC_NUMBER" ]; then
  # Exit 0: every sub-issue was checked. Non-zero WITH the result on stdout:
  # some sub-issues could not be checked (.failures). Non-zero with no result:
  # the epic itself could not be read, and the audit cannot run.
  EPIC_VALIDATE_RC=0
  EPIC_VALIDATE=$("$BINARY" epic validate "$EPIC_NUMBER" --json) || EPIC_VALIDATE_RC=$?
  if ! printf '%s\n' "$EPIC_VALIDATE" | jq -e 'type == "object" and has("failedSubIssues")' >/dev/null 2>&1; then
    echo "ERROR: epic validate #$EPIC_NUMBER could not run (exit $EPIC_VALIDATE_RC)" >&2
    exit 2
  fi
  printf '%s\n' "$EPIC_VALIDATE" | jq -c '.failures // [] | .[]' | while read -r failure; do
    SUB=$(printf '%s\n' "$failure" | jq -r '.subIssueNumber')
    SUB_REPO=$(printf '%s\n' "$failure" | jq -r '.subIssueRepo')
    REASON=$(printf '%s\n' "$failure" | jq -r '.error')
    /* emit UNCHECKED_SUB_ISSUE (CRITICAL) against $SUB_REPO#$SUB with $REASON */
  done
  GAPS=$(printf '%s\n' "$EPIC_VALIDATE" | jq -c '.gaps // []')
  printf '%s\n' "$GAPS" | jq -c '.[]' | while read -r gap; do
    GAP_TYPE=$(printf '%s\n' "$gap" | jq -r '.gapType')
    SUB=$(printf '%s\n' "$gap" | jq -r '.subIssueNumber')
    SUB_REPO=$(printf '%s\n' "$gap" | jq -r '.subIssueRepo')
    BLOCKER=$(printf '%s\n' "$gap" | jq -r '.blockerNumber')
    BLOCKER_REPO=$(printf '%s\n' "$gap" | jq -r '.blockerRepo')
    DETAIL=$(printf '%s\n' "$gap" | jq -r '.detail')
    case "$GAP_TYPE" in
      circular_blocker) /* emit CIRCULAR_BLOCKER (CRITICAL) */ ;;
      stale_blocker)    /* emit STALE_BLOCKED_BY (WARNING) */ ;;
    esac
  done
fi
```

A sub-issue `epic validate` could not read is `UNCHECKED_SUB_ISSUE`
(CRITICAL): its blockers were never checked, so the epic's alignment is
unknown, not clean (#1702). It has no auto-fix; re-run the audit once the
sub-issue can be read.

Issue numbers are per repository and an epic's sub-issues can live in several.
Each gap names the sub-issue's repository (`subIssueRepo`) and the blocker's
(`blockerRepo`); record the finding against `$SUB_REPO#$SUB` and carry both
repositories into the repair (Phase 11).

For body-declared dependencies (`Depends on:` lines and the
`<!-- nightgauge:dependency-metadata -->` YAML block), parse and verify
each declaration has a matching native `blockedBy` edge:

- `MISSING_BLOCKED_BY` (CRITICAL): declared but not wired
- `STALE_BLOCKED_BY_NOT_PLANNED` (WARNING): blocker is closed-as-not-planned
- `CROSS_REPO_BLOCKER_MISSING` (CRITICAL): cross-repo blocker not found

---

### Phase 8: Cross-Repo Consistency

When the audit set spans multiple repos (manifest entries with different
`repo`, or cross-repo body annotations):

```bash
"$BINARY" audit lifecycle --json 2>/dev/null > /tmp/lifecycle.json
```

Parse the lifecycle output for cross-repo references and emit:

- `MISSING_PARENT_BACKREF` (WARNING): cross-repo sub-issue references parent
  but parent epic body has no link back
- `INCONSISTENT_PROJECT_MAPPING` (CRITICAL): two sub-issues in the same
  target repo are in different projects

---

### Phase 9: Knowledge Scaffold

When `knowledge.enabled` resolves true in `.nightgauge/config.yaml` — the
default, and an absent key means the default (ADR-020) — OR the manifest entry
sets `knowledge_path`:

```bash
KNOWLEDGE_PATH=$(printf '%s\n' "$ENTRY" | jq -r '.knowledge_path // empty')
if [ -n "$KNOWLEDGE_PATH" ]; then
  if [ ! -d "$KNOWLEDGE_PATH" ]; then
    FINDINGS+=( /* MISSING_KNOWLEDGE_DIR (WARNING) */ )
  elif [ ! -f "$KNOWLEDGE_PATH/PRD.md" ]; then
    FINDINGS+=( /* MISSING_PRD_FILE (WARNING) */ )
  else
    PRD_STRIPPED=$(sed -e 's/<!--.*-->//g' -e '/^#/d' "$KNOWLEDGE_PATH/PRD.md" | tr -s '[:space:]' ' ' | sed 's/^ *//;s/ *$//')
    if [ ${#PRD_STRIPPED} -lt 30 ]; then
      FINDINGS+=( /* EMPTY_PRD_FILE (INFO) */ )
    fi
  fi
fi
```

---

### Phase 10: Findings Synthesis

Aggregate `${FINDINGS[@]}` into a JSON array. Compute summary counts. Verdict
is `READY` when `critical == 0`, else `NEEDS FIXES`.

Severity-tiered Markdown output (CRITICAL first, then WARNING, then INFO).
Per finding, include:

- Issue (`<owner>/<repo>#<n>`)
- Phase number
- Finding type
- Detail
- Repair command (or `(no auto-fix available)` for human-only items)

---

### Phase 11: Auto-Repair (when `--fix` / `--fix-interactive`)

Walk findings in order. For each repairable category, invoke the listed
primitive (see `docs/ISSUE_AUDIT.md` repair-primitive table). Each repair
appends one JSON line to the audit trail:

```bash
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","issue":"'$ISSUE'","finding":"'$TYPE'","action":"'$ACTION'","before":'$BEFORE',"after":'$AFTER',"actor":"nightgauge-issue-audit"}' \
  | nightgauge layout append pipeline "issue-audit-${TS}.audit.jsonl" >/dev/null
```

#### Hard rules (encoded; pinned by negative test fixture)

1. **Never auto-rewrite human-authored content**: Summary, Acceptance
   Criteria, User Story, Technical Notes, spike artifact body. Findings that
   require human content stay flagged.
2. **`MISSING_SPIKE_RECS_BLOCK` is never auto-fixed**: even with `--fix`, this
   stays CRITICAL.
3. **`OVERSIZED_SCOPE` is never auto-fixed**: decomposing a bundled issue into
   sub-issues under an epic is a human/planning decision. Even with `--fix`,
   this stays a WARNING. The same holds for `FILE_OWNERSHIP_UNSUBSTANTIATED`.
4. **`STALE_BLOCKED_BY_NOT_PLANNED` requires `--fix-interactive`**: under
   `--fix` alone, the finding remains a WARNING. Under `--fix-interactive`,
   prompt the operator and only repair on explicit `yes`.
5. **Repair failures become CRITICAL**: a non-zero exit from the primitive
   reclassifies the finding as CRITICAL with the underlying error in
   `repair_error`. The audit does not retry — operator re-runs.

#### Repair-primitive map (CRITICAL/WARNING items only)

| Finding type                    | Primitive command                                                               |
| ------------------------------- | ------------------------------------------------------------------------------- |
| `MISSING_FROM_BOARD`            | `nightgauge project add <num> --repo <owner/repo> --project <num>`              |
| `WRONG_BOARD`                   | `nightgauge project add <num> --repo <owner/repo> --project <num>` (idempotent) |
| `MISSING_STATUS_FIELD`          | `nightgauge project sync-status <num> <expected_status>`                        |
| `MISSING_PRIORITY_FIELD`        | `nightgauge project set-field <num> Priority <P0\|P1\|P2\|P3>`                  |
| `MISSING_SIZE_FIELD`            | `nightgauge project set-field <num> Size <XS\|S\|M\|L\|XL>`                     |
| `MISSING_SUB_ISSUE_LINK` (same) | `nightgauge issue link-sub <epic> <sub>`                                        |
| `MISSING_PART_OF_ANNOTATION`    | `nightgauge issue edit <num> --append-body "Part of <owner>/<repo>#<epic>"`     |
| `MISSING_BLOCKED_BY`            | `nightgauge issue add-blocked-by <blocked> <blocker>`                           |
| `STALE_BLOCKED_BY` (completed)  | `nightgauge issue remove-blocked-by <blocked> <blocker>` (auto in `--fix`)      |
| `STALE_BLOCKED_BY_NOT_PLANNED`  | `nightgauge issue remove-blocked-by` (only in `--fix-interactive` after `yes`)  |
| `CIRCULAR_BLOCKER`              | `nightgauge issue remove-blocked-by <sub> <epic>` (auto in `--fix`)             |
| `MISSING_PARENT_BACKREF`        | `nightgauge issue edit <epic> --append-body "<cross-repo link line>"`           |

`remove-blocked-by` resolves both numbers in its one `--repo`. Repair a
`CIRCULAR_BLOCKER` or `STALE_BLOCKED_BY` gap from `epic validate` only when its
`subIssueRepo` and `blockerRepo` are the same repository, and pass that
repository as `--repo`. When they differ (a sub-issue in another repository
than its epic, or a blocker elsewhere), do not auto-fix: report the finding
with `(no auto-fix available: cross-repository relationship)` for a human to
remove. By number in one repository, the repair would act on a different
issue's relationship.

`MISSING_TYPE_LABEL`, `MULTIPLE_TYPE_LABELS`, `MISSING_REQUIRED_HEADING`,
`EMPTY_REQUIRED_HEADING`, `MISSING_SPIKE_RECS_BLOCK`, `OVERSIZED_SCOPE`,
`FILE_OWNERSHIP_UNSUBSTANTIATED`, `INCONSISTENT_PROJECT_MAPPING`, `EMPTY_PRD_FILE` — no auto-fix. Human action
required.

---

### Phase 12: Report Write and Exit

```bash
# JSON
jq -n \
  --argjson summary "$SUMMARY_JSON" \
  --argjson findings "$FINDINGS_JSON" \
  --argjson audited "$AUDITED_JSON" \
  --arg verdict "$VERDICT" \
  --arg started "$STARTED_AT" \
  --arg completed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  '{schema_version:"1.0",verdict:$verdict,summary:$summary,
    audited:$audited,findings:$findings,
    started_at:$started,completed_at:$completed}' \
  | nightgauge layout write pipeline "issue-audit-${TS}.json" >/dev/null

# Markdown — assembled line-by-line, then piped into
# `nightgauge layout write pipeline "issue-audit-${TS}.md"` (omitted for brevity)
# ...

# stdout: verdict line and report paths
echo "Verdict: $VERDICT ($CRITICAL_COUNT CRITICAL, $WARNING_COUNT WARNING, $INFO_COUNT INFO)"
echo "Report: $REPORT_MD"
echo "JSON:   $REPORT_JSON"
[ "$WRITE_TRAIL" = "true" ] && echo "Trail:  $TRAIL_JSONL"

if [ "$JSON_OUTPUT" = "true" ]; then
  cat "$REPORT_JSON"
fi

if [ "$CRITICAL_COUNT" -gt 0 ]; then
  exit 1
fi
exit 0
```

---

## Decision Rules

- **Strict vs inferential**: when `--manifest` is set, every assertion
  declared in the manifest must hold; missing fields in inferential mode
  derive expectations from issue type and project config. Strict mode also
  raises `MISSING_REQUIRED_HEADING` from WARNING to CRITICAL — see
  [Phase 5](#phase-5-body-section-completeness); a body authored seconds
  earlier by `issue-create` has no excuse for missing a heading that skill
  prescribes.
- **Closed issues**: by default, closed issues are flagged
  `UNEXPECTED_STATE` (WARNING). `--allow-closed` suppresses this and runs
  only state-relevant phases (existence, body sections, knowledge scaffold).
- **Single-issue runs**: `--issues <single>` is valid; the audit runs every
  applicable phase but skips Phase 6 sub-issue checks.
- **Cross-repo**: when an issue is in a different repo than the current one,
  every `nightgauge forge` call must pass `--repo <owner/repo>`; never rely on the current
  repo default.
- **Idempotency**: re-running `--fix` on a previously-fixed audit MUST be a
  no-op. Repair primitives are idempotent.

## Failure Conditions (exit 2)

The skill exits 2 — distinct from finding-driven exit 1 — when the audit
itself cannot run:

- Go binary not in `$PATH`, `$NIGHTGAUGE_BIN` or `~/go/bin`
- `nightgauge forge` not authenticated
- `--manifest <path>` does not exist or fails JSON parse
- an epic's sub-issue list cannot be read whole (`nightgauge issue list --epic`
  exits non-zero)
- `epic validate` cannot read the epic itself (non-zero exit with no result
  on stdout)
- `.nightgauge/config.yaml` unreadable when project number must be
  derived

## Completion Checklist

- [ ] Mode and target issues resolved
- [ ] Existence + repo placement checked (Phase 2)
- [ ] Label completeness checked (Phase 3)
- [ ] Project board membership and fields checked (Phase 4)
- [ ] Body section completeness checked, including spike contract (Phase 5)
- [ ] Sub-issue and parent linking checked (Phase 6)
- [ ] `blockedBy` alignment checked via `epic validate --json` (Phase 7)
- [ ] Cross-repo consistency checked (Phase 8)
- [ ] Knowledge scaffold checked (Phase 9)
- [ ] Findings synthesized with verdict (Phase 10)
- [ ] Repairs applied (only if `--fix` / `--fix-interactive`); failures
      reclassified as CRITICAL (Phase 11)
- [ ] Markdown + JSON report written; audit trail JSONL written unless
      `--no-audit-trail` (Phase 12)
- [ ] Exit code reflects verdict (0 READY, 1 NEEDS FIXES, 2 skill failure)

### Self-Assessment Epilogue

<!-- include: ../_shared/SELF_ASSESSMENT_EPILOGUE.md -->

Files in this skill

  • CHANGELOG.md5.1 KB
  • SKILL.md30.9 KB
  • tests/authoring-contract-round-trip.test.md5 KB
  • tests/cross-repo-part-of.test.md3.5 KB
  • tests/epic-missing-falsifier.test.md1.8 KB
  • tests/missing-blocked-by.test.md2.6 KB
  • tests/missing-board-membership.test.md2.6 KB
  • tests/spike-contract-violation.test.md2.8 KB
  • tests/strict-vs-inferential.test.md3.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.