Skip to content
Back to skills

Backlog Groom

BSecurity

Groom the open backlog so nothing dispatched is wasted work — re-assess

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added November 5, 2026
developmentpythonrustgoshellbashnodedockergitapisecurity

Works with

  • cursor
  • 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 backlog-groom --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Backlog Groom?

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

Security grade badge for Backlog Groom
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/nightgauge-backlog-groom/badge)](https://www.skillsdirectory.com/skills/nightgauge-backlog-groom)

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: backlog-groom
description: Groom the open backlog so nothing dispatched is wasted work — re-assess
  every open issue against current main on five axes (validity, worth, verification
  completeness, security of the proposed approach, epic fit), adversarially verify
  every proposed mutation, then apply append-only corrections, evidence-cited
  closures, epic moves and a stale-sync sweep. Repo-scoped or workspace-wide with
  cross-repo epic batches. Dry-run by default; use after an epic completes, before a
  planning session, or on a monthly cadence.
license: Apache-2.0
metadata:
  author: nightgauge
  version: "2.0.0"
  source: https://github.com/nightgauge/nightgauge
allowed-tools: Read Write Edit Glob Grep Bash Task Workflow AskUserQuestion
disable-model-invocation: true
---

# Backlog Groom

Semantic re-assessment of the open backlog: is each issue still **valid**
against current `main`, still **worth doing**, **provable** when done,
**safe** as designed, and **filed under the right epic**? Structural checks
(labels, board fields, headings, `blockedBy` wiring) are
`/nightgauge:issue-audit`'s job — see
[docs/ISSUE_AUDIT.md](https://github.com/nightgauge/nightgauge/blob/main/docs/ISSUE_AUDIT.md#boundary-issue-audit-vs-backlog-groom)
for the boundary. This skill reuses issue-audit's deterministic checks as
riders rather than duplicating them; its own verdicts are judgment calls that
issue-audit never makes.

The shape encodes two reference runs. The 2026-08-16 run (103 open issues,
10 theme batches, adversarial closure verification) found that a
mostly-clean backlog still hid under-prioritized correctness bugs, issues
whose premises merged work had falsified, duplicates, and a live
publication-boundary leak. The 2026-08-29 workspace run (224 open issues
across seven repositories, 21 batches, assess → verify → apply fan-out) added
the two failure classes a repo-scoped validity pass cannot see: issues with
**no way to prove the feature works** — dispatched as-filed, the pipeline
self-grants "done" — and issues whose **proposed approach is insecure as
written** (unbounded retries, secrets in settings, unauthenticated mutating
endpoints), which land the vulnerability with green CI. Every one of those
wastes pipeline cycles or ships a defect when dispatched unchanged.

## When to Use

- **After an epic completes** — merged work falsifies premises and unblocks
  re-ranking across the whole board
- **Before a planning session** — the report ends with a ranked
  what-to-work-next and a single recommended next epic
- **Monthly cadence** — pairs with the `/nightgauge:pipeline-health` review
- **After a strategy change** — the worth axis re-judges every issue under the
  current product direction, so drift shows up as `close-not-worth` and
  `clarify` verdicts instead of as wasted runs
- **Not** for post-creation validation of new issues — that is
  `/nightgauge:issue-audit`, which runs automatically after `issue-create`
- **Not** for pre-run readiness of a chosen batch — that is
  `/nightgauge:backlog-preflight`

## Outcomes

- Markdown report at `triage/backlog-groom-<timestamp>.md` in this checkout's
  directory (`nightgauge layout path checkout triage`)
- JSON findings at `triage/backlog-groom-<timestamp>.json` beside it
- Per-issue verdict from the [vocabulary](#verdict-vocabulary) below
- Dry-run by default: every verdict is reported, nothing is mutated —
  **except** publication-boundary redaction, which is live exposure and is
  applied immediately in every mode, with an explicit report line
- With `--apply`: verifier-upheld closures with evidence comments, additive
  body sections (clarification, verification, premise correction, security
  constraints), epic re-parenting, board field corrections, stale-sync
  status repairs

## Input

```
/nightgauge:backlog-groom [--repo <owner/repo> | --workspace] [--apply]
```

| Flag           | Behavior                                                  | Default      |
| -------------- | --------------------------------------------------------- | ------------ |
| `--repo <o/r>` | Target repo for a repo-scoped run                         | current repo |
| `--workspace`  | Every repo in the workspace manifest, cross-repo batches  | off          |
| `--apply`      | Execute verified closures, corrections, and field changes | dry-run      |

`--workspace` reads the repo list from the workspace manifest
(`.vscode/nightgauge-workspace.yaml` or `nightgauge workspace list`) and
adds any repository that **parents** a workspace issue through the sub-issue
graph even when it has no local checkout — epics that live in a planning repo
still group work in the checked-out ones. A repo without a checkout is
assessed at the planning level only (its issues can be moved, clarified, or
closed on merged-PR evidence from sibling repos, never on "the code is gone").

## Hard Rules (non-negotiable)

1. **Closures require independently verified evidence.** A closure lands only
   after the Phase 4 adversarial pass confirms it, citing the specific PR,
   commit, or current file/function state that proves the issue is obsolete,
   done, or contrary to doctrine. "The assessor said so" is not evidence.
2. **Body changes are additive, never rewrites.** Every correction is an
   appended, dated `##` section (`Reassessment correction`, `Clarification`,
   `Verification`, `Security constraints`). Never silently rewrite or delete
   the human-authored original — the same never-rewrite rule issue-audit
   enforces. The one exception is redaction, which replaces only the leaked
   span.
3. **Unverifiable claims stay open.** If neither the assessor nor the
   verifier can ground a verdict in current code or merged history, the issue
   keeps its `keep` status and the report says why. Doubt always resolves
   toward keeping the issue open.
4. **Every issue leaves the run with verification.** An issue that cannot say
   how a reviewer would observe it working is not ready for dispatch. When
   the body has no concrete, falsifiable check, the run appends one — named
   test file or package, command, input, expected observable output. "Add
   tests" is not verification.
5. **Security is judged on the proposed design, not the current code.** If
   implementing the issue as written would introduce a hazard, the run
   appends the hazard and its mitigation as an acceptance criterion so the
   pipeline cannot close the issue without it.
6. **Cross-board listings are deliberate.** An issue appearing on more than
   one board (e.g. the Community Roadmap) is intentional curation, never a
   duplicate — do not "dedupe" across boards.
7. **Never close an epic with open sub-issues.** An epic is judged as a unit
   with its children in one batch.
8. **Boundary-leak redaction happens even in dry-run.** A publication-boundary
   leak in a public-repo issue body is live exposure the moment it is found.
   Redact it immediately per the neutral-referent pattern — replace the
   private detail with a neutral capability-level reference — regardless of
   mode, and report the redaction explicitly. Appended text in a public repo
   must itself be public-safe: no private repo issue numbers, customers,
   economics, or deployment topology. See
   [docs/PUBLIC_CORE_BOUNDARY.md](https://github.com/nightgauge/nightgauge/blob/main/docs/PUBLIC_CORE_BOUNDARY.md).
9. **Close, never delete.** A closed issue with an evidence comment is
   reversible and auditable; a deleted one is neither.

## Verdict Vocabulary

Exactly one verdict per issue — the dominant one when several actions apply:

| Verdict             | Meaning                                                                         | Mutation                                         |
| ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------ |
| `keep`              | Valid, worth doing, clearly specified, verifiable                               | none                                             |
| `clarify`           | Valid but the ask is ambiguous or has drifted from the current tree             | append `## Clarification (<date>)`               |
| `add-verification`  | Valid but carries no falsifiable way to prove it works                          | append `## Verification (added <date>)`          |
| `correct-premise`   | Merged work falsified part of the premise; work remains                         | append `## Reassessment correction (<date>)`     |
| `security-concern`  | The proposed approach is insecure as written                                    | append `## Security constraints (<date>)`        |
| `move-to-epic`      | Belongs under a different open parent epic                                      | re-parent via sub-issue API                      |
| `re-rank`           | Priority/size does not match the fixed rubric                                   | board field correction                           |
| `close-obsolete`    | Subject code/behavior is gone or the work already landed                        | evidence comment + close                         |
| `close-duplicate`   | Folds into a named survivor                                                     | survivor comment with unique ACs + close         |
| `close-not-worth`   | Possible but contrary to current doctrine/strategy (compat shims, dead surface) | reason comment + close                           |
| `redact-leak`       | Public-repo body leaks private material                                         | replace leaked span only — applied in every mode |
| `unverifiable-keep` | A non-keep verdict could not be grounded                                        | none; reason recorded                            |

## Priority Rubric (fixed)

Re-rank against this rubric only — never invent intermediate levels:

| Priority | Meaning                                           |
| -------- | ------------------------------------------------- |
| P0       | Breaking runs / corrupting data today             |
| P1       | Core-path correctness or unblocks high-value work |
| P2       | Real value, not urgent                            |
| P3       | Polish                                            |

## Model Guidance

- **Assessment batches (Phase 3)**: fan out — one agent per batch. The
  assessor authors publishable markdown (verification steps, security
  constraints) that the applier pastes verbatim, so this is not a cheap-tier
  job: a wrong verification section sends a future run down a false path.
- **Adversarial verification (Phase 4)**: strong tier, high effort. A wrongly
  closed issue is lost work; a wrong epic move hides work.
- **Apply (Phase 5)**: mechanical — low effort is correct.
- **Synthesis (Phase 7)**: strong tier; the ranking is where judgment pays.

## Gotchas

- **First page forever** — GitHub-CLI-style auto-pagination (`--paginate`)
  injects the next cursor ONLY into a variable named `$endCursor`. Name it
  anything else and every request silently re-fetches page one: a 300-item
  board reads as 100 items and the run "completes" against a third of the
  backlog. `nightgauge forge graphql` has no auto-pagination — use the
  explicit cursor loop in Phase 1 and keep the variable named `$endCursor`
  so the query stays drop-in compatible with auto-paginating clients.
- **Multi-repo boards cross-contaminate** — a shared project board streams
  items from every member repo. In a repo-scoped run, assessing (or worse,
  closing) a sibling repo's issue is out of scope by definition — filter on
  `.content.repository.nameWithOwner` before batching (Phase 1). In a
  workspace run the opposite holds: an epic's children span repos, and
  splitting them by repo hides the duplicates.
- **Stale sibling checkouts assess against a `main` that no longer exists** —
  `git fetch` every checkout in the workspace before Phase 3 and record each
  repo's `origin/main` SHA in the report. A verdict grounded in a checkout
  three weeks behind is a verdict about history.
- **Epics that live in an un-checked-out repo** — the sub-issue graph
  (`issue { parent { number repository { name } } }`) is the only reliable
  membership signal; body text like "Part of #N" drifts. Query it, do not
  parse it.
- **Label mutations need node IDs** — `nightgauge forge label add`/`remove`
  take label node IDs in `--labels`, not names. Resolve them once with
  `nightgauge label list --json`; a name passed where an ID belongs fails
  opaquely.
- **Append, do not re-send** — `gh issue edit --body` replaces the whole body.
  Fetch the current body to a file, append, and send the file; write the
  section via a quoted heredoc so markdown survives shell quoting. Check for
  the heading first so a re-run is idempotent.

### Cross-cutting gotchas (every Nightgauge skill)

> These apply to **all** skills. Skill-specific gotchas live above this line in
> each skill's `## Gotchas` section. Keep both terse: symptom → why → do-instead.
> When a new failure mode is root-caused (retro / failure-taxonomy), add it here
> or to the owning skill — Gotchas are the highest-signal part of a skill.

- **Bare `nightgauge` is not always on `PATH`.** Run the Phase 0 PREFLIGHT
  binary-discovery cascade first and prepend `dirname($BINARY)` to `PATH`.
  Skipping it surfaces as `command not found` only on some machines.
- **Never emit a foreground `sleep` wait loop.** The harness denies foreground
  `sleep` — an unbounded shell idiom that polls a completion marker in a
  `while`/`until` loop with no iteration cap — the tool call is rejected
  outright. To wait on a long-running background job, start it with the
  backgrounded-command form the harness provides (e.g. a Bash call with
  `run_in_background: true`) and then block on its completion notification /
  the Monitor tool instead of polling on a timer. Never invent a foreground
  poll loop to bridge the wait (#289).
- **Never call the forge CLI directly.** Use `nightgauge forge …`
  (`issue`/`pr`/`project`/`label`/`repo`/`auth`/`graphql`), never a bare
  `gh`/`glab`. The `no-direct-gh` lint fails CI on regressions; the abstraction
  is what lets `IB_FORGE=gitlab` work unchanged.
- **Headless skills must not block on input.** If `AskUserQuestion` is not in
  `allowed-tools`, the skill runs headless — make autonomous decisions or fail
  with a clear error. A blocked prompt hangs the whole pipeline run.
- **Write your handoff/context file before exiting — even on failure.** Pipeline
  stages that exit without their `{stage}-{N}.json` in the clone's pipeline
  state directory (`nightgauge layout path pipeline`) force the orchestrator
  onto a repo-blind deterministic fallback that may misreport state (issue 3114). Do not rely on the fallback.
- **An assertion that cannot go red is decoration.** A test that passes both
  before and after a fix constrains nothing — a regression test written for an
  SSH-push bug asserted "a push succeeds with no auth configured" and passed
  identically on the broken code, because a `file://` remote needs no
  credentials either way. Prove it: `cp <file> /tmp/<file>.bak`, restore the
  pre-fix behavior **from the copy**, confirm the new test FAILS, restore, and
  confirm it passes. Never `git checkout -- <file>` to back a fix out — on an
  uncommitted branch that destroys the fix. See
  [Vacuous Assertion](https://github.com/nightgauge/nightgauge/blob/main/docs/FAILURE_TAXONOMY.md#vacuous-assertion-the-test-that-cannot-go-red).
- **A green suite is a statement about the cases that RAN.** After adding
  assertions, check the reported PASS count rose by exactly the number you
  added; a suite can skip a case silently and still print "all tests passed".
- **Verify the issue's premise before implementing it.** An issue's stated cause
  can be flatly wrong — a request to delete a sweep that was never built, a
  claim that a failure category already classified an error when it fell through
  to `CatUnknown`, a "build it or delete it" fork where building was
  architecturally impossible. Check the claim against the tree first; if the
  premise is false, do the smaller correct thing and say so in the handoff.
- **Never report success when a step failed.** Swallowing a failed build/test
  lets a broken change flow downstream where it is caught later at higher cost
  (#2779). Surface the failure with its output.
- **Never push to `main`.** Use `feat/`/`fix/`/`docs/` branches; merges are
  manual squash only (no `--auto`, no `--admin`).
- **Production safety.** Never run `docker compose down -v` against a production
  stack — `-v` destroys `postgres_data`. Fix credential mismatches with
  `ALTER USER`, not a volume wipe.


## 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 0.5: Run Reflection

Load the previous run so this one leads with **what changed** (newly
obsolete, newly falsified premises, rank movement, newly unverifiable
issues) instead of re-dumping the whole backlog.

```bash
SKILL_NAME="nightgauge-backlog-groom"
RUN_LOG_CLASS="checkout"
RUN_LOG_NAME="triage/backlog-groom-runs.jsonl"
RUN_LOG="$(nightgauge layout path "$RUN_LOG_CLASS" "$RUN_LOG_NAME")"
```

### Run Reflection (memory across runs)

**PURPOSE**: Cadence skills (those run weekly/periodically) should remember their
own prior runs so each execution reports **deltas, not a full re-dump**. Without
memory, every run re-surfaces the same findings and can't say "what changed since
last time." This mirrors `nightgauge-release-watch`'s `last-seen-<provider>.json`.

> Set `RUN_LOG_CLASS`, `RUN_LOG_NAME` and
> `RUN_LOG="$(nightgauge layout path "$RUN_LOG_CLASS" "$RUN_LOG_NAME")"`
> **before** this section. The class is a per-clone class directory
> (`pipeline`, `plans`, `retros`, `logs`) or this checkout's directory
> (`checkout`), e.g. `RUN_LOG_CLASS="checkout"` with
> `RUN_LOG_NAME="doc-snapshots/runs.jsonl"`. Never `${CLAUDE_PLUGIN_DATA}`
> (single source of truth). Both directories are inside the git directory, so
> the log is read by path and appended through `nightgauge layout append`,
> never a redirect.

#### Step 1: Load the previous run

```bash
if [ -f "$RUN_LOG" ]; then
  PREV=$(tail -n 1 "$RUN_LOG")
  PREV_TS=$(printf '%s\n' "$PREV" | jq -r '.ts // "never"')
  echo "Last run: ${PREV_TS}"
else
  PREV='{}'
  PREV_TS="never"
  echo "No prior run — this is the first reflection (full report)."
fi
```

#### Step 2: Report deltas, not the whole world

Compare this run's key signals against `PREV` and lead the output with the
**delta**: what is newly appeared, newly resolved, or newly changed since
`PREV_TS`. Fall back to a full report only when `PREV_TS` is `never`. Examples of
delta signals by skill: new vs. resolved stale issues (backlog-groom), new /
changed / removed doc pages (docs-watch), friction-rate and health-trend
movement (continuous-improvement).

#### Step 3: Append this run (skip on `--dry-run`)

```bash
if [[ "$*" == *"--dry-run"* ]]; then
  echo "Dry-run: not appending to $RUN_LOG"
else
  NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  RECORD=$(jq -nc --arg ts "$NOW" --arg skill "$SKILL_NAME" \
        --argjson counts "${RUN_COUNTS:-{}}" --arg summary "${RUN_SUMMARY:-}" \
        '{ts:$ts, skill:$skill, counts:$counts, summary:$summary}')
  printf '%s\n' "$RECORD" | nightgauge layout append "$RUN_LOG_CLASS" "$RUN_LOG_NAME" >/dev/null
  echo "Appended run record to $RUN_LOG"
fi
```

**Schema** (one JSON object per line): `{ ts, skill, counts:{…}, summary }`.
`counts` holds the skill's headline metrics so the next run can diff them.


Set `RUN_COUNTS` (e.g.
`{"open":N,"closed":N,"corrected":N,"verified":N,"secured":N,"moved":N,"reranked":N}`)
and `RUN_SUMMARY` from the Phase 7 report before the append step.

---

### Phase 1: Inventory

Collect the full board item stream (all states — Phase 6 reuses it) and
every OPEN issue for each target repo with its board fields. Board inventory
pages through the project with an explicit cursor loop — the cursor variable
is named `$endCursor` (see Gotchas):

```bash
QUERY='query($owner:String!,$project:Int!,$endCursor:String){
  organization(login:$owner){
    projectV2(number:$project){
      items(first:100,after:$endCursor){
        pageInfo{hasNextPage endCursor}
        nodes{
          id
          content{... on Issue{
            number title state updatedAt
            repository{nameWithOwner}
            labels(first:20){nodes{name}}
          }}
          fieldValues(first:20){nodes{
            ... on ProjectV2ItemFieldSingleSelectValue{
              name field{... on ProjectV2SingleSelectField{name}}}
          }}
        }
      }
    }
  }
}'
# User-owned projects: swap organization(login:) for user(login:).

CURSOR=""
: > /tmp/board_items.jsonl
while :; do
  PAGE=$(nightgauge forge graphql -f query="$QUERY" \
    -f owner="$OWNER" -F project="$PROJECT_NUMBER" \
    ${CURSOR:+-f endCursor="$CURSOR"})
  printf '%s\n' "$PAGE" | jq -c '.data.organization.projectV2.items.nodes[]' \
    >> /tmp/board_items.jsonl
  HAS_NEXT=$(echo "$PAGE" \
    | jq -r '.data.organization.projectV2.items.pageInfo.hasNextPage')
  [ "$HAS_NEXT" = "true" ] || break
  CURSOR=$(echo "$PAGE" \
    | jq -r '.data.organization.projectV2.items.pageInfo.endCursor')
done
```

Repo-scoped run: filter multi-repo board items to the target repo —
sibling-repo items are out of scope:

```bash
jq -c --arg repo "$OWNER_REPO" \
  'select(.content.repository.nameWithOwner == $repo)' \
  /tmp/board_items.jsonl > /tmp/repo_items.jsonl
```

Workspace run: repeat per board, then pull the open-issue list of every repo
directly (`gh issue list -R <o/r> --state open --limit 500 --json ...`) and
the parent edge of every open issue:

```bash
gh api graphql --paginate -f query='query($endCursor:String){
  repository(owner:"'"$OWNER"'",name:"'"$REPO"'"){
    issues(first:100,states:OPEN,after:$endCursor){
      pageInfo{hasNextPage endCursor}
      nodes{number parent{number repository{name}}}}}}' \
  -q '.data.repository.issues.nodes[]|select(.parent!=null)
      |"'"$REPO"'#\(.number) -> \(.parent.repository.name)#\(.parent.number)"'
```

Any parent repository not in the manifest joins the run at planning level
(see Input). `git fetch` every local checkout and record each `origin/main`
SHA. Cross-check the board against `nightgauge forge issue list --json` —
the command returns open issues only by design, so open issues missing from
the board are still assessed (their absence is itself an issue-audit rider
finding, Phase 3).

---

### Phase 2: Theme Batching

Group the inventory into theme batches (~8–14 issues) so assessors can spot
duplicates and judge epics as units. Batch by, in priority order:

1. **Epic membership from the sub-issue graph** — an epic and its open
   children always share a batch, **across repositories** in a workspace run
2. **Component labels** and shared file-path citations
3. **Title/body keyword clusters** (the duplicate-detection surface)

Issues that fit no cluster form a residual batch. Assert that every open
issue is in exactly one batch before fanning out, and record the batch
manifest in the report so the fan-out is reproducible.

---

### Phase 3: Assessment (fan-out)

Dispatch one assessor per batch. Prefer the `Workflow` tool when it is
available: a `pipeline()` over the batch list with three stages
(assess → verify → apply) lets batch A be applied while batch B is still
being assessed, and the structured-output schema removes parsing. Without
it, fan out with `Task` subagents per phase. Each assessor fetches every
issue's body and comments and returns a per-issue verdict, a one-line
reason, an evidence pointer, `has_verification`, and a list of proposed
actions (`append_section` with the complete markdown, `comment`, `close`,
`move_to_epic`, `add_label`, `remove_label`, `retitle`, `redact_span`),
judging five axes:

- **Validity against current `main`** — verify cited files, functions, and
  behaviors still exist via `Grep`/`Read` on the fetched working tree, and
  search merged history (`git log -S`, `gh pr list --state merged --search`)
  for work that already did this. **Never trust the issue's snapshot of the
  code**: the issue was written against a `main` that may no longer exist.
- **Worth (product lens)** — would doing this now move the product? The
  no-backwards-compat doctrine means compat shims and "keep both paths" asks
  are `close-not-worth` or clarify-to-single-path; dead-code chores whose code
  is already deleted close as obsolete; polish on a surface being replaced
  drops to P3 or closes.
- **Verification completeness** — does the body name how a reviewer would
  observe the feature working: the test file or package to add or extend, the
  command, the input, the expected observable output? If not, author it —
  concrete, falsifiable, scoped to the issue's acceptance criteria, naming
  files that exist (or clearly marked as to-be-created).
- **Security of the proposed approach** — judged against
  `standards/security.md`: would the design as
  written put secrets in settings or logs, expose an unauthenticated or
  unauthorized mutating path, retry or fan out without bound, skip input
  validation at a boundary, write files outside a contained root, or let
  model-authored text reach a mutating tool unreviewed? Name the hazard and
  the mitigation as an acceptance criterion.
- **Epic fit and duplicates** — read the parent edge from the sub-issue
  graph; propose `move-to-epic` only toward an open epic that is clearly the
  better parent. Duplicates propose a fold direction naming the survivor.

Assessors also run issue-audit's deterministic checks as riders (type label,
board membership, required headings) and report those findings verbatim —
they do not re-derive them. Any publication-boundary leak found during
assessment escalates immediately to Phase 5's redaction path — it does not
wait for `--apply`.

---

### Phase 4: Adversarial Verification

Every proposed mutation is re-checked by a second, independent agent
(strong tier, high effort) **instructed to refute it**:

- **Closures**: re-derive the evidence from scratch. Does the cited
  PR/commit actually cover the acceptance criteria? Is the "deleted" code
  really gone, or moved? Does the epic have open children? Partial evidence
  downgrades to `correct-premise` with an appended section.
- **Appended sections**: is every claim true against current `main`? Are the
  named test files, packages, commands and flags real? Is the verification
  falsifiable? Fix the text if it is close; reject if it is wrong. In a
  public repo, reject any text that leaks private material.
- **Epic moves**: is the target open and genuinely the better parent? Reject
  moves between equally plausible parents.
- **Redactions**: does the find-text occur exactly once, and is the
  replacement neutral?
- **Duplicates**: do the two issues share acceptance criteria, or overlap on
  one axis while diverging on another?

A wrongly closed issue is lost work. **Doubt rejects the mutation** — the
issue reverts to `keep` (or `unverifiable-keep`) with the verifier's
objection recorded in the report. Only verifier-upheld actions, in the
verifier's final wording, proceed to Phase 5.

---

### Phase 5: Apply

**Dry-run (default)**: report every verdict; mutate nothing — except
boundary-leak redaction, which is applied in every mode and reported with an
explicit `REDACTED (applied in dry-run)` line.

**With `--apply`**, one applier per batch executes exactly the upheld action
list, in this order, and confirms each mutation took by re-reading the issue:

1. **Duplicate folds** — comment on the survivor folding in the duplicate's
   unique acceptance criteria, then close the duplicate with an evidence
   comment linking the survivor.
2. **Obsolete / not-worth closures** — post the evidence comment, then close
   (`--reason completed` when the work landed, otherwise `not planned`).
3. **Appended sections** — fetch the current body to a file, append the
   section, send the file back; skip when the heading already exists:

   ```bash
   BODY=$(mktemp)
   gh issue view "$N" -R "$OWNER_REPO" --json body -q .body > "$BODY"
   grep -q "^## Verification (added" "$BODY" || {
     printf '\n\n' >> "$BODY"; cat section.md >> "$BODY"
     gh issue edit "$N" -R "$OWNER_REPO" --body-file "$BODY"
   }
   ```

   Section shapes:

   ```markdown
   ## Reassessment correction (<YYYY-MM-DD>)

   <what changed, which PR/commit falsified the original premise, and what remains true>

   ## Verification (added <YYYY-MM-DD>)

   <named test/command, input, expected observable output — one bullet per AC>

   ## Security constraints (<YYYY-MM-DD>)

   <hazard in the proposed design → mitigation stated as an acceptance criterion>

   ## Clarification (<YYYY-MM-DD>)

   <the single intended reading, and what is explicitly out of scope>
   ```

4. **Epic moves** — resolve both node IDs, then re-parent through the
   sub-issue API (cross-repo parents are allowed):

   ```bash
   gh api graphql -f query='mutation{addSubIssue(input:{
     issueId:"<epicNodeId>",subIssueId:"<issueNodeId>",replaceParent:true
   }){issue{number}}}'
   ```

5. **Field corrections** — via the Go binary, never raw mutations:

   ```bash
   nightgauge project set-field <num> --priority P1 --size M
   nightgauge project sync-status <num> <status>
   ```

6. **Label changes** — resolve node IDs first with
   `nightgauge label list --json`, then mutate via the typed forge
   commands:

   ```bash
   nightgauge forge label add --issue-id <node> --labels <label-ids>
   nightgauge forge label remove --issue-id <node> --labels <label-ids>
   ```

   Never raw `nightgauge forge graphql` label mutations — the ADR-008
   raw-GraphQL carve-out does not cover labels, and raw GraphQL breaks the
   GitLab portability the typed commands preserve.

Every mutation appends one line to the report's applied-actions table:
issue, action, evidence pointer. A failed mutation is recorded as failed,
never retried into a different action.

---

### Phase 6: Stale-Sync Sweep

Catch CLOSED issues whose board Status never advanced to Done — the
failure mode that ghosts items in the VSCode extension. Runs against the
full (unfiltered-by-state) board inventory from Phase 1:

```bash
jq -c '
  select(.content.state == "CLOSED")
  | (([.fieldValues.nodes[]?
       | select(.field.name == "Status") | .name] | first) // "unset")
    as $status
  | select($status != "Done")
  | {number: .content.number, title: .content.title, status: $status}
' /tmp/repo_items.jsonl > /tmp/stale_sync.jsonl
```

Under `--apply`, repair each with `nightgauge project sync-status <num> done`;
in dry-run, list them in the report with their current status.

---

### Phase 7: Report

Write `triage/backlog-groom-<timestamp>.md` (and `.json`) to this checkout's
directory through the binary — it is inside the git directory, so pipe the
content to `nightgauge layout write checkout triage/backlog-groom-<timestamp>.md`
(and `.json`), never write it by path — with:

1. **Outcome counts** — issues assessed, kept, closed (obsolete / duplicate /
   not-worth), clarified, verification-added, premise-corrected,
   security-constrained, moved, re-ranked, redacted, stale-synced,
   unverifiable-kept
2. **Per-class listings** — every non-`keep` verdict with its one-line reason
   and evidence pointer; rejected mutations listed with the verifier's
   objection
3. **Security constraints table** — issue, hazard, mitigation added
4. **Epic moves table** — issue, old parent → new parent, reason
5. **Field-correction table** — issue, field, old → new, reason
6. **Boundary redactions** — explicit lines, flagged as applied even in
   dry-run
7. **Checkout SHAs** — the `origin/main` each repo was assessed against
8. **What to work next** — ranked list under the fixed rubric, ending with a
   **single recommended next epic** and the reasoning

In dry-run, close with the exact `--apply` invocation that would execute the
verified verdicts.

---

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


## Decision Rules

- **Semantic vs structural**: rank, worth, validity, premise truth,
  verification completeness, security of design, epic fit → this skill.
  Labels, board fields, headings, `blockedBy` wiring → issue-audit riders;
  report them, repair them only through issue-audit's primitives.
- **Groom vs preflight**: this skill judges the whole open backlog on a
  cadence; `backlog-preflight` judges a chosen batch immediately before a
  run. Preflight does not author verification — it fails an issue that lacks
  it, which is why this skill adds it first.
- **Closure bar**: independent evidence citing PR/commit/file, confirmed by
  the adversarial pass. Anything less stays open.
- **Verification bar**: a reviewer with the tree and the command could tell
  pass from fail without asking the author.
- **Security bar**: judged against `standards/security.md`; a hazard becomes
  an acceptance criterion, never a comment the pipeline can ignore.
- **Epic verdicts**: an epic is judged as a unit with its sub-issues in one
  batch; never close an epic while it has open sub-issues.
- **Repo scope**: in a repo-scoped run, sibling-repo items on a shared board
  are inventory-filtered out, never assessed, never mutated. In a workspace
  run, every repo in the manifest plus every parent repo is in scope.
- **Redaction scope**: redact only the leaked span (neutral-referent
  replacement); the rest of the body is untouched human-authored content.
- **Idempotency**: re-running after `--apply` must find nothing to re-apply —
  closures, appended headings, parents and field states are all convergent.

## Failure Conditions (exit 2)

Distinct from a clean run that simply finds nothing:

- Go binary missing (Phase 0 preflight cascade exhausts)
- Forge unauthenticated, or a project board number cannot be resolved from
  `.nightgauge/config.yaml` / the workspace manifest
- Board inventory pagination fails mid-run (partial inventory would produce
  false "missing from board" riders — abort instead)
- A batch manifest that does not cover every open issue exactly once
  (Phase 2) — a silent gap reads as "assessed and kept"

## Completion Checklist

- [ ] Inventory paginated to exhaustion (`hasNextPage` false), scoped to the
      target repo or the full workspace, checkouts fetched (Phase 1)
- [ ] Theme batches recorded in the report and proven to cover every open
      issue exactly once (Phase 2)
- [ ] Every open issue carries a verdict, a one-line reason, and a
      `has_verification` flag (Phase 3)
- [ ] Every proposed mutation adversarially verified or rejected (Phase 4)
- [ ] Dry-run mutated nothing except boundary redactions; `--apply` actions
      each carry an evidence pointer and a re-read confirmation (Phase 5)
- [ ] No open issue left without verification steps (Phase 5)
- [ ] Stale-sync sweep ran over CLOSED board items (Phase 6)
- [ ] Report written with outcome counts, per-class listings, security and
      epic-move tables, checkout SHAs, and a single recommended next epic
      (Phase 7)
- [ ] Run record appended to `RUN_LOG` (skipped in dry-run)

Files in this skill

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