Skip to content
Back to skills

Issue Audit

BSecurity

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

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

Works with

  • vscode
  • terminal
  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

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

Scanned November 5, 2026

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

Installs into .claude/skills of the current project.

Are you the author of Issue Audit?

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

Security grade badge for Issue Audit
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/nightgauge-issue-audit/badge)](https://www.skillsdirectory.com/skills/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: 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](https://github.com/nightgauge/nightgauge/blob/main/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

### 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 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](https://github.com/nightgauge/nightgauge/blob/main/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](https://github.com/nightgauge/nightgauge/blob/main/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](https://github.com/nightgauge/nightgauge/blob/main/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

### 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
```

Files in this skill

  • CHANGELOG.md5.1 KB
  • SKILL.md42.4 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.