fix(#2431): self-suppress TDD Audit section when all commits are missing (#2467)

* fix(#2431): self-suppress TDD Audit section when all commits are missing

The TDD Audit section in ship.md step 8 was always emitted — but the
execute pipeline only writes gate_status: git trailers when TDD mode is
active. Without TDD mode (the default), every commit's trailer is absent
and the section normalizes to 100% missing, producing a noise table with
no way to disable it.

Fix: add a self-suppress instruction at the point where gate_status
values are normalized. When every commit in the scan normalizes to
'missing', skip both step 8 (TDD Audit section) and step 9 (aggregate
gate_status trailer) entirely. Only emit when at least one commit carries
a real value (skill, fallback, or exempt).

This is data-driven, NOT config-gated. An earlier iteration used inline
'gsd_run query config-get workflow.tdd_mode' — but workflow.tdd_mode is
owned by the tdd capability, and ADR-857 Phase 6 forbids host loop
workflows from reading capability-owned keys via inline config-get. The
self-suppress approach avoids any config-get entirely; it checks the
actual trailer data and skips when there's nothing real to report.

Matches the triage's suggested approach: 'have the audit gracefully
degrade (skip the section)' when there is no real signal.

Tests: tests/workflow-compat.test.cjs gains 3 #2431 assertions:
- documents self-suppress when every commit is missing
- step 9 (aggregate trailer) is also gated on real values existing
- does NOT read workflow.tdd_mode inline (ADR-857 Phase 6 compliant)

The existing feat-41 assertions still pass — the section content is
preserved, only gated by the self-suppress instruction.

References: #2431; PR #585 (consumer shipped, producer never wired);
ADR-857 Phase 6 (capability-owned config keys must not be read inline by
host loop workflows).

* chore(#2431): backfill pr:2467 in .changeset/clever-moles-frolic.md
This commit is contained in:
Tom Boucher
2026-07-20 19:24:08 -04:00
committed by GitHub
parent 6140627f5c
commit bb97ffb5aa
22 changed files with 60 additions and 20 deletions

View File

@@ -289,6 +289,8 @@ Pair commits by their conventional-commit type (the `type:` prefix of the subjec
Surface each commit's `gate_status:` value, normalized to exactly one of `skill`, `fallback`, `exempt`, or `missing` — never the raw trailer text. A commit whose trailer is absent, whose value is none of the first three, or which carries more than one `gate_status:` trailer (ambiguous) is counted as **missing** and still listed. This section is informational; it never blocks the ship.
**Self-suppress when every commit is missing (#2431):** the execute pipeline only writes `gate_status:` trailers when TDD mode is active. If every commit in the scan normalizes to `missing`, skip this section and the aggregate trailer (step 9) entirely — a 100%-missing table is pure noise. Only emit when at least one commit carries a real value (`skill`, `fallback`, or `exempt`).
Harden every table cell against injection, not just subjects: escape `|` as `\|` and strip `\r`/`\n` from both commit subjects and the rendered `gate_status` value. Prefer NUL (`-z` / `%x00`) record separation, and reject any record whose fields contain the `\x1f`/`\x1e` delimiters, so an adversarial commit message cannot corrupt record or field boundaries.
```markdown
@@ -305,7 +307,7 @@ Aggregate: 2 skill, 1 fallback, 1 exempt — 0 missing.
This `## TDD Audit` section is the final body section — it renders after the configured `pr_body_sections`, immediately before the aggregate trailer — so the frozen core sections and the append-only configured sections both keep their existing order.
**9. Aggregate gate_status trailer (final line):**
**9. Aggregate gate_status trailer (final line)** (only when step 8 was emitted — i.e., at least one real `gate_status` value exists):
After every other section — including any configured `pr_body_sections` — emit the audit aggregate as a single Git trailer on the **final line** of the PR body, preceded by a blank line so it parses as a valid trailer: