create-sdd-project 0.18.4 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -312,7 +312,7 @@ SDD DevFlow combines three proven practices:
312
312
  | `/review-plan` | Cross-model plan review — runs automatically after plan self-review when external CLIs are available; catches implementation blind spots |
313
313
  | `/context-prompt` | Generates a context recovery prompt after `/compact` with Workflow Recovery to prevent checkpoint skipping |
314
314
  | `/review-project` | Comprehensive project-level review using up to 3 AI models in parallel — 6 domains, audit context, consolidated report with action plan |
315
- | `/audit-merge` | Automated compliance audit — 11 pre-merge checks (ticket, tracker, evidence, merge base, working tree, data files). Auto-fixes issues. |
315
+ | `/audit-merge` | Automated compliance audit — 13 structural pre-merge checks (ticket, tracker, evidence, merge base, working tree, data files, CI state via `gh pr checks`, MCE Action 9 + Audit Merge Output) + 17 advisory drift patterns. Auto-fixes issues. |
316
316
 
317
317
  ### Spec & Plan Quality
318
318
 
@@ -372,7 +372,7 @@ Classify complexity for each (Simple / Standard / Complex):
372
372
 
373
373
  After classification, the loop runs autonomously: spec → plan → implement → review → merge for each feature, with all quality gates enforced.
374
374
 
375
- **9 guardrails** prevent runaway execution: max 5 features/session, circuit breaker (3 failures → stop), kill switch (`stop pm`), session lock, post-merge sanity check (`npm test`), rolling batch (1-3 features at a time), clean workspace validation, quality gates always on, `/audit-merge` auto-execution.
375
+ **11 guardrails** prevent runaway execution: max 5 features/session, mandatory compact after 2 features, target-branch baseline check, intra-batch dependency check, circuit breaker (3 failures → stop), kill switch (`stop pm`), session lock, post-merge sanity check (`npm test`), rolling batch (1-3 features at a time), clean workspace validation, quality gates always on (`/audit-merge` included).
376
376
 
377
377
  **Recovery**: after `/compact` or terminal restart, run `continue pm` — the session state in `pm-session.md` tracks exactly where you left off.
378
378
 
package/lib/doctor.js CHANGED
@@ -82,6 +82,9 @@ function runDoctor(cwd) {
82
82
  // 15. .sdd-meta.json structural integrity (v0.17.0)
83
83
  results.push(checkMetaJson(cwd, aiTools, projectType));
84
84
 
85
+ // 16. PM session coherence (v0.20.0) — pm-session.md SDD version vs current
86
+ results.push(checkSessionCoherence(cwd));
87
+
85
88
  return results;
86
89
  }
87
90
 
@@ -1179,7 +1182,94 @@ function checkMetaJson(cwd, aiTools, projectType) {
1179
1182
  };
1180
1183
  }
1181
1184
 
1185
+ /**
1186
+ * checkSessionCoherence — v0.20.0 Item B (PM session SDD-version drift).
1187
+ *
1188
+ * If a PM session is in-progress AND its recorded "SDD version at start"
1189
+ * differs from the currently-installed .sdd-version, emit WARN. The cached
1190
+ * PM session context (system prompt + skill references loaded at session
1191
+ * start) does NOT auto-refresh on SDD upgrade — only /compact reloads.
1192
+ *
1193
+ * Returns PASS if:
1194
+ * - No pm-session.md (no active session to check).
1195
+ * - pm-session.md has Status: done (terminated session).
1196
+ * - SDD version at start matches current .sdd-version.
1197
+ *
1198
+ * Returns WARN if:
1199
+ * - pm-session.md Status: in-progress AND versions differ.
1200
+ *
1201
+ * Returns PASS (N/A note) if pm-session.md exists but lacks the field
1202
+ * (legacy session pre-v0.20.0; no actionable signal).
1203
+ */
1204
+ function checkSessionCoherence(cwd) {
1205
+ const pmSessionPath = path.join(cwd, 'docs', 'project_notes', 'pm-session.md');
1206
+ const sddVersionPath = path.join(cwd, '.sdd-version');
1207
+
1208
+ if (!fs.existsSync(pmSessionPath)) {
1209
+ return {
1210
+ status: PASS,
1211
+ message: 'PM session: none active (N/A)',
1212
+ details: [],
1213
+ };
1214
+ }
1215
+
1216
+ if (!fs.existsSync(sddVersionPath)) {
1217
+ // SDD not installed cleanly; let earlier checks flag this.
1218
+ return {
1219
+ status: PASS,
1220
+ message: 'PM session coherence: N/A (no .sdd-version)',
1221
+ details: [],
1222
+ };
1223
+ }
1224
+
1225
+ const sessionContent = fs.readFileSync(pmSessionPath, 'utf8');
1226
+ const currentVersion = fs.readFileSync(sddVersionPath, 'utf8').trim();
1227
+
1228
+ const statusMatch = sessionContent.match(/^\*\*Status:\*\*\s*(\S+)/m);
1229
+ if (!statusMatch || statusMatch[1].toLowerCase() !== 'in-progress') {
1230
+ return {
1231
+ status: PASS,
1232
+ message: 'PM session: not in-progress (N/A)',
1233
+ details: [],
1234
+ };
1235
+ }
1236
+
1237
+ const versionFieldMatch = sessionContent.match(
1238
+ /^\*\*SDD version at start:\*\*\s*(\S+)/m,
1239
+ );
1240
+ if (!versionFieldMatch) {
1241
+ return {
1242
+ status: PASS,
1243
+ message: 'PM session coherence: legacy session (no version field; v0.20.0+ records it)',
1244
+ details: [
1245
+ 'Pre-v0.20.0 PM sessions did not record SDD version at start.',
1246
+ 'Drift detection unavailable for this session; consider /compact + continue pm to refresh.',
1247
+ ],
1248
+ };
1249
+ }
1250
+
1251
+ const startVersion = versionFieldMatch[1];
1252
+ if (startVersion === currentVersion) {
1253
+ return {
1254
+ status: PASS,
1255
+ message: `PM session coherence: in sync (SDD ${currentVersion})`,
1256
+ details: [],
1257
+ };
1258
+ }
1259
+
1260
+ return {
1261
+ status: WARN,
1262
+ message: `PM session coherence: SDD drift detected — session started under ${startVersion}, current ${currentVersion}`,
1263
+ details: [
1264
+ 'Cached workflow references in this session may be stale.',
1265
+ 'Recommendation: run `/compact` then `continue pm` to reload templates.',
1266
+ 'See pm-orchestrator/SKILL.md "Session-coherence check" for details.',
1267
+ ],
1268
+ };
1269
+ }
1270
+
1182
1271
  module.exports = {
1183
1272
  runDoctor,
1184
1273
  printResults,
1274
+ checkSessionCoherence,
1185
1275
  };
@@ -1329,6 +1329,36 @@ function generateUpgrade(config) {
1329
1329
  );
1330
1330
  }
1331
1331
 
1332
+ // v0.20.0 Item B — upgrade-time PM session coherence advisory.
1333
+ // If a pm-session.md is in-progress, the agent's cached context (loaded at
1334
+ // session start under the prior SDD version) survives the upgrade in memory.
1335
+ // Recommend /compact + continue pm to reload templates before next Step.
1336
+ const pmSessionPath = path.join(config.projectDir, 'docs', 'project_notes', 'pm-session.md');
1337
+ if (fs.existsSync(pmSessionPath)) {
1338
+ try {
1339
+ const sessionContent = fs.readFileSync(pmSessionPath, 'utf8');
1340
+ const statusMatch = sessionContent.match(/^\*\*Status:\*\*\s*(\S+)/m);
1341
+ if (statusMatch && statusMatch[1].toLowerCase() === 'in-progress') {
1342
+ console.log('\n ⚠ PM SESSION COHERENCE (v0.20.0):');
1343
+ console.log(
1344
+ ' An in-progress PM session was detected (docs/project_notes/pm-session.md).'
1345
+ );
1346
+ console.log(
1347
+ ' The cached PM orchestrator context loaded under the prior SDD version'
1348
+ );
1349
+ console.log(
1350
+ ' does NOT auto-refresh on this upgrade. Run `/compact` and then'
1351
+ );
1352
+ console.log(
1353
+ ' `continue pm` to reload templates before resuming development work.'
1354
+ );
1355
+ console.log(' Doctor check #16 will WARN until /compact runs.');
1356
+ }
1357
+ } catch (_err) {
1358
+ // best-effort advisory; never block the upgrade on read failure
1359
+ }
1360
+ }
1361
+
1332
1362
  console.log(`\nNext: git add -A && git commit -m "chore: upgrade SDD DevFlow to ${newVersion}"\n`);
1333
1363
  }
1334
1364
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.18.4",
3
+ "version": "0.20.0",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"
@@ -35,6 +35,7 @@ Go beyond checklist review — actively try to break the implementation:
35
35
  - **Malicious input**: What data could a malicious user inject? Are all inputs validated at system boundaries?
36
36
  - **State corruption**: What if the database is slow, a transaction fails midway, or cache is stale?
37
37
  - **Missing validation**: Are values range-checked, type-checked, and null-checked before use?
38
+ - **Defensive guards**: For every `if (cond)` / `if (!cond)` / SQL `WHERE` predicate / type-narrowing check that exists "to prevent X", trace the boolean with three concrete value scenarios — (a) the danger case the guard targets, (b) a normal/expected case, (c) an edge case (null/0/empty/duplicate). Does the guard fire ONLY on the danger case? Common failure: a defensive predicate is written that fires on the SAFE case and skips the DANGER case (boolean inverted). Treat any guard whose conditions you cannot mentally trace through values as a critical-review item — request author justification or write the trace yourself.
38
39
 
39
40
  ### 4. Categorize Findings
40
41
 
@@ -48,11 +48,82 @@ If DIVERGED, flag as FAIL with instruction to merge target branch first.
48
48
 
49
49
  Run only if `git diff origin/<target-branch>..HEAD --name-only` shows `.json` files in seed-data or fixtures directories.
50
50
 
51
+ **12. CI State** (added v0.19.0) — Verify GitHub Actions / CI checks for the current PR show no FAILURE / ERROR / CANCELLED / TIMED_OUT conclusion. PENDING is acceptable (still running). Emits distinct `N/A` messages when `gh` is unavailable, `jq` is unavailable, or no PR is open for the current branch — these are not blockers. Empirical motivation: F107a (PR #279) shipped with `ci-success` BLOCKED on 3 real failures (test-api lint, test-web build, branch-protection gate); the agent claimed "CI: green" without verification. C3 makes the claim auditable structurally.
52
+ ```bash
53
+ if ! command -v gh >/dev/null 2>&1; then
54
+ echo "C3 N/A: gh CLI unavailable"
55
+ elif ! command -v jq >/dev/null 2>&1; then
56
+ echo "C3 N/A: jq unavailable"
57
+ else
58
+ # In GitHub Actions `pull_request` jobs, `actions/checkout` defaults to a
59
+ # detached HEAD (no branch context), so `gh pr view` with no arg cannot
60
+ # resolve "the PR for the current branch". Read the PR number from
61
+ # GITHUB_REF (`refs/pull/<N>/merge`) when available so C3 still queries
62
+ # the right PR. Local runs (no GITHUB_REF) keep the no-arg behavior.
63
+ PR_NUM_FROM_CI=""
64
+ if [ -n "${GITHUB_REF:-}" ]; then
65
+ PR_NUM_FROM_CI=$(printf '%s' "$GITHUB_REF" | sed -n 's@^refs/pull/\([0-9]\{1,\}\)/.*@\1@p')
66
+ fi
67
+ if [ -n "$PR_NUM_FROM_CI" ]; then
68
+ PR_JSON=$(gh pr view "$PR_NUM_FROM_CI" --json number,statusCheckRollup 2>/dev/null || true)
69
+ else
70
+ PR_JSON=$(gh pr view --json number,statusCheckRollup 2>/dev/null || true)
71
+ fi
72
+ if [ -z "$PR_JSON" ]; then
73
+ echo "C3 N/A: no PR open for current branch"
74
+ else
75
+ PR_NUM=$(echo "$PR_JSON" | jq -r '.number // "unknown"')
76
+ FAILURES=$(echo "$PR_JSON" | jq -r '
77
+ .statusCheckRollup[]?
78
+ | select((.conclusion // .status) as $s
79
+ | $s == "FAILURE" or $s == "ERROR" or $s == "CANCELLED" or $s == "TIMED_OUT")
80
+ | .name // .context
81
+ ' | sort -u)
82
+ if [ -n "$FAILURES" ]; then
83
+ flag "C3 BLOCKER: PR #$PR_NUM has failing checks — $(echo "$FAILURES" | tr '\n' ',' | sed 's/,$//')"
84
+ else
85
+ echo "C3 PASS: PR #$PR_NUM — all checks pass or pending"
86
+ fi
87
+ fi
88
+ fi
89
+ ```
90
+
91
+ **13. MCE Action 9 present** (D1, added v0.20.0) — Verify the ticket's `## Merge Checklist Evidence` table includes row 9 marked `[x]`, AND the dedicated `## Audit Merge Output` section below the MCE table has non-empty body content containing a structural-compliance signal (e.g. `Structural: 13/13 PASS` or `Verdict: APPROVE`). Stable ID `D1`. Empirical motivation: fx PR #299 F-WEB-HISTORY merged without `/audit-merge` having been run — MCE table contained Actions 0-8 only (no row 9). The prose instruction at `merge-checklist.md:87` ("Run /audit-merge to verify all compliance checks pass automatically") was easier to skip than a structural table row. D1 makes the skip falsifiable: row 9 stays unfilled if the audit didn't run.
92
+ ```bash
93
+ ACTION_9_LINE=$(awk '/^## Merge Checklist Evidence/,/^---|^## (Audit Merge Output|Acceptance|$)/' "$TICKET" | grep -E '^\| 9\. ' | head -1)
94
+ AUDIT_SECTION=$(awk '/^## Audit Merge Output/,/^## |^---|^\*Ticket/' "$TICKET")
95
+ AUDIT_SECTION_BODY=$(printf '%s' "$AUDIT_SECTION" | sed '1d;$d' | grep -vE '^[[:space:]]*>' | tr -d '[:space:]')
96
+ # Compliance signal: case-insensitive. Mirrors JS twin's `.*?` semantics so
97
+ # `Structural: N/N PASS` (colon), `Structural — N/N` (em-dash), `Structural N/N`
98
+ # (space) all match. The `/audit-merge` Output Format section MUST emit at least
99
+ # one canonical signal per the Self-Verification Guarantee (see Output Format).
100
+ COMPLIANCE_HIT=$(printf '%s' "$AUDIT_SECTION" | grep -ciE '(structural[^[:alnum:]]+[0-9]+/[0-9]+)|(\b13/13\b)|(\ball[[:space:]]+(checks[[:space:]]+)?pass)|(verdict[[:space:]]*:[[:space:]]*approve)')
101
+
102
+ if [ -z "$ACTION_9_LINE" ] && [ -z "$AUDIT_SECTION" ]; then
103
+ # Legacy ticket predating v0.20.0 — neither row 9 NOR Audit Merge Output section exists.
104
+ # Emit N/A with migration hint instead of BLOCKER. Forward-looking: tickets created
105
+ # post-v0.20.0 will have at least the section header (from ticket-template.md), so
106
+ # legitimate skips still surface via the other branches below.
107
+ echo "D1 N/A: legacy ticket (no row 9 and no ## Audit Merge Output section) — add both per merge-checklist.md to comply with v0.20.0"
108
+ elif [ -z "$ACTION_9_LINE" ]; then
109
+ flag "D1 BLOCKER: MCE Action 9 row missing (required since v0.20.0; ## Audit Merge Output section exists but row 9 not added to MCE table)"
110
+ elif printf '%s' "$ACTION_9_LINE" | grep -qE '\| \[ \] \|'; then
111
+ flag "D1 BLOCKER: MCE Action 9 row unchecked — /audit-merge not run"
112
+ elif [ -z "$AUDIT_SECTION_BODY" ]; then
113
+ flag "D1 BLOCKER: ## Audit Merge Output section missing or empty"
114
+ elif [ "$COMPLIANCE_HIT" -eq 0 ]; then
115
+ flag "D1 BLOCKER: ## Audit Merge Output section present but no structural compliance signal (expect 'Structural: N/N PASS' or 'Verdict: APPROVE')"
116
+ else
117
+ CHAR_COUNT=$(printf '%s' "$AUDIT_SECTION" | wc -c | tr -d ' ')
118
+ echo "D1 PASS: ## Audit Merge Output section pasted (~${CHAR_COUNT} chars)"
119
+ fi
120
+ ```
121
+
51
122
  ### Drift Checks (added v0.18.0) — ADVISORY, not blocking
52
123
 
53
- Eleven empirically-validated drift patterns. Failures are NOT blockers for the compliance verdict, but MUST be refreshed before requesting user authorization (the user will otherwise catch them during audit and send the PR back). Each check has a concrete shell recipe — use BSD-grep-compatible regex (no `\K`).
124
+ Sixteen empirically-validated drift patterns. Failures are NOT blockers for the compliance verdict, but MUST be refreshed before requesting user authorization (the user will otherwise catch them during audit and send the PR back). Each check has a concrete shell recipe — use BSD-grep-compatible regex (no `\K`).
54
125
 
55
- **12. P1 — PR body test count stale (v0.18.3 multi-workspace extension — C1).** The PR body's test ratios should all appear in ticket evidence (AC / DoD / Completion Log). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body numbers become stale. In monorepos with multiple workspaces (e.g. api, web, bot, scraper) the PR body may quote several `N/N` ratios; v0.18.3 walks them all instead of comparing only the first. Subset direction: PR ratios ⊆ ticket ratios (the ticket Completion Log is the more comprehensive record and accumulates intermediate per-step ratios that the PR body legitimately omits). Three fallback cases: (a) ≥ 1 ratio on each side → verify each PR ratio appears in ticket; (b) PR has ratios but ticket has none (or vice versa) → emit explicit `P1 N/A` note, no drift flag; (c) neither side has ratios → emit `P1 N/A` note.
126
+ **14. P1 — PR body test count stale (v0.18.3 multi-workspace extension — C1).** The PR body's test ratios should all appear in ticket evidence (AC / DoD / Completion Log). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body numbers become stale. In monorepos with multiple workspaces (e.g. api, web, bot, scraper) the PR body may quote several `N/N` ratios; v0.18.3 walks them all instead of comparing only the first. Subset direction: PR ratios ⊆ ticket ratios (the ticket Completion Log is the more comprehensive record and accumulates intermediate per-step ratios that the PR body legitimately omits). Three fallback cases: (a) ≥ 1 ratio on each side → verify each PR ratio appears in ticket; (b) PR has ratios but ticket has none (or vice versa) → emit explicit `P1 N/A` note, no drift flag; (c) neither side has ratios → emit `P1 N/A` note.
56
127
  ```bash
57
128
  TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
58
129
  PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
@@ -69,14 +140,14 @@ else
69
140
  fi
70
141
  ```
71
142
 
72
- **13. P2 — Merge Checklist Evidence rows aspirational.** Rows marked `[x]` with future-tense Evidence ("will land", "to be created", "pending", "next commit", "TBD") — the row claims done but the work hasn't happened yet.
143
+ **15. P2 — Merge Checklist Evidence rows aspirational.** Rows marked `[x]` with future-tense Evidence ("will land", "to be created", "pending", "next commit", "TBD") — the row claims done but the work hasn't happened yet.
73
144
  ```bash
74
145
  awk '/^## Merge Checklist Evidence/{flag=1; next} /^## /{flag=0} flag' "$TICKET" \
75
146
  | grep -E '^\|.*\[x\].*(to be |will |pending|TBD|Will be |to be created|next commit|aspirational)' \
76
147
  && flag "P2 drift: aspirational row(s) found"
77
148
  ```
78
149
 
79
- **14. P3 — Post-merge actions not logged** (only fires if PR is MERGED and ticket Status is Done). Items marked as post-merge operator actions (AC / DoD / Test plan unchecked with post-merge keywords) should have a Completion Log row documenting execution.
150
+ **16. P3 — Post-merge actions not logged** (only fires if PR is MERGED and ticket Status is Done). Items marked as post-merge operator actions (AC / DoD / Test plan unchecked with post-merge keywords) should have a Completion Log row documenting execution.
80
151
  ```bash
81
152
  # Strip checkbox prefix before comparison; use grep -Fq fixed-string match.
82
153
  grep -E "^- \[ \].*(post-merge|operator|prod rollout|pending verification)" "$TICKET" \
@@ -89,14 +160,14 @@ while IFS= read -r item; do
89
160
  done < /tmp/pm_items.txt
90
161
  ```
91
162
 
92
- **15. P4 — Remote branch orphan after "deleted".** Workflow Step 6 claims `[x] branch deleted` but origin still has the branch.
163
+ **17. P4 — Remote branch orphan after "deleted".** Workflow Step 6 claims `[x] branch deleted` but origin still has the branch.
93
164
  ```bash
94
165
  BRANCH=$(grep -oE '\*\*[Bb]ranch:\*\*[[:space:]]*[^[:space:]|()]+' "$TICKET" | head -1 | sed -E 's/^\*\*[Bb]ranch:\*\*[[:space:]]*//')
95
166
  git fetch origin --prune --quiet
96
167
  git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q refs/heads && flag "P4 drift: remote branch $BRANCH still exists (run: git push origin --delete $BRANCH)"
97
168
  ```
98
169
 
99
- **16. P5 — Frozen ticket Status post-merge.** Scan all tickets in `docs/tickets/`; flag any with Status ≠ Done whose ticket-ID appears in `git log --all --grep`. Multi-word Status values like "Ready for Merge" must be handled (use `sed -E` char class, not `\w+`).
170
+ **18. P5 — Frozen ticket Status post-merge.** Scan all tickets in `docs/tickets/`; flag any with Status ≠ Done whose ticket-ID appears in `git log --all --grep`. Multi-word Status values like "Ready for Merge" must be handled (use `sed -E` char class, not `\w+`).
100
171
  ```bash
101
172
  FROZEN_COUNT=0
102
173
  for t in docs/tickets/*.md; do
@@ -114,30 +185,49 @@ done
114
185
  [ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
115
186
  ```
116
187
 
117
- **17. P6 — AC count off-by-N.** Merge Checklist Evidence row 1 claim diverges from actual count. Two canonical forms: `all N marked` (N = total, implies all are `[x]`) and `AC: X/Y done` (X = marked, Y = total — supports deferred ACs where Y > X intentionally). For `AC: X/Y` form, compare Y to actual total AND X to actual marked. For `all N marked` form, compare N to actual total.
188
+ **19. P6 — AC count off-by-N (header-form aware since v0.19.0).** Merge Checklist Evidence row 1 claim diverges from actual count. Supports two canonical AC forms: `[x]`/`[ ]` checkbox form and `### AC<N>` header form. When header form is present (≥ 1 `### AC<N>` heading), headers are authoritative — checkbox sub-items under each header are NOT double-counted. A header is "deferred" (not marked) when its body contains `**Status**: Deferred|Pending|Skipped|Blocked` before the next `### ` or section terminator. Two MCE claim shapes: `all N marked` (N = total, implies all marked) and `AC: X/Y done` (X = marked, Y = total — supports deferred ACs where Y > X intentionally). v0.19.0 drops the v0.18.3 `-ge 2` tolerance comparison is now exact-match (off-by-1 advisories surface).
118
189
  ```bash
119
190
  AC_BLOCK=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
120
- ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
121
- ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
191
+ HEADER_ACS=$(echo "$AC_BLOCK" | grep -cE '^### AC[0-9]+')
192
+
193
+ if [ "$HEADER_ACS" -gt 0 ]; then
194
+ ACTUAL_TOTAL=$HEADER_ACS
195
+ # Per-AC pass: emit one line per AC ("marked" or "deferred"). Then count.
196
+ PER_AC=$(echo "$AC_BLOCK" | awk '
197
+ function flush() { if (have) { print (deferred ? "deferred" : "marked"); have=0; deferred=0 } }
198
+ /^### AC[0-9]+/ { flush(); have=1; next }
199
+ /^## / { flush(); next }
200
+ have && /^\*\*Status\*\*:[[:space:]]*(Deferred|Pending|Skipped|Blocked)/ { deferred=1 }
201
+ END { flush() }
202
+ ')
203
+ ACTUAL_MARKED=$(printf '%s\n' "$PER_AC" | grep -c '^marked$' || true)
204
+ else
205
+ ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
206
+ ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
207
+ fi
208
+
122
209
  CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
123
210
  if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
124
211
  CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
125
212
  CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
126
213
  [ -n "$CLAIMED_TOTAL" ] && [ "$CLAIMED_TOTAL" != "$ACTUAL_TOTAL" ] \
127
- && [ $((ACTUAL_TOTAL - CLAIMED_TOTAL)) -ge 2 -o $((CLAIMED_TOTAL - ACTUAL_TOTAL)) -ge 2 ] \
128
- && flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL"
214
+ && flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
129
215
  [ -n "$CLAIMED_MARKED" ] && [ "$CLAIMED_MARKED" != "$ACTUAL_MARKED" ] \
130
- && [ $((ACTUAL_MARKED - CLAIMED_MARKED)) -ge 2 -o $((CLAIMED_MARKED - ACTUAL_MARKED)) -ge 2 ] \
131
- && flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED"
216
+ && flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
132
217
  elif [ -n "$CLAIM_LINE" ]; then
133
218
  CLAIMED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
134
- [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
135
- && [ $((ACTUAL_TOTAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL_TOTAL)) -ge 2 ] \
136
- && flag "P6 drift: 'all $CLAIMED marked' vs actual AC count $ACTUAL_TOTAL"
219
+ if [ -n "$CLAIMED" ]; then
220
+ [ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
221
+ && flag "P6 drift: 'all $CLAIMED marked' vs actual AC total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
222
+ # `all N marked` IMPLIES all ACs are marked; flag when actual marked < N
223
+ # (e.g. some ACs deferred via `**Status**: Deferred` or unchecked `[ ]`).
224
+ [ "$CLAIMED" != "$ACTUAL_MARKED" ] \
225
+ && flag "P6 drift: 'all $CLAIMED marked' but only $ACTUAL_MARKED actually marked (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
226
+ fi
137
227
  fi
138
228
  ```
139
229
 
140
- **18. P7 — Test count drift within ticket (final-sections only).** Only flag AC / DoD / tracker Active-Session numbers diverging from Completion Log terminal. Intermediate rows are legitimate.
230
+ **20. P7 — Test count drift within ticket (final-sections only).** Only flag AC / DoD / tracker Active-Session numbers diverging from Completion Log terminal. Intermediate rows are legitimate.
141
231
  ```bash
142
232
  TERMINAL=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET" | grep -iE "(test|pass|green)" | grep -oE "[0-9]+/[0-9]+" | tail -1)
143
233
  AC=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
@@ -148,7 +238,7 @@ for n in $FINAL_NUMS; do
148
238
  done
149
239
  ```
150
240
 
151
- **19. P8 — Completion Log gap vs Workflow Checklist.** Each `[x]` Step N in Workflow should have ≥1 Completion Log row mentioning "Step N". Use `while-read` on unique step numbers (not `for-in` which splits on whitespace).
241
+ **21. P8 — Completion Log gap vs Workflow Checklist.** Each `[x]` Step N in Workflow should have ≥1 Completion Log row mentioning "Step N". Use `while-read` on unique step numbers (not `for-in` which splits on whitespace).
152
242
  ```bash
153
243
  WORKFLOW=$(awk '/^## Workflow Checklist/,/^## Completion Log/' "$TICKET")
154
244
  COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
@@ -159,7 +249,7 @@ while read -r step_num; do
159
249
  done <<< "$CHECKED_STEPS"
160
250
  ```
161
251
 
162
- **20. P9 — Tracker header "Last Updated" stale.** The `**Last Updated:**` header and the `**Active Feature:**` detail should agree on step number (e.g., both say 5/6). Mismatch suggests the header wasn't refreshed after state transitions.
252
+ **22. P9 — Tracker header "Last Updated" stale.** The `**Last Updated:**` header and the `**Active Feature:**` detail should agree on step number (e.g., both say 5/6). Mismatch suggests the header wasn't refreshed after state transitions.
163
253
  ```bash
164
254
  TRACKER=docs/project_notes/product-tracker.md
165
255
  HEADER_STEP=$(grep '^\*\*Last Updated:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
@@ -168,7 +258,7 @@ DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE '(Step
168
258
  && flag "P9 drift: tracker header says $HEADER_STEP, Active Feature says $DETAIL_STEP"
169
259
  ```
170
260
 
171
- **21. P10 — Duplicate Completion Log rows.** Hash `date | action | first-80-of-notes`. Duplicates suggest copy-paste error during editing.
261
+ **23. P10 — Duplicate Completion Log rows.** Hash `date | action | first-80-of-notes`. Duplicates suggest copy-paste error during editing.
172
262
  ```bash
173
263
  awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
174
264
  key = $2 "|" $3 "|" substr($4, 1, 80)
@@ -178,7 +268,7 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
178
268
  | while read -r dup; do flag "P10 drift: duplicate Completion Log row: $dup"; done
179
269
  ```
180
270
 
181
- **22. P11 — Tracker Features table status vs ticket Status mismatch.** Ticket Status=Ready for Merge / Review → tracker expects `in-progress`. Ticket Status=Done → tracker expects `done`. Mismatch means one side wasn't updated after the state change.
271
+ **24. P11 — Tracker Features table status vs ticket Status mismatch.** Ticket Status=Ready for Merge / Review → tracker expects `in-progress`. Ticket Status=Done → tracker expects `done`. Mismatch means one side wasn't updated after the state change.
182
272
  ```bash
183
273
  TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
184
274
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
@@ -215,7 +305,7 @@ case "$TICKET_BASENAME" in
215
305
  esac
216
306
  ```
217
307
 
218
- **23. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
308
+ **25. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
219
309
  ```bash
220
310
  TRACKER=docs/project_notes/product-tracker.md
221
311
  if [ -f "$TRACKER" ]; then
@@ -235,7 +325,7 @@ if [ -f "$TRACKER" ]; then
235
325
  fi
236
326
  ```
237
327
 
238
- **24. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** When the ticket's Completion Log or MCE quantifies a delta against `key_facts.md` (e.g. `+8 atoms`, `+5 aliases`, `+27 dishes`), the corresponding feature row in `key_facts.md` should record the same delta. Whitespace-safe iteration via `while IFS= read -r`; FEATURE_ID-anchored block scan avoids false-pass on identical deltas elsewhere in the file. English keyword set; Spanish (`átomos`, `platos`) deferred to v0.19.x.
328
+ **26. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** When the ticket's Completion Log or MCE quantifies a delta against `key_facts.md` (e.g. `+8 atoms`, `+5 aliases`, `+27 dishes`), the corresponding feature row in `key_facts.md` should record the same delta. Whitespace-safe iteration via `while IFS= read -r`; FEATURE_ID-anchored block scan avoids false-pass on identical deltas elsewhere in the file. English keyword set; Spanish (`átomos`, `platos`) deferred to v0.19.x.
239
329
  ```bash
240
330
  KEY_FACTS=docs/project_notes/key_facts.md
241
331
  if [ -f "$KEY_FACTS" ]; then
@@ -251,7 +341,7 @@ if [ -f "$KEY_FACTS" ]; then
251
341
  fi
252
342
  ```
253
343
 
254
- **25. P14 — MCE Action 1 row stale post-merge (added v0.18.3).** When ticket Status normalizes to `Done` AND the MCE Action 1 row still has `Step 6 [ ]` / `Step 6 [-]`, the row was written pre-merge and not updated post-squash. **Strict scoping**: awk state machine terminates the MCE block at the NEXT `^## ` line of any name (NOT `[^M]` — that incorrectly absorbs subsequent `## M*` sections). **Strict signal**: only `Step 6 [ ]` / `Step 6 [-]` patterns flag; standalone `(this merge)` is omitted because it commonly appears in past-tense narrative ("merged at SHA (this merge)") and produces false positives. Reuses `TICKET_STATUS` defined in P11; do NOT use `$status` from the P5 loop. NIT severity.
344
+ **27. P14 — MCE Action 1 row stale post-merge (added v0.18.3).** When ticket Status normalizes to `Done` AND the MCE Action 1 row still has `Step 6 [ ]` / `Step 6 [-]`, the row was written pre-merge and not updated post-squash. **Strict scoping**: awk state machine terminates the MCE block at the NEXT `^## ` line of any name (NOT `[^M]` — that incorrectly absorbs subsequent `## M*` sections). **Strict signal**: only `Step 6 [ ]` / `Step 6 [-]` patterns flag; standalone `(this merge)` is omitted because it commonly appears in past-tense narrative ("merged at SHA (this merge)") and produces false positives. Reuses `TICKET_STATUS` defined in P11; do NOT use `$status` from the P5 loop. NIT severity.
255
345
  ```bash
256
346
  if [ "$TICKET_STATUS" = "Done" ]; then
257
347
  MCE_BLOCK=$(awk '
@@ -265,7 +355,7 @@ if [ "$TICKET_STATUS" = "Done" ]; then
265
355
  fi
266
356
  ```
267
357
 
268
- **26. P15 — AC with `post-deploy` keyword admitted without Completion Log evidence (added v0.18.3).** ACs containing production-parity keywords (`post-deploy`, `post-merge`, `production parity`, `prod verification`, `on dev API`, `on prod`) are explicit gates; marking them `[x]` without a dated Completion Log row defeats their purpose. Empirical origin: fx F-CATALOG-COV-001 AC-NEW-qa-battery silent-PASS until external audit caught it. Line-safe iteration via `while IFS= read -r`.
358
+ **28. P15 — AC with `post-deploy` keyword admitted without Completion Log evidence (added v0.18.3).** ACs containing production-parity keywords (`post-deploy`, `post-merge`, `production parity`, `prod verification`, `on dev API`, `on prod`) are explicit gates; marking them `[x]` without a dated Completion Log row defeats their purpose. Empirical origin: fx F-CATALOG-COV-001 AC-NEW-qa-battery silent-PASS until external audit caught it. Line-safe iteration via `while IFS= read -r`.
269
359
  ```bash
270
360
  COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
271
361
  AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
@@ -281,7 +371,7 @@ while IFS= read -r line; do
281
371
  done <<< "$AC_LINES"
282
372
  ```
283
373
 
284
- **27. P16 — Feature missing from Features table (added v0.18.3).** Ticket Status `Ready for Merge` / `Done` should have a row in some `## Features — *` table in `product-tracker.md`. **Strict signal**: requires the feature ID to appear as the first cell of a pipe-table row (`| FEATURE_ID |` with optional whitespace), NOT just anywhere in the tracker — narrative mentions or `**Active Feature:**` references must not silence the drift. NIT severity. Reuses `TICKET_STATUS` and `FEATURE_ID` defined in P11.
374
+ **29. P16 — Feature missing from Features table (added v0.18.3).** Ticket Status `Ready for Merge` / `Done` should have a row in some `## Features — *` table in `product-tracker.md`. **Strict signal**: requires the feature ID to appear as the first cell of a pipe-table row (`| FEATURE_ID |` with optional whitespace), NOT just anywhere in the tracker — narrative mentions or `**Active Feature:**` references must not silence the drift. NIT severity. Reuses `TICKET_STATUS` and `FEATURE_ID` defined in P11.
285
375
  ```bash
286
376
  TRACKER=docs/project_notes/product-tracker.md
287
377
  case "$TICKET_STATUS" in
@@ -295,9 +385,66 @@ case "$TICKET_STATUS" in
295
385
  esac
296
386
  ```
297
387
 
388
+ **30. P17 — Auth-touching backend change requires bearer-flow integration test on non-/me route** (added v0.20.0). When the diff modifies a backend auth-adjacent module (`auth*`, `actorResolver*`, `bearer*`, `rateLimit*`), the diff should ALSO include or modify an integration test that exercises the bearer flow on a NON-`/me` route. Empirically double-confirmed: BUG-PROD-013 (actorResolver bearer-path 500 on `/conversation/*` — non-`/me`), BUG-API-RATELIMIT-BEARER-001 (rateLimit helpers tested only api-key + IP, no bearer-through-global-limiter integration coverage). The `/me`-itself integrity bug class (F107a-FU2) is a distinct pattern — opt-out annotation `P17 N/A: <reason ≥ 1 word>` is recognized for frontend-only auth changes or other edge cases. Advisory, never blocks merge.
389
+ ```bash
390
+ # P17 — Auth-touching change requires bearer-flow integration test on non-/me route.
391
+ AUTH_TOUCHED=$(git diff --name-only "$BASE_REF...HEAD" 2>/dev/null | \
392
+ grep -iE '(auth|actorResolver|bearer|rateLimit)' | \
393
+ grep -E '^(packages/api|api|server|backend|src/server|src/api)/' | \
394
+ grep -vE '\.(test|spec)\.[jt]sx?$|__tests__/|/web/|/frontend/|/client/' | head -20)
395
+
396
+ if [ -z "$AUTH_TOUCHED" ]; then
397
+ echo "P17 N/A: no backend auth-adjacent files in diff"
398
+ else
399
+ TOUCHED_COUNT=$(printf '%s\n' "$AUTH_TOUCHED" | wc -l | tr -d ' ')
400
+
401
+ # Explicit opt-out (Gemini R1 M4 strengthened: require `: <reason ≥ 1 word>`)
402
+ OPTOUT=$(grep -E 'P17[[:space:]]+(N/A|opt[- ]?out|exempt)[[:space:]]*:[[:space:]]+[A-Za-z0-9].{2,}' "$TICKET" 2>/dev/null | head -1)
403
+ if [ -n "$OPTOUT" ]; then
404
+ echo "P17 N/A: explicit opt-out — $(printf '%s' "$OPTOUT" | head -c 120)"
405
+ else
406
+ INTEGRATION_TESTS=$(git diff --name-only "$BASE_REF...HEAD" 2>/dev/null | \
407
+ grep -iE '(integration|__tests__/.*\.integration\.|/integration/)' | \
408
+ grep -E '\.(test|spec)\.[jt]sx?$')
409
+
410
+ # Two extraction passes (Codex R2 I-1 multi-line fix + Gemini R2 I-1 backticks):
411
+ # Pass A: dot-method calls — `.get('/path')` / `.post(\`/path\`)` (single-line).
412
+ # Pass B: `url: '...'` property values within multi-line app.inject({ ... }).
413
+ BEARER_NON_ME_HITS=0
414
+ while IFS= read -r test_file; do
415
+ [ -z "$test_file" ] && continue
416
+ [ ! -f "$test_file" ] && continue
417
+ if ! grep -qiE '(bearer|authorization)' "$test_file" 2>/dev/null; then
418
+ continue
419
+ fi
420
+ ROUTES_A=$(grep -iE "\.(get|post|put|patch|delete)\([\`\"'](/[^\`\"']+)[\`\"']" "$test_file" 2>/dev/null | \
421
+ grep -oE "[\`\"'](/[^\`\"']+)[\`\"']" | sed -E "s/^[\`\"']|[\`\"']\$//g")
422
+ ROUTES_B=$(grep -iE "url[[:space:]]*:[[:space:]]*[\`\"'](/[^\`\"']+)[\`\"']" "$test_file" 2>/dev/null | \
423
+ grep -oE "[\`\"'](/[^\`\"']+)[\`\"']" | sed -E "s/^[\`\"']|[\`\"']\$//g")
424
+ ROUTE_STRINGS=$(printf '%s\n%s\n' "$ROUTES_A" "$ROUTES_B" | grep -vE '^$' | sort -u)
425
+ # Strip ${...} template-literal interpolations
426
+ ROUTE_STRINGS=$(printf '%s\n' "$ROUTE_STRINGS" | sed -E 's/\$\{[^}]*\}//g')
427
+ if [ -n "$ROUTE_STRINGS" ]; then
428
+ if printf '%s\n' "$ROUTE_STRINGS" | grep -qvE '^/me($|/)' ; then
429
+ BEARER_NON_ME_HITS=$((BEARER_NON_ME_HITS + 1))
430
+ fi
431
+ fi
432
+ done <<EOF
433
+ $INTEGRATION_TESTS
434
+ EOF
435
+
436
+ if [ "$BEARER_NON_ME_HITS" -eq 0 ]; then
437
+ flag "P17 drift (advisory): $TOUCHED_COUNT backend auth-adjacent file(s) but no integration test exercises bearer on a non-/me route (BUG-PROD-013 / BUG-RATELIMIT-BEARER pattern). Add an integration test or annotate \`P17 N/A: <reason>\` in the ticket."
438
+ else
439
+ echo "P17 PASS: $TOUCHED_COUNT auth-adjacent backend file(s), $BEARER_NON_ME_HITS integration test(s) cover bearer non-/me routes"
440
+ fi
441
+ fi
442
+ fi
443
+ ```
444
+
298
445
  ### Execution discipline (added v0.18.1)
299
446
 
300
- For each of the 16 drift checks (P1–P16), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
447
+ For each of the 17 drift checks (P1–P17), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
301
448
 
302
449
  Recommended pattern:
303
450
 
@@ -316,7 +463,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
316
463
  ```
317
464
  ## Merge Compliance Audit — [FEATURE-ID]
318
465
 
319
- ### Structural (1-11) — blocking merge gate
466
+ ### Structural (1-13) — blocking merge gate
320
467
 
321
468
  | # | Check | Status | Detail |
322
469
  |---|-------|:------:|--------|
@@ -324,46 +471,69 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
324
471
  | 2 | Acceptance Criteria | PASS | 14/14 |
325
472
  | 3 | Definition of Done | PASS | 7/7 |
326
473
  | 4 | Workflow Checklist | PASS | 7/8 (Step 6 pending) |
327
- | 5 | Merge Checklist Evidence | PASS | 8/8 with evidence |
474
+ | 5 | Merge Checklist Evidence | PASS | 9/9 with evidence |
328
475
  | 6 | Completion Log | PASS | 5 entries, bugs documented |
329
476
  | 7 | Tracker Sync | PASS | Active Session + Features table correct |
330
477
  | 8 | key_facts.md | PASS | N/A — no new infrastructure |
331
478
  | 9 | Merge Base | PASS | Up to date with develop |
332
479
  | 10 | Working Tree | PASS | Clean |
333
480
  | 11 | Data Files | PASS | N/A — no JSON seed files |
481
+ | 12 | CI State | PASS | PR #123 — all checks pass or pending |
482
+ | 13 | MCE Action 9 (D1) | PASS | ## Audit Merge Output section pasted (~3200 chars) |
334
483
 
335
484
  **STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
336
485
 
337
- ### Drift (12-27) — advisory, refresh before user authorization
486
+ ### Drift (14-30) — advisory, refresh before user authorization
338
487
 
339
488
  | # | Pattern | Status | Detail |
340
489
  |---|---------|:------:|--------|
341
- | 12 | P1 PR body test count stale | PASS | matches ticket terminal |
342
- | 13 | P2 Aspirational Evidence rows | PASS | all rows past-tense |
343
- | 14 | P3 Post-merge actions logged | PASS | N/A pre-merge |
344
- | 15 | P4 Remote branch orphan | PASS | not checked pre-merge |
345
- | 16 | P5 Frozen ticket Status | PASS | 0 frozen |
346
- | 17 | P6 AC count off-by-N | PASS | claim matches actual |
347
- | 18 | P7 Intra-ticket test drift | PASS | final sections = terminal |
348
- | 19 | P8 Completion Log gap | PASS | every [x] step has narrative |
349
- | 20 | P9 Tracker header stale | PASS | header = detail |
350
- | 21 | P10 Duplicate log rows | PASS | no duplicates |
351
- | 22 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
352
- | 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
353
- | 24 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
354
- | 25 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
355
- | 26 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
356
- | 27 | P16 Feature missing from tracker | PASS | feature in Features table |
490
+ | 14 | P1 PR body test count stale | PASS | matches ticket terminal |
491
+ | 15 | P2 Aspirational Evidence rows | PASS | all rows past-tense |
492
+ | 16 | P3 Post-merge actions logged | PASS | N/A pre-merge |
493
+ | 17 | P4 Remote branch orphan | PASS | not checked pre-merge |
494
+ | 18 | P5 Frozen ticket Status | PASS | 0 frozen |
495
+ | 19 | P6 AC count off-by-N | PASS | claim matches actual (form: header) |
496
+ | 20 | P7 Intra-ticket test drift | PASS | final sections = terminal |
497
+ | 21 | P8 Completion Log gap | PASS | every [x] step has narrative |
498
+ | 22 | P9 Tracker header stale | PASS | header = detail |
499
+ | 23 | P10 Duplicate log rows | PASS | no duplicates |
500
+ | 24 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
501
+ | 25 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
502
+ | 26 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
503
+ | 27 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
504
+ | 28 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
505
+ | 29 | P16 Feature missing from tracker | PASS | feature in Features table |
506
+ | 30 | P17 Auth integration coverage | PASS | 2 backend file(s), 1 integration test covers bearer non-/me |
357
507
 
358
508
  **DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
359
509
 
360
510
  ### Combined verdict
361
511
 
362
- - Both PASS → **READY FOR MERGE** (compliance 11/11, drift clean)
512
+ - Both PASS → **READY FOR MERGE** (compliance 13/13, drift clean)
363
513
  - Structural fail → **NEEDS FIX — N structural blockers** (any drift noted separately)
364
514
  - Structural pass + drift advisories → **READY FOR MERGE PENDING DRIFT CLEANUP — N advisories**
365
515
  ```
366
516
 
517
+ **Recipe-output verbatim rule** (added v0.19.0): When filling Detail columns of either the structural or drift tables, use the **literal output of the corresponding recipe** — do not normalize, summarize, or smooth values. Specifically:
518
+ - Numeric ratios (`N/M`): preserve as emitted. `18/19` MUST NOT become `18/18` even when N < M.
519
+ - MCE row 1 claim text: when a check (e.g. P6) references it in its Detail message, quote it verbatim including parentheticals such as `(AC19 deploy-deferred)` or `(operator action pending)`. Do not strip qualifiers.
520
+ - "Form" annotations (e.g. P6 emits `(form: header)` or `(form: checkbox)`): preserve them so the reader can tell which AC form the ticket used.
521
+ - `N/A` reasons: quote the literal reason emitted by the recipe (`no PR open`, `gh CLI unavailable`, `jq unavailable`, `not checked pre-merge`).
522
+
523
+ This rule exists because an LLM auditor, given header-form tickets where checkbox count is 0, will hallucinate `18/18` to "agree" with the MCE row 1 claim. The recipe says `0` and the MCE says `18/19`; the audit Detail must reflect both honestly so the reader can spot the form mismatch.
524
+
525
+ **Audit Output Self-Verification Guarantee** (added v0.20.0, required by D1): after emitting all structural rows + drift rows + combined verdict, ALWAYS append a final single-line summary in this exact form, regardless of pass/fail:
526
+
527
+ ```
528
+ Structural: <passed>/<total> PASS | Drift: <flagged> advisory | Verdict: <APPROVE|REVISE>
529
+ ```
530
+
531
+ Examples:
532
+ - `Structural: 13/13 PASS | Drift: 0 advisory | Verdict: APPROVE`
533
+ - `Structural: 11/13 PASS | Drift: 4 advisory | Verdict: REVISE`
534
+
535
+ This line is the canonical compliance signal that `D1` (MCE Action 9 present, line 92) parses. Without it, a verbatim paste of legitimate audit output may fail D1's signal check. The line must always be the last non-empty line in the output.
536
+
367
537
  ### If issues are found
368
538
 
369
539
  Fix them directly:
@@ -372,8 +542,9 @@ Fix them directly:
372
542
  - Tracker stale → update Active Session and Features table
373
543
  - Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
374
544
  - Data file issues → fix the data
545
+ - CI failures (C3) → re-run failed jobs after addressing root cause; do NOT merge while `ci-success` is BLOCKED
375
546
 
376
- **Drift advisories (12-27) fixes:**
547
+ **Drift advisories (13-28) fixes:**
377
548
  - **P1 (PR body test count stale)** → edit PR body "Quality Gates" / "npm test" line to match ticket terminal count; add "(+N new tests)" delta note
378
549
  - **P2 (Aspirational Evidence)** → rewrite `[x]` rows with past-tense text + commit SHA + concrete numbers
379
550
  - **P3 (Post-merge action unlogged)** → add a Completion Log row documenting the post-merge execution with date + action + empirical result